From 22ac89e67dc3b5da8d72b242aeb0b67e47f6699a Mon Sep 17 00:00:00 2001 From: "A.Lepe" Date: Tue, 16 May 2023 15:47:23 +0900 Subject: [PATCH] Fix for #1273, #375 (Merged PR#980) --- DIFFERENCES.md | 35 +++++++++++++++ PR-STATUS.md | 6 +++ README.md | 2 + pom.xml | 6 +++ src/main/java/spark/Service.java | 39 +++++++++++++++- src/main/java/spark/Spark.java | 18 ++++++++ .../spark/embeddedserver/EmbeddedServer.java | 10 +++++ .../jetty/EmbeddedJettyServer.java | 15 ++++++- .../EventSourceHandlerClassWrapper.java | 20 +++++++++ .../EventSourceHandlerInstanceWrapper.java | 18 ++++++++ .../EventSourceHandlerWrapper.java | 23 ++++++++++ ...entSourceServletContextHandlerFactory.java | 40 +++++++++++++++++ ...WebSocketServletContextHandlerFactory.java | 4 +- .../spark/http/matching/ResponseWrapper.java | 5 +++ .../java/spark/GenericIntegrationTest.java | 32 +++++++++++++- src/test/java/spark/ServiceTest.java | 40 +++++++++++++++++ .../jetty/eventsource/EventSourceClient.java | 44 +++++++++++++++++++ ...ourceServletContextHandlerFactoryTest.java | 44 +++++++++++++++++++ .../eventsource/EventSourceTestHandler.java | 38 ++++++++++++++++ .../eventsource/EventSourceExample.java | 38 ++++++++++++++++ 20 files changed, 471 insertions(+), 6 deletions(-) create mode 100644 src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerClassWrapper.java create mode 100644 src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerInstanceWrapper.java create mode 100644 src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerWrapper.java create mode 100644 src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactory.java create mode 100644 src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceClient.java create mode 100644 src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactoryTest.java create mode 100644 src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceTestHandler.java create mode 100644 src/test/java/spark/examples/eventsource/EventSourceExample.java diff --git a/DIFFERENCES.md b/DIFFERENCES.md index efe7b53d91..f78c9a9018 100644 --- a/DIFFERENCES.md +++ b/DIFFERENCES.md @@ -216,3 +216,38 @@ keytool -importkeystore -deststorepass yourpasswordhere -destkeypass yourpasswor -srckeystore example.p12 -srcstoretype PKCS12 -srcstorepass thePasswordYouSetInTheStepBefore \ -deststoretype JKS -alias example.com ``` + +--------------------------------- +# Server Sent Events + +Example showing how to use server-sent-events + +```java +public class EventSourceExample { + public static void main(String... args){ + Spark.eventSource("/eventsource", EventSourceServletExample.class); + Spark.init(); + } + public static class EventSourceServletExample extends EventSourceServlet{ + final Queue emitters = new ConcurrentLinkedQueue<>(); + @Override + protected EventSource newEventSource(HttpServletRequest request) { + return new EventSource() { + Emitter emmitter; + @Override + public void onOpen(Emitter emitter) throws IOException { + this.emmitter = emitter; + emitter.data("Event source data message"); + emitters.add(emitter); + } + + @Override + public void onClose() { + emitters.remove(this.emmitter); + this.emmitter = null; + } + }; + } + } +} +``` diff --git a/PR-STATUS.md b/PR-STATUS.md index 218aa41741..f0dc449743 100644 --- a/PR-STATUS.md +++ b/PR-STATUS.md @@ -74,6 +74,12 @@ This is the current status for each PR: * :green_circle: **CHERRY PICKED**: Solve the problem of non-ASCII characters in URL. Try to fix #1026 * perwendel/spark#1222 opened on Apr 23, 2021 by Bugjudger +### Merged (Release 5) +* :green_circle: **FIXED**: NullPointerException in response.header + * perwendel/spark/issues/1273 opened on Mar1, 2023 by mpkusnierz +* :green_circle: **MERGED**: Server Sent Events support (perwendel/spark/issues/375) + * perwendel/spark#980 opened on Feb 24, 2018 by mtzagkarakis + ### Rejected diff --git a/README.md b/README.md index 3defacd6a1..b3d3d45882 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,8 @@ These are the patches included in `unofficial-5`: Bug fixes: +Improvements: +* Added `Server Sent Events` support (issue perwendel#375) (PR: perwendel/spark#980) More details and examples on the differences between the Official version and this one: [DIFFERENCES.md](DIFFERENCES.md) diff --git a/pom.xml b/pom.xml index e9ecfa3424..fbd82de16f 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,12 @@ ${jetty.version} + + org.eclipse.jetty + jetty-servlets + ${jetty.version} + + org.eclipse.jetty jetty-webapp diff --git a/src/main/java/spark/Service.java b/src/main/java/spark/Service.java index 4f135a8d2a..5248c4fe18 100644 --- a/src/main/java/spark/Service.java +++ b/src/main/java/spark/Service.java @@ -33,6 +33,9 @@ import spark.embeddedserver.EmbeddedServer; import spark.embeddedserver.EmbeddedServers; +import spark.embeddedserver.jetty.eventsource.EventSourceHandlerClassWrapper; +import spark.embeddedserver.jetty.eventsource.EventSourceHandlerInstanceWrapper; +import spark.embeddedserver.jetty.eventsource.EventSourceHandlerWrapper; import spark.embeddedserver.jetty.websocket.WebSocketHandlerClassWrapper; import spark.embeddedserver.jetty.websocket.WebSocketHandlerInstanceWrapper; import spark.embeddedserver.jetty.websocket.WebSocketHandlerWrapper; @@ -72,6 +75,8 @@ public final class Service extends Routable { protected Map webSocketHandlers = null; + protected Map eventSourceHandlers = null; + protected int maxThreads = -1; protected int minThreads = -1; protected int threadIdleTimeoutMillis = -1; @@ -533,6 +538,37 @@ public synchronized Service webSocketIdleTimeoutMillis(long timeoutMillis) { return this; } + /** + * Maps the given path to the given EventSource servlet class. + *

