Skip to content

Commit

Permalink
Fix for perwendel#1273, perwendel#375 (Merged PR#980)
Browse files Browse the repository at this point in the history
  • Loading branch information
A.Lepe committed May 16, 2023
1 parent 17ac17c commit 22ac89e
Show file tree
Hide file tree
Showing 20 changed files with 471 additions and 6 deletions.
35 changes: 35 additions & 0 deletions DIFFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<EventSource.Emitter> 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;
}
};
}
}
}
```
6 changes: 6 additions & 0 deletions PR-STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
<version>${jetty.version}</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>${jetty.version}</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
Expand Down
39 changes: 38 additions & 1 deletion src/main/java/spark/Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,6 +75,8 @@ public final class Service extends Routable {

protected Map<String, WebSocketHandlerWrapper> webSocketHandlers = null;

protected Map<String, EventSourceHandlerWrapper> eventSourceHandlers = null;

protected int maxThreads = -1;
protected int minThreads = -1;
protected int threadIdleTimeoutMillis = -1;
Expand Down Expand Up @@ -533,6 +538,37 @@ public synchronized Service webSocketIdleTimeoutMillis(long timeoutMillis) {
return this;
}

/**
* Maps the given path to the given EventSource servlet class.
* <p>
* 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
*
Expand Down Expand Up @@ -588,7 +624,7 @@ private void throwBeforeRouteMappingException() {
}

private boolean hasMultipleHandlers() {
return webSocketHandlers != null;
return webSocketHandlers != null || eventSourceHandlers != null;
}


Expand Down Expand Up @@ -711,6 +747,7 @@ public synchronized void init() {

server.configureWebSockets(webSocketHandlers, webSocketIdleTimeoutMillis);
server.trustForwardHeaders(trustForwardHeaders);
server.configureEventSourcing(eventSourceHandlers);

port = server.ignite(
ipAddress,
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/spark/Spark.java
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,24 @@ public static void webSocketIdleTimeoutMillis(int timeoutMillis) {
getInstance().webSocketIdleTimeoutMillis(timeoutMillis);
}

/////////////////
// EventSource //

/**
* Maps the given path to the given EventSource handler.
* <p>
* 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
*/
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/spark/embeddedserver/EmbeddedServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -70,6 +71,15 @@ default void configureWebSockets(Map<String, WebSocketHandlerWrapper> 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<String, EventSourceHandlerWrapper> eventSourceHandlers) {
NotSupportedException.raise(getClass().getSimpleName(), "Event Source Servlets");
}

/**
* Joins the embedded server thread(s).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -55,6 +57,7 @@ public class EmbeddedJettyServer implements EmbeddedServer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());

private Map<String, WebSocketHandlerWrapper> webSocketHandlers;
private Map<String, EventSourceHandlerWrapper> eventSourceHandlers;
private Optional<Long> webSocketIdleTimeoutMillis;

private ThreadPool threadPool = null;
Expand All @@ -73,6 +76,11 @@ public void configureWebSockets(Map<String, WebSocketHandlerWrapper> webSocketHa
this.webSocketIdleTimeoutMillis = webSocketIdleTimeoutMillis;
}

@Override
public void configureEventSourcing(Map<String, EventSourceHandlerWrapper> eventSourceHandlers) {
this.eventSourceHandlers = eventSourceHandlers;
}

@Override
public void trustForwardHeaders(boolean trust) {
this.trustForwardHeaders = trust;
Expand Down Expand Up @@ -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<Handler> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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'");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, EventSourceHandlerWrapper> 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<String, EventSourceHandlerWrapper> eventSourceHandlers){
if (eventSourceHandlers == null)
return;
eventSourceHandlers.forEach((path, servletWrapper)->
contextHandler.addServlet(new ServletHolder((EventSourceServlet)servletWrapper.getHandler()), path));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ public static ServletContextHandler create(Map<String, WebSocketHandlerWrapper>
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.
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/spark/http/matching/ResponseWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 22ac89e

Please sign in to comment.