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('