+ * This is currently only available in the embedded server mode. + * + * @param path the EventSource path. + * @param handlerClass the handler class that will manage the EventSource connection to the given path. + */ + public void eventSource(String path, Class handlerClass) { + addEventSourceHandler(path, new EventSourceHandlerClassWrapper(handlerClass)); + } + + public void eventSource(String path, Object handler) { + addEventSourceHandler(path, new EventSourceHandlerInstanceWrapper(handler)); + } + + private synchronized void addEventSourceHandler(String path, EventSourceHandlerWrapper handlerWrapper) { + if (initialized) { + throwBeforeRouteMappingException(); + } + if (isRunningFromServlet()) { + throw new IllegalStateException("EventSource are only supported in the embedded server"); + } + requireNonNull(path, "EventSource path cannot be null"); + if (eventSourceHandlers == null) { + eventSourceHandlers = new HashMap<>(); + } + + eventSourceHandlers.put(path, handlerWrapper); + } + /** * Maps 404 errors to the provided custom page * @@ -588,7 +624,7 @@ private void throwBeforeRouteMappingException() { } private boolean hasMultipleHandlers() { - return webSocketHandlers != null; + return webSocketHandlers != null || eventSourceHandlers != null; } @@ -711,6 +747,7 @@ public synchronized void init() { server.configureWebSockets(webSocketHandlers, webSocketIdleTimeoutMillis); server.trustForwardHeaders(trustForwardHeaders); + server.configureEventSourcing(eventSourceHandlers); port = server.ignite( ipAddress, diff --git a/src/main/java/spark/Spark.java b/src/main/java/spark/Spark.java index c0e389fc5d..06e15352b4 100644 --- a/src/main/java/spark/Spark.java +++ b/src/main/java/spark/Spark.java @@ -1286,6 +1286,24 @@ public static void webSocketIdleTimeoutMillis(int timeoutMillis) { getInstance().webSocketIdleTimeoutMillis(timeoutMillis); } + ///////////////// + // EventSource // + + /** + * Maps the given path to the given EventSource handler. + *

+ * This is currently only available in the embedded server mode. + * + * @param path the EventSource path. + * @param handler the handler class that will manage the EventSource connection to the given path. + */ + public static void eventSource(String path, Class handler){ + getInstance().eventSource(path, handler); + } + + public static void eventSource(String path, Object handler){ + getInstance().eventSource(path, handler); + } /** * Maps 404 Not Found errors to the provided custom page */ diff --git a/src/main/java/spark/embeddedserver/EmbeddedServer.java b/src/main/java/spark/embeddedserver/EmbeddedServer.java index 44fe65d795..8f9ead80e9 100644 --- a/src/main/java/spark/embeddedserver/EmbeddedServer.java +++ b/src/main/java/spark/embeddedserver/EmbeddedServer.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Optional; +import spark.embeddedserver.jetty.eventsource.EventSourceHandlerWrapper; import spark.embeddedserver.jetty.websocket.WebSocketHandlerWrapper; import spark.ssl.SslStores; @@ -70,6 +71,15 @@ default void configureWebSockets(Map webSocketH NotSupportedException.raise(getClass().getSimpleName(), "Web Sockets"); } + /** + * Configures the event source servlets for the embedded server. + * + * @param eventSourceHandlers - event source handlers. + */ + default void configureEventSourcing(Map eventSourceHandlers) { + NotSupportedException.raise(getClass().getSimpleName(), "Event Source Servlets"); + } + /** * Joins the embedded server thread(s). */ diff --git a/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java b/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java index b634d3f0e1..c1afcfa0e9 100644 --- a/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java +++ b/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java @@ -34,6 +34,8 @@ import org.slf4j.LoggerFactory; import spark.embeddedserver.EmbeddedServer; +import spark.embeddedserver.jetty.eventsource.EventSourceHandlerWrapper; +import spark.embeddedserver.jetty.eventsource.EventSourceServletContextHandlerFactory; import spark.embeddedserver.jetty.websocket.WebSocketHandlerWrapper; import spark.embeddedserver.jetty.websocket.WebSocketServletContextHandlerFactory; import spark.ssl.SslStores; @@ -55,6 +57,7 @@ public class EmbeddedJettyServer implements EmbeddedServer { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private Map webSocketHandlers; + private Map eventSourceHandlers; private Optional webSocketIdleTimeoutMillis; private ThreadPool threadPool = null; @@ -73,6 +76,11 @@ public void configureWebSockets(Map webSocketHa this.webSocketIdleTimeoutMillis = webSocketIdleTimeoutMillis; } + @Override + public void configureEventSourcing(Map eventSourceHandlers) { + this.eventSourceHandlers = eventSourceHandlers; + } + @Override public void trustForwardHeaders(boolean trust) { this.trustForwardHeaders = trust; @@ -135,19 +143,24 @@ public int ignite(String host, ServletContextHandler webSocketServletContextHandler = WebSocketServletContextHandlerFactory.create(webSocketHandlers, webSocketIdleTimeoutMillis); + ServletContextHandler eventSourceServletContextHandler = + EventSourceServletContextHandlerFactory.create(eventSourceHandlers); // Handle web socket routes - if (webSocketServletContextHandler == null) { + if (webSocketServletContextHandler == null && eventSourceServletContextHandler == null) { server.setHandler(handler); } else { List handlersInList = new ArrayList<>(); JettyHandler jettyHandler = (JettyHandler) handler; jettyHandler.consume(webSocketHandlers.keySet()); + jettyHandler.consume(eventSourceHandlers.keySet()); handlersInList.add(jettyHandler); // WebSocket handler must be the last one if (webSocketServletContextHandler != null) { handlersInList.add(webSocketServletContextHandler); + } else { + handlersInList.add(eventSourceServletContextHandler); } HandlerList handlers = new HandlerList(); diff --git a/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerClassWrapper.java b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerClassWrapper.java new file mode 100644 index 0000000000..d221ef771b --- /dev/null +++ b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerClassWrapper.java @@ -0,0 +1,20 @@ +package spark.embeddedserver.jetty.eventsource; + +import static java.util.Objects.requireNonNull; + +public class EventSourceHandlerClassWrapper implements EventSourceHandlerWrapper { + private final Class handlerClass; + public EventSourceHandlerClassWrapper(Class handlerClass) { + requireNonNull(handlerClass, "EventSource handler class cannot be null"); + EventSourceHandlerWrapper.validateHandlerClass(handlerClass); + this.handlerClass = handlerClass; + } + @Override + public Object getHandler() { + try { + return handlerClass.newInstance(); + } catch (InstantiationException | IllegalAccessException ex) { + throw new RuntimeException("Could not instantiate event source handler", ex); + } + } +} diff --git a/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerInstanceWrapper.java b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerInstanceWrapper.java new file mode 100644 index 0000000000..6b7ed8f92a --- /dev/null +++ b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerInstanceWrapper.java @@ -0,0 +1,18 @@ +package spark.embeddedserver.jetty.eventsource; + +import static java.util.Objects.requireNonNull; + +public class EventSourceHandlerInstanceWrapper implements EventSourceHandlerWrapper { + final Object handler; + + public EventSourceHandlerInstanceWrapper(Object handler) { + requireNonNull(handler, "EventSource handler cannot be null"); + EventSourceHandlerWrapper.validateHandlerClass(handler.getClass()); + this.handler = handler; + } + + @Override + public Object getHandler() { + return handler; + } +} diff --git a/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerWrapper.java b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerWrapper.java new file mode 100644 index 0000000000..16e4192808 --- /dev/null +++ b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceHandlerWrapper.java @@ -0,0 +1,23 @@ +package spark.embeddedserver.jetty.eventsource; + +import org.eclipse.jetty.servlets.EventSourceServlet; + +/** + * A wrapper for event source handler classes/instances. + */ +public interface EventSourceHandlerWrapper { + /** + * Gets the actual handler - if necessary, instantiating an object. + * + * @return The handler instance. + */ + Object getHandler(); + + static void validateHandlerClass(Class handlerClass) { + boolean valid = EventSourceServlet.class.isAssignableFrom(handlerClass); + if (!valid) { + throw new IllegalArgumentException( + "EventSource handler must extend 'EventSourceServlet'"); + } + } +} diff --git a/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactory.java b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactory.java new file mode 100644 index 0000000000..bb8049a5af --- /dev/null +++ b/src/main/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactory.java @@ -0,0 +1,40 @@ +package spark.embeddedserver.jetty.eventsource; + +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.servlets.EventSourceServlet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +public class EventSourceServletContextHandlerFactory { + private static final Logger logger = LoggerFactory.getLogger(EventSourceServletContextHandlerFactory.class); + + /** + * Creates a new eventSource servlet context handler. + * + * @param eventSourceHandlers eventSourceHandlers + * @return a new eventSource servlet context handler or 'null' if creation failed. + */ + public static ServletContextHandler create(Map eventSourceHandlers) { + ServletContextHandler eventSourceServletContextHandler = null; + if (eventSourceHandlers != null) { + try { + eventSourceServletContextHandler = new ServletContextHandler(null, "/", true, false); + addToExistingContext(eventSourceServletContextHandler, eventSourceHandlers); + } catch (Exception ex) { + logger.error("creation of event source context handler failed.", ex); + eventSourceServletContextHandler = null; + } + } + return eventSourceServletContextHandler; + } + + public static void addToExistingContext(ServletContextHandler contextHandler, Map eventSourceHandlers){ + if (eventSourceHandlers == null) + return; + eventSourceHandlers.forEach((path, servletWrapper)-> + contextHandler.addServlet(new ServletHolder((EventSourceServlet)servletWrapper.getHandler()), path)); + } +} diff --git a/src/main/java/spark/embeddedserver/jetty/websocket/WebSocketServletContextHandlerFactory.java b/src/main/java/spark/embeddedserver/jetty/websocket/WebSocketServletContextHandlerFactory.java index a2fa3a6e68..b02a0063d3 100644 --- a/src/main/java/spark/embeddedserver/jetty/websocket/WebSocketServletContextHandlerFactory.java +++ b/src/main/java/spark/embeddedserver/jetty/websocket/WebSocketServletContextHandlerFactory.java @@ -48,9 +48,7 @@ public static ServletContextHandler create(Map try { webSocketServletContextHandler = new ServletContextHandler(null, "/", true, false); WebSocketUpgradeFilter webSocketUpgradeFilter = WebSocketUpgradeFilter.configureContext(webSocketServletContextHandler); - if (webSocketIdleTimeoutMillis.isPresent()) { - webSocketUpgradeFilter.getFactory().getPolicy().setIdleTimeout(webSocketIdleTimeoutMillis.get()); - } + webSocketIdleTimeoutMillis.ifPresent(webSocketUpgradeFilter.getFactory().getPolicy()::setIdleTimeout); // Since we are configuring WebSockets before the ServletContextHandler and WebSocketUpgradeFilter is // even initialized / started, then we have to pre-populate the configuration that will eventually // be used by Jetty's WebSocketUpgradeFilter. diff --git a/src/main/java/spark/http/matching/ResponseWrapper.java b/src/main/java/spark/http/matching/ResponseWrapper.java index 1c8233af49..296a94be91 100644 --- a/src/main/java/spark/http/matching/ResponseWrapper.java +++ b/src/main/java/spark/http/matching/ResponseWrapper.java @@ -101,6 +101,11 @@ public void header(String header, String value) { delegate.header(header, value); } + @Override + public void header(String header, int value) { + delegate.header(header, value); + } + @Override public String toString() { return delegate.toString(); diff --git a/src/test/java/spark/GenericIntegrationTest.java b/src/test/java/spark/GenericIntegrationTest.java index 2b686190f6..c5ca8876dc 100644 --- a/src/test/java/spark/GenericIntegrationTest.java +++ b/src/test/java/spark/GenericIntegrationTest.java @@ -1,9 +1,11 @@ package spark; +import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.net.Socket; import java.net.URI; import java.net.URLEncoder; import java.nio.ByteBuffer; @@ -21,7 +23,8 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import spark.embeddedserver.jetty.eventsource.EventSourceClient; +import spark.embeddedserver.jetty.eventsource.EventSourceTestHandler; import spark.embeddedserver.jetty.websocket.WebSocketTestClient; import spark.embeddedserver.jetty.websocket.WebSocketTestHandler; import spark.examples.exception.BaseException; @@ -34,6 +37,7 @@ import static spark.Spark.after; import static spark.Spark.afterAfter; import static spark.Spark.before; +import static spark.Spark.eventSource; import static spark.Spark.exception; import static spark.Spark.externalStaticFileLocation; import static spark.Spark.get; @@ -70,6 +74,7 @@ public static void setup() throws IOException { staticFileLocation("/public"); externalStaticFileLocation(System.getProperty("java.io.tmpdir")); webSocket("/ws", WebSocketTestHandler.class); + eventSource("/es", EventSourceTestHandler.class); before("/secretcontent/*", (q, a) -> { a.header("WWW-Authenticate", "Bearer"); @@ -513,6 +518,31 @@ public void testWebSocketConversation() throws Exception { Assert.assertEquals("onClose: 1000 Bye!", events.get(2)); } + @Test + public void testEventSourceConversation() throws Exception{ + String uri = "http://localhost:4567/es"; + String response = ""; + Socket socket = new Socket("localhost", 4567); + EventSourceClient eventSourceClient = new EventSourceClient(socket); + eventSourceClient.writeHTTPRequest(uri); + BufferedReader reader = eventSourceClient.readAndDiscardHTTPResponse(); + String line; + while((line = reader.readLine()) != null){ + if (line.startsWith("data:")){ + response = line; + } + if (line.isEmpty()) + break; + } + + eventSourceClient.close(); + Assert.assertEquals("data: " + EventSourceTestHandler.ES_MESSAGE, response); + Assert.assertEquals(3, EventSourceTestHandler.events.size()); + Assert.assertEquals(EventSourceTestHandler.ON_CONNECT, EventSourceTestHandler.events.get(0)); + Assert.assertEquals(EventSourceTestHandler.ES_MESSAGE, EventSourceTestHandler.events.get(1)); + Assert.assertEquals(EventSourceTestHandler.ON_CLOSE, EventSourceTestHandler.events.get(2)); + } + @Test public void path_should_prefix_routes() throws Exception { UrlResponse response = doMethod("GET", "/firstPath/test", null, "application/json"); diff --git a/src/test/java/spark/ServiceTest.java b/src/test/java/spark/ServiceTest.java index 284b3e41ee..70df98773f 100644 --- a/src/test/java/spark/ServiceTest.java +++ b/src/test/java/spark/ServiceTest.java @@ -1,7 +1,12 @@ package spark; +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.servlets.EventSource; +import org.eclipse.jetty.servlets.EventSourceServlet; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.junit.Before; import org.junit.Rule; @@ -308,4 +313,39 @@ public void awaitStopBlocksUntilExtinguished() { @WebSocket protected static class DummyWebSocketListener { } + + @Test + public void testEventSource_whenInitializedTrue_thenThrowIllegalStateException() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("This must be done before route mapping has begun"); + + Whitebox.setInternalState(service, "initialized", true); + service.eventSource("/", EventSourceListener.class); + } + + @Test + public void testEventSource_whenPathNull_thenThrowNullPointerException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("EventSource path cannot be null"); + service.eventSource(null, new EventSourceListener()); + } + + @Test + public void testEventSource_whenHandlerNull_thenThrowNullPointerException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("EventSource handler class cannot be null"); + service.eventSource("/", null); + } + + protected static class EventSourceListener extends EventSourceServlet{ + @Override + protected EventSource newEventSource(HttpServletRequest request) { + return new EventSource() { + @Override + public void onOpen(Emitter emitter) throws IOException { } + @Override + public void onClose() {} + }; + } + } } diff --git a/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceClient.java b/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceClient.java new file mode 100644 index 0000000000..5cb615ddbf --- /dev/null +++ b/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceClient.java @@ -0,0 +1,44 @@ +package spark.embeddedserver.jetty.eventsource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.sql.SQLSyntaxErrorException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EventSourceClient { + private final Socket socket; + public EventSourceClient(Socket socket) { + this.socket = socket; + } + + public void close() throws IOException, InterruptedException { + socket.close(); + TimeUnit.SECONDS.sleep(15); + } + public void writeHTTPRequest(String path) throws IOException { + int serverPort = socket.getPort(); + final OutputStream output = socket.getOutputStream(); + String handshake = ""; + handshake += "GET " + path + " HTTP/1.1\r\n"; + handshake += "Host: localhost:" + serverPort + "\r\n"; + handshake += "Accept: text/event-stream\r\n"; + handshake += "\r\n"; + + output.write(handshake.getBytes("UTF-8")); + output.flush(); + } + + public BufferedReader readAndDiscardHTTPResponse() throws IOException { + final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + if (line.length() == 0) + break; + } + return reader; + } +} diff --git a/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactoryTest.java b/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactoryTest.java new file mode 100644 index 0000000000..d7268229f6 --- /dev/null +++ b/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceServletContextHandlerFactoryTest.java @@ -0,0 +1,44 @@ +package spark.embeddedserver.jetty.eventsource; + +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertNull; + +@RunWith(PowerMockRunner.class) +public class EventSourceServletContextHandlerFactoryTest { + final String eventSourcePath = "/eventsource"; + private ServletContextHandler servletContextHandler; + + @Test + public void testCreate_whenEventSourceHandlersIsNull_thenReturnNull() throws Exception { + + servletContextHandler = EventSourceServletContextHandlerFactory.create(null); + + assertNull("Should return null because no EventSource Handlers were passed", servletContextHandler); + + } + + @Test + @PrepareForTest(EventSourceServletContextHandlerFactory.class) + public void testCreate_whenEventSourceContextHandlerCreationFails_thenThrowException() throws Exception { + + PowerMockito.whenNew(ServletContextHandler.class).withAnyArguments().thenThrow(new Exception("")); + + Map eventSourceHandlers = new HashMap<>(); + + eventSourceHandlers.put(eventSourcePath, new EventSourceHandlerClassWrapper(EventSourceTestHandler.class)); + + servletContextHandler = EventSourceServletContextHandlerFactory.create(eventSourceHandlers); + + assertNull("Should return null because EventSource context handler was not created", servletContextHandler); + + } +} diff --git a/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceTestHandler.java b/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceTestHandler.java new file mode 100644 index 0000000000..fb8933b93a --- /dev/null +++ b/src/test/java/spark/embeddedserver/jetty/eventsource/EventSourceTestHandler.java @@ -0,0 +1,38 @@ +package spark.embeddedserver.jetty.eventsource; + +import org.eclipse.jetty.servlets.EventSource; +import org.eclipse.jetty.servlets.EventSourceServlet; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.TemporalUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static java.util.Collections.synchronizedList; + +public class EventSourceTestHandler extends EventSourceServlet { + public static final List events = synchronizedList(new ArrayList<>()); + public static final String ON_CONNECT = "onConnect"; + public static final String ON_CLOSE = "onClose"; + public static final String ES_MESSAGE = "a_message"; + @Override + protected EventSource newEventSource(HttpServletRequest request) { + return new EventSource() { + @Override + public void onOpen(Emitter emitter) throws IOException { + events.add(ON_CONNECT); + events.add(ES_MESSAGE); + emitter.data(ES_MESSAGE); + } + + @Override + public void onClose() { + events.add(ON_CLOSE); + } + }; + } +} diff --git a/src/test/java/spark/examples/eventsource/EventSourceExample.java b/src/test/java/spark/examples/eventsource/EventSourceExample.java new file mode 100644 index 0000000000..cb815f55f0 --- /dev/null +++ b/src/test/java/spark/examples/eventsource/EventSourceExample.java @@ -0,0 +1,38 @@ +package spark.examples.eventsource; + +import org.eclipse.jetty.servlets.EventSource; +import org.eclipse.jetty.servlets.EventSourceServlet; +import spark.Spark; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class EventSourceExample { + public static void main(String... args){ + Spark.eventSource("/eventsource", EventSourceServletExample.class); + Spark.init(); + } + public static class EventSourceServletExample extends EventSourceServlet{ + final Queue emitters = new ConcurrentLinkedQueue<>(); + @Override + protected EventSource newEventSource(HttpServletRequest request) { + return new EventSource() { + Emitter emmitter; + @Override + public void onOpen(Emitter emitter) throws IOException { + this.emmitter = emitter; + emitter.data("Event source data message"); + emitters.add(emitter); + } + + @Override + public void onClose() { + emitters.remove(this.emmitter); + this.emmitter = null; + } + }; + } + } +}