diff --git a/pom.xml b/pom.xml index 70728ade51..777fa0f472 100644 --- a/pom.xml +++ b/pom.xml @@ -124,6 +124,16 @@ ${jetty.version} test + + org.json + json + 20160810 + + + com.j2html + j2html + 1.2.0 + diff --git a/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java b/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java index f143efda43..96833722d1 100644 --- a/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java +++ b/src/main/java/spark/embeddedserver/jetty/EmbeddedJettyServer.java @@ -132,7 +132,9 @@ public int ignite(String host, server.setHandler(handler); } else { List handlersInList = new ArrayList<>(); - handlersInList.add(handler); + JettyHandler jettyHandler = (JettyHandler) handler; + jettyHandler.consume(webSocketHandlers.keySet()); + handlersInList.add(jettyHandler); // WebSocket handler must be the last one if (webSocketServletContextHandler != null) { diff --git a/src/main/java/spark/embeddedserver/jetty/JettyHandler.java b/src/main/java/spark/embeddedserver/jetty/JettyHandler.java index ef0c432c9a..aa72b628dd 100644 --- a/src/main/java/spark/embeddedserver/jetty/JettyHandler.java +++ b/src/main/java/spark/embeddedserver/jetty/JettyHandler.java @@ -17,6 +17,7 @@ package spark.embeddedserver.jetty; import java.io.IOException; +import java.util.Set; import javax.servlet.Filter; import javax.servlet.ServletException; @@ -35,6 +36,8 @@ public class JettyHandler extends SessionHandler { private Filter filter; + private Set consume; + public JettyHandler(Filter filter) { this.filter = filter; } @@ -47,14 +50,22 @@ public void doHandle( HttpServletResponse response) throws IOException, ServletException { HttpRequestWrapper wrapper = new HttpRequestWrapper(request); - filter.doFilter(wrapper, response, null); - - if (wrapper.notConsumed()) { - baseRequest.setHandled(false); - } else { - baseRequest.setHandled(true); + if(consume!=null && consume.contains(baseRequest.getRequestURI())){ + if (wrapper instanceof HttpRequestWrapper) { + ((HttpRequestWrapper) wrapper).notConsumed(true); + } + } + else { + filter.doFilter(wrapper, response, null); } + baseRequest.setHandled(!wrapper.notConsumed()); + } + public void consume(Set consume){ + this.consume=consume; } -} \ No newline at end of file + public Set consume(){ + return this.consume; + } +} diff --git a/src/main/java/spark/http/matching/MatcherFilter.java b/src/main/java/spark/http/matching/MatcherFilter.java index 3fa05bea0a..80679e2f6c 100644 --- a/src/main/java/spark/http/matching/MatcherFilter.java +++ b/src/main/java/spark/http/matching/MatcherFilter.java @@ -156,13 +156,6 @@ public void doFilter(ServletRequest servletRequest, body.set(""); } - if (body.notSet() && hasOtherHandlers) { - if (servletRequest instanceof HttpRequestWrapper) { - ((HttpRequestWrapper) servletRequest).notConsumed(true); - return; - } - } - if (body.notSet()) { LOG.info("The requested route [{}] has not been mapped in Spark for {}: [{}]", uri, ACCEPT_TYPE_REQUEST_MIME_HEADER, acceptType); diff --git a/src/test/java/spark/WebSocketRequestTest.java b/src/test/java/spark/WebSocketRequestTest.java new file mode 100644 index 0000000000..b09511948f --- /dev/null +++ b/src/test/java/spark/WebSocketRequestTest.java @@ -0,0 +1,161 @@ +package spark; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.*; +import org.json.JSONObject; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static j2html.TagCreator.*; +import static org.junit.Assert.assertEquals; +import static spark.Spark.*; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import spark.util.SparkTestUtil; + +public class WebSocketRequestTest { + + public static final int PORT = 4567; + + public static final String CHAT = "/hat"; + + public static final String HELLO = "/hello"; + + public static final String OTHER = "/:param"; + + private static final SparkTestUtil http = new SparkTestUtil(4567); + + @AfterClass + public static void tearDown() { + Spark.stop(); + } + + @BeforeClass + public static void setup() throws IOException { + staticFiles.location("/public/chatPages"); //index.html is served at localhost:4567 (default port) + staticFiles.expireTime(600); + webSocket("/chat", ChatWebSocketHandler.class); + get(HELLO, (req, res) -> "Hello!"); + get(OTHER, (req, res) -> "other"); + init(); + } + + @Test + public void testUrl1() throws Exception { + try { + Map requestHeader = new HashMap<>(); + requestHeader.put("Host", "localhost:" + PORT); + requestHeader.put("User-Agent", "curl/7.55.1"); + SparkTestUtil.UrlResponse response = http.doMethod("GET",HELLO, "", false, "*/*", requestHeader); + assertEquals(200, response.status); + assertEquals("Hello!",response.body); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testUrl2() throws Exception { + try { + Map requestHeader = new HashMap<>(); + requestHeader.put("Host", "localhost:" + PORT); + requestHeader.put("User-Agent", "curl/7.55.1"); + SparkTestUtil.UrlResponse response = http.doMethod("GET","/aya", "", false, "*/*", requestHeader); + assertEquals(200, response.status); + assertEquals("other",response.body); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testUrl3() throws Exception { + try { + Map requestHeader = new HashMap<>(); + requestHeader.put("Host", "localhost:" + PORT); + requestHeader.put("User-Agent", "curl/7.55.1"); + SparkTestUtil.UrlResponse response = http.doMethod("GET","/chat", "", false, "*/*", requestHeader); + assertEquals(404, response.status); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testUrl4() throws Exception { + try { + Map requestHeader = new HashMap<>(); + requestHeader.put("Host", "localhost:" + PORT); + requestHeader.put("User-Agent", "curl/7.55.1"); + SparkTestUtil.UrlResponse response = http.doMethod("GET","/", "", false, "*/*", requestHeader); + assertEquals(200, response.status); + } + catch (Exception e) { + e.printStackTrace(); + } + } +} + +class Chat{ + // this map is shared between sessions and threads, so it needs to be thread-safe (http://stackoverflow.com/a/2688817) + static Map userUsernameMap = new ConcurrentHashMap<>(); + static int nextUserNumber = 1; //Assign to username for next connecting user + + //Sends a message from one user to all users, along with a list of current usernames + public static void broadcastMessage(String sender, String message) { + userUsernameMap.keySet().stream().filter(Session::isOpen).forEach(session -> { + try { + session.getRemote().sendString(String.valueOf(new JSONObject() + .put("userMessage", createHtmlMessageFromSender(sender, message)) + .put("userlist", userUsernameMap.values()) + )); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + //Builds a HTML element with a sender-name, a message, and a timestamp, + private static String createHtmlMessageFromSender(String sender, String message) { + return article( + b(sender + " says:"), + span(attrs(".timestamp"), new SimpleDateFormat("HH:mm:ss").format(new Date())), + p(message) + ).render(); + } +} + +@WebSocket +class ChatWebSocketHandler { + + private String sender, msg; + + @OnWebSocketConnect + public void onConnect(Session user) throws Exception { + String username = "User" + Chat.nextUserNumber++; + Chat.userUsernameMap.put(user, username); + Chat.broadcastMessage(sender = "Server", msg = (username + " joined the chat")); + } + + @OnWebSocketClose + public void onClose(Session user, int statusCode, String reason) { + String username = Chat.userUsernameMap.get(user); + Chat.userUsernameMap.remove(user); + Chat.broadcastMessage(sender = "Server", msg = (username + " left the chat")); + } + + @OnWebSocketMessage + public void onMessage(Session user, String message) { + Chat.broadcastMessage(sender = Chat.userUsernameMap.get(user), msg = message); + } +} diff --git a/src/test/resources/public/chatPages/index.html b/src/test/resources/public/chatPages/index.html new file mode 100644 index 0000000000..fd6a432b6a --- /dev/null +++ b/src/test/resources/public/chatPages/index.html @@ -0,0 +1,17 @@ + + + + + WebsSockets + + + +
+ + +
+
+
+ + + diff --git a/src/test/resources/public/chatPages/style.css b/src/test/resources/public/chatPages/style.css new file mode 100644 index 0000000000..a28a34ac8f --- /dev/null +++ b/src/test/resources/public/chatPages/style.css @@ -0,0 +1,78 @@ +* { + box-sizing: border-box; +} + +html { + overflow-y: scroll; +} + +body { + font-family: monospace; + font-size: 14px; + max-width: 480px; + margin: 0 auto; + padding: 20px +} + +input { + width: 100%; + padding: 5px; + margin: 5px 0; +} + +button { + float: right; +} + +li { + margin: 5px 0; +} + +#chatControls { + overflow: auto; + margin: 0 0 5px 0 +} + +#userlist { + position: fixed; + left: 50%; + list-style: none; + margin-left: 250px; + background: #f0f0f9; + padding: 5px 10px; + width: 150px; + top: 11px; +} + +#chat p { + margin: 5px 0; + font-weight: 300 +} + +#chat .timestamp { + position: absolute; + top: 10px; + right: 10px; + font-size: 12px; +} + +#chat article { + background: #f1f1f1; + padding: 10px; + margin: 10px 0; + border-left: 5px solid #aaa; + position: relative; + word-wrap: break-word; +} + +#chat article:first-of-type { + background: #c9edc3; + border-left-color: #74a377; + animation: enter .2s 1; +} + +@keyframes enter { + from { transform: none; } + 50% { transform: scale(1.05); } + to { transform: none; } +} \ No newline at end of file diff --git a/src/test/resources/public/chatPages/websocketDemo.js b/src/test/resources/public/chatPages/websocketDemo.js new file mode 100644 index 0000000000..c59383c2ea --- /dev/null +++ b/src/test/resources/public/chatPages/websocketDemo.js @@ -0,0 +1,42 @@ +//Establish the WebSocket connection and set up event handlers +var webSocket = new WebSocket("ws://" + location.hostname + ":" + location.port + "/chat"); +webSocket.onmessage = function (msg) { updateChat(msg); }; +webSocket.onclose = function () { alert("WebSocket connection closed") }; + +//Send message if "Send" is clicked +id("send").addEventListener("click", function () { + sendMessage(id("message").value); +}); + +//Send message if enter is pressed in the input field +id("message").addEventListener("keypress", function (e) { + if (e.keyCode === 13) { sendMessage(e.target.value); } +}); + +//Send a message if it's not empty, then clear the input field +function sendMessage(message) { + if (message !== "") { + webSocket.send(message); + id("message").value = ""; + } +} + +//Update the chat-panel, and the list of connected users +function updateChat(msg) { + var data = JSON.parse(msg.data); + insert("chat", data.userMessage); + id("userlist").innerHTML = ""; + data.userlist.forEach(function (user) { + insert("userlist", "
  • " + user + "
  • "); + }); +} + +//Helper function for inserting HTML as the first child of an element +function insert(targetId, message) { + id(targetId).insertAdjacentHTML("afterbegin", message); +} + +//Helper function for selecting element by id +function id(id) { + return document.getElementById(id); +} \ No newline at end of file