diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e5b30d8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: + - java +dist: + - trusty +jdk: + - oraclejdk8 +install: + - mvn test-compile -DskipTests=true -Dmaven.javadoc.skip=true -B -V +script: + - mvn verify jacoco:report +after_success: + - mvn coveralls:report \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a69073 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017 Adam Zimowski (mrazjava) + +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. \ No newline at end of file diff --git a/docs/images/moo-jmx-console.png b/docs/images/moo-jmx-console.png new file mode 100644 index 0000000..41323eb Binary files /dev/null and b/docs/images/moo-jmx-console.png differ diff --git a/docs/images/moo-ui-shell-tmux.png b/docs/images/moo-ui-shell-tmux.png new file mode 100644 index 0000000..8084a37 Binary files /dev/null and b/docs/images/moo-ui-shell-tmux.png differ diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..07c01e1 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,2 @@ +Miscellaneous docs for improved documentation. For example, contains images +which can be linked from the master readme. \ No newline at end of file diff --git a/moo-api/pom.xml b/moo-api/pom.xml index 7aba2f4..4cc9797 100644 --- a/moo-api/pom.xml +++ b/moo-api/pom.xml @@ -3,10 +3,10 @@ pl.zimowski moo - 1.1.0-FINAL + 1.2.0-SNAPSHOT moo-api - Moo (api) + Moo API Common data structures, contracts and utilities used for communication by server and client. @@ -15,14 +15,8 @@ commons-io - org.mockito - mockito-core - test - - - org.hamcrest - hamcrest-integration - test + com.github.stefanbirkner + system-rules diff --git a/moo-api/readme.md b/moo-api/readme.md index cdb58b6..ee1b30d 100644 --- a/moo-api/readme.md +++ b/moo-api/readme.md @@ -1,2 +1,9 @@ -# Moo - common API shared by client and server -===================== +# Moo - Chat API +--------------------- +Common API shared by UI, client and server. Because UI is bound to an +API, and client powers UI at runtime, it is easy to build any new UI +client by simply implementing this API. + +A UI application essentially must inject implementation of `ClientHandling` +(which is provided by client runtime), and register `ClientListener` which +will process chat events. \ No newline at end of file diff --git a/moo-api/src/main/java/pl/zimowski/moo/api/ClientAction.java b/moo-api/src/main/java/pl/zimowski/moo/api/ClientAction.java index 0b2af0d..c0e795e 100644 --- a/moo-api/src/main/java/pl/zimowski/moo/api/ClientAction.java +++ b/moo-api/src/main/java/pl/zimowski/moo/api/ClientAction.java @@ -13,6 +13,11 @@ public enum ClientAction { * After this operation user is ready to chat. */ Signin, + + /** + * Client would like server to randomly generate user nick name. + */ + GenerateNick, /** * Client emitted a chat message to a server on behalf of end user. diff --git a/moo-api/src/main/java/pl/zimowski/moo/api/ClientHandling.java b/moo-api/src/main/java/pl/zimowski/moo/api/ClientHandling.java new file mode 100644 index 0000000..ef189a2 --- /dev/null +++ b/moo-api/src/main/java/pl/zimowski/moo/api/ClientHandling.java @@ -0,0 +1,16 @@ +package pl.zimowski.moo.api; + +/** + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public interface ClientHandling { + + boolean connect(ClientListener listener); + + boolean isConnected(); + + void disconnect(); + + void send(ClientEvent event); +} diff --git a/moo-api/src/main/java/pl/zimowski/moo/api/ClientListener.java b/moo-api/src/main/java/pl/zimowski/moo/api/ClientListener.java new file mode 100644 index 0000000..519f56b --- /dev/null +++ b/moo-api/src/main/java/pl/zimowski/moo/api/ClientListener.java @@ -0,0 +1,10 @@ +package pl.zimowski.moo.api; + +public interface ClientListener { + + void onEvent(ServerEvent event); + + void onBeforeServerConnect(String host, int port); + + void onConnectToServerError(String error); +} diff --git a/moo-api/src/main/java/pl/zimowski/moo/api/ServerAction.java b/moo-api/src/main/java/pl/zimowski/moo/api/ServerAction.java index a4c2b21..abbd5fb 100644 --- a/moo-api/src/main/java/pl/zimowski/moo/api/ServerAction.java +++ b/moo-api/src/main/java/pl/zimowski/moo/api/ServerAction.java @@ -8,9 +8,24 @@ */ public enum ServerAction { + /** + * Upon client request to establish connection with the server, server confirmed + * successful connection. Broadcast only to the thread that established + * connection. + */ ConnectionEstablished, + /** + * Upon client request to generate a random nick name, server generated + * the nick name. Broadcast only to thread that generated {@link ClientAction#GenerateNick}. + */ NickGenerated, + + /** + * Confirmation of a successful user sign in. Broadcast only to thread that + * generated {@link ClientAction#Signin}. + */ + SigninConfirmed, /** * report count of online users due to change to client collection (login, @@ -22,9 +37,19 @@ public enum ServerAction { * server aborted client connection due to inactivity */ ConnectionTimeOut, + + /** + * Client voluntarily chose to disconnect. + */ + ClientDisconnected, /** * chat message from another user */ - Message + Message, + + /** + * Server process terminated and all client connections were aborted + */ + ServerExit } diff --git a/moo-api/src/main/java/pl/zimowski/moo/api/ServerEvent.java b/moo-api/src/main/java/pl/zimowski/moo/api/ServerEvent.java index 4181680..b0de7cc 100644 --- a/moo-api/src/main/java/pl/zimowski/moo/api/ServerEvent.java +++ b/moo-api/src/main/java/pl/zimowski/moo/api/ServerEvent.java @@ -11,6 +11,11 @@ */ public class ServerEvent implements Serializable { + /** + * Name reported for messages authored by the server. + */ + public static final String AUTHOR = "server"; + private static final long serialVersionUID = -6175363790070655216L; private long timestamp; @@ -21,7 +26,14 @@ public class ServerEvent implements Serializable { private String message; - private String author = ApiUtils.APP_NAME; + /** + * Author which caused this event. Not every server event is caused by + * the server. In fact, most events are caused by clients and server + * simply echoes them back by broadcasting equivalent server event. + */ + private String author = ServerEvent.AUTHOR; + + private String note; private int participantCount; @@ -46,6 +58,17 @@ public ServerEvent withAuthor(String author) { return this; } + /** + * Additional data associated with this an event. Often empty. + * + * @param note free text that could mean different things depending on the context + * @return metadata (note) associated with an event + */ + public ServerEvent withNote(String note) { + this.note = note; + return this; + } + public ServerEvent withParticipantCount(int count) { participantCount = count; return this; @@ -84,7 +107,15 @@ public String getAuthor() { return author; } - /** + public String getNote() { + return note; + } + + public void setNote(String note) { + this.note = note; + } + + /** * @return id of a client which triggered this server event; may be * {@code null} if server itself triggered the event */ @@ -95,6 +126,6 @@ public String getClientId() { @Override public String toString() { return "ServerEvent [timestamp=" + timestamp + ", action=" + action + ", clientId=" + clientId + ", author=" + author - + ", message=" + message + ", participantCount=" + participantCount + "]"; + + ", message=" + message + ", note=" + note + ", participantCount=" + participantCount + "]"; } } \ No newline at end of file diff --git a/moo-api/src/test/java/pl/zimowski/moo/api/ApiUtilsTest.java b/moo-api/src/test/java/pl/zimowski/moo/api/ApiUtilsTest.java index 16f8a50..7f85828 100644 --- a/moo-api/src/test/java/pl/zimowski/moo/api/ApiUtilsTest.java +++ b/moo-api/src/test/java/pl/zimowski/moo/api/ApiUtilsTest.java @@ -2,7 +2,15 @@ import static org.junit.Assert.assertEquals; +import java.nio.charset.Charset; + +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.SystemOutRule; +import org.junit.runners.model.Statement; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Ensures that {@link ApiUtils} operates as expected. @@ -12,6 +20,13 @@ */ public class ApiUtilsTest { + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private Charset charset; + + @Test public void shouldFetchResource() { @@ -20,4 +35,23 @@ public void shouldFetchResource() { assertEquals(expectedContent, fetchedContent); } -} + + @Test + public void shouldNotFetchResource() { + + ApiUtils.fetchResource("/blah-blah"); + } + + @Test + public void shouldPrintPrompt() { + + SystemOutRule systemOutMock = new SystemOutRule().mute().enableLog(); + systemOutMock.apply(new Statement() { + @Override + public void evaluate() throws Throwable { + ApiUtils.printPrompt(); + assertEquals("> ", systemOutMock.getLog()); + } + }, null); + } +} \ No newline at end of file diff --git a/moo-api/src/test/java/pl/zimowski/moo/api/ClientEventTest.java b/moo-api/src/test/java/pl/zimowski/moo/api/ClientEventTest.java index 7464c52..4ed11dc 100644 --- a/moo-api/src/test/java/pl/zimowski/moo/api/ClientEventTest.java +++ b/moo-api/src/test/java/pl/zimowski/moo/api/ClientEventTest.java @@ -21,8 +21,10 @@ public void shouldProduceEventVia1ArgConstructor() { assertTrue(event.getTimestamp() > 0); assertEquals(ClientAction.Signin, event.getAction()); + assertEquals(ClientAction.Signin, ClientAction.valueOf("Signin")); assertNull(event.getAuthor()); assertNull(event.getMessage()); + assertNull(event.getId()); event.withAuthor("foo").withMessage("bar"); diff --git a/moo-api/src/test/java/pl/zimowski/moo/api/MockLoggerTest.java b/moo-api/src/test/java/pl/zimowski/moo/api/MockLoggerTest.java deleted file mode 100644 index 5444aa5..0000000 --- a/moo-api/src/test/java/pl/zimowski/moo/api/MockLoggerTest.java +++ /dev/null @@ -1,282 +0,0 @@ -package pl.zimowski.moo.api; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsMapContaining.hasEntry; -import static org.junit.Assert.assertNull; -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.After; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.slf4j.Logger; -import org.slf4j.event.Level; - -/** - * Ensures that {@link MockLogger} is correctly logging messages given various - * configuration options. - * - * @since 2.5.0 - * @author Adam Zimowski (mrazjava) - */ -public class MockLoggerTest { - - @Rule - public MockitoRule mockito = MockitoJUnit.rule(); - - @InjectMocks - private DummyBean dummyBean; - - @Spy - private TestLogger testLogger; - - private static final String THE_QUICK = "the quick"; - - private static final String BROWN_FOX = "brown fox"; - - private static final String JUMPS_OVER = "jumps over"; - - private static final String THE_LAZY = "the lazy"; - - private static final String DOG = "dog"; - - // logged messages are stored so that assertions can be made - private static Map results = new HashMap<>(); - - - @After - public void initResultsMap() { - results.clear(); - } - - @Test - public void shouldLogEverything() { - - MockLogger.resetSilence(); - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(5)); - assertThat(results, hasEntry(Level.TRACE, THE_QUICK)); - assertThat(results, hasEntry(Level.DEBUG, BROWN_FOX)); - assertThat(results, hasEntry(Level.INFO, JUMPS_OVER)); - assertThat(results, hasEntry(Level.WARN, THE_LAZY)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldNotLogTrace() { - - MockLogger.silentTraceOn(); - testDebugUp(); - } - - @Test - public void shouldLogDebugUp() { - - MockLogger.silentLevel = Level.TRACE; - testDebugUp(); - } - - private void testDebugUp() { - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(4)); - assertNull(results.get(Level.TRACE)); - assertThat(results, hasEntry(Level.DEBUG, BROWN_FOX)); - assertThat(results, hasEntry(Level.INFO, JUMPS_OVER)); - assertThat(results, hasEntry(Level.WARN, THE_LAZY)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldNotLogDebug() { - - MockLogger.silentDebugOn(); - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(4)); - assertThat(results, hasEntry(Level.TRACE, THE_QUICK)); - assertNull(results.get(Level.DEBUG)); - assertThat(results, hasEntry(Level.INFO, JUMPS_OVER)); - assertThat(results, hasEntry(Level.WARN, THE_LAZY)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldLogInfoUp() { - - MockLogger.silentLevel = Level.DEBUG; - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(3)); - assertNull(results.get(Level.TRACE)); - assertNull(results.get(Level.DEBUG)); - assertThat(results, hasEntry(Level.INFO, JUMPS_OVER)); - assertThat(results, hasEntry(Level.WARN, THE_LAZY)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldNotLogInfo() { - - MockLogger.silentInfoOn(); - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(4)); - assertThat(results, hasEntry(Level.TRACE, THE_QUICK)); - assertThat(results, hasEntry(Level.DEBUG, BROWN_FOX)); - assertNull(results.get(Level.INFO)); - assertThat(results, hasEntry(Level.WARN, THE_LAZY)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldLogWarnUp() { - - MockLogger.silentLevel = Level.INFO; - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(2)); - assertNull(results.get(Level.TRACE)); - assertNull(results.get(Level.DEBUG)); - assertNull(results.get(Level.INFO)); - assertThat(results, hasEntry(Level.WARN, THE_LAZY)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldNotLogWarn() { - - MockLogger.silentWarnOn(); - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(4)); - assertThat(results, hasEntry(Level.TRACE, THE_QUICK)); - assertThat(results, hasEntry(Level.DEBUG, BROWN_FOX)); - assertThat(results, hasEntry(Level.INFO, JUMPS_OVER)); - assertNull(results.get(Level.WARN)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldLogErrorOnly() { - - MockLogger.silentLevel = Level.WARN; - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(1)); - assertNull(results.get(Level.TRACE)); - assertNull(results.get(Level.DEBUG)); - assertNull(results.get(Level.INFO)); - assertNull(results.get(Level.WARN)); - assertThat(results, hasEntry(Level.ERROR, DOG)); - } - - @Test - public void shouldNotLogError() { - - MockLogger.silentErrorOn(); - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(4)); - assertThat(results, hasEntry(Level.TRACE, THE_QUICK)); - assertThat(results, hasEntry(Level.DEBUG, BROWN_FOX)); - assertThat(results, hasEntry(Level.INFO, JUMPS_OVER)); - assertThat(results, hasEntry(Level.WARN, THE_LAZY)); - assertNull(results.get(Level.ERROR)); - } - - @Test - public void shouldLogNothing() { - - MockLogger.silentLevel = Level.ERROR; - - dummyBean.logIt(); - - assertThat(results.values(), hasSize(0)); - } - - /** - * Dummy pojo used to invoke {@link MockLogger} at all levels. - * - * @since 2.5.0 - * @author Adam Zimowski (mrazjava) - */ - static class DummyBean { - - private TestLogging log; - - public void logIt() { - - log.trace(THE_QUICK); - log.debug(BROWN_FOX); - log.info(JUMPS_OVER); - log.warn(THE_LAZY); - log.error(DOG); - } - } - - /** - * Marker interface needed to make mockito and {@link TestLogger} happy. - * - * @since 2.5.0 - * @author Adam Zimowski (mrazjava) - */ - static interface TestLogging extends Logger { - } - - /** - * Wrapper over actual logger being tested with sole purpose of recording - * results so that assertions can be made. - * - * @since 2.5.0 - * @author Adam Zimowski (mrazjava) - */ - static class TestLogger extends MockLogger implements TestLogging { - - @Override - public void trace(String msg) { - super.trace(msg); - if(!isTraceSilent()) results.put(Level.TRACE, msg); - } - - @Override - public void debug(String msg) { - super.debug(msg); - if(!isDebugSilent()) results.put(Level.DEBUG, msg); - } - - @Override - public void info(String msg) { - super.info(msg); - if(!isInfoSilent()) results.put(Level.INFO, msg); - } - - @Override - public void warn(String msg) { - super.warn(msg); - if(!isWarnSilent()) results.put(Level.WARN, msg); - } - - @Override - public void error(String msg) { - super.error(msg); - if(!isErrorSilent()) results.put(Level.ERROR, msg); - } - } -} \ No newline at end of file diff --git a/moo-api/src/test/java/pl/zimowski/moo/api/ServerEventTest.java b/moo-api/src/test/java/pl/zimowski/moo/api/ServerEventTest.java index ab1303c..b08fa5d 100644 --- a/moo-api/src/test/java/pl/zimowski/moo/api/ServerEventTest.java +++ b/moo-api/src/test/java/pl/zimowski/moo/api/ServerEventTest.java @@ -1,6 +1,7 @@ package pl.zimowski.moo.api; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -21,13 +22,15 @@ public void shouldProduceEventVia1ArgConstructor() { assertTrue(event.getTimestamp() > 0); assertEquals(ServerAction.ParticipantCount, event.getAction()); + assertEquals(ServerAction.ParticipantCount, ServerAction.valueOf("ParticipantCount")); assertEquals(0, event.getParticipantCount()); assertNull(event.getMessage()); - event.withParticipantCount(2).withMessage("bar"); + event.setMessage("foo"); + event.setNote("bar"); - assertEquals(2, event.getParticipantCount()); - assertEquals("bar", event.getMessage()); + assertEquals("foo", event.getMessage()); + assertEquals("bar", event.getNote()); } @Test @@ -40,4 +43,22 @@ public void shouldProduceEventVia2ArgConstructor() { assertEquals(0, event.getParticipantCount()); assertEquals("hello world", event.getMessage()); } -} + + @Test + public void shouldProduceEventWithFluidSetters() { + + ServerEvent event = new ServerEvent(ServerAction.Message) + .withAuthor("johnie") + .withClientId("foo-bar") + .withMessage("howdy") + .withNote("what?") + .withParticipantCount(27); + + assertNotNull(event.getDateTime()); + assertEquals("johnie", event.getAuthor()); + assertEquals("foo-bar", event.getClientId()); + assertEquals("howdy", event.getMessage()); + assertEquals("what?", event.getNote()); + assertEquals(27, event.getParticipantCount()); + } +} \ No newline at end of file diff --git a/moo-client/pom.xml b/moo-client-socket/pom.xml similarity index 77% rename from moo-client/pom.xml rename to moo-client-socket/pom.xml index 4f86d33..2087715 100644 --- a/moo-client/pom.xml +++ b/moo-client-socket/pom.xml @@ -3,11 +3,11 @@ pl.zimowski moo - 1.1.0-FINAL + 1.2.0-SNAPSHOT - moo-client - Client app for mooing - Moo (client: console) + moo-client-socket + Moo socket client (requires socket server) + Moo Client (socket) @@ -23,16 +23,6 @@ - - org.hamcrest - hamcrest-integration - test - - - org.mockito - mockito-core - test - javax.enterprise cdi-api @@ -52,6 +42,14 @@ pl.zimowski moo-api + + + pl.zimowski + moo-test-utils + + + pl.zimowski + moo-commons \ No newline at end of file diff --git a/moo-client-socket/readme.md b/moo-client-socket/readme.md new file mode 100644 index 0000000..11b0479 --- /dev/null +++ b/moo-client-socket/readme.md @@ -0,0 +1,5 @@ +# Moo - chat client +--------------------- + +Socket based client runtime. If using socket server implementation, +then this runtime is placed as dependency of a UI. \ No newline at end of file diff --git a/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ClientHandler.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ClientHandler.java new file mode 100644 index 0000000..30b76d0 --- /dev/null +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ClientHandler.java @@ -0,0 +1,43 @@ +package pl.zimowski.moo.client.socket; + +import javax.inject.Inject; + +import org.springframework.stereotype.Component; + +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.api.ClientListener; + +/** + * Handles operations generated by the client. This implementation delegates + * actual work to {@link ConnectionManagement}. + * + * @since 1.1.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class ClientHandler implements ClientHandling { + + @Inject + private ConnectionManagement connectionManager; + + @Override + public void send(ClientEvent event) { + connectionManager.send(event); + } + + @Override + public boolean connect(ClientListener listener) { + return connectionManager.connect(listener); + } + + @Override + public boolean isConnected() { + return connectionManager.isConnected(); + } + + @Override + public void disconnect() { + connectionManager.disconnect(); + } +} \ No newline at end of file diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/ConnectionManagement.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ConnectionManagement.java similarity index 85% rename from moo-client/src/main/java/pl/zimowski/moo/client/ConnectionManagement.java rename to moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ConnectionManagement.java index 25745e2..aa21cb6 100644 --- a/moo-client/src/main/java/pl/zimowski/moo/client/ConnectionManagement.java +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ConnectionManagement.java @@ -1,6 +1,7 @@ -package pl.zimowski.moo.client; +package pl.zimowski.moo.client.socket; import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientListener; /** * Ability to establish connection from a client to a server and do basic @@ -17,7 +18,7 @@ public interface ConnectionManagement { * * @return {@code true} if connection was established; {@code false} if connection failed */ - boolean connect(); + boolean connect(ClientListener listener); void disconnect(); diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/ConnectionManager.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ConnectionManager.java similarity index 61% rename from moo-client/src/main/java/pl/zimowski/moo/client/ConnectionManager.java rename to moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ConnectionManager.java index 1973617..8cf746e 100644 --- a/moo-client/src/main/java/pl/zimowski/moo/client/ConnectionManager.java +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ConnectionManager.java @@ -1,19 +1,19 @@ -package pl.zimowski.moo.client; +package pl.zimowski.moo.client.socket; import java.io.IOException; import java.io.ObjectOutputStream; import java.net.Socket; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import pl.zimowski.moo.api.ApiUtils; import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientListener; /** * Socket based manager. Expects that server is capable talking over web @@ -27,37 +27,45 @@ public class ConnectionManager implements ConnectionManagement { @Inject private Logger log; - - @Value("${server.host}") - private String host; - - @Value("${server.port}") - private int port; - + + @Inject + private SocketProducer socketProducer; + private Socket socket; - + private boolean connected; - + @Inject - private NickNameAssigning nickNameAssigner; + private ServerListenerInitializer serverListenerInit; + + @PostConstruct + public void init() { + log.info("\n{}", ApiUtils.fetchResource("/logo")); + } + + @PreDestroy + public void cleanup() { + disconnect(); + } @Override - public boolean connect() { + public boolean connect(ClientListener clientListener) { + + String host = socketProducer.getSocketHost(); + int port = socketProducer.getSocketPort(); log.info("establishing connection to {}:{}", host, port); - ExecutorService executor = Executors.newSingleThreadExecutor(); - - try { - socket = new Socket(host, port); - Thread serverListener = new ServerListener(socket, this) - .withNickNameAssigner(nickNameAssigner); - executor.submit(serverListener); + socket = socketProducer.getSocket(); + + if(socket != null) { + clientListener.onBeforeServerConnect(host, port); + serverListenerInit.initialize(new ServerListener(socket, clientListener)); connected = true; } - catch(IOException e) { - log.error("could not connect to server: {}", e.getMessage()); + else { + clientListener.onConnectToServerError(socketProducer.getStatus()); } return connected; @@ -72,12 +80,7 @@ public void disconnect() { public boolean isConnected() { return connected; } - - @PreDestroy - public void cleanup() { - // server will close the socket for us :-) - } - + @Override public void send(ClientEvent event) { @@ -90,6 +93,7 @@ public void send(ClientEvent event) { ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); out.writeObject(event); out.flush(); + log.debug("sent:\n{}", event); } catch(IOException e) { log.error("message transmission failed: {}", e.getMessage()); diff --git a/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ServerListener.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ServerListener.java new file mode 100644 index 0000000..f4459ad --- /dev/null +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ServerListener.java @@ -0,0 +1,66 @@ +package pl.zimowski.moo.client.socket; + +import java.io.EOFException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.Socket; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pl.zimowski.moo.api.ClientListener; +import pl.zimowski.moo.api.ServerEvent; + +/** + * Listens for incoming events from the server and echoes them back to the + * client. + * + * @since 1.0.0 + * @author Adam Zimowski (mrazjava) + */ +public class ServerListener extends Thread { + + private static final Logger log = LoggerFactory.getLogger(ServerListener.class); + + private Socket socket; + + private ClientListener clientListener; + + private boolean hardExit = true; + + + public ServerListener(Socket socket, ClientListener clientListener) { + this.socket = socket; + this.clientListener = clientListener; + } + + void setHardExit(boolean hardExit) { + this.hardExit = hardExit; + } + + @Override + public void run() { + + try { + while(!socket.isInputShutdown()) { + ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); + ServerEvent serverEvent = (ServerEvent)in.readObject(); + clientListener.onEvent(serverEvent); + } + } + catch(EOFException e) { + log.info("server was shut down at administrator's request"); + } + catch (IOException | ClassNotFoundException e) { + log.error("unexpected connection error: {}; aborting!", e.getMessage()); + } + + if(hardExit) { + System.exit(MAX_PRIORITY); + } + } + + public boolean isListening() { + return !socket.isClosed(); + } +} \ No newline at end of file diff --git a/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ServerListenerInitializer.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ServerListenerInitializer.java new file mode 100644 index 0000000..9e185fc --- /dev/null +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/ServerListenerInitializer.java @@ -0,0 +1,30 @@ +package pl.zimowski.moo.client.socket; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.springframework.stereotype.Component; + +/** + * Ensures that {@link ServerListener} is properly setup for + * listening. This implementation kicks off the listener on a + * separate single thread. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class ServerListenerInitializer { + + /** + * Sets up server listener within the framework of execution so that + * it immediately starts listening for the events. + * + * @param listener to be initialized + */ + public void initialize(ServerListener listener) { + + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.submit(listener); + } +} diff --git a/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/SocketProducer.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/SocketProducer.java new file mode 100644 index 0000000..ad12037 --- /dev/null +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/SocketProducer.java @@ -0,0 +1,84 @@ +package pl.zimowski.moo.client.socket; + +import java.io.IOException; +import java.net.Socket; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Given internal configuration, generates a web socket. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class SocketProducer { + + @Inject + private Logger log; + + @Value("${server.host}") + private String host; + + @Value("${server.port}") + private int port; + + private Socket socket; + + private String status; + + + /** + * Generates live web socket based on configured. Attempts to open + * socket every time, therefore once invoked, it is up to the caller + * to ensure that socket is closed before invoking again. + * + * @return socket on success; {@code null} on error + */ + public Socket getSocket() { + + if(socket == null) { + try { + socket = new Socket(host, port); + status = String.format("connected to %s@%d", host, port); + log.debug(status); + } + catch(IOException e) { + status = "SOCKET INIT FAILED: " + e.getMessage(); + log.error("could not connect to server: {}", status); + } + } + + return socket; + } + + /** + * @return host to which socket establishes connection + */ + public String getSocketHost() { + return host; + } + + /** + * @return port on which socket establishes connection + */ + public int getSocketPort() { + return port; + } + + /** + * Status of socket opening operation performed via {@link #getSocket()}. Note + * this is not the current status of a socket as socket may have been closed + * or otherwise corrupted. This status is simply indication if there was error + * when socket was being opened or if opening a socket succeeded. + * + * @return status of the last invocation of {@code #getSocket()} + */ + public String getStatus() { + return status; + } +} diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/configuration/ServerSettings.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/configuration/ServerSettings.java similarity index 94% rename from moo-client/src/main/java/pl/zimowski/moo/client/configuration/ServerSettings.java rename to moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/configuration/ServerSettings.java index 7c323cd..245b3b4 100644 --- a/moo-client/src/main/java/pl/zimowski/moo/client/configuration/ServerSettings.java +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/configuration/ServerSettings.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.client.configuration; +package pl.zimowski.moo.client.socket.configuration; import javax.validation.constraints.NotNull; diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/configuration/package-info.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/configuration/package-info.java similarity index 79% rename from moo-client/src/main/java/pl/zimowski/moo/client/configuration/package-info.java rename to moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/configuration/package-info.java index e1d6b5f..b67fac8 100644 --- a/moo-client/src/main/java/pl/zimowski/moo/client/configuration/package-info.java +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/configuration/package-info.java @@ -5,4 +5,4 @@ * @since 1.0.0 * @author Adam Zimowski (mrazjava) */ -package pl.zimowski.moo.client.configuration; \ No newline at end of file +package pl.zimowski.moo.client.socket.configuration; \ No newline at end of file diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/package-info.java b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/package-info.java similarity index 80% rename from moo-client/src/main/java/pl/zimowski/moo/client/package-info.java rename to moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/package-info.java index 207021d..112e73f 100644 --- a/moo-client/src/main/java/pl/zimowski/moo/client/package-info.java +++ b/moo-client-socket/src/main/java/pl/zimowski/moo/client/socket/package-info.java @@ -4,4 +4,4 @@ * @since 1.0.0 * @author Adam Zimowski (mrazjava) */ -package pl.zimowski.moo.client; \ No newline at end of file +package pl.zimowski.moo.client.socket; \ No newline at end of file diff --git a/moo-client/src/main/resources-filtered/logo b/moo-client-socket/src/main/resources-filtered/logo similarity index 74% rename from moo-client/src/main/resources-filtered/logo rename to moo-client-socket/src/main/resources-filtered/logo index e097334..50002cf 100644 --- a/moo-client/src/main/resources-filtered/logo +++ b/moo-client-socket/src/main/resources-filtered/logo @@ -1,6 +1,6 @@ (___) (o o)_____/ @@ ` \ moo! v:${project.version} - \ ____, / CLIENT + \ ____, / SOCKET CLIENT // // jgs ^^ ^^ \ No newline at end of file diff --git a/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ClientHandlerTest.java b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ClientHandlerTest.java new file mode 100644 index 0000000..a74a6f7 --- /dev/null +++ b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ClientHandlerTest.java @@ -0,0 +1,99 @@ +package pl.zimowski.moo.client.socket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientListener; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link ClientHandler} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ClientHandlerTest extends MooTest { + + @InjectMocks + private ClientHandler handler; + + @Mock + private ConnectionManagement manager; + + @Mock + private ClientListener listener; + + + @Test + public void shouldSendClientEvent() { + + SendClientEventAnswer answer = new SendClientEventAnswer(); + doAnswer(answer).when(manager).send(Mockito.any()); + + assertNull(answer.event); + handler.send(new ClientEvent(ClientAction.Signin)); + assertNotNull(answer.event); + assertEquals(ClientAction.Signin, answer.event.getAction()); + } + + @Test + public void shouldConnect() { + + when(manager.connect(Mockito.any())).thenReturn(true); + assertTrue(handler.connect(listener)); + } + + @Test + public void shouldReportConnectionStatus() { + + when(manager.isConnected()).thenReturn(true); + assertTrue(handler.isConnected()); + } + + @Test + public void shouldDisconnect() { + + DisconnectAnswer answer = new DisconnectAnswer(); + doAnswer(answer).when(manager).disconnect(); + + assertFalse(answer.disconnected); + handler.disconnect(); + assertTrue(answer.disconnected); + } + + class SendClientEventAnswer implements Answer { + + ClientEvent event; + + @Override + public ClientEvent answer(InvocationOnMock invocation) throws Throwable { + event = invocation.getArgument(0); + return null; + } + } + + class DisconnectAnswer implements Answer { + + boolean disconnected; + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + disconnected = true; + return null; + } + } +} \ No newline at end of file diff --git a/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ConnectionManagerTest.java b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ConnectionManagerTest.java new file mode 100644 index 0000000..c8877f3 --- /dev/null +++ b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ConnectionManagerTest.java @@ -0,0 +1,104 @@ +package pl.zimowski.moo.client.socket; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.Socket; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.springframework.test.util.ReflectionTestUtils; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientListener; +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link ConnectionManager} performs as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ConnectionManagerTest extends MooTest { + + @InjectMocks + private ConnectionManager manager; + + @Spy + private MockLogger mockLog; + + @Mock + private SocketProducer producer; + + @Mock + private Socket socket; + + @Mock + private ClientListener clientListener; + + @Mock + private ServerListenerInitializer serverListenerInit; + + + @Test + public void shouldConnect() { + + given(producer.getSocketHost()).willReturn("localhost"); + given(producer.getSocketPort()).willReturn(8001); + given(producer.getSocket()).willReturn(socket); + + assertFalse(manager.isConnected()); + manager.connect(clientListener); + assertTrue(manager.isConnected()); + } + + @Test + public void shouldFailConnectNullSocket() { + + // no need to setup mocks as by default nulls are returned + + assertFalse(manager.isConnected()); + manager.connect(clientListener); + assertFalse(manager.isConnected()); + } + + @Test + public void shouldDisconnect() { + + ReflectionTestUtils.setField(manager, "connected", true); + + assertTrue(manager.isConnected()); + manager.init(); + manager.cleanup(); + assertFalse(manager.isConnected()); + } + + @Test + public void shouldNotSendEventNullSocket() { + + ReflectionTestUtils.setField(manager, "socket", null); + manager.send(new ClientEvent(ClientAction.Signin)); + } + + @Test + public void shouldNotSendEventOnIOException() throws IOException { + + Mockito.doThrow(IOException.class).when(socket).getOutputStream(); + manager.send(new ClientEvent(ClientAction.Signin)); + } + + @Test + public void shouldSendEvent() throws IOException { + + given(socket.getOutputStream()).willReturn(new ByteArrayOutputStream()); + manager.send(new ClientEvent(ClientAction.Signin)); + } +} diff --git a/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ServerListenerInitializerTest.java b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ServerListenerInitializerTest.java new file mode 100644 index 0000000..472871b --- /dev/null +++ b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ServerListenerInitializerTest.java @@ -0,0 +1,23 @@ +package pl.zimowski.moo.client.socket; + +import org.junit.Test; + +/** + * Ensures that {@link ServerListenerInitializer} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ServerListenerInitializerTest { + + /** + * Naive test to ensure that submission indeed takes place. No need to + * submit real listener, simple null will do as it will produce NPE if + * submission took place. + */ + @Test(expected = NullPointerException.class) + public void shouldNotInitialize() { + + new ServerListenerInitializer().initialize(null); + } +} diff --git a/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ServerListenerTest.java b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ServerListenerTest.java new file mode 100644 index 0000000..34af25d --- /dev/null +++ b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/ServerListenerTest.java @@ -0,0 +1,94 @@ +package pl.zimowski.moo.client.socket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.net.UnknownHostException; + +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import pl.zimowski.moo.api.ClientListener; +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link ServerListener} correctly listens for events. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ServerListenerTest extends MooTest { + + @Mock + private Socket socket; + + @Mock + private ClientListener clientListener; + + private ServerEvent testServerEvent = null; + + + @Test + public void shouldRun() throws UnknownHostException, IOException, InterruptedException { + + ServerEvent serverEvent = new ServerEvent(ServerAction.ConnectionEstablished).withMessage("hello"); + + ByteArrayOutputStream b = new ByteArrayOutputStream(); + ObjectOutputStream o = new ObjectOutputStream(b); + o.writeObject(serverEvent); + o.close(); + + //Socket socket = new Socket("localhost", 8000); + Mockito.doReturn(false).when(socket).isInputShutdown(); + Mockito.doReturn(new ByteArrayInputStream(b.toByteArray())).when(socket).getInputStream(); + Mockito.doAnswer(new Answer() { + + @Override + public ServerEvent answer(InvocationOnMock invocation) throws Throwable { + testServerEvent = invocation.getArgument(0); + return null; + } + + }).when(clientListener).onEvent(Mockito.any()); + + ServerListener serverListener = new ServerListener(socket, clientListener); + + assertNull(testServerEvent); + serverListener.setHardExit(false); + serverListener.run(); + assertNotNull(testServerEvent); + assertEquals(ServerAction.ConnectionEstablished, testServerEvent.getAction()); + } + + @Test + public void shouldExitOnIOException() { + + Mockito.doThrow(IOException.class).when(socket).isInputShutdown(); + ServerListener serverListener = new ServerListener(socket, null); + + serverListener.setHardExit(false); + serverListener.run(); + } + + @Test + public void shouldBeListening() { + + Mockito.doReturn(false).when(socket).isClosed(); + ServerListener listener = new ServerListener(socket, null); + listener.setHardExit(true); + + assertTrue(listener.isListening()); + } +} diff --git a/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/SocketProducerTest.java b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/SocketProducerTest.java new file mode 100644 index 0000000..29255da --- /dev/null +++ b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/SocketProducerTest.java @@ -0,0 +1,86 @@ +package pl.zimowski.moo.client.socket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.util.ReflectionTestUtils; + +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link SocketProducer} correctly opens a socket. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class SocketProducerTest extends MooTest { + + private static final Logger log = LoggerFactory.getLogger(SocketProducerTest.class); + + @InjectMocks + private SocketProducer producer; + + @Spy + private MockLogger mockLog; + + + @Test + public void shouldGetSocket() throws IOException { + + String host = "127.0.0.1"; + int port = 8001; + + ServerSocket server = new ServerSocket(8001); + + ReflectionTestUtils.setField(producer, "host", host); + ReflectionTestUtils.setField(producer, "port", port); + + assertNull(producer.getStatus()); + Socket socket = producer.getSocket(); + assertNotNull(socket); + + log.debug("{}", socket.getPort()); + + assertEquals(host, producer.getSocketHost()); + assertEquals(port, producer.getSocketPort()); + assertEquals(host, socket.getInetAddress().getHostAddress()); + assertEquals(port, socket.getPort()); + assertFalse(socket.isClosed()); + assertEquals(String.format("connected to %s@%d", host, port), producer.getStatus()); + + socket.close(); + server.close(); + + assertTrue(socket.isClosed()); + } + + @Test + public void shouldNotGetSocketWithoutServer() throws IOException { + + String host = "localhost"; + int port = 8001; + + ReflectionTestUtils.setField(producer, "host", host); + ReflectionTestUtils.setField(producer, "port", port); + + assertNull(producer.getStatus()); + assertNull(producer.getSocket()); + + log.info(producer.getStatus()); + + assertTrue(producer.getStatus().startsWith("SOCKET INIT FAILED:")); + } +} diff --git a/moo-client/src/test/java/pl/zimowski/moo/client/configuration/ServerSettingsTest.java b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/configuration/ServerSettingsTest.java similarity index 81% rename from moo-client/src/test/java/pl/zimowski/moo/client/configuration/ServerSettingsTest.java rename to moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/configuration/ServerSettingsTest.java index cb2fc6e..ae3bcfa 100644 --- a/moo-client/src/test/java/pl/zimowski/moo/client/configuration/ServerSettingsTest.java +++ b/moo-client-socket/src/test/java/pl/zimowski/moo/client/socket/configuration/ServerSettingsTest.java @@ -1,9 +1,11 @@ -package pl.zimowski.moo.client.configuration; +package pl.zimowski.moo.client.socket.configuration; import static org.junit.Assert.assertEquals; import org.junit.Test; +import pl.zimowski.moo.client.socket.configuration.ServerSettings; + /** * Ensures opration of {@link ServerSettings}. * diff --git a/moo-client/readme.md b/moo-client/readme.md deleted file mode 100644 index 853b866..0000000 --- a/moo-client/readme.md +++ /dev/null @@ -1,4 +0,0 @@ -# Moo - chat client -===================== - -Console (text based) client for Moo chat service. \ No newline at end of file diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/App.java b/moo-client/src/main/java/pl/zimowski/moo/client/App.java deleted file mode 100644 index 76667d8..0000000 --- a/moo-client/src/main/java/pl/zimowski/moo/client/App.java +++ /dev/null @@ -1,105 +0,0 @@ -package pl.zimowski.moo.client; - -import java.util.Scanner; - -import javax.annotation.PreDestroy; -import javax.inject.Inject; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InjectionPoint; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Scope; - -import pl.zimowski.moo.api.ApiUtils; -import pl.zimowski.moo.api.ClientAction; -import pl.zimowski.moo.api.ClientEvent; - -/** - * Text based client UI of a Moo chat service. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -@SpringBootApplication -public class App implements ApplicationRunner { - - public static final Logger LOG_CHAT = LoggerFactory.getLogger("CHAT_ECHO"); - - @Inject - private Logger log; - - @Inject - private NickNameManager nickNameManager; - - @Inject - private ConnectionManagement connMgr; - - - /** - * Application entry point. - * - * @param args such as config overrides as per Spring Boot features - */ - public static final void main(String[] args) { - - SpringApplication.run(App.class, args); - } - - @Override - public void run(ApplicationArguments arg0) throws Exception { - - log.info("\n{}", ApiUtils.fetchResource("/logo")); - - try (Scanner scanner = new Scanner(System.in)) { - - if(!connMgr.connect()) { - return; - } - - // allow connect to buffer console output before printing more - Thread.sleep(300); - - log.info("How do you want to moo? (type nickname or just hit enter, ctrl-c to exit)"); - - nickNameManager.setNickName(scanner.nextLine()); - - connMgr.send(new ClientEvent(ClientAction.Signin) - .withAuthor(nickNameManager.getNickName()) - ); - - while(scanner.hasNextLine()) { - - String input = scanner.nextLine(); - ClientEvent event = new ClientEvent(ClientAction.Message, nickNameManager.getNickName(), input); - log.debug("sending: {}", event); - connMgr.send(event); - } - } - } - - @PreDestroy - public void shutdown() { - String nick = nickNameManager.getNickName(); - if(connMgr.isConnected() && nick != null) { - LOG_CHAT.info("(client) done mooing? bye {}!", nick); - if(nick != null) { - connMgr.send(new ClientEvent(ClientAction.Signoff).withAuthor(nick)); - } - } - if(connMgr.isConnected()) { - connMgr.send(new ClientEvent(ClientAction.Disconnect)); - } - } - - @Bean - @Scope("prototype") - static Logger logger(InjectionPoint injectionPoint){ - return LoggerFactory.getLogger(injectionPoint.getMember().getDeclaringClass()); - - } -} \ No newline at end of file diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/NickNameAssigning.java b/moo-client/src/main/java/pl/zimowski/moo/client/NickNameAssigning.java deleted file mode 100644 index 2530796..0000000 --- a/moo-client/src/main/java/pl/zimowski/moo/client/NickNameAssigning.java +++ /dev/null @@ -1,12 +0,0 @@ -package pl.zimowski.moo.client; - -/** - * Modification of a nick name. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -public interface NickNameAssigning { - - void setNickName(String nickName); -} diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/NickNameManager.java b/moo-client/src/main/java/pl/zimowski/moo/client/NickNameManager.java deleted file mode 100644 index f8b1965..0000000 --- a/moo-client/src/main/java/pl/zimowski/moo/client/NickNameManager.java +++ /dev/null @@ -1,29 +0,0 @@ -package pl.zimowski.moo.client; - -import org.springframework.stereotype.Component; - -/** - * Tracks state of users nixk name. Nick name can be defined by the user but - * is optional, in which case it will be defined (randomly) by the server. - * This manager allows to set nick name from different components depending on - * how it was generated. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -@Component -public class NickNameManager implements NickNameAssigning { - - private String nickName; - - - public String getNickName() { - return nickName; - } - - @Override - public void setNickName(String nickName) { - this.nickName = nickName; - } - -} diff --git a/moo-client/src/main/java/pl/zimowski/moo/client/ServerListener.java b/moo-client/src/main/java/pl/zimowski/moo/client/ServerListener.java deleted file mode 100644 index 1bb1b17..0000000 --- a/moo-client/src/main/java/pl/zimowski/moo/client/ServerListener.java +++ /dev/null @@ -1,72 +0,0 @@ -package pl.zimowski.moo.client; - -import java.io.EOFException; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.net.Socket; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import pl.zimowski.moo.api.ApiUtils; -import pl.zimowski.moo.api.ServerAction; -import pl.zimowski.moo.api.ServerEvent; - -/** - * Listens for incoming events from the server and echoes them back to the - * client. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -public class ServerListener extends Thread { - - private static final Logger log = LoggerFactory.getLogger(ServerListener.class); - - private Socket socket; - - private ConnectionManagement connectionManagement; - - private NickNameAssigning nickNameAssigner; - - - public ServerListener(Socket socket, ConnectionManagement connectionManagement) { - this.socket = socket; - this.connectionManagement = connectionManagement; - } - - public ServerListener withNickNameAssigner(NickNameAssigning assigner) { - this.nickNameAssigner = assigner; - return this; - } - - @Override - public void run() { - - try { - while(!socket.isInputShutdown()) { - ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); - ServerEvent serverEvent = (ServerEvent)in.readObject(); - if(serverEvent.getAction() == ServerAction.ConnectionEstablished) { - serverEvent.setMessage("connected, client id: " + serverEvent.getClientId()); - } - if(serverEvent.getAction() == ServerAction.NickGenerated && nickNameAssigner != null) { - String assignedNickName = serverEvent.getMessage(); - nickNameAssigner.setNickName(assignedNickName); - serverEvent.setMessage(String.format("You will be known as '%s'", assignedNickName)); - - } - App.LOG_CHAT.info("({}) {}", serverEvent.getAuthor(), serverEvent.getMessage()); - } - } - catch(EOFException e) { - App.LOG_CHAT.info("({}) connection terminated by server; bye!", ApiUtils.APP_NAME); - } - catch (IOException | ClassNotFoundException e) { - log.error("unexpected connection error: {}; aborting!", e.getMessage()); - } - - connectionManagement.disconnect(); - System.exit(MAX_PRIORITY); - } -} \ No newline at end of file diff --git a/moo-client/src/test/java/pl/zimowski/moo/client/NickNameManagerTest.java b/moo-client/src/test/java/pl/zimowski/moo/client/NickNameManagerTest.java deleted file mode 100644 index 537555f..0000000 --- a/moo-client/src/test/java/pl/zimowski/moo/client/NickNameManagerTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package pl.zimowski.moo.client; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import org.junit.Test; - -public class NickNameManagerTest { - - @Test - public void shouldManageNickName() { - - NickNameManager mgr = new NickNameManager(); - - assertNull(mgr.getNickName()); - mgr.setNickName("foo bar"); - assertEquals("foo bar", mgr.getNickName()); - } -} diff --git a/moo-commons/pom.xml b/moo-commons/pom.xml new file mode 100644 index 0000000..8340b56 --- /dev/null +++ b/moo-commons/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + pl.zimowski + moo + 1.2.0-SNAPSHOT + + moo-commons + Moo Commons + Common components useful to all moo modules. + + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + pl.zimowski + moo-test-utils + + + \ No newline at end of file diff --git a/moo-commons/readme.md b/moo-commons/readme.md new file mode 100644 index 0000000..cc0e02f --- /dev/null +++ b/moo-commons/readme.md @@ -0,0 +1,3 @@ +# Moo Commons +--------------------- +Components and utilities that are reusable across all `moo` modules. \ No newline at end of file diff --git a/moo-commons/src/main/java/pl/zimowski/moo/commons/ComponentProducer.java b/moo-commons/src/main/java/pl/zimowski/moo/commons/ComponentProducer.java new file mode 100644 index 0000000..41c13a8 --- /dev/null +++ b/moo-commons/src/main/java/pl/zimowski/moo/commons/ComponentProducer.java @@ -0,0 +1,31 @@ +package pl.zimowski.moo.commons; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +/** + * Customized producers of spring managed components. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class ComponentProducer { + + /** + * Produces injectable logger. + * + * @param injectionPoint where log member is defined + * @return log instance bound to injection point + */ + @Bean + @Scope("prototype") + Logger logger(InjectionPoint injectionPoint){ + return LoggerFactory.getLogger(injectionPoint.getMember().getDeclaringClass()); + + } +} diff --git a/moo-commons/src/test/java/pl/zimowski/moo/commons/ComponentProducerTest.java b/moo-commons/src/test/java/pl/zimowski/moo/commons/ComponentProducerTest.java new file mode 100644 index 0000000..622de1a --- /dev/null +++ b/moo-commons/src/test/java/pl/zimowski/moo/commons/ComponentProducerTest.java @@ -0,0 +1,38 @@ +package pl.zimowski.moo.commons; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.lang.reflect.Field; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.slf4j.Logger; +import org.springframework.beans.factory.InjectionPoint; + +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link ComponentProducerTest} produces components as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ComponentProducerTest extends MooTest { + + @InjectMocks + private ComponentProducer producer; + + + @Test + public void shouldProduceLogger() throws NoSuchFieldException, SecurityException { + + Field field = getClass().getDeclaredField("producer"); + + InjectionPoint ip = new InjectionPoint(field); + Logger producedLogger = producer.logger(ip); + + assertNotNull(producedLogger); + assertEquals(ComponentProducerTest.class.getName(), producedLogger.getName()); + } +} diff --git a/moo-reports/pom.xml b/moo-reports/pom.xml new file mode 100644 index 0000000..59eb4c3 --- /dev/null +++ b/moo-reports/pom.xml @@ -0,0 +1,68 @@ + + 4.0.0 + + pl.zimowski + moo + 1.2.0-SNAPSHOT + + moo-reports + 1.0.0-FINAL + pom + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + + + + + + + + pl.zimowski + moo-api + + + pl.zimowski + moo-test-utils + compile + + + pl.zimowski + moo-commons + + + pl.zimowski + moo-client-socket + + + pl.zimowski + moo-server-socket + + + pl.zimowski + moo-ui-shell-commons + + + pl.zimowski + moo-ui-shell-reader + + + pl.zimowski + moo-ui-shell-writer + + + + Reports + Build help artifact which generates useful code/stat reports (eg: jacoco). + \ No newline at end of file diff --git a/moo-reports/readme.md b/moo-reports/readme.md new file mode 100644 index 0000000..58e0ab2 --- /dev/null +++ b/moo-reports/readme.md @@ -0,0 +1,5 @@ +# Moo Reports +--------------------- + +Aggregated Jacoco (code coverage) reports. Generated on `mvn verify`. + diff --git a/moo-server/pom.xml b/moo-server-socket/pom.xml similarity index 80% rename from moo-server/pom.xml rename to moo-server-socket/pom.xml index d343dbc..c9de008 100644 --- a/moo-server/pom.xml +++ b/moo-server-socket/pom.xml @@ -3,10 +3,10 @@ pl.zimowski moo - 1.1.0-FINAL + 1.2.0-SNAPSHOT - moo-server - Moo (server) + moo-server-socket + Moo Server (socket) @@ -22,16 +22,6 @@ - - org.hamcrest - hamcrest-integration - test - - - org.mockito - mockito-core - test - javax.enterprise cdi-api @@ -52,10 +42,19 @@ pl.zimowski moo-api + + pl.zimowski + moo-test-utils + + + pl.zimowski + moo-commons + joda-time joda-time + Moo server based on a socket. Requires compatible socket client. \ No newline at end of file diff --git a/moo-server/readme.md b/moo-server-socket/readme.md similarity index 73% rename from moo-server/readme.md rename to moo-server-socket/readme.md index 1b0f8e0..4e2465e 100644 --- a/moo-server/readme.md +++ b/moo-server-socket/readme.md @@ -1,4 +1,4 @@ # Moo - chat server -===================== +--------------------- Web sockets server for Moo chat service. \ No newline at end of file diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/App.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/App.java similarity index 63% rename from moo-server/src/main/java/pl/zimowski/moo/server/App.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/App.java index ed89953..55298f8 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/App.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/App.java @@ -1,18 +1,15 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import javax.inject.Inject; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InjectionPoint; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ComponentScan; -import pl.zimowski.moo.server.jmx.JmxReportingSupport; +import pl.zimowski.moo.server.socket.jmx.JmxReportingSupport; /** * Sever app. Bootstraps actual server engine, which depending on @@ -22,6 +19,7 @@ * @since 1.0.0 * @author Adam Zimowski (mrazjava) */ +@ComponentScan(basePackages = "pl.zimowski.moo") @SpringBootApplication public class App implements ApplicationRunner { @@ -50,17 +48,4 @@ public void run(ApplicationArguments args) throws Exception { log.info("server terminated"); } - - /** - * Produces injectable logger. - * - * @param injectionPoint where log member is defined - * @return log instance bound to injection point - */ - @Bean - @Scope("prototype") - static Logger logger(InjectionPoint injectionPoint){ - return LoggerFactory.getLogger(injectionPoint.getMember().getDeclaringClass()); - - } } \ No newline at end of file diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/ChatService.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ChatService.java similarity index 95% rename from moo-server/src/main/java/pl/zimowski/moo/server/ChatService.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ChatService.java index c661157..5bf47a0 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/ChatService.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ChatService.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; /** * High level operations that can be invoked on a chat service. diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/ClientNotification.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ClientNotification.java similarity index 96% rename from moo-server/src/main/java/pl/zimowski/moo/server/ClientNotification.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ClientNotification.java index 2816356..b305613 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/ClientNotification.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ClientNotification.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import pl.zimowski.moo.api.ServerEvent; diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/ClientThread.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ClientThread.java similarity index 85% rename from moo-server/src/main/java/pl/zimowski/moo/server/ClientThread.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ClientThread.java index c734e04..9ba1b52 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/ClientThread.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ClientThread.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import java.io.IOException; import java.io.ObjectInputStream; @@ -45,7 +45,7 @@ public class ClientThread extends Thread implements ClientNotification { /** - * Constructs an live link between client and a server. The link is + * Constructs a live link between client and a server. The link is * established over a socket and server notifier. This is all that a * running thread needs to exchange information between client and a * server. @@ -77,8 +77,7 @@ public void run() { log.debug("in: {}", msg); serverNotifier.broadcast(this, msg); if(ClientAction.Disconnect == msg.getAction()) { - log.info("closing connection: {}", socket); - socket.close(); + disconnect(); break; } } @@ -110,6 +109,9 @@ public boolean isConnected() { * is possible and essentially this thread is dead. */ public void disconnect() { + + log.info("closing connection: {}", socket); + try { socket.close(); } @@ -139,6 +141,17 @@ public boolean notify(ServerEvent event) { } @Override + public boolean equals(Object otherThread) { + + if(otherThread == null || !(otherThread instanceof ClientThread)) + return false; + + ClientThread that = (ClientThread)otherThread; + + return getClientId().equals(that.getClientId()); + } + + @Override public int hashCode() { int result = 23; @@ -147,8 +160,9 @@ public int hashCode() { return result; } - @Override - public String toString() { - return "ClientThread [socket=" + socket + "]"; - } + @Override + public String toString() { + return "ClientThread [clientId=" + clientId + ", socket=" + socket + ", serverNotifier=" + serverNotifier + + ", lastActivity=" + lastActivity + "]"; + } } \ No newline at end of file diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/EventBroadcasting.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/EventBroadcasting.java similarity index 94% rename from moo-server/src/main/java/pl/zimowski/moo/server/EventBroadcasting.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/EventBroadcasting.java index ef7d1f5..141d40d 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/EventBroadcasting.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/EventBroadcasting.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import pl.zimowski.moo.api.ClientEvent; diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/NickNameElements.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/NickNameElements.java similarity index 90% rename from moo-server/src/main/java/pl/zimowski/moo/server/NickNameElements.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/NickNameElements.java index ff385b1..22a6781 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/NickNameElements.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/NickNameElements.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; /** * Operations needed to assemble random nickname. diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/PropertyNickNameElementProvider.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/PropertyNickNameElementProvider.java similarity index 94% rename from moo-server/src/main/java/pl/zimowski/moo/server/PropertyNickNameElementProvider.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/PropertyNickNameElementProvider.java index 0f2f6ce..1fb9094 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/PropertyNickNameElementProvider.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/PropertyNickNameElementProvider.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ServerSocketFactory.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ServerSocketFactory.java new file mode 100644 index 0000000..16f9820 --- /dev/null +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ServerSocketFactory.java @@ -0,0 +1,18 @@ +package pl.zimowski.moo.server.socket; + +import java.io.IOException; +import java.net.ServerSocket; + +import org.springframework.stereotype.Component; + +/** + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class ServerSocketFactory { + + ServerSocket getServerSocket(int port) throws IOException { + return new ServerSocket(port); + } +} diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/ServerUtils.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ServerUtils.java similarity index 96% rename from moo-server/src/main/java/pl/zimowski/moo/server/ServerUtils.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ServerUtils.java index 40a7868..caeedc1 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/ServerUtils.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/ServerUtils.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import java.util.Random; diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/WebSocketChatService.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/WebSocketChatService.java similarity index 78% rename from moo-server/src/main/java/pl/zimowski/moo/server/WebSocketChatService.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/WebSocketChatService.java index fd3fa0c..e6b4970 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/WebSocketChatService.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/WebSocketChatService.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import java.io.IOException; import java.net.ServerSocket; @@ -24,7 +24,7 @@ import pl.zimowski.moo.api.ClientEvent; import pl.zimowski.moo.api.ServerAction; import pl.zimowski.moo.api.ServerEvent; -import pl.zimowski.moo.server.jmx.JmxReportingSupport; +import pl.zimowski.moo.server.socket.jmx.JmxReportingSupport; /** * Core implementation of chat server based on web sockets. Supports multiple @@ -38,8 +38,13 @@ @Component public class WebSocketChatService implements ChatService, EventBroadcasting { + private static final String ANONYMOUS = "AnonymousCoward"; + @Inject private Logger log; + + @Inject + private ServerSocketFactory serverSocketFactory; @Value("${port}") private int port; @@ -79,7 +84,7 @@ public void start() { log.info("\n{}", ApiUtils.fetchResource("/logo")); log.info("listening on port {} (ctrl-c to exit)", port); - try (ServerSocket server = new ServerSocket(port)) { + try (ServerSocket server = serverSocketFactory.getServerSocket(port)) { listen(server); } catch (IOException e) { @@ -113,6 +118,9 @@ private void listen(ServerSocket serverSocket) { client.notify(new ServerEvent(ServerAction.ConnectionEstablished) .withClientId(client.getClientId()) ); + client.notify(new ServerEvent(ServerAction.ParticipantCount) + .withMessage(String.format("%d participant(s)", participantCount)) + ); } catch(IOException e) { log.error("unexpected problem; aborting!", e); @@ -127,7 +135,6 @@ public int getConnectedClientCount() { return connectedClients.size(); } - @SuppressWarnings("unlikely-arg-type") @Override public int broadcast(ClientThread clientThread, ClientEvent clientEvent) { @@ -135,15 +142,30 @@ public int broadcast(ClientThread clientThread, ClientEvent clientEvent) { log.debug("broadcasting {} from {}", clientEvent, clientThread); - verifySignin(clientThread, clientEvent); - - ServerEvent serverEvent = clientEventToServerEvent(clientThread, clientEvent); - int notifiedClients = 0; - if(clientEvent.getAction() == ClientAction.Disconnect) { - connectedClients.remove(clientEvent); - return notifiedClients; + connectedClients.remove(clientThread); } + + ServerEvent serverEvent = clientEventToServerEvent(clientThread, clientEvent); + + if(clientEvent.getAction() == ClientAction.GenerateNick) { + clientThread.notify(serverEvent); + return 1; // only nick requestor gets notified with generated nick + } + if(clientEvent.getAction() == ClientAction.Signin) { + clientThread.notify( + new ServerEvent(ServerAction.SigninConfirmed) + .withAuthor(clientEvent.getAuthor()) + ); + } + clientThread.notify(serverEvent); + + return broadcast(clientThread, serverEvent); + } + + private int broadcast(ClientNotification source, ServerEvent event) { + + int notifiedClients = 0; // could be perf bottleneck - should re-think for optimization; this // needs to be thread safe as otherwise iterator would get screwed up @@ -152,9 +174,12 @@ public int broadcast(ClientThread clientThread, ClientEvent clientEvent) { // standard message broadcast for(ClientNotification connectedClient : connectedClients) { + if(source != null && StringUtils.equals(source.getClientId(), connectedClient.getClientId())) + continue; + log.debug("broadcasting to: {}", connectedClient); - if(connectedClient.notify(serverEvent)) { + if(connectedClient.notify(event)) { notifiedClients++; } } @@ -165,23 +190,6 @@ public int broadcast(ClientThread clientThread, ClientEvent clientEvent) { return notifiedClients; } - /** - * Ensures that user is always signed in with a valid nick name. Client - * may opt to produce signin event without a user nick name in which case - * server must ensure that one is provided (randomly). - * - * @param clientThread associated with the client attached to the user - * @param clientEvent to verify (and modify if necessary) - */ - private void verifySignin(ClientThread clientThread, ClientEvent clientEvent) { - - if(clientEvent.getAction() == ClientAction.Signin && StringUtils.isBlank(clientEvent.getAuthor())) { - String nick = serverUtils.randomNickName(); - clientThread.notify(new ServerEvent(ServerAction.NickGenerated).withMessage(nick)); - clientEvent.setAuthor(nick); - } - } - private synchronized void evictInactiveClients() { for(Iterator iterator = connectedClients.iterator(); iterator.hasNext();) { @@ -190,7 +198,7 @@ private synchronized void evictInactiveClients() { DateTime lastActive = new DateTime(connectedClient.getLastActivity()); int inactiveSeconds = Seconds.secondsBetween(lastActive, new DateTime()).getSeconds(); - if(evictionTimeout != null && inactiveSeconds > evictionTimeout) { + if(evictionTimeout != null && inactiveSeconds >= evictionTimeout) { log.info("{} inactive for {} seconds, evicting!", connectedClient, inactiveSeconds); connectedClient.notify(new ServerEvent(ServerAction.ConnectionTimeOut).withMessage("disconnected due to inactivity")); connectedClient.disconnect(); @@ -217,13 +225,14 @@ private ServerEvent clientEventToServerEvent(ClientThread clientThread, ClientEv ServerAction serverAction = null; String serverMessage = null; String author = null; + String note = null; switch(clientAction) { case Signin: serverAction = ServerAction.ParticipantCount; participantCount++; - author = ApiUtils.APP_NAME; + author = ServerEvent.AUTHOR; StringBuilder msg = new StringBuilder(String.format("%s joined;", clientEvent.getAuthor())); if(participantCount > 1) msg.append(String.format(" %d participants", participantCount)); @@ -235,18 +244,25 @@ private ServerEvent clientEventToServerEvent(ClientThread clientThread, ClientEv case Signoff: serverAction = ServerAction.ParticipantCount; participantCount--; - author = ApiUtils.APP_NAME; + author = ServerEvent.AUTHOR; serverMessage = String.format("%s left; %d participant(s) remaining", clientEvent.getAuthor(), participantCount); break; case Message: serverAction = ServerAction.Message; author = clientEvent.getAuthor(); + if(StringUtils.isEmpty(author)) author = ANONYMOUS; serverMessage = String.format("%s", clientEvent.getMessage()); break; + case GenerateNick: + serverAction = ServerAction.NickGenerated; + author = ServerEvent.AUTHOR; + serverMessage = serverUtils.randomNickName(); + break; case Disconnect: - synchronized(this) { - connectedClients.remove(clientThread); - } + serverAction = ServerAction.ClientDisconnected; + author = ServerEvent.AUTHOR; + serverMessage = String.format("client disconnect: %s", clientThread.getClientId()); + note = clientThread.getClientId(); break; } @@ -255,7 +271,8 @@ private ServerEvent clientEventToServerEvent(ClientThread clientThread, ClientEv return new ServerEvent(serverAction, serverMessage) .withParticipantCount(participantCount) .withAuthor(author) - .withClientId(clientEvent.getId()); + .withClientId(clientEvent.getId()) + .withNote(note); } @Override @@ -279,6 +296,7 @@ public void setPort(int port) { @PreDestroy public void shutdown() { + broadcast(null, new ServerEvent(ServerAction.ServerExit).withMessage("deliberate server shutdown")); log.debug("engine shutdown ({} clients, {} participants)", getConnectedClientCount(), participantCount); } } \ No newline at end of file diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/configuration/Settings.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/configuration/Settings.java similarity index 83% rename from moo-server/src/main/java/pl/zimowski/moo/server/configuration/Settings.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/configuration/Settings.java index f9d98ec..47aa316 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/configuration/Settings.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/configuration/Settings.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server.configuration; +package pl.zimowski.moo.server.socket.configuration; import javax.validation.constraints.NotNull; @@ -7,8 +7,8 @@ import org.springframework.validation.annotation.Validated; /** - * Spring managed generic server config. Getters intentionally omitted to - * encourage direct value injection. + * Spring managed generic server config. Getters intentionally limited in + * scope to encourage direct value injection. * * @since 1.0.0 * @author Adam Zimowski (mrazjava) @@ -26,7 +26,6 @@ public class Settings { * server will terminate client connection. If not defined, server never * terminates client connection. */ - @SuppressWarnings("unused") private Integer evictionTimeout; @NotNull(message = "At least 1 adjective is required; eg: 'fantastic'") @@ -40,9 +39,17 @@ public void setPort(int port) { this.port = port; } + Integer getPort() { + return port; + } + public void setEvictionTimeout(Integer evictionTimeout) { this.evictionTimeout = evictionTimeout; } + + Integer getEvictionTimeout() { + return evictionTimeout; + } /** * Set of adjectives available to build anonymous nick name. Since nick @@ -56,6 +63,10 @@ public void setEvictionTimeout(Integer evictionTimeout) { public void setNickAdjectives(String[] nickAdjectives) { this.nickAdjectives = nickAdjectives; } + + String[] getNickAdjectives() { + return nickAdjectives; + } /** * Set of nouns available to build anonymous nick name. Since nick @@ -69,4 +80,8 @@ public void setNickAdjectives(String[] nickAdjectives) { public void setNickNouns(String[] nickNouns) { this.nickNouns = nickNouns; } + + String[] getNickNouns() { + return nickNouns; + } } \ No newline at end of file diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/configuration/package-info.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/configuration/package-info.java similarity index 59% rename from moo-server/src/main/java/pl/zimowski/moo/server/configuration/package-info.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/configuration/package-info.java index 52dc0ed..3a2db02 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/configuration/package-info.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/configuration/package-info.java @@ -1,9 +1,9 @@ /** * Spring managed configuration beans. Each component operates on a - * specific config context. {@link pl.zimowski.moo.server.configuration.Settings} + * specific config context. {@link pl.zimowski.moo.server.socket.configuration.Settings} * operates on a global context. * * @since 1.0.0 * @author Adam Zimowski (mrazjava) */ -package pl.zimowski.moo.server.configuration; \ No newline at end of file +package pl.zimowski.moo.server.socket.configuration; \ No newline at end of file diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/ClientAnalytics.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/ClientAnalytics.java similarity index 92% rename from moo-server/src/main/java/pl/zimowski/moo/server/jmx/ClientAnalytics.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/ClientAnalytics.java index 70c0c02..83e3213 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/ClientAnalytics.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/ClientAnalytics.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server.jmx; +package pl.zimowski.moo.server.socket.jmx; /** * Reports interesting stats about client connections to JMX console. diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/ClientAnalyticsMBean.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/ClientAnalyticsMBean.java similarity index 91% rename from moo-server/src/main/java/pl/zimowski/moo/server/jmx/ClientAnalyticsMBean.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/ClientAnalyticsMBean.java index ab3a1ac..b0dfaf8 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/ClientAnalyticsMBean.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/ClientAnalyticsMBean.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server.jmx; +package pl.zimowski.moo.server.socket.jmx; /** * Client stats reportable over JMX. diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/JmxReporter.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/JmxReporter.java similarity index 95% rename from moo-server/src/main/java/pl/zimowski/moo/server/jmx/JmxReporter.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/JmxReporter.java index c4bb8e8..482f11f 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/JmxReporter.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/JmxReporter.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server.jmx; +package pl.zimowski.moo.server.socket.jmx; import javax.inject.Inject; diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/JmxReportingSupport.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/JmxReportingSupport.java similarity index 92% rename from moo-server/src/main/java/pl/zimowski/moo/server/jmx/JmxReportingSupport.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/JmxReportingSupport.java index 84e410f..4a2ae62 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/JmxReportingSupport.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/JmxReportingSupport.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server.jmx; +package pl.zimowski.moo.server.socket.jmx; /** * Maintains stats needed for JMX reporting. diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/MBeanAccess.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/MBeanAccess.java similarity index 98% rename from moo-server/src/main/java/pl/zimowski/moo/server/jmx/MBeanAccess.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/MBeanAccess.java index 5fae2f5..8e3bc67 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/MBeanAccess.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/MBeanAccess.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server.jmx; +package pl.zimowski.moo.server.socket.jmx; import static java.util.Objects.requireNonNull; diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/package-info.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/package-info.java similarity index 77% rename from moo-server/src/main/java/pl/zimowski/moo/server/jmx/package-info.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/package-info.java index 3138e69..9af9f57 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/jmx/package-info.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/jmx/package-info.java @@ -4,4 +4,4 @@ * @since 1.0.0 * @author Adam Zimowski (mrazjava) */ -package pl.zimowski.moo.server.jmx; \ No newline at end of file +package pl.zimowski.moo.server.socket.jmx; \ No newline at end of file diff --git a/moo-server/src/main/java/pl/zimowski/moo/server/package-info.java b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/package-info.java similarity index 79% rename from moo-server/src/main/java/pl/zimowski/moo/server/package-info.java rename to moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/package-info.java index c45276d..25a1622 100644 --- a/moo-server/src/main/java/pl/zimowski/moo/server/package-info.java +++ b/moo-server-socket/src/main/java/pl/zimowski/moo/server/socket/package-info.java @@ -4,4 +4,4 @@ * @since 1.0.0 * @author Adam Zimowski (mrazjava) */ -package pl.zimowski.moo.server; \ No newline at end of file +package pl.zimowski.moo.server.socket; \ No newline at end of file diff --git a/moo-server/src/main/resources-filtered/logo b/moo-server-socket/src/main/resources-filtered/logo similarity index 74% rename from moo-server/src/main/resources-filtered/logo rename to moo-server-socket/src/main/resources-filtered/logo index ca040dd..2747e9d 100644 --- a/moo-server/src/main/resources-filtered/logo +++ b/moo-server-socket/src/main/resources-filtered/logo @@ -1,5 +1,5 @@ ()___() < @ @ > | | moo ... v:${project.version} - {o_o} SERVER + {o_o} SOCKET SERVER (|) \ No newline at end of file diff --git a/moo-server/src/main/resources/application.properties b/moo-server-socket/src/main/resources/application.properties similarity index 100% rename from moo-server/src/main/resources/application.properties rename to moo-server-socket/src/main/resources/application.properties diff --git a/moo-server/src/main/resources/logback.xml b/moo-server-socket/src/main/resources/logback.xml similarity index 92% rename from moo-server/src/main/resources/logback.xml rename to moo-server-socket/src/main/resources/logback.xml index 80aaad0..76c7413 100644 --- a/moo-server/src/main/resources/logback.xml +++ b/moo-server-socket/src/main/resources/logback.xml @@ -17,8 +17,9 @@ + - + diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/AppTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/AppTest.java new file mode 100644 index 0000000..006a4f8 --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/AppTest.java @@ -0,0 +1,62 @@ +package pl.zimowski.moo.server.socket; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import pl.zimowski.moo.server.socket.jmx.JmxReportingSupport; +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that server {@link App} correctly runs application + * entry point (sequence). + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class AppTest extends MooTest { + + @InjectMocks + private App server; + + @Spy + private MockLogger mockLog; + + @Mock + private ChatService chatService; + + @Mock + private JmxReportingSupport jmx; + + boolean jmxInitialized = false; + + boolean chatServiceStarted = false; + + + @Test + public void shouldRun() throws Exception { + + Mockito.doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + jmxInitialized = true; + return null; + } + }).when(jmx).initialize(); + + Mockito.doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + chatServiceStarted = true; + return null; + } + }).when(chatService).start(); + + server.run(null); + } +} diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ClientThreadTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ClientThreadTest.java new file mode 100644 index 0000000..201a1e6 --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ClientThreadTest.java @@ -0,0 +1,204 @@ +package pl.zimowski.moo.server.socket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.BDDMockito.given; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.util.UUID; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.util.ReflectionTestUtils; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures operation of {@link ClientThread} is as expected. + * + * @since 1.0.0 + * @author Adam Zimowski (mrazjava) + */ +public class ClientThreadTest extends MooTest { + + private static final Logger log = LoggerFactory.getLogger(ClientThreadTest.class); + + @InjectMocks + private ClientThread clientThread; + + @Mock + private Socket socket; + + @Spy + private ServerNotificationMock clientThreadObserver; + + + @Test + public void shouldRunClientThreadAndExit() throws IOException { + + Object event = new ClientEvent(ClientAction.Signoff, "zorro", null); + InputStream is = eventAsInputStream(event); + + given(socket.getInputStream()).willReturn(is); + + assertNotNull(UUID.fromString(clientThread.getClientId())); + assertTrue(clientThread.getLastActivity() > 0L); + + clientThread.run(); + + assertNotNull(clientThreadObserver.getClientThread()); + assertNotNull(clientThreadObserver.getClientEvent()); + + // intentionally comparing references + assertTrue(clientThreadObserver.getClientThread() == clientThread); + } + + @Test + public void shouldRunClientThreadAndDisconnect() throws IOException { + + Object event = new ClientEvent(ClientAction.Disconnect); + InputStream is = eventAsInputStream(event); + + given(socket.getInputStream()).willReturn(is); + + SocketClosedAnswer answer = new SocketClosedAnswer(new ClassNotFoundException()); + Mockito.doAnswer(answer).when(socket).close(); + + assertFalse(answer.closed); + clientThread.run(); + assertTrue(answer.closed); + } + + @Test + public void shouldDisconnect() throws IOException { + + SocketClosedAnswer answer = new SocketClosedAnswer(); + Mockito.doAnswer(answer).when(socket).close(); + + assertFalse(answer.closed); + clientThread.disconnect(); + assertTrue(answer.closed); + } + + @Test + public void shouldHandleDisconnectIOException() throws IOException { + + Mockito.doThrow(IOException.class).when(socket).close(); + clientThread.disconnect(); + } + + @Test + public void shouldProcessServerNotification() throws IOException, ClassNotFoundException { + + given(socket.getOutputStream()).willReturn(new ByteArrayOutputStream()); + assertTrue(clientThread.notify(new ServerEvent(ServerAction.Message, "hello there"))); + } + + @Test + public void shouldHandleIOExceptionOnNotify() throws IOException { + + given(socket.getOutputStream()).willThrow(IOException.class); + assertFalse(clientThread.notify(new ServerEvent(ServerAction.ConnectionEstablished))); + } + + @Test + public void shouldReportConnectionStatus() { + + given(socket.isClosed()).willReturn(false); + assertTrue(clientThread.isConnected()); + + given(socket.isClosed()).willReturn(true); + assertFalse(clientThread.isConnected()); + } + + @Test + public void shouldHaveFriendlyToString() { + + String logged = clientThread.toString(); + log.debug(logged); + + assertTrue(logged.startsWith(ClientThread.class.getSimpleName() + " [")); + } + + @Test + public void shouldNotEqual() { + + ClientThread thread1 = new ClientThread(null, null); + ClientThread thread2 = new ClientThread(null, null); + + assertNotEquals(thread1, thread2); + } + + @Test + public void shouldEqual() { + + ClientThread thread1 = new ClientThread(null, null); + ClientThread thread2 = new ClientThread(null, null); + + ReflectionTestUtils.setField(thread2, "clientId", thread1.getClientId()); + + assertEquals(thread1, thread1); + assertEquals(thread2, thread2); + assertEquals(thread1, thread2); + } + + private InputStream eventAsInputStream(Object event) throws IOException { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + + oos.writeObject(event); + + return new ByteArrayInputStream(baos.toByteArray()); + } + + /** + * Mock answer to process when closing a mocked socket. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ + class SocketClosedAnswer implements Answer { + + boolean closed; + + Throwable throwable; // optionally to throw during close operation + + + public SocketClosedAnswer() { + } + + public SocketClosedAnswer(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + + closed = true; + if(throwable != null) throw throwable; + + return null; + } + + } +} \ No newline at end of file diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/PropertyNickNameElementProviderTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/PropertyNickNameElementProviderTest.java new file mode 100644 index 0000000..70462d8 --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/PropertyNickNameElementProviderTest.java @@ -0,0 +1,34 @@ +package pl.zimowski.moo.server.socket; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Ensures that {@link PropertyNickNameElementProvider} correctly + * provides nick name elements. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class PropertyNickNameElementProviderTest { + + @Test + public void shouldGetAdjectives() { + + PropertyNickNameElementProvider provider = new PropertyNickNameElementProvider(); + ReflectionTestUtils.setField(provider, "nickAdjectives", new String[] { "adj1", "adj2" }); + + assertArrayEquals(new String[] { "adj1", "adj2" }, provider.getAdjectives()); + } + + @Test + public void shouldGetNouns() { + + PropertyNickNameElementProvider provider = new PropertyNickNameElementProvider(); + ReflectionTestUtils.setField(provider, "nickNouns", new String[] { "noun1", "noun2" }); + + assertArrayEquals(new String[] { "noun1", "noun2" }, provider.getNouns()); + } +} diff --git a/moo-server/src/test/java/pl/zimowski/moo/server/ServerNotificationMock.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerNotificationMock.java similarity index 87% rename from moo-server/src/test/java/pl/zimowski/moo/server/ServerNotificationMock.java rename to moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerNotificationMock.java index 2efea42..4aada6b 100644 --- a/moo-server/src/test/java/pl/zimowski/moo/server/ServerNotificationMock.java +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerNotificationMock.java @@ -1,6 +1,8 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.server.socket.ClientThread; +import pl.zimowski.moo.server.socket.EventBroadcasting; /** * Useful mock which simulates server notifier and allows to test validity of diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerSocketFactoryTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerSocketFactoryTest.java new file mode 100644 index 0000000..0b983c2 --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerSocketFactoryTest.java @@ -0,0 +1,36 @@ +package pl.zimowski.moo.server.socket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static pl.zimowski.moo.server.socket.WebSocketChatServiceTest.TEST_PORT; + +import java.io.IOException; +import java.net.ServerSocket; + +import org.junit.Test; + +/** + * Ensures that {@link ServerSocketFactory} correctly produces {@link ServerSocket}. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ServerSocketFactoryTest { + + @Test + public void shouldCreateServerSocket() throws IOException { + + ServerSocket serverSocket = new ServerSocketFactory().getServerSocket(TEST_PORT); + + assertNotNull(serverSocket); + assertEquals(TEST_PORT, serverSocket.getLocalPort()); + assertFalse(serverSocket.isClosed()); + + serverSocket.close(); + + assertTrue(serverSocket.isClosed()); + + } +} diff --git a/moo-server/src/test/java/pl/zimowski/moo/server/ServerUtilsTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerUtilsTest.java similarity index 87% rename from moo-server/src/test/java/pl/zimowski/moo/server/ServerUtilsTest.java rename to moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerUtilsTest.java index 9e11106..fc7f769 100644 --- a/moo-server/src/test/java/pl/zimowski/moo/server/ServerUtilsTest.java +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/ServerUtilsTest.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.server; +package pl.zimowski.moo.server.socket; import static org.junit.Assert.assertEquals; import static org.mockito.BDDMockito.given; @@ -10,6 +10,9 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import pl.zimowski.moo.server.socket.NickNameElements; +import pl.zimowski.moo.server.socket.ServerUtils; + /** * Ensures that {@link ClientUtils} performs as expected. * diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/WebSocketChatServiceTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/WebSocketChatServiceTest.java new file mode 100644 index 0000000..b0d073a --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/WebSocketChatServiceTest.java @@ -0,0 +1,165 @@ +package pl.zimowski.moo.server.socket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.BDDMockito; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.test.util.ReflectionTestUtils; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; +import pl.zimowski.moo.server.socket.jmx.JmxReporter; +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link WebSocketChatService} operates as expected. + * + * @since 1.0.0 + * @author Adam Zimowski (mrazjava) + */ +public class WebSocketChatServiceTest extends MooTest { + + static final int TEST_PORT = 8001; + + @InjectMocks + private WebSocketChatService chatService; + + @Spy + private MockLogger mockLog; + + @Mock + private ClientThread clientThread; + + @Mock + private ServerUtils serverUtils; + + @Mock + private ServerSocket serverSocket; + + @Mock + private ServerSocketFactory serverSocketFactory; + + @Mock + private Socket socket; + + @Mock + private JmxReporter jmxReporter; + + @Mock + private ClientNotification clientNotification; + + @Mock + private ByteArrayOutputStream byteArrayOutputStream; + + private SocketAcceptAnswer socketAcceptAnswer; + + + @Before + public void setupServer() throws IOException { + + Mockito.when(serverSocket.accept()).thenAnswer(socketAcceptAnswer = new SocketAcceptAnswer()); + BDDMockito.given(serverSocketFactory.getServerSocket(TEST_PORT)).willReturn(serverSocket); + BDDMockito.given(socket.getOutputStream()).willReturn(byteArrayOutputStream); + + chatService.setPort(TEST_PORT); + } + + @Test + public void shouldAcceptClientAndBroadcast() { + + + assertEquals(0, chatService.getConnectedClientCount()); + + chatService.start(); + + assertEquals(1, chatService.getConnectedClientCount()); + assertEquals(1, chatService.broadcast(clientThread, new ClientEvent(ClientAction.Signin))); + assertEquals(1, chatService.broadcast(clientThread, new ClientEvent(ClientAction.Message))); + assertEquals(1, chatService.broadcast(clientThread, new ClientEvent(ClientAction.GenerateNick))); + assertEquals(1, chatService.broadcast(clientThread, new ClientEvent(ClientAction.Signoff))); + assertEquals(1, chatService.broadcast(clientThread, new ClientEvent(ClientAction.Disconnect))); + + chatService.stop(); + + assertFalse(chatService.isRunning()); + + ArgumentCaptor serverEventsCaptor = ArgumentCaptor.forClass(ServerEvent.class); + Mockito.verify(clientThread, Mockito.times(6)).notify(serverEventsCaptor.capture()); + + List serverEvents = serverEventsCaptor.getAllValues(); + + assertEquals(6, serverEvents.size()); + + assertEquals(ServerAction.SigninConfirmed, serverEvents.get(0).getAction()); + assertEquals(ServerAction.ParticipantCount, serverEvents.get(1).getAction()); + assertEquals(ServerAction.Message, serverEvents.get(2).getAction()); + assertEquals(ServerAction.NickGenerated, serverEvents.get(3).getAction()); + assertEquals(ServerAction.ParticipantCount, serverEvents.get(4).getAction()); + assertEquals(ServerAction.ClientDisconnected, serverEvents.get(5).getAction()); + + chatService.shutdown(); + } + + @Test + public void shouldEvictInactiveClient() { + + ReflectionTestUtils.setField(chatService, "evictionTimeout", 0); + chatService.start(); + + assertEquals(1, chatService.getConnectedClientCount()); + assertEquals(0, chatService.broadcast(clientThread, new ClientEvent(ClientAction.Message))); + assertEquals(0, chatService.getConnectedClientCount()); + } + + @Test + public void shouldStartAndHandleIOException() throws IOException { + + Mockito.ignoreStubs(serverSocket.accept(), socket.getOutputStream()); + Mockito.when(serverSocketFactory.getServerSocket(TEST_PORT)).thenThrow(IOException.class); + chatService.start(); + } + + @Test + public void shouldListenAndHandleIOException() throws IOException { + + socketAcceptAnswer.throwable = new IOException(); + + chatService.start(); + chatService.stop(); + } + + + /** + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ + class SocketAcceptAnswer implements Answer { + + Throwable throwable; + + @Override + public Socket answer(InvocationOnMock invocation) throws Throwable { + ReflectionTestUtils.setField(chatService, "running", false); + if(throwable != null) throw throwable; + return socket; + } + } +} \ No newline at end of file diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/configuration/SettingsTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/configuration/SettingsTest.java new file mode 100644 index 0000000..ed1086a --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/configuration/SettingsTest.java @@ -0,0 +1,37 @@ +package pl.zimowski.moo.server.socket.configuration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Ensures that {@link Settings} reliably manages configuration. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class SettingsTest { + + @Test + public void shouldRetainValues() { + + Settings settings = new Settings(); + + assertNull(settings.getPort()); + assertNull(settings.getEvictionTimeout()); + assertNull(settings.getNickAdjectives()); + assertNull(settings.getNickNouns()); + + settings.setPort(1234); + settings.setEvictionTimeout(99999); + settings.setNickAdjectives(new String[] { "Invisible", "Happy" }); + settings.setNickNouns(new String[] { "Baboon", "Fish" }); + + assertEquals(Integer.valueOf(1234), settings.getPort()); + assertEquals(Integer.valueOf(99999), settings.getEvictionTimeout()); + assertTrue(2 == settings.getNickAdjectives().length); + assertTrue(2 == settings.getNickNouns().length); + } +} diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/jmx/ClientAnalyticsTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/jmx/ClientAnalyticsTest.java new file mode 100644 index 0000000..2104c08 --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/jmx/ClientAnalyticsTest.java @@ -0,0 +1,26 @@ +package pl.zimowski.moo.server.socket.jmx; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Ensures that {@link ClientAnalytics} MBean correctly reports data. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ClientAnalyticsTest { + + @Test + public void shouldReportCounts() { + + ClientAnalytics analytics = new ClientAnalytics(); + + analytics.connectedClientCount = 22; + analytics.disconnectedClientCount = 7; + + assertEquals(22, analytics.getConnectedClientCount()); + assertEquals(7, analytics.getDisconnectedClientCount()); + } +} diff --git a/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/jmx/JmxReporterTest.java b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/jmx/JmxReporterTest.java new file mode 100644 index 0000000..60aec60 --- /dev/null +++ b/moo-server-socket/src/test/java/pl/zimowski/moo/server/socket/jmx/JmxReporterTest.java @@ -0,0 +1,66 @@ +package pl.zimowski.moo.server.socket.jmx; + +import static org.junit.Assert.assertEquals; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link JmxReporter} correctly initializes itself and + * that it correctly reports stats. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class JmxReporterTest extends MooTest { + + private static final Logger log = LoggerFactory.getLogger(JmxReporterTest.class); + + @InjectMocks + private JmxReporter reporter; + + @Spy + private MockLogger mockLog; + + @Spy + private ClientAnalytics analytics; + + + @Test + public void shouldInitialize() { + + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + int beforeInitCount = mbs.getMBeanCount(); + log.debug("default mbean count: {}", mbs.getMBeanCount()); + + reporter.initialize(); + + assertEquals(Integer.valueOf(beforeInitCount+1), mbs.getMBeanCount()); + } + + @Test + public void shouldTrackConnectedClient() { + + assertEquals(0, analytics.getConnectedClientCount()); + reporter.clientConnected(); + assertEquals(1, analytics.getConnectedClientCount()); + } + + @Test + public void shouldTrackDisconnectedClient() { + + assertEquals(0, analytics.getDisconnectedClientCount()); + reporter.clientDisconnected(); + assertEquals(1, analytics.getDisconnectedClientCount()); + } +} diff --git a/moo-server/src/test/java/pl/zimowski/moo/server/ChatEngineTest.java b/moo-server/src/test/java/pl/zimowski/moo/server/ChatEngineTest.java deleted file mode 100644 index f87715b..0000000 --- a/moo-server/src/test/java/pl/zimowski/moo/server/ChatEngineTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package pl.zimowski.moo.server; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.IOException; -import java.net.Socket; -import java.net.UnknownHostException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.junit.Rule; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import pl.zimowski.moo.api.MockLogger; - -/** - * Ensures that {@link WebSocketChatService} operates as expected. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -public class ChatEngineTest { - - private static final Logger log = LoggerFactory.getLogger(ChatEngineTest.class); - - @Rule - public MockitoRule mockito = MockitoJUnit.rule(); - - @InjectMocks - private WebSocketChatService engine; - - @Spy - private MockLogger mockLog; - - - @Test - public void shouldStartAndStop() throws InterruptedException { - - assertFalse(engine.isRunning()); - startEngine(8000); - assertTrue(engine.isRunning()); - engine.stop(); - assertFalse(engine.isRunning()); - } - - @Test - public void shouldAcceptClient() throws InterruptedException, UnknownHostException, IOException { - - int testPort = 8001; - - startEngine(testPort); - assertEquals(0, engine.getConnectedClientCount()); - Socket socket = establishTestConnection(testPort); - assertEquals(1, engine.getConnectedClientCount()); - - engine.stop(); - Thread.sleep(500); // allow engine to shut down - socket.close(); - } - - private void startEngine(int port) throws InterruptedException { - - engine.setPort(port); - ExecutorService executor = Executors.newSingleThreadExecutor(); - executor.submit(() -> { engine.start(); }); - Thread.sleep(500); // allow engine to fully initialize - } - - private Socket establishTestConnection(int port) throws InterruptedException, UnknownHostException, IOException { - - Socket socket = new Socket("localhost", port); - log.debug("establishing test connection: {}", socket); - Thread.sleep(500); // allow connection to fully establish a link - - return socket; - } -} \ No newline at end of file diff --git a/moo-server/src/test/java/pl/zimowski/moo/server/ClientThreadTest.java b/moo-server/src/test/java/pl/zimowski/moo/server/ClientThreadTest.java deleted file mode 100644 index abda460..0000000 --- a/moo-server/src/test/java/pl/zimowski/moo/server/ClientThreadTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package pl.zimowski.moo.server; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.BDDMockito.given; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectOutputStream; -import java.net.Socket; - -import org.junit.Rule; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import pl.zimowski.moo.api.ClientAction; -import pl.zimowski.moo.api.ClientEvent; -import pl.zimowski.moo.api.ServerAction; -import pl.zimowski.moo.api.ServerEvent; - -/** - * Ensures operation of {@link ClientThread} is as expected. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -public class ClientThreadTest { - - @Rule - public MockitoRule mockito = MockitoJUnit.rule(); - - @InjectMocks - private ClientThread clientThread; - - @Mock - private Socket socket; - - @Spy - private ServerNotificationMock clientThreadObserver; - - - @Test - public void shouldRunClientThreadAndExit() throws IOException { - - Object event = new ClientEvent(ClientAction.Signoff, "zorro", null); - InputStream is = eventAsInputStream(event); - - given(socket.getInputStream()).willReturn(is); - - clientThread.run(); - - assertNotNull(clientThreadObserver.getClientThread()); - assertNotNull(clientThreadObserver.getClientEvent()); - - // intentionally comparing references - assertTrue(clientThreadObserver.getClientThread() == clientThread); - } - - @Test - public void shouldProcessServerNotification() throws IOException, ClassNotFoundException { - - given(socket.getOutputStream()).willReturn(new ByteArrayOutputStream()); - assertTrue(clientThread.notify(new ServerEvent(ServerAction.Message, "hello there"))); - } - - private InputStream eventAsInputStream(Object event) throws IOException { - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos); - - oos.writeObject(event); - - return new ByteArrayInputStream(baos.toByteArray()); - } -} \ No newline at end of file diff --git a/moo-test-utils/pom.xml b/moo-test-utils/pom.xml new file mode 100644 index 0000000..f6f23f3 --- /dev/null +++ b/moo-test-utils/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + pl.zimowski + moo + 1.2.0-SNAPSHOT + + moo-test-utils + Moo Test (utils) + Utilities and mocks useful for all moo modules when building tests. + + + pl.zimowski + moo-api + + + junit + junit + compile + + + org.mockito + mockito-core + compile + + + \ No newline at end of file diff --git a/moo-test-utils/readme.md b/moo-test-utils/readme.md new file mode 100644 index 0000000..92bdd0c --- /dev/null +++ b/moo-test-utils/readme.md @@ -0,0 +1,4 @@ +# Moo Test Utils +--------------------- +Utilities and mocks useful for all `moo` modules when building tests. This +depedency should be imported with `test` scope. \ No newline at end of file diff --git a/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/EventAwareClientHandlerMock.java b/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/EventAwareClientHandlerMock.java new file mode 100644 index 0000000..9a58bc1 --- /dev/null +++ b/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/EventAwareClientHandlerMock.java @@ -0,0 +1,45 @@ +package pl.zimowski.moo.test.utils; + +import java.util.List; + +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.api.ClientListener; + +/** + * Handy mock useful for spying and verifying generated client events. + * Mocked to assume connection is successfull every time. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class EventAwareClientHandlerMock implements ClientHandling { + + private List eventList; + + + @Override + public boolean connect(ClientListener listener) { + return true; + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public void disconnect() { + } + + @Override + public void send(ClientEvent event) { + if(eventList != null) { + eventList.add(event); + } + } + + public void setEventList(List eventList) { + this.eventList = eventList; + } +} \ No newline at end of file diff --git a/moo-api/src/main/java/pl/zimowski/moo/api/MockLogger.java b/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/MockLogger.java similarity index 99% rename from moo-api/src/main/java/pl/zimowski/moo/api/MockLogger.java rename to moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/MockLogger.java index 26ddcf8..931e5e2 100644 --- a/moo-api/src/main/java/pl/zimowski/moo/api/MockLogger.java +++ b/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/MockLogger.java @@ -1,4 +1,4 @@ -package pl.zimowski.moo.api; +package pl.zimowski.moo.test.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,7 +16,7 @@ public class MockLogger implements Logger { private static final Logger log = LoggerFactory.getLogger(MockLogger.class); - + /** * Disables {@link Level#TRACE} if set to {@code true}. Other levels may * or may not be silent depending on their own switch. @@ -195,7 +195,9 @@ public void debug(String msg) { @Override public void debug(String format, Object arg) { - if(!isDebugSilent()) log.debug(format, arg); + if(!isDebugSilent()) { + log.debug(format, arg); + } } @Override diff --git a/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/MooTest.java b/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/MooTest.java new file mode 100644 index 0000000..5a338d4 --- /dev/null +++ b/moo-test-utils/src/main/java/pl/zimowski/moo/test/utils/MooTest.java @@ -0,0 +1,27 @@ +package pl.zimowski.moo.test.utils; + +import org.junit.BeforeClass; +import org.junit.Rule; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Recommended base for moo unit tests, especially if they are based on mockito. + * Setups basic mockito rule as well as other commonnalities such as required + * system props, etc. Super simple tests can probably do without this base, but + * in general, a moo test should extend this base. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public abstract class MooTest { + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + + @BeforeClass + public static void setupUnit() { + System.setProperty("LOG_DIR", "target/logs-test"); + } +} diff --git a/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/DummyLogWorker.java b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/DummyLogWorker.java new file mode 100644 index 0000000..bdae684 --- /dev/null +++ b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/DummyLogWorker.java @@ -0,0 +1,131 @@ +package pl.zimowski.moo.test.utils; + +import static ch.qos.logback.classic.ClassicConstants.FINALIZE_SESSION_MARKER; + +import org.junit.Assert; + +/** + * Dummy pojo used to invoke {@link MockLogger} at all levels. + * + * @since 1.0.0 + * @author Adam Zimowski (mrazjava) + */ +public class DummyLogWorker { + + static final String THE_QUICK = "the quick"; + + static final String BROWN_FOX = "brown fox"; + + static final String JUMPS_OVER = "jumps over"; + + static final String THE_LAZY = "the lazy"; + + static final String DOG = "dog"; + + private TestLogging log; + + + void logIt() { + + log.trace(THE_QUICK); + log.debug(BROWN_FOX); + log.info(JUMPS_OVER); + log.warn(THE_LAZY); + log.error(DOG); + } + + /** + * invoke all other (overriden) trace calls + */ + void logAlternateTrace() { + + Assert.assertTrue(log.isTraceEnabled()); + Assert.assertTrue(log.isTraceEnabled(FINALIZE_SESSION_MARKER)); + + log.trace(FINALIZE_SESSION_MARKER, null); + log.trace("", 1); + log.trace("", 1, 2, 3); + log.trace("", new Exception()); + log.trace(FINALIZE_SESSION_MARKER, "", 1); + log.trace(FINALIZE_SESSION_MARKER, "", 1, 2); + log.trace(FINALIZE_SESSION_MARKER, "", new Exception()); + log.trace("", 1, 2); + log.trace(FINALIZE_SESSION_MARKER, "", 1, 2, 3); + } + + /** + * invoke all other (overriden) debug calls + */ + void logAlternateDebug() { + + Assert.assertTrue(log.isDebugEnabled()); + Assert.assertTrue(log.isDebugEnabled(FINALIZE_SESSION_MARKER)); + + log.debug(FINALIZE_SESSION_MARKER, null); + log.debug("", 1); + log.debug("", 1, 2, 3); + log.debug("", new Exception()); + log.debug(FINALIZE_SESSION_MARKER, "", 1); + log.debug(FINALIZE_SESSION_MARKER, "", 1, 2); + log.debug(FINALIZE_SESSION_MARKER, "", new Exception()); + log.debug("", 1, 2); + log.debug(FINALIZE_SESSION_MARKER, "", 1, 2, 3); + } + + /** + * invoke all other (overriden) info calls + */ + void logAlternateInfo() { + + Assert.assertTrue(log.isInfoEnabled()); + Assert.assertTrue(log.isInfoEnabled(FINALIZE_SESSION_MARKER)); + + log.info(FINALIZE_SESSION_MARKER, null); + log.info("", 1); + log.info("", 1, 2, 3); + log.info("", new Exception()); + log.info(FINALIZE_SESSION_MARKER, "", 1); + log.info(FINALIZE_SESSION_MARKER, "", 1, 2); + log.info(FINALIZE_SESSION_MARKER, "", new Exception()); + log.info("", 1, 2); + log.info(FINALIZE_SESSION_MARKER, "", 1, 2, 3); + } + + /** + * invoke all other (overriden) warn calls + */ + void logAlternateWarn() { + + Assert.assertTrue(log.isWarnEnabled()); + Assert.assertTrue(log.isWarnEnabled(FINALIZE_SESSION_MARKER)); + + log.warn(FINALIZE_SESSION_MARKER, null); + log.warn("", 1); + log.warn("", 1, 2, 3); + log.warn("", new Exception()); + log.warn(FINALIZE_SESSION_MARKER, "", 1); + log.warn(FINALIZE_SESSION_MARKER, "", 1, 2); + log.warn(FINALIZE_SESSION_MARKER, "", new Exception()); + log.warn("", 1, 2); + log.warn(FINALIZE_SESSION_MARKER, "", 1, 2, 3); + } + + /** + * invoke all other (overriden) error calls + */ + void logAlternateError() { + + Assert.assertTrue(log.isErrorEnabled()); + Assert.assertTrue(log.isErrorEnabled(FINALIZE_SESSION_MARKER)); + + log.error(FINALIZE_SESSION_MARKER, null); + log.error("", 1); + log.error("", 1, 2, 3); + log.error("", new Exception()); + log.error(FINALIZE_SESSION_MARKER, "", 1); + log.error(FINALIZE_SESSION_MARKER, "", 1, 2); + log.error(FINALIZE_SESSION_MARKER, "", new Exception()); + log.error("", 1, 2); + log.error(FINALIZE_SESSION_MARKER, "", 1, 2, 3); + } +} \ No newline at end of file diff --git a/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/EventAwareClientHandlerMockTest.java b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/EventAwareClientHandlerMockTest.java new file mode 100644 index 0000000..ea36d5d --- /dev/null +++ b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/EventAwareClientHandlerMockTest.java @@ -0,0 +1,37 @@ +package pl.zimowski.moo.test.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; + +/** + * Ensures that {@link EventAwareClientHandlerMock} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class EventAwareClientHandlerMockTest { + + @Test + public void shouldReturnCorrectValues() { + + List events = new ArrayList<>(); + EventAwareClientHandlerMock mock = new EventAwareClientHandlerMock(); + + mock.setEventList(events); + + mock.send(new ClientEvent(ClientAction.Message)); + + assertTrue(mock.connect(null)); + assertTrue(mock.isConnected()); + assertTrue(1 == events.size()); + assertEquals(ClientAction.Message, events.get(0).getAction()); + } +} diff --git a/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/MockLoggerTest.java b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/MockLoggerTest.java new file mode 100644 index 0000000..9e622cf --- /dev/null +++ b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/MockLoggerTest.java @@ -0,0 +1,255 @@ +package pl.zimowski.moo.test.utils; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsMapContaining.hasEntry; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import static pl.zimowski.moo.test.utils.DummyLogWorker.THE_QUICK; +import static pl.zimowski.moo.test.utils.DummyLogWorker.BROWN_FOX; +import static pl.zimowski.moo.test.utils.DummyLogWorker.JUMPS_OVER; +import static pl.zimowski.moo.test.utils.DummyLogWorker.THE_LAZY; +import static pl.zimowski.moo.test.utils.DummyLogWorker.DOG; + +import java.util.Map; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.slf4j.event.Level; + +/** + * Ensures that {@link MockLogger} is correctly logging messages given various + * configuration options. + * + * @since 2.5.0 + * @author Adam Zimowski (mrazjava) + */ +public class MockLoggerTest { + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @InjectMocks + private DummyLogWorker logWorker; + + @Spy + private TestLogger testLogger; + + + @After + public void initResultsMap() { + getResults().clear(); + } + + private Map getResults() { + return testLogger.getResults(); + } + + @Test + public void shouldLogEverything() { + + MockLogger.resetSilence(); + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(5)); + assertThat(getResults(), hasEntry(Level.TRACE, THE_QUICK)); + assertThat(getResults(), hasEntry(Level.DEBUG, BROWN_FOX)); + assertThat(getResults(), hasEntry(Level.INFO, JUMPS_OVER)); + assertThat(getResults(), hasEntry(Level.WARN, THE_LAZY)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldNotLogTrace() { + + MockLogger.silentTraceOn(); + testDebugUp(); + } + + @Test + public void shouldLogDebugUp() { + + MockLogger.silentLevel = Level.TRACE; + testDebugUp(); + } + + private void testDebugUp() { + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(4)); + assertNull(getResults().get(Level.TRACE)); + assertThat(getResults(), hasEntry(Level.DEBUG, BROWN_FOX)); + assertThat(getResults(), hasEntry(Level.INFO, JUMPS_OVER)); + assertThat(getResults(), hasEntry(Level.WARN, THE_LAZY)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldNotLogDebug() { + + MockLogger.silentDebugOn(); + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(4)); + assertThat(getResults(), hasEntry(Level.TRACE, THE_QUICK)); + assertNull(getResults().get(Level.DEBUG)); + assertThat(getResults(), hasEntry(Level.INFO, JUMPS_OVER)); + assertThat(getResults(), hasEntry(Level.WARN, THE_LAZY)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldLogInfoUp() { + + MockLogger.silentLevel = Level.DEBUG; + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(3)); + assertNull(getResults().get(Level.TRACE)); + assertNull(getResults().get(Level.DEBUG)); + assertThat(getResults(), hasEntry(Level.INFO, JUMPS_OVER)); + assertThat(getResults(), hasEntry(Level.WARN, THE_LAZY)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldNotLogInfo() { + + MockLogger.silentInfoOn(); + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(4)); + assertThat(getResults(), hasEntry(Level.TRACE, THE_QUICK)); + assertThat(getResults(), hasEntry(Level.DEBUG, BROWN_FOX)); + assertNull(getResults().get(Level.INFO)); + assertThat(getResults(), hasEntry(Level.WARN, THE_LAZY)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldLogWarnUp() { + + MockLogger.silentLevel = Level.INFO; + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(2)); + assertNull(getResults().get(Level.TRACE)); + assertNull(getResults().get(Level.DEBUG)); + assertNull(getResults().get(Level.INFO)); + assertThat(getResults(), hasEntry(Level.WARN, THE_LAZY)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldNotLogWarn() { + + MockLogger.silentWarnOn(); + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(4)); + assertThat(getResults(), hasEntry(Level.TRACE, THE_QUICK)); + assertThat(getResults(), hasEntry(Level.DEBUG, BROWN_FOX)); + assertThat(getResults(), hasEntry(Level.INFO, JUMPS_OVER)); + assertNull(getResults().get(Level.WARN)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldLogErrorOnly() { + + MockLogger.silentLevel = Level.WARN; + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(1)); + assertNull(getResults().get(Level.TRACE)); + assertNull(getResults().get(Level.DEBUG)); + assertNull(getResults().get(Level.INFO)); + assertNull(getResults().get(Level.WARN)); + assertThat(getResults(), hasEntry(Level.ERROR, DOG)); + } + + @Test + public void shouldNotLogError() { + + MockLogger.silentErrorOn(); + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(4)); + assertThat(getResults(), hasEntry(Level.TRACE, THE_QUICK)); + assertThat(getResults(), hasEntry(Level.DEBUG, BROWN_FOX)); + assertThat(getResults(), hasEntry(Level.INFO, JUMPS_OVER)); + assertThat(getResults(), hasEntry(Level.WARN, THE_LAZY)); + assertNull(getResults().get(Level.ERROR)); + } + + @Test + public void shouldLogNothing() { + + MockLogger.silentLevel = Level.ERROR; + + logWorker.logIt(); + + assertThat(getResults().values(), hasSize(0)); + } + + @Test + public void shouldLogAlternateTraceCalls() { + + TestLogger.resetSilence(); + logWorker.logAlternateTrace(); + assertEquals(9, testLogger.getTraceCount()); + } + + @Test + public void shouldLogAlternateDebugCalls() { + + TestLogger.resetSilence(); + logWorker.logAlternateDebug(); + assertEquals(9, testLogger.getDebugCount()); + } + + @Test + public void shouldLogAlternateInfoCalls() { + + TestLogger.resetSilence(); + logWorker.logAlternateInfo(); + assertEquals(9, testLogger.getInfoCount()); + } + + @Test + public void shouldLogAlternateWarnCalls() { + + TestLogger.resetSilence(); + logWorker.logAlternateWarn(); + assertEquals(9, testLogger.getWarnCount()); + } + + @Test + public void shouldLogAlternateErrorCalls() { + + TestLogger.resetSilence(); + logWorker.logAlternateError(); + assertEquals(9, testLogger.getErrorCount()); + } + + @Test + public void shouldGetLoggerName() { + + assertEquals(MockLogger.class.getName(), testLogger.getName()); + } +} \ No newline at end of file diff --git a/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/MooTestTest.java b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/MooTestTest.java new file mode 100644 index 0000000..7e92b46 --- /dev/null +++ b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/MooTestTest.java @@ -0,0 +1,20 @@ +package pl.zimowski.moo.test.utils; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Ensures that base for moo tests operates correctly. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class MooTestTest extends MooTest { + + @Test + public void shouldSetLogSystemProperty() { + + assertEquals("target/logs-test", System.getProperty("LOG_DIR")); + } +} diff --git a/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/TestLogger.java b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/TestLogger.java new file mode 100644 index 0000000..b8fd326 --- /dev/null +++ b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/TestLogger.java @@ -0,0 +1,391 @@ +package pl.zimowski.moo.test.utils; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Marker; +import org.slf4j.event.Level; + +/** + * Wrapper over actual logger being tested with sole purpose of recording + * results so that assertions can be made. + * + * @since 1.0.0 + * @author Adam Zimowski (mrazjava) + */ +public class TestLogger extends MockLogger implements TestLogging { + + private int traceCount; + + private int debugCount; + + private int infoCount; + + private int warnCount; + + private int errorCount; + + // logged messages are stored so that assertions can be made + private Map results = new HashMap<>(); + + + Map getResults() { + return results; + } + + @Override + public void trace(String msg) { + super.trace(msg); + if(!isTraceSilent()) { + results.put(Level.TRACE, msg); + traceCount++; + } + } + + @Override + public void trace(String format, Object arg) { + super.trace(format, arg); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + super.trace(format, arg1, arg2); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(String format, Object... arguments) { + super.trace(format, arguments); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(String msg, Throwable t) { + super.trace(msg, t); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(Marker marker, String msg) { + super.trace(marker, msg); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(Marker marker, String format, Object arg) { + super.trace(marker, format, arg); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + super.trace(marker, format, arg1, arg2); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + super.trace(marker, format, argArray); + if(!isTraceSilent()) traceCount++; + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + super.trace(marker, msg, t); + if(!isTraceSilent()) traceCount++; + } + + @Override + public int getTraceCount() { + return traceCount; + } + + @Override + public void resetTraceCount() { + traceCount = 0; + } + + @Override + public void debug(String msg) { + super.debug(msg); + if(!isDebugSilent()) { + results.put(Level.DEBUG, msg); + debugCount++; + } + } + + @Override + public void debug(String format, Object arg) { + super.debug(format, arg); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + super.debug(format, arg1, arg2); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(String format, Object... arguments) { + super.debug(format, arguments); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(String msg, Throwable t) { + super.debug(msg, t); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(Marker marker, String msg) { + super.debug(marker, msg); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(Marker marker, String format, Object arg) { + super.debug(marker, format, arg); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + super.debug(marker, format, arg1, arg2); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + super.debug(marker, format, arguments); + if(!isDebugSilent()) debugCount++; + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + super.debug(marker, msg, t); + if(!isDebugSilent()) debugCount++; + } + + @Override + public int getDebugCount() { + return debugCount; + } + + @Override + public void resetDebugCount() { + debugCount = 0; + } + + @Override + public void info(String msg) { + super.info(msg); + if(!isInfoSilent()) results.put(Level.INFO, msg); + } + + @Override + public void info(String format, Object arg) { + super.info(format, arg); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(String format, Object arg1, Object arg2) { + super.info(format, arg1, arg2); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(String format, Object... arguments) { + super.info(format, arguments); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(String msg, Throwable t) { + super.info(msg, t); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(Marker marker, String msg) { + super.info(marker, msg); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(Marker marker, String format, Object arg) { + super.info(marker, format, arg); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + super.info(marker, format, arg1, arg2); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + super.info(marker, format, arguments); + if(!isInfoSilent()) infoCount++; + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + super.info(marker, msg, t); + if(!isInfoSilent()) infoCount++; + } + + @Override + public int getInfoCount() { + return infoCount; + } + + @Override + public void resetInfoCount() { + infoCount = 0; + } + + @Override + public void warn(String msg) { + super.warn(msg); + if(!isWarnSilent()) results.put(Level.WARN, msg); + } + + @Override + public void warn(String format, Object arg) { + super.warn(format, arg); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(String format, Object... arguments) { + super.warn(format, arguments); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + super.warn(format, arg1, arg2); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(String msg, Throwable t) { + super.warn(msg, t); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(Marker marker, String msg) { + super.warn(marker, msg); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(Marker marker, String format, Object arg) { + super.warn(marker, format, arg); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + super.warn(marker, format, arg1, arg2); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + super.warn(marker, format, arguments); + if(!isWarnSilent()) warnCount++; + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + super.warn(marker, msg, t); + if(!isWarnSilent()) warnCount++; + } + + @Override + public int getWarnCount() { + return warnCount; + } + + @Override + public void resetWarnCount() { + warnCount = 0; + } + + @Override + public void error(String msg) { + super.error(msg); + if(!isErrorSilent()) results.put(Level.ERROR, msg); + } + + @Override + public void error(String format, Object arg) { + super.error(format, arg); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(String format, Object arg1, Object arg2) { + super.error(format, arg1, arg2); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(String format, Object... arguments) { + super.error(format, arguments); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(String msg, Throwable t) { + super.error(msg, t); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(Marker marker, String msg) { + super.error(marker, msg); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(Marker marker, String format, Object arg) { + super.error(marker, format, arg); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + super.error(marker, format, arg1, arg2); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + super.error(marker, format, arguments); + if(!isErrorSilent()) errorCount++; + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + super.error(marker, msg, t); + if(!isErrorSilent()) errorCount++; + } + + @Override + public int getErrorCount() { + return errorCount; + } + + @Override + public void resetErrorCount() { + errorCount = 0; + } +} \ No newline at end of file diff --git a/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/TestLogging.java b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/TestLogging.java new file mode 100644 index 0000000..1415d7b --- /dev/null +++ b/moo-test-utils/src/test/java/pl/zimowski/moo/test/utils/TestLogging.java @@ -0,0 +1,72 @@ +package pl.zimowski.moo.test.utils; + +import org.slf4j.Logger; + +/** + * Marker interface needed to make mockito and {@link TestLogger} happy. + * + * @since 1.0.0 + * @author Adam Zimowski (mrazjava) + */ +public interface TestLogging extends Logger { + + /** + * @return number of times any TRACE was invoked + * @since 1.2.0 + */ + int getTraceCount(); + + /** + * Resets tracking of logged TRACE messages to starting point. + * @since 1.2.0 + */ + void resetTraceCount(); + + /** + * @return number of times any DEBUG was invoked + * @since 1.2.0 + */ + int getDebugCount(); + + /** + * Resets tracking of logged DEBUG messages to starting point. + * @since 1.2.0 + */ + void resetDebugCount(); + + /** + * @return number of times any INFO was invoked + * @since 1.2.0 + */ + int getInfoCount(); + + /** + * Resets tracking of logged TRACE messages to starting point. + * @since 1.2.0 + */ + void resetInfoCount(); + + /** + * @return number of times any WARN was invoked + * @since 1.2.0 + */ + int getWarnCount(); + + /** + * Resets tracking of logged TRACE messages to starting point. + * @since 1.2.0 + */ + void resetWarnCount(); + + /** + * @return number of times any ERROR was invoked + * @since 1.2.0 + */ + int getErrorCount(); + + /** + * Resets tracking of logged TRACE messages to starting point. + * @since 1.2.0 + */ + void resetErrorCount(); +} diff --git a/moo-test-utils/src/test/resources/logback-test.xml b/moo-test-utils/src/test/resources/logback-test.xml new file mode 100644 index 0000000..17cbc12 --- /dev/null +++ b/moo-test-utils/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%level] %logger [%M] - %m%n + + + + + + + + + + \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-commons/pom.xml b/moo-ui-shell/moo-ui-shell-commons/pom.xml new file mode 100644 index 0000000..a094919 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + pl.zimowski + moo-ui-shell + 1.0.0-SNAPSHOT + + moo-ui-shell-commons + Moo UI (shell: commons) + Common components for shell UI reusable by reader and writer. + + + pl.zimowski + moo-test-utils + + + \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-commons/readme.md b/moo-ui-shell/moo-ui-shell-commons/readme.md new file mode 100644 index 0000000..3280520 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/readme.md @@ -0,0 +1,3 @@ +# Moo Shell Commons +--------------------- +Components and utilities that are reusable between reader and writer. \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/AbstractClientListener.java b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/AbstractClientListener.java new file mode 100644 index 0000000..be86c40 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/AbstractClientListener.java @@ -0,0 +1,51 @@ +package pl.zimowski.moo.ui.shell.commons; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pl.zimowski.moo.api.ClientListener; + +/** + * Recommended base for shell based listeners for server events. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public abstract class AbstractClientListener implements ClientListener { + + public static final Logger LOG = LoggerFactory.getLogger("CHAT_ECHO"); + + protected String nick; + + protected String clientId; + + /** + * Name to be reported when messages are generated by UI listener. + * Not to confuse with chat user (author of chat events). + * + * @return author reported as listener (eg: reader or writer) + */ + public abstract String getAuthor(); + + @Override + public void onBeforeServerConnect(String host, int port) { + LOG.info("({}) establishing connection to {}:{}", getAuthor(), host, port); + } + + @Override + public void onConnectToServerError(String error) { + LOG.info("({}) could not establish server connection: {}", getAuthor(), error); + } + + public String getClientId() { + return clientId; + } + + public String getNick() { + return nick; + } + + public void setNick(String nick) { + this.nick = nick; + } +} \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ExecutionThrottling.java b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ExecutionThrottling.java new file mode 100644 index 0000000..065b518 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ExecutionThrottling.java @@ -0,0 +1,22 @@ +package pl.zimowski.moo.ui.shell.commons; + +/** + * Delay of application execution. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public interface ExecutionThrottling { + + /** + * Delay execution by some pre-configured time period. + */ + void throttle(); + + /** + * Delay execution by desired period of time. + * + * @param delay in milliseconds + */ + void throttle(long delay); +} diff --git a/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ShutdownAgent.java b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ShutdownAgent.java new file mode 100644 index 0000000..09f6000 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ShutdownAgent.java @@ -0,0 +1,26 @@ +package pl.zimowski.moo.ui.shell.commons; + +import javax.inject.Inject; + +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * Mechanism used to gracefully exit spring managed app upon + * a request. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class ShutdownAgent { + + @Inject + private ApplicationContext appContext; + + + public int initiateShutdown(int statusCode) { + return SpringApplication.exit(appContext, () -> statusCode); + } +} diff --git a/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ThreadDelay.java b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ThreadDelay.java new file mode 100644 index 0000000..24ebb27 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/main/java/pl/zimowski/moo/ui/shell/commons/ThreadDelay.java @@ -0,0 +1,45 @@ +package pl.zimowski.moo.ui.shell.commons; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * In process delay of execution based on standard thread sleep. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class ThreadDelay implements ExecutionThrottling { + + @Inject + private Logger log; + + @Value("${shell.commons.throttle}") Long delay; + + + @Override + public void throttle() { + log.warn("sleeping {}ms ... zzzzzzzz", delay); + delay(this.delay); + } + + @Override + public void throttle(long delay) { + log.warn("sleeping {}ms ... zzzzzzzz", delay); + delay(delay); + } + + private void delay(long delay) { + + try { + Thread.sleep(delay); + } + catch(InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/AbstractClientListenerTest.java b/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/AbstractClientListenerTest.java new file mode 100644 index 0000000..db445c9 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/AbstractClientListenerTest.java @@ -0,0 +1,41 @@ +package pl.zimowski.moo.ui.shell.commons; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; + +/** + * Ensures that {@link AbstractClientListener} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class AbstractClientListenerTest { + + @Test + public void shouldRetainInformation() { + + AbstractClientListener listener = new AbstractClientListener() { + + @Override + public void onEvent(ServerEvent event) { + } + + @Override + public String getAuthor() { + return "moo"; + } + }; + + listener.setNick("baboon"); + listener.onBeforeServerConnect("localhost", 8001); + listener.onConnectToServerError("ooops"); + listener.onEvent(new ServerEvent(ServerAction.ClientDisconnected)); + + assertEquals("moo", listener.getAuthor()); + assertEquals("baboon", listener.getNick()); + } +} diff --git a/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/ShutdownAgentTest.java b/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/ShutdownAgentTest.java new file mode 100644 index 0000000..64d6c07 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/ShutdownAgentTest.java @@ -0,0 +1,35 @@ +package pl.zimowski.moo.ui.shell.commons; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.context.ApplicationContext; + +import pl.zimowski.moo.test.utils.MooTest; +import pl.zimowski.moo.ui.shell.commons.ShutdownAgent; + +/** + * Ensures that {@link ShutdownAgent} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ShutdownAgentTest extends MooTest { + + @InjectMocks + private ShutdownAgent agent; + + @Mock + private ApplicationContext context; + + + @Test + public void shouldShutDown() { + + int shutdownCode = 101; + + assertEquals(shutdownCode, agent.initiateShutdown(shutdownCode)); + } +} diff --git a/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/ThreadDelayTest.java b/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/ThreadDelayTest.java new file mode 100644 index 0000000..32727e8 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/test/java/pl/zimowski/moo/ui/shell/commons/ThreadDelayTest.java @@ -0,0 +1,41 @@ +package pl.zimowski.moo.ui.shell.commons; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; + +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class ThreadDelayTest extends MooTest { + + @InjectMocks + private ThreadDelay threadDelay; + + @Spy + private MockLogger mockLog; + + + @Test + public void shouldThrotle() throws InterruptedException { + + threadDelay.delay = 10L; + + threadDelay.throttle(); + + Thread testThread = new Thread() { + + @Override + public void run() { + threadDelay.throttle(2000L); + } + }; + + testThread.start(); + testThread.interrupt(); + } +} diff --git a/moo-ui-shell/moo-ui-shell-commons/src/test/resources/logback-test.xml b/moo-ui-shell/moo-ui-shell-commons/src/test/resources/logback-test.xml new file mode 100644 index 0000000..204fa42 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-commons/src/test/resources/logback-test.xml @@ -0,0 +1,28 @@ + + + + + + + + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%level]) %clr(%logger [%M]){cyan} - %m%n + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-reader/pom.xml b/moo-ui-shell/moo-ui-shell-reader/pom.xml new file mode 100644 index 0000000..81cdf54 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-reader/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + pl.zimowski + moo-ui-shell + 1.0.0-SNAPSHOT + + moo-ui-shell-reader + Moo UI (shell: reader) + Text based UI for displaying chat events (read only). In text based console, reading and writing events is most convenient when managed via separate apps. + + + pl.zimowski + moo-ui-shell-commons + + + pl.zimowski + moo-test-utils + + + \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-reader/readme.md b/moo-ui-shell/moo-ui-shell-reader/readme.md new file mode 100644 index 0000000..98a7420 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-reader/readme.md @@ -0,0 +1,4 @@ +# Moo Shell Reader +--------------------- +Console based reader of Moo events. If ran stand alone, allows to view public +chats. If ran in combination with shell writer, allows for full chat experience. \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-reader/src/main/java/pl/zimowski/moo/ui/shell/reader/App.java b/moo-ui-shell/moo-ui-shell-reader/src/main/java/pl/zimowski/moo/ui/shell/reader/App.java new file mode 100644 index 0000000..e9679e6 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-reader/src/main/java/pl/zimowski/moo/ui/shell/reader/App.java @@ -0,0 +1,68 @@ +package pl.zimowski.moo.ui.shell.reader; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.ui.shell.commons.ExecutionThrottling; + +/** + * Text based UI client of a Moo chat service with read only + * capabilities. + * + * @since 1.0.0 + * @author Adam Zimowski (mrazjava) + */ +@ComponentScan(basePackages = "pl.zimowski.moo") +@SpringBootApplication +public class App implements ApplicationRunner { + + @Inject + private ClientHandling clientHandler; + + @Inject + private EventReporter eventReporter; + + @Inject + private ExecutionThrottling throttler; + + + /** + * Application entry point. + * + * @param args such as config overrides as per Spring Boot features + */ + public static final void main(String[] args) { + + SpringApplication.run(App.class, args); + } + + @Override + public void run(ApplicationArguments args) throws Exception { + + if(!clientHandler.connect(eventReporter)) { + return; + } + + // allow connection thru while console buffers the output + while(eventReporter.getClientId() == null) { + throttler.throttle(); + } + } + + @PreDestroy + public void shutdown() { + + if(clientHandler.isConnected()) { + clientHandler.send(new ClientEvent(ClientAction.Disconnect)); + } + } +} \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-reader/src/main/java/pl/zimowski/moo/ui/shell/reader/EventReporter.java b/moo-ui-shell/moo-ui-shell-reader/src/main/java/pl/zimowski/moo/ui/shell/reader/EventReporter.java new file mode 100644 index 0000000..a93a932 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-reader/src/main/java/pl/zimowski/moo/ui/shell/reader/EventReporter.java @@ -0,0 +1,63 @@ +package pl.zimowski.moo.ui.shell.reader; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; +import pl.zimowski.moo.ui.shell.commons.AbstractClientListener; + +/** + * Reports chat events to the UI console. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class EventReporter extends AbstractClientListener { + + private static final Logger log = LoggerFactory.getLogger(EventReporter.class); + + /** + * Name reported as author of messages generated by the UI reader. + */ + static final String AUTHOR = "reader"; + + @Inject + private ClientHandling clientHandler; + + + @Override + public String getAuthor() { + return AUTHOR; + } + + @Override + public void onEvent(ServerEvent event) { + + log.debug(event.toString()); + + String author = event.getAuthor(); + + if(event.getAction() == ServerAction.ConnectionEstablished) { + clientId = event.getClientId(); + LOG.info("({}): connected, client id: {}", author, clientId); + } + else if(event.getAction() == ServerAction.NickGenerated) { + nick = event.getMessage(); + LOG.info("({}): You will be known as '{}'", author, nick); + + } + else if(event.getAction() == ServerAction.ServerExit) { + LOG.info("({}) connection terminated by server; bye!", author); + clientHandler.disconnect(); + } + else { + LOG.info("({}): {}", author, event.getMessage()); + } + } +} diff --git a/moo-client/src/main/resources/application.properties b/moo-ui-shell/moo-ui-shell-reader/src/main/resources/application.properties similarity index 58% rename from moo-client/src/main/resources/application.properties rename to moo-ui-shell/moo-ui-shell-reader/src/main/resources/application.properties index 781df27..4247a02 100644 --- a/moo-client/src/main/resources/application.properties +++ b/moo-ui-shell/moo-ui-shell-reader/src/main/resources/application.properties @@ -1,6 +1,9 @@ -# Moo server configuration +# Moo server # host where server is running on server.host=localhost # port on which server accepts client connections -server.port=8000 \ No newline at end of file +server.port=8000 + +# Moo shell commons +shell.commons.throttle=50 \ No newline at end of file diff --git a/moo-client/src/main/resources/logback.xml b/moo-ui-shell/moo-ui-shell-reader/src/main/resources/logback.xml similarity index 53% rename from moo-client/src/main/resources/logback.xml rename to moo-ui-shell/moo-ui-shell-reader/src/main/resources/logback.xml index 8e4e215..3274886 100644 --- a/moo-client/src/main/resources/logback.xml +++ b/moo-ui-shell/moo-ui-shell-reader/src/main/resources/logback.xml @@ -19,18 +19,37 @@ + + ${LOG_DIR}/moo-shell-reader.log + + + ${LOG_DIR}/moo-shell-reader.%d{yyyy-MM-dd}.log + + 30 + 1GB + + + + %date{yyyy-MM-dd HH:mm:ss} [%level] %logger [%M] - %msg%n + + + + - - + + + + + - + diff --git a/moo-ui-shell/moo-ui-shell-reader/src/test/java/pl/zimowski/moo/ui/shell/reader/AppTest.java b/moo-ui-shell/moo-ui-shell-reader/src/test/java/pl/zimowski/moo/ui/shell/reader/AppTest.java new file mode 100644 index 0000000..85075d3 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-reader/src/test/java/pl/zimowski/moo/ui/shell/reader/AppTest.java @@ -0,0 +1,109 @@ +package pl.zimowski.moo.ui.shell.reader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.quality.Strictness; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.test.utils.EventAwareClientHandlerMock; +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; +import pl.zimowski.moo.ui.shell.commons.ExecutionThrottling; +import pl.zimowski.moo.ui.shell.commons.ShutdownAgent; + +/** + * Ensures that {@link App} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class AppTest extends MooTest { + + @InjectMocks + private App reader; + + @Spy + private MockLogger mockLog; + + @Spy + private EventAwareClientHandlerMock clientHandler; + + @Mock + private EventReporter eventReporter; + + @Mock + private ShutdownAgent shutdownAgent; + + @Mock + private ExecutionThrottling throttler; + + @BeforeClass + public static void dor() { + System.setProperty("LOG_DIR", "target/logs"); + } + + @Test + public void shouldNotConnect() throws Exception { + + List eventList = new ArrayList<>(); + + clientHandler.setEventList(eventList); + when(clientHandler.connect(eventReporter)).thenReturn(false); + + reader.run(null); + + assertTrue(eventList.isEmpty()); + } + + @Test(expected = IllegalStateException.class) + public void shouldConnectButThrottleOnNullClientId() throws Exception { + + doThrow(IllegalStateException.class).when(throttler).throttle(); + + reader.run(null); + } + + /** + * Very similar setup to {@link #shouldConnectButThrottleOnNullClientId()} + * as throttler is configured to throw exceptione very time, however, + * because reporter is also configured to return client id, throttler + * will never throttle and so exception should never be thrown. + * + * @throws Exception whenever {@link App#run(org.springframework.boot.ApplicationArguments)} does + */ + @Test + public void shouldConnectAndExitWithoutException() throws Exception { + + mockito.strictness(Strictness.LENIENT); + + doThrow(IllegalStateException.class).when(throttler).throttle(); + when(eventReporter.getClientId()).thenReturn("foo-bar"); + + reader.run(null); + } + + @Test + public void shouldShutdownAndDisconnect() { + + List events = new ArrayList<>(); + + clientHandler.setEventList(events); + assertTrue(events.isEmpty()); + reader.shutdown(); + + assertEquals(1, events.size()); + assertEquals(ClientAction.Disconnect, events.get(0).getAction()); + } +} diff --git a/moo-ui-shell/moo-ui-shell-reader/src/test/java/pl/zimowski/moo/ui/shell/reader/EventReporterTest.java b/moo-ui-shell/moo-ui-shell-reader/src/test/java/pl/zimowski/moo/ui/shell/reader/EventReporterTest.java new file mode 100644 index 0000000..b41742d --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-reader/src/test/java/pl/zimowski/moo/ui/shell/reader/EventReporterTest.java @@ -0,0 +1,85 @@ +package pl.zimowski.moo.ui.shell.reader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link EventReporter} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class EventReporterTest extends MooTest { + + @InjectMocks + private EventReporter reporter; + + @Mock + private ClientHandling clientHandler; + + private boolean connected; + + + @Test + public void shouldEstablishConnection() { + + assertNull(reporter.getClientId()); + reporter.onEvent(new ServerEvent(ServerAction.ConnectionEstablished).withClientId("foo-bar")); + assertEquals("foo-bar", reporter.getClientId()); + } + + @Test + public void shouldReturnAuthor() { + + assertEquals(EventReporter.AUTHOR, reporter.getAuthor()); + } + + @Test + public void shouldRecordNick() { + + assertNull(reporter.getNick()); + reporter.onEvent(new ServerEvent(ServerAction.NickGenerated).withMessage("johnie")); + assertEquals("johnie", reporter.getNick()); + } + + @Test + public void shouldDisconnectOnExit() { + + connected = true; + + Mockito.doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock arg0) throws Throwable { + connected = false; + return null; + } + + }).when(clientHandler).disconnect(); + + reporter.onEvent(new ServerEvent(ServerAction.ServerExit)); + assertFalse(connected); + } + + @Test + public void shouldHandleMessage() { + + reporter.onEvent(new ServerEvent(ServerAction.Message).withMessage("hello")); + + // nothing to assert as message is simply logged + // at least we covered extra branch :-) + } +} \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-writer/pom.xml b/moo-ui-shell/moo-ui-shell-writer/pom.xml new file mode 100644 index 0000000..50d0396 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + pl.zimowski + moo-ui-shell + 1.0.0-SNAPSHOT + + moo-ui-shell-writer + Moo UI (shell: writer) + Text based UI for generating chat events (write only). In text based console, reading and writing events is most convenient when managed via separate apps. + + + pl.zimowski + moo-ui-shell-commons + + + pl.zimowski + moo-test-utils + + + \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-writer/readme.md b/moo-ui-shell/moo-ui-shell-writer/readme.md new file mode 100644 index 0000000..85293ea --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/readme.md @@ -0,0 +1,3 @@ +# Moo Shell Writer +--------------------- +Console based writer of Moo events. Requires a running instance of Moo reader. \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-writer/src/main/java/pl/zimowski/moo/ui/shell/writer/App.java b/moo-ui-shell/moo-ui-shell-writer/src/main/java/pl/zimowski/moo/ui/shell/writer/App.java new file mode 100644 index 0000000..3662ca2 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/src/main/java/pl/zimowski/moo/ui/shell/writer/App.java @@ -0,0 +1,132 @@ +package pl.zimowski.moo.ui.shell.writer; + +import java.util.Scanner; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +import pl.zimowski.moo.api.ApiUtils; +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.ui.shell.commons.ExecutionThrottling; +import pl.zimowski.moo.ui.shell.commons.ShutdownAgent; + +/** + * Text based UI client of a Moo chat service with write only + * capability. Allows to generate chat events. Requires a UI + * reader to display incoming chats. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@ComponentScan(basePackages = "pl.zimowski.moo") +@SpringBootApplication +public class App implements ApplicationRunner { + + @Inject + private Logger log; + + @Inject + private ClientHandling clientHandler; + + @Inject + private EventHandler eventReporter; + + @Inject + private ShutdownAgent shutdownAgent; + + @Inject + private ExecutionThrottling throttler; + + + /** + * Application entry point. + * + * @param args such as config overrides as per Spring Boot features + */ + public static final void main(String[] args) { + + SpringApplication.run(App.class, args); + } + + @Override + public void run(ApplicationArguments args) throws Exception { + + try (Scanner scanner = new Scanner(System.in)) { + + if(!clientHandler.connect(eventReporter)) { + return; + } + + // allow connection thru while console buffers the output + while(eventReporter.getClientId() == null) { + throttler.throttle(); + } + + EventHandler.LOG.info("({}) type nickname or just hit enter; ctrl-c to exit", EventHandler.AUTHOR); + + String nickName = scanner.nextLine(); + ClientEvent signinEvent = new ClientEvent(ClientAction.Signin); + + if(StringUtils.isNotBlank(nickName)) { + eventReporter.setNick(nickName); + } + else { + clientHandler.send(new ClientEvent(ClientAction.GenerateNick)); + } + + // wait until server responds with a nick; event reporter listens + // for nick confirmation and sets nick accordingly + while(eventReporter.getNick() == null) { + throttler.throttle(); + } + + signinEvent.setAuthor(eventReporter.getNick()); + clientHandler.send(signinEvent); + + ApiUtils.printPrompt(); + + while(scanner.hasNextLine()) { + + String input = scanner.nextLine(); + + if(input.equals("moo:exit")) { + break; // in the future we should expand moo: into a managable predicates + } + + ClientEvent event = new ClientEvent(ClientAction.Message, eventReporter.getNick(), input); + log.debug("sending: {}", event); + clientHandler.send(event); + + ApiUtils.printPrompt(); + } + } + + shutdownAgent.initiateShutdown(0); + } + + @PreDestroy + public void shutdown() { + + String nick = eventReporter.getNick(); + + if(clientHandler.isConnected()) { + + if(nick != null) { // may be null if connected, then exit before signin + EventHandler.LOG.info("({}) done mooing? bye {}!", EventHandler.AUTHOR, nick); + clientHandler.send(new ClientEvent(ClientAction.Signoff).withAuthor(nick)); + } + + clientHandler.send(new ClientEvent(ClientAction.Disconnect)); + } + } +} diff --git a/moo-ui-shell/moo-ui-shell-writer/src/main/java/pl/zimowski/moo/ui/shell/writer/EventHandler.java b/moo-ui-shell/moo-ui-shell-writer/src/main/java/pl/zimowski/moo/ui/shell/writer/EventHandler.java new file mode 100644 index 0000000..9715efe --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/src/main/java/pl/zimowski/moo/ui/shell/writer/EventHandler.java @@ -0,0 +1,72 @@ +package pl.zimowski.moo.ui.shell.writer; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; +import pl.zimowski.moo.ui.shell.commons.AbstractClientListener; + +/** + * Listens for critical server events which are required for + * proper operation of a writer. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +@Component +public class EventHandler extends AbstractClientListener { + + private static final Logger log = LoggerFactory.getLogger(EventHandler.class); + + /** + * Name reported as author of messages generated by the UI writer. + */ + static final String AUTHOR = "writer"; + + @Inject + private ClientHandling clientHandler; + + + @Override + public String getAuthor() { + return AUTHOR; + } + + @Override + public void onEvent(ServerEvent event) { + + log.debug(event.toString()); + + String author = event.getAuthor(); + + if(event.getAction() == ServerAction.ConnectionEstablished) { + clientId = event.getClientId(); + LOG.info("({}): connected, client id: {}", author, clientId); + } + else if(event.getAction() == ServerAction.NickGenerated) { + nick = event.getMessage(); + LOG.info("({}): You will be known as '{}'", author, nick); + + } + else if(event.getAction() == ServerAction.ServerExit) { + System.out.println(); + LOG.info("({}) connection terminated by server; bye!", author); + clientHandler.disconnect(); + } + } + + @Override + public void onBeforeServerConnect(String host, int port) { + LOG.info("({}) establishing connection to {}:{}", AUTHOR, host, port); + } + + @Override + public void onConnectToServerError(String error) { + LOG.info("({}) could not establish server connection: {}", AUTHOR, error); + } +} \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-writer/src/main/resources/application.properties b/moo-ui-shell/moo-ui-shell-writer/src/main/resources/application.properties new file mode 100644 index 0000000..4247a02 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/src/main/resources/application.properties @@ -0,0 +1,9 @@ +# Moo server + +# host where server is running on +server.host=localhost +# port on which server accepts client connections +server.port=8000 + +# Moo shell commons +shell.commons.throttle=50 \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-writer/src/main/resources/logback.xml b/moo-ui-shell/moo-ui-shell-writer/src/main/resources/logback.xml new file mode 100644 index 0000000..040ba8d --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/src/main/resources/logback.xml @@ -0,0 +1,57 @@ + + + + + + + + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%level]) %clr(%logger [%M]){cyan} - %m%n + + + + + + + + + %date{yyyy-MM-dd HH:mm:ss} - %msg%n + + + + + ${LOG_DIR}/moo-shell-writer.log + + + ${LOG_DIR}/moo-shell-writer.%d{yyyy-MM-dd}.log + + 30 + 1GB + + + + %date{yyyy-MM-dd HH:mm:ss} [%level] %logger [%M] - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/moo-ui-shell/moo-ui-shell-writer/src/test/java/pl/zimowski/moo/ui/shell/writer/AppTest.java b/moo-ui-shell/moo-ui-shell-writer/src/test/java/pl/zimowski/moo/ui/shell/writer/AppTest.java new file mode 100644 index 0000000..aa2b176 --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/src/test/java/pl/zimowski/moo/ui/shell/writer/AppTest.java @@ -0,0 +1,139 @@ +package pl.zimowski.moo.ui.shell.writer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.contrib.java.lang.system.TextFromStandardInputStream.emptyStandardInputStream; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.TextFromStandardInputStream; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import pl.zimowski.moo.api.ClientAction; +import pl.zimowski.moo.api.ClientEvent; +import pl.zimowski.moo.test.utils.EventAwareClientHandlerMock; +import pl.zimowski.moo.test.utils.MockLogger; +import pl.zimowski.moo.test.utils.MooTest; +import pl.zimowski.moo.ui.shell.commons.ExecutionThrottling; +import pl.zimowski.moo.ui.shell.commons.ShutdownAgent; + +/** + * Ensures that {@link App} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class AppTest extends MooTest { + + @Rule + public final TextFromStandardInputStream systemInMock = emptyStandardInputStream(); + + @InjectMocks + private App writer; + + @Spy + private MockLogger mockLog; + + @Spy + private EventAwareClientHandlerMock clientHandler; + + @Mock + private EventHandler eventHandler; + + @Mock + private ShutdownAgent shutdownAgent; + + @Mock + private ExecutionThrottling throttler; + + + /** + * Ensures that app can establish connection, produce user nick and + * generate chat message before terminating. + */ + @Test + public void shouldProduceChatWithoutNickAndExit() throws Exception { + + List events = produceChatAndExit(" "); + + assertTrue(events.size() == 5); + + assertEquals(ClientAction.GenerateNick, events.get(0).getAction()); + assertEquals(ClientAction.Signin, events.get(1).getAction()); + assertEquals(ClientAction.Message, events.get(2).getAction()); + assertEquals(ClientAction.Signoff, events.get(3).getAction()); + assertEquals(ClientAction.Disconnect, events.get(4).getAction()); + } + + @Test + public void shouldProduceChatWithNickAndExit() throws Exception { + + List events = produceChatAndExit("misiu"); + + assertTrue(events.size() == 4); + + assertEquals(ClientAction.Signin, events.get(0).getAction()); + assertEquals(ClientAction.Message, events.get(1).getAction()); + assertEquals(ClientAction.Signoff, events.get(2).getAction()); + assertEquals(ClientAction.Disconnect, events.get(3).getAction()); + } + + /** + * @param nick to use or empty string if one should be auto generated + * @return events produced by this setup + */ + private List produceChatAndExit(String nick) throws Exception { + + List eventList = new ArrayList<>(); + + clientHandler.setEventList(eventList); + when(eventHandler.getClientId()).thenReturn("foo-bar"); + when(eventHandler.getNick()).thenReturn("Rocky"); + + systemInMock.provideLines(nick, "Howdy", "moo:exit"); + + writer.run(null); + writer.shutdown(); + + return eventList; + } + + @Test + public void shouldNotConnect() throws Exception { + + List eventList = new ArrayList<>(); + + clientHandler.setEventList(eventList); + when(clientHandler.connect(eventHandler)).thenReturn(false); + + writer.run(null); + + assertTrue(eventList.isEmpty()); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrottleWhenMissingClientId() throws Exception { + + doThrow(IllegalStateException.class).when(throttler).throttle(); + + writer.run(null); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrottleWhenMissingNick() throws Exception { + + doThrow(IllegalStateException.class).when(throttler).throttle(); + when(eventHandler.getClientId()).thenReturn("foo-bar"); + + systemInMock.provideLines(" ", "Howdy", "moo:exit"); + + writer.run(null); + } +} diff --git a/moo-ui-shell/moo-ui-shell-writer/src/test/java/pl/zimowski/moo/ui/shell/writer/EventHandlerTest.java b/moo-ui-shell/moo-ui-shell-writer/src/test/java/pl/zimowski/moo/ui/shell/writer/EventHandlerTest.java new file mode 100644 index 0000000..7d10b1c --- /dev/null +++ b/moo-ui-shell/moo-ui-shell-writer/src/test/java/pl/zimowski/moo/ui/shell/writer/EventHandlerTest.java @@ -0,0 +1,112 @@ +package pl.zimowski.moo.ui.shell.writer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import pl.zimowski.moo.api.ClientHandling; +import pl.zimowski.moo.api.ServerAction; +import pl.zimowski.moo.api.ServerEvent; +import pl.zimowski.moo.test.utils.MooTest; + +/** + * Ensures that {@link EventHandler} operates as expected. + * + * @since 1.2.0 + * @author Adam Zimowski (mrazjava) + */ +public class EventHandlerTest extends MooTest { + + @InjectMocks + private EventHandler eventHandler; + + @Mock + private ClientHandling clientHandler; + + /** + * stubs connected state of a client handler + */ + private boolean connected = true; + + + @Test + public void shouldReportCorrectAuthor() { + assertEquals(EventHandler.AUTHOR, eventHandler.getAuthor()); + } + + @Test + public void shouldHandleConnectionEstablishedEvent() { + + String clientId = "foo-bar"; + + ServerEvent event = new ServerEvent(ServerAction.ConnectionEstablished) + .withClientId(clientId); + + assertNull(eventHandler.getClientId()); + eventHandler.onEvent(event); + assertEquals(clientId, eventHandler.getClientId()); + } + + @Test + public void shouldHandleNickGeneratedEvent() { + + String nick = "johnie"; + + ServerEvent event = new ServerEvent(ServerAction.NickGenerated) + .withMessage(nick); + + assertNull(eventHandler.getNick()); + eventHandler.onEvent(event); + assertEquals(nick, eventHandler.getNick()); + } + + @Test + public void shouldHandleServerExit() { + + ServerEvent event = new ServerEvent(ServerAction.ServerExit); + + Mockito.doAnswer(new Answer() { + + @Override + public Boolean answer(InvocationOnMock arg0) throws Throwable { + return connected; + } + + }).when(clientHandler).isConnected(); + + Mockito.doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock arg0) throws Throwable { + connected = false; + return null; + } + + }).when(clientHandler).disconnect(); + + + assertTrue(clientHandler.isConnected()); + eventHandler.onEvent(event); + assertFalse(clientHandler.isConnected()); + } + + @Test + public void shouldHandleOnBeforeServerConnect() { + + eventHandler.onBeforeServerConnect("localhost", 8000); + } + + @Test + public void shouldHandleOnConnectToServerError() { + + eventHandler.onConnectToServerError("foo bar"); + } +} diff --git a/moo-ui-shell/pom.xml b/moo-ui-shell/pom.xml new file mode 100644 index 0000000..96142d4 --- /dev/null +++ b/moo-ui-shell/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + pl.zimowski + moo + 1.2.0-SNAPSHOT + + moo-ui-shell + 1.0.0-SNAPSHOT + pom + Moo UI (shell) + Parent POM for Moo shell UI. + + + + javax.enterprise + cdi-api + + + org.hibernate.validator + hibernate-validator + + + org.springframework.boot + spring-boot-starter + + + org.apache.commons + commons-lang3 + + + pl.zimowski + moo-api + + + pl.zimowski + moo-commons + + + pl.zimowski + moo-client-socket + runtime + + + com.github.stefanbirkner + system-rules + + + + + moo-ui-shell-reader + moo-ui-shell-writer + moo-ui-shell-commons + + \ No newline at end of file diff --git a/moo-ui-shell/readme.md b/moo-ui-shell/readme.md new file mode 100644 index 0000000..8b16848 --- /dev/null +++ b/moo-ui-shell/readme.md @@ -0,0 +1,15 @@ +# Moo Shell +--------------------- +Console (text) based UI for Moo. This is a default Moo UI. Powered by +two components: reader and writer. + +## Why reader and writer? +--------------------- +Without advanced text UI such as curses, which natively is not available to +Java, it is near impossible to control input and output in a synchronized +way. Imagine typing a message into System.in and getting overriden by incoming +message from System.out. That's really the bulk of the problem. Separating +chat reading and writing into their own applications not only solves the +problem, but allows for additional flexibility. For instance, if one wants +to only observe public chat messages, there is no need for writing +functionality, so writer does not need to be started. \ No newline at end of file diff --git a/moo-ui-web1/pom.xml b/moo-ui-web1/pom.xml deleted file mode 100644 index 9a9532c..0000000 --- a/moo-ui-web1/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - 4.0.0 - - pl.zimowski - moo - 1.1.0-FINAL - - moo-ui-web1 - - - - pl.zimowski - moo-api - - - org.apache.commons - commons-lang3 - - - javax.enterprise - cdi-api - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-websocket - ${spring.version} - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.version} - - - - - - pl.zimowski.moo.ui.web1.App - 1.5.6.RELEASE - UTF-8 - UTF-8 - - - Moo (client: angular, rest, springboot) - 1.1.0-SNAPSHOT - \ No newline at end of file diff --git a/moo-ui-web1/readme.md b/moo-ui-web1/readme.md deleted file mode 100644 index 71ed334..0000000 --- a/moo-ui-web1/readme.md +++ /dev/null @@ -1,37 +0,0 @@ -# Moo - browser based UI client -===================== - -Very simple single page web app based on Angular, websockets, REST and Spring -Boot. Graphical client for moo-server. - -## Running ------------ -Before starting the client, ensure that server is running: -``` -cd moo/moo-server -mvn spring-boot:run -``` -Once server is up and running, angular client can be started: -``` -cd moo/moo-ui-angular -mvn spring-boot:run -``` -The app immediately listens (and records) server activity, even if page was -not loaded yet. - -Point your favorite browser at: -``` -localhost:8080 -``` - -## Limitations ------------ -UI server side does not track users. It probably should, using HttpSession. As -a result, re-loading page aborts established chat link with the server without -notifying the server. Server continues to track an orphaned user link (reports -incorrect stats). This can be resolved by UI server tracking user session with -timeouts and informing server when UI user is no longer active. In addition, -the moo server should probably do something similar at the connection level, -so if connection had no activity in some time period, it should be ejected. UI -client would then have to introduce some sort of a heartbeat to prevent its -global connection being ejected by the server if there was no chat activity. \ No newline at end of file diff --git a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/App.java b/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/App.java deleted file mode 100644 index d7972d0..0000000 --- a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/App.java +++ /dev/null @@ -1,20 +0,0 @@ -package pl.zimowski.moo.ui.web1; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * UI processing backend entry point. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -@SpringBootApplication -public class App { - - public static final String SESSION_ATTR_NICK = "nick"; - - public static void main(String[] args) { - SpringApplication.run(App.class, args); - } -} \ No newline at end of file diff --git a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/EventProvider.java b/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/EventProvider.java deleted file mode 100644 index 9d9ba42..0000000 --- a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/EventProvider.java +++ /dev/null @@ -1,150 +0,0 @@ -package pl.zimowski.moo.ui.web1; - -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.net.Socket; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import javax.servlet.http.HttpSession; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import pl.zimowski.moo.api.ClientAction; -import pl.zimowski.moo.api.ClientEvent; -import pl.zimowski.moo.api.ServerEvent; - -/** - * Manager of server connection. Exposes REST endpoints for angular UI - * communication. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -@RestController -@Component -public class EventProvider { - - private static final Logger log = LoggerFactory.getLogger(EventProvider.class); - - @Inject - private SimpMessagingTemplate template; - - private ServerListener serverListener; - - private Socket socket; - - - @PostConstruct - public void init() { - - ExecutorService executor = Executors.newSingleThreadExecutor(); - - try { - log.debug("initializing...."); - socket = new Socket("localhost", 8000); - serverListener = new ServerListener(socket, template); - executor.submit(serverListener); - } - catch(IOException e) { - log.error("could not connect to server: {}", e.getMessage()); - } - - log.debug("READY!"); - } - - /** - * User invoked a login operation on the client. - * - * @param nick of the user that logged in - * @return {@code true} on success - */ - @RequestMapping(value = "/moo/login", method = RequestMethod.POST) - public boolean mooLogin(@RequestBody String nick) { - - log.debug("login: [{}]", nick); - - ClientEvent event = new ClientEvent(ClientAction.Signin).withAuthor(nick); - return sendClientEvent(event); - } - - /** - * User invoked a logout operation on a client. - * - * @param nick of the user that logged out - * @return {@code true} on success - */ - @RequestMapping(value = "/moo/logout", method = RequestMethod.POST) - public boolean mooLogout(@RequestBody String nick, HttpSession httpSession) { - - log.debug("logout: {}", nick); - - if(httpSession != null) { - httpSession.removeAttribute(App.SESSION_ATTR_NICK); - } - - ClientEvent event = new ClientEvent(ClientAction.Signoff).withAuthor(nick); - return sendClientEvent(event); - } - - /** - * User transmitted a chat message. - * - * @param event holding information about a trasmitted message - * @return {@code true} on success - */ - @RequestMapping("/moo/msg") - public boolean mooMsg(@RequestBody Map event) { - - log.debug(event.toString()); - String nickName = event.get("nickName"); - String message = event.get("msg"); - ClientEvent clientEvent = new ClientEvent(ClientAction.Message) - .withAuthor(nickName).withMessage(message); - return sendClientEvent(clientEvent); - } - - /** - * Allows UI client to peek at few recently processed messages. Useful - * when initializing new view. - * - * @return recently processed messages - */ - @RequestMapping("/latest-events") - public List getCachedEvents() { - return serverListener.getRecentEvents(); - } - - private boolean sendClientEvent(ClientEvent event) { - - boolean status = false; - - if(socket == null) { - log.warn("null socket"); - return status; - } - - try { - ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); - out.writeObject(event); - out.flush(); - status = true; - } - catch(IOException e) { - log.error("message transmission failed: {}", e.getMessage()); - } - - return status; - } -} \ No newline at end of file diff --git a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/HttpSessionListenerImpl.java b/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/HttpSessionListenerImpl.java deleted file mode 100644 index 534bc21..0000000 --- a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/HttpSessionListenerImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package pl.zimowski.moo.ui.web1; - -import javax.inject.Inject; -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionEvent; -import javax.servlet.http.HttpSessionListener; - -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Component; - -/** - * Standard listener for create/destroy events of {@link HttpSession}. - * - * @since 1.1.0 - * @author Adam Zimowski (mrazjava) - */ -@Component -public class HttpSessionListenerImpl implements HttpSessionListener { - - private static final Logger log = LoggerFactory.getLogger(HttpSessionListenerImpl.class); - - @Inject - private EventProvider eventProvider; - - @Inject - private SimpMessagingTemplate sender; - - - @Override - public void sessionCreated(HttpSessionEvent event) { - - if(log.isDebugEnabled()) { - HttpSession session = event.getSession(); - log.debug("created session {}", session.getId()); - } - } - - @Override - public void sessionDestroyed(HttpSessionEvent event) { - - HttpSession session = event.getSession(); - String nick = (String)session.getAttribute(App.SESSION_ATTR_NICK); - - log.debug("destrogying session {} | user = {}", session.getId(), nick); - - if(StringUtils.isNotEmpty(nick)) { - // user never signed off and session was destoryed, so invoke the - // signoff event; had user signed off, session would no longer - // contain nick attribute - eventProvider.mooLogout(nick, null); - sender.convertAndSend("/topic/session-expired", new String[] { nick }); - } - } -} \ No newline at end of file diff --git a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/ServerListener.java b/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/ServerListener.java deleted file mode 100644 index edbaacf..0000000 --- a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/ServerListener.java +++ /dev/null @@ -1,81 +0,0 @@ -package pl.zimowski.moo.ui.web1; - -import java.io.EOFException; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.net.Socket; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.messaging.simp.SimpMessageSendingOperations; - -import pl.zimowski.moo.api.ServerAction; -import pl.zimowski.moo.api.ServerEvent; - -/** - * Listens for incoming events from the server and echoes them back to the - * client. - * - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -public class ServerListener extends Thread { - - private static final Logger log = LoggerFactory.getLogger(ServerListener.class); - - private Socket socket; - - private SimpMessageSendingOperations sender; - - private LinkedList recentMessages; - - - public ServerListener(Socket socket, SimpMessageSendingOperations sender) { - this.socket = socket; - this.sender = sender; - recentMessages = new LinkedList<>(); - } - - public List getRecentEvents() { - return Collections.unmodifiableList(recentMessages); - } - - @Override - public void run() { - - try { - while(true) { - ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); - ServerEvent serverEvent = (ServerEvent)in.readObject(); - - log.info(serverEvent.toString()); - - if(serverEvent.getAction() == ServerAction.ConnectionEstablished) { - continue; - } - - if(serverEvent.getAction() == ServerAction.NickGenerated) { - String nick = serverEvent.getMessage(); - serverEvent.setMessage(String.format("You will be known as '%s'", nick)); - sender.convertAndSend("/topic/nick-generated", new String[] { nick }); - } - - if(recentMessages.size() == 15) - recentMessages.removeFirst(); - - recentMessages.add(serverEvent); - - sender.convertAndSend("/topic/viewchats", recentMessages); - } - } - catch(EOFException e) { - log.info("(client) connection terminated by server; bye!"); - } - catch (IOException | ClassNotFoundException e) { - log.error("unexpected connection error: {}; aborting!", e.getMessage()); - } - } -} \ No newline at end of file diff --git a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/SocketConfig.java b/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/SocketConfig.java deleted file mode 100644 index b557aa4..0000000 --- a/moo-ui-web1/src/main/java/pl/zimowski/moo/ui/web1/SocketConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -package pl.zimowski.moo.ui.web1; - -import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.simp.config.MessageBrokerRegistry; -import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; -import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; -import org.springframework.web.socket.config.annotation.StompEndpointRegistry; - -/** - * @since 1.0.0 - * @author Adam Zimowski (mrazjava) - */ -@Configuration -@EnableWebSocketMessageBroker -public class SocketConfig extends AbstractWebSocketMessageBrokerConfigurer { - - @Override - public void configureMessageBroker(MessageBrokerRegistry config) { - - config.enableSimpleBroker("/topic"); - config.setApplicationDestinationPrefixes("/app"); - } - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/chat-websocket").setAllowedOrigins("*"). withSockJS(); - } -} diff --git a/moo-ui-web1/src/main/resources/application.properties b/moo-ui-web1/src/main/resources/application.properties deleted file mode 100644 index dc165c6..0000000 --- a/moo-ui-web1/src/main/resources/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -server.session.timeout=10 -server.session-timeout=10 \ No newline at end of file diff --git a/moo-ui-web1/src/main/resources/logback-spring.xml b/moo-ui-web1/src/main/resources/logback-spring.xml deleted file mode 100644 index c9f97b6..0000000 --- a/moo-ui-web1/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%level]) %clr(%logger [%M]){cyan} - %m%n - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/moo-ui-web1/src/main/resources/static/assets/css/style.css b/moo-ui-web1/src/main/resources/static/assets/css/style.css deleted file mode 100644 index 8915f78..0000000 --- a/moo-ui-web1/src/main/resources/static/assets/css/style.css +++ /dev/null @@ -1,39 +0,0 @@ -.liveChat { - color: green; - width: 100%; - height: 350px; - border: solid 1px #E6E6E6; - overflow-y: scroll; - margin-top: 10px; - } -li{ - list-style: none; -} -#login { - width: 150px; -} -#loginPanel { - padding: 5px; - background-color: #F7F8E0; - float:right; -} -#logoutButton { - display: none; -} -#msgOutPanel { - padding: 5px; - background-color: #F6CECE; -} -#msgOut { - width: 100%; -} -#msgOutPanel { - display: none; -} -#sessionExpired { - display: none; - -} -#sessionExpired span { - color: red; -} diff --git a/moo-ui-web1/src/main/resources/static/assets/images/moo.png b/moo-ui-web1/src/main/resources/static/assets/images/moo.png deleted file mode 100644 index f7ab5df..0000000 Binary files a/moo-ui-web1/src/main/resources/static/assets/images/moo.png and /dev/null differ diff --git a/moo-ui-web1/src/main/resources/static/assets/js/controller.js b/moo-ui-web1/src/main/resources/static/assets/js/controller.js deleted file mode 100644 index 7c28d80..0000000 --- a/moo-ui-web1/src/main/resources/static/assets/js/controller.js +++ /dev/null @@ -1,79 +0,0 @@ -var app = angular.module('mooChatDemo', ['ngStomp']); - -app.controller('MainController', function ($stomp, $scope, $http) { - - $scope.mooLogin = function() { - var nickTxt = angular.element(document.querySelector('#nickName'))[0]; - var nick = $scope.usr == undefined || $scope.usr.nickName == '' ? ' ' : $scope.usr.nickName; - - console.log("moo login: [" + nick + "]"); - angular.element(document.querySelector('#loginButton'))[0].style.display = "none"; - angular.element(document.querySelector('#logoutButton'))[0].style.display = "inline"; - angular.element(document.querySelector('#sessionExpired'))[0].style.display = "none"; - angular.element(document.querySelector('#msgOutPanel'))[0].style.display = "block"; - nickTxt.disabled = true; - - $http.post('/moo/login', nick).success(function(data) { - console.log("login result: " + data); - }); - }; - - $scope.mooLogout = function() { - console.log("moo logout: " + $scope.usr.nickName); - - $http.post('/moo/logout', $scope.usr.nickName).success(function(data) { - console.log("logout result: " + data); - }); - - if($scope.usr.autogen) { - $scope.usr.nickName = ''; - $scope.usr.autogen = false; - } - - resetUiAfterLogout(); - }; - - $scope.events = []; - - $http.get('/latest-events').success(function(data) { - $scope.events = data; - }); - $stomp.connect('http://localhost:8080/chat-websocket', {}) - .then(function (frame) { - var subscription1 = $stomp.subscribe('/topic/viewchats', - function (payload, headers, res) { - $scope.events = payload; - $scope.$apply($scope.events); - var chatDiv = angular.element(document.querySelector('.liveChat'))[0]; - chatDiv.scrollTop = chatDiv.scrollHeight; - }); - var subscription2 = $stomp.subscribe('/topic/session-expired', - function (payload, headers, res) { - console.log('session expired! (' + payload + ')'); - resetUiAfterLogout(); - angular.element(document.querySelector('#sessionExpired'))[0].style.display = "inline"; - }); - var subscription3 = $stomp.subscribe('/topic/nick-generated', - function (payload, headers, res) { - console.log('server generated nick:! (' + payload + ')'); - $scope.usr = {nickName: payload[0], autogen: true}; - }); - }); -}); - -app.controller('MooMsgController', function ($scope, $http) { - - $scope.mooMsg = function() { - console.log("msg out: " + $scope.usr); - $http.post('/moo/msg', $scope.usr); - angular.element(document.querySelector('#msgOut'))[0].value = ''; - }; -}); - -function resetUiAfterLogout() { - - angular.element(document.querySelector('#loginButton'))[0].style.display = "inline"; - angular.element(document.querySelector('#logoutButton'))[0].style.display = "none"; - angular.element(document.querySelector('#nickName'))[0].disabled = false; - angular.element(document.querySelector('#msgOutPanel'))[0].style.display = "none"; -} \ No newline at end of file diff --git a/moo-ui-web1/src/main/resources/static/assets/js/ng-stomp.standalone.min.js b/moo-ui-web1/src/main/resources/static/assets/js/ng-stomp.standalone.min.js deleted file mode 100644 index cb98a95..0000000 --- a/moo-ui-web1/src/main/resources/static/assets/js/ng-stomp.standalone.min.js +++ /dev/null @@ -1,10 +0,0 @@ -!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;"undefined"!=typeof window?b=window:"undefined"!=typeof global?b=global:"undefined"!=typeof self&&(b=self),b.SockJS=a()}}(function(){var a;return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;ge;e++)d[e-1]=arguments[e];for(var f=0;f1?this._listeners[a]=c.slice(0,d).concat(c.slice(d+1)):delete this._listeners[a]):void 0}},c.prototype.dispatchEvent=function(){var a=arguments[0],b=a.type,c=1===arguments.length?[a]:Array.apply(null,arguments);if(this["on"+b]&&this["on"+b].apply(this,c),b in this._listeners)for(var d=this._listeners[b],e=0;e=3e3&&4999>=a}a("./shims");var f,g=a("url-parse"),h=a("inherits"),i=a("json3"),j=a("./utils/random"),k=a("./utils/escape"),l=a("./utils/url"),m=a("./utils/event"),n=a("./utils/transport"),o=a("./utils/object"),p=a("./utils/browser"),q=a("./utils/log"),r=a("./event/event"),s=a("./event/eventtarget"),t=a("./location"),u=a("./event/close"),v=a("./event/trans-message"),w=a("./info-receiver");h(d,s),d.prototype.close=function(a,b){if(a&&!e(a))throw new Error("InvalidAccessError: Invalid code");if(b&&b.length>123)throw new SyntaxError("reason argument has an invalid length");if(this.readyState!==d.CLOSING&&this.readyState!==d.CLOSED){var c=!0;this._close(a||1e3,b||"Normal closure",c)}},d.prototype.send=function(a){if("string"!=typeof a&&(a=""+a),this.readyState===d.CONNECTING)throw new Error("InvalidStateError: The connection has not been established yet");this.readyState===d.OPEN&&this._transport.send(k.quote(a))},d.version=a("./version"),d.CONNECTING=0,d.OPEN=1,d.CLOSING=2,d.CLOSED=3,d.prototype._receiveInfo=function(a,b){if(this._ir=null,!a)return void this._close(1002,"Cannot connect to server");this._rto=this.countRTO(b),this._transUrl=a.base_url?a.base_url:this.url,a=o.extend(a,this._urlInfo);var c=f.filterToEnabled(this._transportsWhitelist,a);this._transports=c.main,this._connect()},d.prototype._connect=function(){for(var a=this._transports.shift();a;a=this._transports.shift()){if(a.needBody&&(!c.document.body||"undefined"!=typeof c.document.readyState&&"complete"!==c.document.readyState&&"interactive"!==c.document.readyState))return this._transports.unshift(a),void m.attachEvent("load",this._connect.bind(this));var b=this._rto*a.roundTrips||5e3;this._transportTimeoutId=setTimeout(this._transportTimeout.bind(this),b);var d=l.addPath(this._transUrl,"/"+this._server+"/"+this._generateSessionId()),e=this._transportOptions[a.transportName],f=new a(d,this._transUrl,e);return f.on("message",this._transportMessage.bind(this)),f.once("close",this._transportClose.bind(this)),f.transportName=a.transportName,void(this._transport=f)}this._close(2e3,"All transports failed",!1)},d.prototype._transportTimeout=function(){this.readyState===d.CONNECTING&&this._transportClose(2007,"Transport timed out")},d.prototype._transportMessage=function(a){var b,c=this,d=a.slice(0,1),e=a.slice(1);switch(d){case"o":return void this._open();case"h":return void this.dispatchEvent(new r("heartbeat"))}if(e)try{b=i.parse(e)}catch(a){}if("undefined"!=typeof b)switch(d){case"a":Array.isArray(b)&&b.forEach(function(a){c.dispatchEvent(new v(a))});break;case"m":this.dispatchEvent(new v(b));break;case"c":Array.isArray(b)&&2===b.length&&this._close(b[0],b[1],!0)}},d.prototype._transportClose=function(a,b){return this._transport&&(this._transport.removeAllListeners(),this._transport=null,this.transport=null),e(a)||2e3===a||this.readyState!==d.CONNECTING?void this._close(a,b):void this._connect()},d.prototype._open=function(){this.readyState===d.CONNECTING?(this._transportTimeoutId&&(clearTimeout(this._transportTimeoutId),this._transportTimeoutId=null),this.readyState=d.OPEN,this.transport=this._transport.transportName,this.dispatchEvent(new r("open"))):this._close(1006,"Server lost session")},d.prototype._close=function(a,b,c){var e=!1;if(this._ir&&(e=!0,this._ir.close(),this._ir=null),this._transport&&(this._transport.close(),this._transport=null,this.transport=null),this.readyState===d.CLOSED)throw new Error("InvalidStateError: SockJS has already been closed");this.readyState=d.CLOSING,setTimeout(function(){this.readyState=d.CLOSED,e&&this.dispatchEvent(new r("error"));var f=new u("close");f.wasClean=c||!1,f.code=a||1e3,f.reason=b,this.dispatchEvent(f),this.onmessage=this.onclose=this.onerror=null}.bind(this),0)},d.prototype.countRTO=function(a){return a>100?4*a:300+a},b.exports=function(b){return f=n(b),a("./iframe-bootstrap")(d,b),d}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./event/close":2,"./event/event":4,"./event/eventtarget":5,"./event/trans-message":6,"./iframe-bootstrap":8,"./info-receiver":12,"./location":13,"./shims":15,"./utils/browser":44,"./utils/escape":45,"./utils/event":46,"./utils/log":48,"./utils/object":49,"./utils/random":50,"./utils/transport":51,"./utils/url":52,"./version":53,debug:void 0,inherits:54,json3:55,"url-parse":56}],15:[function(){"use strict";function a(a){var b=+a;return b!==b?b=0:0!==b&&b!==1/0&&b!==-(1/0)&&(b=(b>0||-1)*Math.floor(Math.abs(b))),b}function b(a){return a>>>0}function c(){}var d,e=Array.prototype,f=Object.prototype,g=Function.prototype,h=String.prototype,i=e.slice,j=f.toString,k=function(a){return"[object Function]"===f.toString.call(a)},l=function(a){return"[object Array]"===j.call(a)},m=function(a){return"[object String]"===j.call(a)},n=Object.defineProperty&&function(){try{return Object.defineProperty({},"x",{}),!0}catch(a){return!1}}();d=n?function(a,b,c,d){!d&&b in a||Object.defineProperty(a,b,{configurable:!0,enumerable:!1,writable:!0,value:c})}:function(a,b,c,d){!d&&b in a||(a[b]=c)};var o=function(a,b,c){for(var e in b)f.hasOwnProperty.call(b,e)&&d(a,e,b[e],c)},p=function(a){if(null==a)throw new TypeError("can't convert "+a+" to object");return Object(a)};o(g,{bind:function(a){var b=this;if(!k(b))throw new TypeError("Function.prototype.bind called on incompatible "+b);for(var d=i.call(arguments,1),e=function(){if(this instanceof j){var c=b.apply(this,d.concat(i.call(arguments)));return Object(c)===c?c:this}return b.apply(a,d.concat(i.call(arguments)))},f=Math.max(0,b.length-d.length),g=[],h=0;f>h;h++)g.push("$"+h);var j=Function("binder","return function ("+g.join(",")+"){ return binder.apply(this, arguments); }")(e);return b.prototype&&(c.prototype=b.prototype,j.prototype=new c,c.prototype=null),j}}),o(Array,{isArray:l});var q=Object("a"),r="a"!==q[0]||!(0 in q),s=function(a){var b=!0,c=!0;return a&&(a.call("foo",function(a,c,d){"object"!=typeof d&&(b=!1)}),a.call([1],function(){c="string"==typeof this},"x")),!!a&&b&&c};o(e,{forEach:function(a){var b=p(this),c=r&&m(this)?this.split(""):b,d=arguments[1],e=-1,f=c.length>>>0;if(!k(a))throw new TypeError;for(;++e>>0;if(!d)return-1;var e=0;for(arguments.length>1&&(e=a(arguments[1])),e=e>=0?e:Math.max(0,d+e);d>e;e++)if(e in c&&c[e]===b)return e;return-1}},t);var u=h.split;2!=="ab".split(/(?:ab)*/).length||4!==".".split(/(.?)(.?)/).length||"t"==="tesst".split(/(s)*/)[1]||4!=="test".split(/(?:)/,-1).length||"".split(/.?/).length||".".split(/()()/).length>1?!function(){var a=void 0===/()??/.exec("")[1];h.split=function(c,d){var f=this;if(void 0===c&&0===d)return[];if("[object RegExp]"!==j.call(c))return u.call(this,c,d);var g,h,i,k,l=[],m=(c.ignoreCase?"i":"")+(c.multiline?"m":"")+(c.extended?"x":"")+(c.sticky?"y":""),n=0;for(c=new RegExp(c.source,m+"g"),f+="",a||(g=new RegExp("^"+c.source+"$(?!\\s)",m)),d=void 0===d?-1>>>0:b(d);(h=c.exec(f))&&(i=h.index+h[0].length,!(i>n&&(l.push(f.slice(n,h.index)),!a&&h.length>1&&h[0].replace(g,function(){for(var a=1;a1&&h.index=d)));)c.lastIndex===h.index&&c.lastIndex++;return n===f.length?(k||!c.test(""))&&l.push(""):l.push(f.slice(n)),l.length>d?l.slice(0,d):l}}():"0".split(void 0,0).length&&(h.split=function(a,b){return void 0===a&&0===b?[]:u.call(this,a,b)});var v="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff",w="​",x="["+v+"]",y=new RegExp("^"+x+x+"*"),z=new RegExp(x+x+"*$"),A=h.trim&&(v.trim()||!w.trim());o(h,{trim:function(){if(void 0===this||null===this)throw new TypeError("can't convert "+this+" to object");return String(this).replace(y,"").replace(z,"")}},A);var B=h.substr,C="".substr&&"b"!=="0b".substr(-1);o(h,{substr:function(a,b){return B.call(this,0>a&&(a=this.length+a)<0?0:a,b)}},C)},{}],16:[function(a,b){"use strict";b.exports=[a("./transport/websocket"),a("./transport/xhr-streaming"),a("./transport/xdr-streaming"),a("./transport/eventsource"),a("./transport/lib/iframe-wrap")(a("./transport/eventsource")),a("./transport/htmlfile"),a("./transport/lib/iframe-wrap")(a("./transport/htmlfile")),a("./transport/xhr-polling"),a("./transport/xdr-polling"),a("./transport/lib/iframe-wrap")(a("./transport/xhr-polling")),a("./transport/jsonp-polling")]},{"./transport/eventsource":20,"./transport/htmlfile":21,"./transport/jsonp-polling":23,"./transport/lib/iframe-wrap":26,"./transport/websocket":38,"./transport/xdr-polling":39,"./transport/xdr-streaming":40,"./transport/xhr-polling":41,"./transport/xhr-streaming":42}],17:[function(a,b){(function(c){"use strict";function d(a,b,c,d){var f=this;e.call(this),setTimeout(function(){f._start(a,b,c,d)},0)}var e=a("events").EventEmitter,f=a("inherits"),g=a("../../utils/event"),h=a("../../utils/url"),i=c.XMLHttpRequest;f(d,e),d.prototype._start=function(a,b,c,e){var f=this;try{this.xhr=new i}catch(a){}if(!this.xhr)return this.emit("finish",0,"no xhr support"),void this._cleanup();b=h.addQuery(b,"t="+ +new Date),this.unloadRef=g.unloadAdd(function(){f._cleanup(!0)});try{this.xhr.open(a,b,!0),this.timeout&&"timeout"in this.xhr&&(this.xhr.timeout=this.timeout,this.xhr.ontimeout=function(){f.emit("finish",0,""),f._cleanup(!1)})}catch(a){return this.emit("finish",0,""),void this._cleanup(!1)}if(e&&e.noCredentials||!d.supportsCORS||(this.xhr.withCredentials="true"),e&&e.headers)for(var j in e.headers)this.xhr.setRequestHeader(j,e.headers[j]);this.xhr.onreadystatechange=function(){if(f.xhr){var a,b,c=f.xhr;switch(c.readyState){case 3:try{b=c.status,a=c.responseText}catch(a){}1223===b&&(b=204),200===b&&a&&a.length>0&&f.emit("chunk",b,a);break;case 4:b=c.status,1223===b&&(b=204),(12005===b||12029===b)&&(b=0),f.emit("finish",b,c.responseText),f._cleanup(!1)}}};try{f.xhr.send(c)}catch(a){f.emit("finish",0,""),f._cleanup(!1)}},d.prototype._cleanup=function(a){if(this.xhr){if(this.removeAllListeners(),g.unloadDel(this.unloadRef),this.xhr.onreadystatechange=function(){},this.xhr.ontimeout&&(this.xhr.ontimeout=null),a)try{this.xhr.abort()}catch(a){}this.unloadRef=this.xhr=null}},d.prototype.close=function(){this._cleanup(!0)},d.enabled=!!i;var j=["Active"].concat("Object").join("X");!d.enabled&&j in c&&(i=function(){try{return new c[j]("Microsoft.XMLHTTP")}catch(a){return null}},d.enabled=!!new i);var k=!1;try{k="withCredentials"in new i}catch(a){}d.supportsCORS=k,b.exports=d}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../../utils/event":46,"../../utils/url":52,debug:void 0,events:3,inherits:54}],18:[function(a,b){(function(a){b.exports=a.EventSource}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],19:[function(a,b){(function(a){"use strict";var c=a.WebSocket||a.MozWebSocket;c&&(b.exports=function(a){return new c(a)})}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],20:[function(a,b){"use strict";function c(a){if(!c.enabled())throw new Error("Transport created when disabled");e.call(this,a,"/eventsource",f,g)}var d=a("inherits"),e=a("./lib/ajax-based"),f=a("./receiver/eventsource"),g=a("./sender/xhr-cors"),h=a("eventsource");d(c,e),c.enabled=function(){return!!h},c.transportName="eventsource",c.roundTrips=2,b.exports=c},{"./lib/ajax-based":24,"./receiver/eventsource":29,"./sender/xhr-cors":35,eventsource:18,inherits:54}],21:[function(a,b){"use strict";function c(a){if(!e.enabled)throw new Error("Transport created when disabled");g.call(this,a,"/htmlfile",e,f)}var d=a("inherits"),e=a("./receiver/htmlfile"),f=a("./sender/xhr-local"),g=a("./lib/ajax-based");d(c,g),c.enabled=function(a){return e.enabled&&a.sameOrigin},c.transportName="htmlfile",c.roundTrips=2,b.exports=c},{"./lib/ajax-based":24,"./receiver/htmlfile":30,"./sender/xhr-local":37,inherits:54}],22:[function(a,b){"use strict";function c(a,b,d){if(!c.enabled())throw new Error("Transport created when disabled");f.call(this);var e=this;this.origin=h.getOrigin(d),this.baseUrl=d,this.transUrl=b,this.transport=a,this.windowId=k.string(8);var g=h.addPath(d,"/iframe.html")+"#"+this.windowId;this.iframeObj=i.createIframe(g,function(a){e.emit("close",1006,"Unable to load an iframe ("+a+")"),e.close()}),this.onmessageCallback=this._message.bind(this),j.attachEvent("message",this.onmessageCallback)}var d=a("inherits"),e=a("json3"),f=a("events").EventEmitter,g=a("../version"),h=a("../utils/url"),i=a("../utils/iframe"),j=a("../utils/event"),k=a("../utils/random");d(c,f),c.prototype.close=function(){if(this.removeAllListeners(),this.iframeObj){j.detachEvent("message",this.onmessageCallback);try{this.postMessage("c")}catch(a){}this.iframeObj.cleanup(),this.iframeObj=null,this.onmessageCallback=this.iframeObj=null}},c.prototype._message=function(a){if(h.isOriginEqual(a.origin,this.origin)){var b;try{b=e.parse(a.data)}catch(a){return}if(b.windowId===this.windowId)switch(b.type){case"s":this.iframeObj.loaded(),this.postMessage("s",e.stringify([g,this.transport,this.transUrl,this.baseUrl]));break;case"t":this.emit("message",b.data);break;case"c":var c;try{c=e.parse(b.data)}catch(a){return}this.emit("close",c[0],c[1]),this.close()}}},c.prototype.postMessage=function(a,b){this.iframeObj.post(e.stringify({windowId:this.windowId,type:a,data:b||""}),this.origin)},c.prototype.send=function(a){this.postMessage("m",a)},c.enabled=function(){return i.iframeEnabled},c.transportName="iframe",c.roundTrips=2,b.exports=c},{"../utils/event":46,"../utils/iframe":47,"../utils/random":50,"../utils/url":52,"../version":53,debug:void 0,events:3,inherits:54,json3:55}],23:[function(a,b){(function(c){"use strict";function d(a){if(!d.enabled())throw new Error("Transport created when disabled");f.call(this,a,"/jsonp",h,g)}var e=a("inherits"),f=a("./lib/sender-receiver"),g=a("./receiver/jsonp"),h=a("./sender/jsonp");e(d,f),d.enabled=function(){return!!c.document},d.transportName="jsonp-polling",d.roundTrips=1,d.needBody=!0,b.exports=d}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./lib/sender-receiver":28,"./receiver/jsonp":31,"./sender/jsonp":33,inherits:54}],24:[function(a,b){"use strict";function c(a){return function(b,c,d){var e={};"string"==typeof c&&(e.headers={"Content-type":"text/plain"});var g=f.addPath(b,"/xhr_send"),h=new a("POST",g,c,e);return h.once("finish",function(a){return h=null,200!==a&&204!==a?d(new Error("http status "+a)):void d()}),function(){h.close(),h=null;var a=new Error("Aborted");a.code=1e3,d(a)}}}function d(a,b,d,e){g.call(this,a,b,c(e),d,e)}var e=a("inherits"),f=a("../../utils/url"),g=a("./sender-receiver");e(d,g),b.exports=d},{"../../utils/url":52,"./sender-receiver":28,debug:void 0,inherits:54}],25:[function(a,b){"use strict";function c(a,b){e.call(this),this.sendBuffer=[],this.sender=b,this.url=a}var d=a("inherits"),e=a("events").EventEmitter;d(c,e),c.prototype.send=function(a){this.sendBuffer.push(a),this.sendStop||this.sendSchedule()},c.prototype.sendScheduleWait=function(){var a,b=this;this.sendStop=function(){b.sendStop=null,clearTimeout(a)},a=setTimeout(function(){b.sendStop=null,b.sendSchedule()},25)},c.prototype.sendSchedule=function(){var a=this;if(this.sendBuffer.length>0){var b="["+this.sendBuffer.join(",")+"]";this.sendStop=this.sender(this.url,b,function(b){a.sendStop=null,b?(a.emit("close",b.code||1006,"Sending error: "+b),a._cleanup()):a.sendScheduleWait()}),this.sendBuffer=[]}},c.prototype._cleanup=function(){this.removeAllListeners()},c.prototype.stop=function(){this._cleanup(),this.sendStop&&(this.sendStop(),this.sendStop=null)},b.exports=c},{debug:void 0,events:3,inherits:54}],26:[function(a,b){(function(c){"use strict";var d=a("inherits"),e=a("../iframe"),f=a("../../utils/object");b.exports=function(a){function b(b,c){e.call(this,a.transportName,b,c)}return d(b,e),b.enabled=function(b,d){if(!c.document)return!1;var g=f.extend({},d);return g.sameOrigin=!0,a.enabled(g)&&e.enabled()},b.transportName="iframe-"+a.transportName,b.needBody=!0,b.roundTrips=e.roundTrips+a.roundTrips-1,b.facadeTransport=a,b}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../../utils/object":49,"../iframe":22,inherits:54}],27:[function(a,b){"use strict";function c(a,b,c){e.call(this),this.Receiver=a,this.receiveUrl=b,this.AjaxObject=c,this._scheduleReceiver()}var d=a("inherits"),e=a("events").EventEmitter;d(c,e),c.prototype._scheduleReceiver=function(){var a=this,b=this.poll=new this.Receiver(this.receiveUrl,this.AjaxObject);b.on("message",function(b){a.emit("message",b)}),b.once("close",function(c,d){a.poll=b=null,a.pollIsClosing||("network"===d?a._scheduleReceiver():(a.emit("close",c||1006,d),a.removeAllListeners()))})},c.prototype.abort=function(){this.removeAllListeners(),this.pollIsClosing=!0,this.poll&&this.poll.abort()},b.exports=c},{debug:void 0,events:3,inherits:54}],28:[function(a,b){"use strict";function c(a,b,c,d,h){var i=e.addPath(a,b),j=this;f.call(this,a,c),this.poll=new g(d,i,h),this.poll.on("message",function(a){j.emit("message",a)}),this.poll.once("close",function(a,b){j.poll=null,j.emit("close",a,b),j.close()})}var d=a("inherits"),e=a("../../utils/url"),f=a("./buffered-sender"),g=a("./polling");d(c,f),c.prototype.close=function(){this.removeAllListeners(),this.poll&&(this.poll.abort(),this.poll=null),this.stop()},b.exports=c},{"../../utils/url":52,"./buffered-sender":25,"./polling":27,debug:void 0,inherits:54}],29:[function(a,b){"use strict";function c(a){e.call(this);var b=this,c=this.es=new f(a);c.onmessage=function(a){b.emit("message",decodeURI(a.data))},c.onerror=function(a){var d=2!==c.readyState?"network":"permanent";b._cleanup(),b._close(d)}}var d=a("inherits"),e=a("events").EventEmitter,f=a("eventsource");d(c,e),c.prototype.abort=function(){this._cleanup(),this._close("user")},c.prototype._cleanup=function(){var a=this.es;a&&(a.onmessage=a.onerror=null,a.close(),this.es=null)},c.prototype._close=function(a){var b=this;setTimeout(function(){b.emit("close",null,a),b.removeAllListeners()},200)},b.exports=c},{debug:void 0,events:3,eventsource:18,inherits:54}],30:[function(a,b){(function(c){"use strict";function d(a){h.call(this);var b=this;f.polluteGlobalNamespace(),this.id="a"+i.string(6),a=g.addQuery(a,"c="+decodeURIComponent(f.WPrefix+"."+this.id));var e=d.htmlfileEnabled?f.createHtmlfile:f.createIframe;c[f.WPrefix][this.id]={start:function(){b.iframeObj.loaded()},message:function(a){b.emit("message",a)},stop:function(){b._cleanup(),b._close("network")}},this.iframeObj=e(a,function(){b._cleanup(),b._close("permanent")})}var e=a("inherits"),f=a("../../utils/iframe"),g=a("../../utils/url"),h=a("events").EventEmitter,i=a("../../utils/random");e(d,h),d.prototype.abort=function(){this._cleanup(),this._close("user")},d.prototype._cleanup=function(){this.iframeObj&&(this.iframeObj.cleanup(),this.iframeObj=null),delete c[f.WPrefix][this.id]},d.prototype._close=function(a){this.emit("close",null,a),this.removeAllListeners()},d.htmlfileEnabled=!1;var j=["Active"].concat("Object").join("X");if(j in c)try{d.htmlfileEnabled=!!new c[j]("htmlfile")}catch(a){}d.enabled=d.htmlfileEnabled||f.iframeEnabled,b.exports=d}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../../utils/iframe":47,"../../utils/random":50,"../../utils/url":52,debug:void 0,events:3,inherits:54}],31:[function(a,b){(function(c){"use strict";function d(a){var b=this;j.call(this),e.polluteGlobalNamespace(),this.id="a"+f.string(6);var g=h.addQuery(a,"c="+encodeURIComponent(e.WPrefix+"."+this.id));c[e.WPrefix][this.id]=this._callback.bind(this),this._createScript(g),this.timeoutId=setTimeout(function(){b._abort(new Error("JSONP script loaded abnormally (timeout)"))},d.timeout)}var e=a("../../utils/iframe"),f=a("../../utils/random"),g=a("../../utils/browser"),h=a("../../utils/url"),i=a("inherits"),j=a("events").EventEmitter;i(d,j),d.prototype.abort=function(){if(c[e.WPrefix][this.id]){var a=new Error("JSONP user aborted read");a.code=1e3,this._abort(a)}},d.timeout=35e3,d.scriptErrorTimeout=1e3,d.prototype._callback=function(a){this._cleanup(),this.aborting||(a&&this.emit("message",a),this.emit("close",null,"network"),this.removeAllListeners())},d.prototype._abort=function(a){this._cleanup(),this.aborting=!0,this.emit("close",a.code,a.message),this.removeAllListeners()},d.prototype._cleanup=function(){if(clearTimeout(this.timeoutId),this.script2&&(this.script2.parentNode.removeChild(this.script2),this.script2=null),this.script){var a=this.script;a.parentNode.removeChild(a),a.onreadystatechange=a.onerror=a.onload=a.onclick=null,this.script=null}delete c[e.WPrefix][this.id]},d.prototype._scriptError=function(){var a=this;this.errorTimer||(this.errorTimer=setTimeout(function(){a.loadedOkay||a._abort(new Error("JSONP script loaded abnormally (onerror)"))},d.scriptErrorTimeout))},d.prototype._createScript=function(a){var b,d=this,e=this.script=c.document.createElement("script");if(e.id="a"+f.string(8),e.src=a,e.type="text/javascript",e.charset="UTF-8",e.onerror=this._scriptError.bind(this),e.onload=function(){d._abort(new Error("JSONP script loaded abnormally (onload)"))},e.onreadystatechange=function(){if(/loaded|closed/.test(e.readyState)){if(e&&e.htmlFor&&e.onclick){d.loadedOkay=!0;try{e.onclick()}catch(a){}}e&&d._abort(new Error("JSONP script loaded abnormally (onreadystatechange)")); -}},"undefined"==typeof e.async&&c.document.attachEvent)if(g.isOpera())b=this.script2=c.document.createElement("script"),b.text="try{var a = document.getElementById('"+e.id+"'); if(a)a.onerror();}catch(x){};",e.async=b.async=!1;else{try{e.htmlFor=e.id,e.event="onclick"}catch(a){}e.async=!0}"undefined"!=typeof e.async&&(e.async=!0);var h=c.document.getElementsByTagName("head")[0];h.insertBefore(e,h.firstChild),b&&h.insertBefore(b,h.firstChild)},b.exports=d}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../../utils/browser":44,"../../utils/iframe":47,"../../utils/random":50,"../../utils/url":52,debug:void 0,events:3,inherits:54}],32:[function(a,b){"use strict";function c(a,b){e.call(this);var c=this;this.bufferPosition=0,this.xo=new b("POST",a,null),this.xo.on("chunk",this._chunkHandler.bind(this)),this.xo.once("finish",function(a,b){c._chunkHandler(a,b),c.xo=null;var d=200===a?"network":"permanent";c.emit("close",null,d),c._cleanup()})}var d=a("inherits"),e=a("events").EventEmitter;d(c,e),c.prototype._chunkHandler=function(a,b){if(200===a&&b)for(var c=-1;;this.bufferPosition+=c+1){var d=b.slice(this.bufferPosition);if(c=d.indexOf("\n"),-1===c)break;var e=d.slice(0,c);e&&this.emit("message",e)}},c.prototype._cleanup=function(){this.removeAllListeners()},c.prototype.abort=function(){this.xo&&(this.xo.close(),this.emit("close",null,"user"),this.xo=null),this._cleanup()},b.exports=c},{debug:void 0,events:3,inherits:54}],33:[function(a,b){(function(c){"use strict";function d(a){try{return c.document.createElement('