ASF Bugzilla – Attachment 30909 Details for
Bug 55639
Add Drawboard Websocket Example
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Patch to add the Drawboard example
DrawboardPatch.patch (text/plain), 91.99 KB, created by
Konstantin Preißer
on 2013-10-07 23:16:40 UTC
(
hide
)
Description:
Patch to add the Drawboard example
Filename:
MIME Type:
Creator:
Konstantin Preißer
Created:
2013-10-07 23:16:40 UTC
Size:
91.99 KB
patch
obsolete
>Index: webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (working copy) >@@ -0,0 +1,211 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import java.awt.BasicStroke; >+import java.awt.Color; >+import java.awt.Graphics2D; >+ >+/** >+ * A message that represents a drawing action. >+ * Note that we use primitive types instead of Point, Color etc. >+ * to reduce object allocation.<br><br> >+ * >+ * TODO: But a Color objects needs to be created anyway for drawing this >+ * onto a Graphics2D object, so this probably does not save much. >+ */ >+public final class DrawMessage { >+ >+ >+ private int type; >+ private byte colorR, colorG, colorB, colorA; >+ private double thickness; >+ private int x1, y1, x2, y2; >+ >+ /** >+ * The type. 1: Line. >+ * @return >+ */ >+ public int getType() { >+ return type; >+ } >+ public void setType(int type) { >+ this.type = type; >+ } >+ >+ public double getThickness() { >+ return thickness; >+ } >+ public void setThickness(double thickness) { >+ this.thickness = thickness; >+ } >+ >+ public byte getColorR() { >+ return colorR; >+ } >+ public void setColorR(byte colorR) { >+ this.colorR = colorR; >+ } >+ public byte getColorG() { >+ return colorG; >+ } >+ public void setColorG(byte colorG) { >+ this.colorG = colorG; >+ } >+ public byte getColorB() { >+ return colorB; >+ } >+ public void setColorB(byte colorB) { >+ this.colorB = colorB; >+ } >+ public byte getColorA() { >+ return colorA; >+ } >+ public void setColorA(byte colorA) { >+ this.colorA = colorA; >+ } >+ >+ public long getX1() { >+ return x1; >+ } >+ public void setX1(int x1) { >+ this.x1 = x1; >+ } >+ public int getX2() { >+ return x2; >+ } >+ public void setX2(int x2) { >+ this.x2 = x2; >+ } >+ public int getY1() { >+ return y1; >+ } >+ public void setY1(int y1) { >+ this.y1 = y1; >+ } >+ public int getY2() { >+ return y2; >+ } >+ public void setY2(int y2) { >+ this.y2 = y2; >+ } >+ >+ >+ >+ public DrawMessage(int type, byte colorR, byte colorG, byte colorB, >+ byte colorA, double thickness, int x1, int x2, int y1, int y2) { >+ >+ this.type = type; >+ this.colorR = colorR; >+ this.colorG = colorG; >+ this.colorB = colorB; >+ this.colorA = colorA; >+ this.thickness = thickness; >+ this.x1 = x1; >+ this.x2 = x2; >+ this.y1 = y1; >+ this.y2 = y2; >+ } >+ >+ >+ /** >+ * Draws this DrawMessage onto the given Graphics2D. >+ * @param g >+ */ >+ public void draw(Graphics2D g) { >+ switch (type) { >+ case 1: >+ // Draw a line. >+ g.setStroke(new BasicStroke((float) thickness, >+ BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)); >+ g.setColor(new Color(colorR & 0xFF, colorG & 0xFF, colorB & 0xFF, >+ colorA & 0xFF)); >+ g.drawLine(x1, y1, x2, y2); >+ break; >+ } >+ } >+ >+ /** >+ * Converts this message into a String representation that >+ * can be sent over WebSocket.<br> >+ * Since a DrawMessage consists only of numbers, >+ * we concatenate those numbers with a ",". >+ */ >+ @Override >+ public String toString() { >+ >+ return type + "," + (colorR & 0xFF) + "," + (colorG & 0xFF) + "," >+ + (colorB & 0xFF) + "," + (colorA & 0xFF) + "," + thickness >+ + "," + x1 + "," + y1 + "," + x2 + "," + y2; >+ } >+ >+ public static DrawMessage parseFromString(String str) >+ throws ParseException { >+ >+ int type; >+ byte[] colors = new byte[4]; >+ double thickness; >+ int[] coords = new int[4]; >+ >+ try { >+ String[] elements = str.split(","); >+ >+ type = Integer.parseInt(elements[0]); >+ if (type != 1) >+ throw new ParseException("Invalid type: " + type); >+ >+ for (int i = 0; i < colors.length; i++) { >+ colors[i] = (byte) Integer.parseInt(elements[1 + i]); >+ } >+ >+ thickness = Double.parseDouble(elements[5]); >+ if (Double.isNaN(thickness) || thickness < 0 || thickness > 100) >+ throw new ParseException("Invalid thickness: " + thickness); >+ >+ for (int i = 0; i < coords.length; i++) { >+ coords[i] = Integer.parseInt(elements[6 + i]); >+ if (coords[i] < -1000000L || coords[i] > 1000000L) >+ throw new ParseException("Invalid coordinate: " >+ + coords[i]); >+ } >+ >+ >+ } catch (RuntimeException ex) { >+ throw new ParseException(ex); >+ } >+ >+ DrawMessage m = new DrawMessage(type, colors[0], colors[1], >+ colors[2], colors[3], thickness, coords[0], coords[2], >+ coords[1], coords[3]); >+ >+ return m; >+ } >+ >+ public static class ParseException extends Exception { >+ private static final long serialVersionUID = -6651972769789842960L; >+ >+ public ParseException(Throwable root) { >+ super(root); >+ } >+ >+ public ParseException(String message) { >+ super(message); >+ } >+ } >+ >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (working copy) >@@ -0,0 +1,210 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import java.io.EOFException; >+ >+import javax.websocket.CloseReason; >+import javax.websocket.Endpoint; >+import javax.websocket.EndpointConfig; >+import javax.websocket.MessageHandler; >+import javax.websocket.Session; >+ >+import org.apache.juli.logging.Log; >+import org.apache.juli.logging.LogFactory; >+ >+import websocket.drawboard.DrawMessage.ParseException; >+import websocket.drawboard.wsmessages.StringWebsocketMessage; >+ >+ >+public final class DrawboardEndpoint extends Endpoint { >+ >+ private static final Log log = >+ LogFactory.getLog(DrawboardEndpoint.class); >+ >+ >+ /** >+ * Our room where players can join. >+ */ >+ private static final Room room = new Room(); >+ >+ public static Room getRoom() { >+ return room; >+ } >+ >+ /** >+ * The player that is associated with this Endpoint and the current room. >+ * Note that this variable is only accessed from the Room Thread.<br><br> >+ * >+ * TODO: Currently, Tomcat uses an Endpoint instance once - however >+ * the java doc of endpoint says: >+ * "Each instance of a websocket endpoint is guaranteed not to be called by >+ * more than one thread at a time per active connection." >+ * This could mean that after calling onClose(), the instance >+ * could be reused for another connection so onOpen() will get called >+ * (possibly from another thread).<br> >+ * If this is the case, we would need a variable holder for the variables >+ * that are accessed by the Room thread, and read the reference to the holder >+ * at the beginning of onOpen, onMessage, onClose methods to ensure the room >+ * thread always gets the correct instance of the variable holder. >+ */ >+ private Room.Player player; >+ >+ >+ @Override >+ public void onOpen(Session session, EndpointConfig config) { >+ // Set maximum messages size to 10.000 bytes. >+ session.setMaxTextMessageBufferSize(10000); >+ session.addMessageHandler(stringHandler); >+ final Client client = new Client(session.getAsyncRemote()); >+ >+ room.invoke(new Runnable() { >+ @Override >+ public void run() { >+ try { >+ >+ // Create a new Player and add it to the room. >+ try { >+ player = room.createAndAddPlayer(client); >+ } catch (IllegalStateException ex) { >+ // Probably the max. number of players has been >+ // reached. >+ client.sendMessage(new StringWebsocketMessage( >+ "0" + ex.getLocalizedMessage())); >+ } >+ >+ } catch (RuntimeException ex) { >+ log.error("Unexpected exception: " + ex.toString(), ex); >+ } >+ } >+ }); >+ >+ } >+ >+ >+ @Override >+ public void onClose(Session session, CloseReason closeReason) { >+ room.invoke(new Runnable() { >+ @Override >+ public void run() { >+ try { >+ >+ // Player can be null if it couldn't enter the room >+ if (player != null) { >+ // Remove this player from the room. >+ player.removeFromRoom(); >+ } >+ >+ } catch (RuntimeException ex) { >+ log.error("Unexpected exception: " + ex.toString(), ex); >+ } >+ } >+ }); >+ >+ } >+ >+ >+ >+ @Override >+ public void onError(Session session, Throwable t) { >+ // Most likely cause is a user closing their browser. Check to see if >+ // the root cause is EOF and if it is ignore it. >+ // Protect against infinite loops. >+ int count = 0; >+ Throwable root = t; >+ while (root.getCause() != null && count < 20) { >+ root = root.getCause(); >+ count ++; >+ } >+ if (root instanceof EOFException) { >+ // Assume this is triggered by the user closing their browser and >+ // ignore it. >+ } else { >+ log.error("onError: " + t.toString(), t); >+ } >+ } >+ >+ >+ >+ private final MessageHandler.Whole<String> stringHandler = >+ new MessageHandler.Whole<String>() { >+ >+ @Override >+ public void onMessage(final String message) { >+ // Invoke handling of the message in the room. >+ room.invoke(new Runnable() { >+ @Override >+ public void run() { >+ try { >+ >+ // Currently, the only types of messages the client will send >+ // are draw messages prefixed by a Message ID >+ // (starting with char '1'), and pong messages (starting >+ // with char '0'). >+ // Draw messages should look like this: >+ // ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2 >+ >+ boolean dontSwallowException = false; >+ try { >+ char messageType = message.charAt(0); >+ switch (messageType) { >+ case '0': >+ // Pong message. >+ // Do nothing. >+ break; >+ >+ case '1': >+ // Draw message >+ int indexOfChar = message.indexOf('|'); >+ long msgId = Long.parseLong( >+ message.substring(0, indexOfChar)); >+ >+ DrawMessage msg = DrawMessage.parseFromString( >+ message.substring(indexOfChar + 1)); >+ >+ // Don't ingore RuntimeExceptions thrown by >+ // this method >+ // TODO: Find a better solution than this variable >+ dontSwallowException = true; >+ if (player != null) { >+ player.handleDrawMessage(msg, msgId); >+ } >+ dontSwallowException = false; >+ >+ break; >+ } >+ >+ } catch (RuntimeException|ParseException ex) { >+ // Client sent invalid data. >+ // Ignore, TODO: maybe close connection >+ if (dontSwallowException >+ && ex instanceof RuntimeException) { >+ throw (RuntimeException) ex; >+ } >+ } >+ >+ } catch (RuntimeException ex) { >+ log.error("Unexpected exception: " + ex.toString(), ex); >+ } >+ } >+ }); >+ >+ } >+ }; >+ >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (working copy) >@@ -0,0 +1,436 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import java.awt.Color; >+import java.awt.Graphics2D; >+import java.awt.RenderingHints; >+import java.awt.image.BufferedImage; >+import java.io.ByteArrayOutputStream; >+import java.io.IOException; >+import java.nio.ByteBuffer; >+import java.util.ArrayList; >+import java.util.List; >+import java.util.Timer; >+import java.util.TimerTask; >+import java.util.concurrent.ExecutionException; >+import java.util.concurrent.ExecutorService; >+import java.util.concurrent.Executors; >+import java.util.concurrent.Future; >+ >+import javax.imageio.ImageIO; >+ >+import websocket.drawboard.wsmessages.BinaryWebsocketMessage; >+import websocket.drawboard.wsmessages.StringWebsocketMessage; >+ >+/** >+ * A Room represents a drawboard where a number of >+ * users participate.<br><br> >+ * >+ * Each Room has its own "Room Thread" which manages all the actions >+ * to be done in this Room. Instance methods should only be invoked >+ * from this Room's thread by calling {@link #invoke(Runnable)} or >+ * {@link #invokeAndWait(Runnable)}. >+ */ >+public final class Room { >+ >+ /** >+ * Specifies the type of a room message that is sent to a client.<br> >+ * Note: Currently we are sending simple string messages - for production >+ * apps, a JSON lib should be used for object-level messages.<br><br> >+ * >+ * The number (single char) will be prefixed to the string when sending >+ * the message. >+ */ >+ public static enum MessageType { >+ /** >+ * '0': Error: contains error message. >+ */ >+ ERROR('0'), >+ /** >+ * '1': DrawMesssage: contains serialized DrawMessage(s) prefixed >+ * with the current Player's {@link Player#lastReceivedMessageId} >+ * and ",".<br> >+ * Multiple draw messages are concatenated with "|" as separator. >+ */ >+ DRAW_MESSAGE('1'), >+ /** >+ * '2': ImageMessage: Contains number of current players in this room. >+ * After this message a Binary Websocket message will follow, >+ * containing the current Room image as PNG.<br> >+ * This is the first message that a Room sends to a new Player. >+ */ >+ IMAGE_MESSAGE('2'), >+ /** >+ * '3': PlayerChanged: contains "+" or "-" which indicate a player >+ * was added or removed to this Room. >+ */ >+ PLAYER_CHANGED('3'); >+ >+ private final char flag; >+ >+ private MessageType(char flag) { >+ this.flag = flag; >+ } >+ >+ } >+ >+ >+ >+ /** >+ * If <code>true</code>, outgoing DrawMessages will be buffered until the >+ * drawmessageBroadcastTimer ticks. Otherwise they will be sent >+ * immediately. >+ */ >+ private static final boolean BUFFER_DRAW_MESSAGES = true; >+ >+ /** >+ * A single-threaded ExecutorService where tasks >+ * are scheduled that are to be run in the Room Thread. >+ */ >+ private final ExecutorService roomExecutor = >+ Executors.newSingleThreadExecutor(); >+ >+ /** >+ * A timer which sends buffered drawmessages to the client at once >+ * at a regular interval, to avoid sending a lot of very small >+ * messages which would cause TCP overhead and high CPU usage. >+ */ >+ private final Timer drawmessageBroadcastTimer = new Timer(); >+ >+ >+ /** >+ * The current image of the room drawboard. DrawMessages that are >+ * received from Players will be drawn onto this image. >+ */ >+ private final BufferedImage roomImage = >+ new BufferedImage(900, 600, BufferedImage.TYPE_INT_RGB); >+ private final Graphics2D roomGraphics = roomImage.createGraphics(); >+ >+ >+ /** >+ * The maximum number of players that can join this room. >+ */ >+ private static final int MAX_PLAYER_COUNT = 2; >+ >+ /** >+ * List of all currently joined players. >+ */ >+ private final List<Player> players = new ArrayList<>(); >+ >+ >+ >+ public Room() { >+ roomGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, >+ RenderingHints.VALUE_ANTIALIAS_ON); >+ >+ // Clear the image with white background. >+ roomGraphics.setBackground(Color.WHITE); >+ roomGraphics.clearRect(0, 0, roomImage.getWidth(), >+ roomImage.getHeight()); >+ >+ // Schedule a TimerTask that broadcasts draw messages. >+ drawmessageBroadcastTimer.schedule(new TimerTask() { >+ @Override >+ public void run() { >+ try { >+ invokeAndWait(new Runnable() { >+ @Override >+ public void run() { >+ broadcastTimerTick(); >+ } >+ }); >+ } catch (InterruptedException | ExecutionException e) { >+ // TODO >+ } >+ } >+ }, 30, 30); >+ } >+ >+ /** >+ * Creates a Player from the given Client and adds it to this room. >+ * @param c the client >+ * @return >+ */ >+ public Player createAndAddPlayer(Client client) { >+ if (players.size() >= MAX_PLAYER_COUNT) { >+ throw new IllegalStateException("MAX_PLAYER_COUNT has been reached."); >+ } >+ >+ Player p = new Player(this, client); >+ >+ // Broadcast to the other players that one player joined. >+ broadcastRoomMessage(MessageType.PLAYER_CHANGED, "+"); >+ >+ // Add the new player to the list. >+ players.add(p); >+ >+ // Send him the current number of players and the current room image. >+ String content = String.valueOf(players.size()); >+ p.sendRoomMessage(MessageType.IMAGE_MESSAGE, content); >+ >+ // Store image as PNG >+ ByteArrayOutputStream bout = new ByteArrayOutputStream(); >+ try { >+ ImageIO.write(roomImage, "PNG", bout); >+ } catch (IOException e) { /* Should never happen */ } >+ >+ >+ // Send the image as binary message. >+ BinaryWebsocketMessage msg = new BinaryWebsocketMessage( >+ ByteBuffer.wrap(bout.toByteArray())); >+ p.getClient().sendMessage(msg); >+ >+ return p; >+ >+ } >+ >+ /** >+ * @see Player#removeFromRoom() >+ * @param p >+ */ >+ private void internalRemovePlayer(Player p) { >+ players.remove(p); >+ >+ // Broadcast that one player is removed. >+ broadcastRoomMessage(MessageType.PLAYER_CHANGED, "-"); >+ } >+ >+ /** >+ * @see Player#handleDrawMessage(DrawMessage, long) >+ * @param p >+ * @param msg >+ * @param msgId >+ */ >+ private void internalHandleDrawMessage(Player p, DrawMessage msg, >+ long msgId) { >+ p.setLastReceivedMessageId(msgId); >+ >+ // Draw the RoomMessage onto our Room Image. >+ msg.draw(roomGraphics); >+ >+ // Broadcast the Draw Message. >+ broadcastDrawMessage(msg); >+ } >+ >+ >+ /** >+ * Broadcasts the given drawboard message to all connected players. >+ * Note: For DrawMessages, please use >+ * {@link #broadcastDrawMessage(DrawMessage)} >+ * as this method will buffer them and prefix them with the correct >+ * last received Message ID. >+ * @param type >+ * @param content >+ */ >+ private void broadcastRoomMessage(MessageType type, String content) { >+ for (Player p : players) { >+ p.sendRoomMessage(type, content); >+ } >+ } >+ >+ >+ /** >+ * Broadcast the given DrawMessage. This will buffer the message >+ * and the {@link #drawmessageBroadcastTimer} will broadcast them >+ * at a regular interval, prefixing them with the player's current >+ * {@link Player#lastReceivedMessageId}. >+ * @param msg >+ */ >+ private void broadcastDrawMessage(DrawMessage msg) { >+ if (!BUFFER_DRAW_MESSAGES) { >+ String msgStr = msg.toString(); >+ >+ for (Player p : players) { >+ String s = String.valueOf(p.getLastReceivedMessageId()) >+ + "," + msgStr; >+ p.sendRoomMessage(MessageType.DRAW_MESSAGE, s); >+ } >+ } else { >+ for (Player p : players) { >+ p.getBufferedDrawMessages().add(msg); >+ } >+ } >+ } >+ >+ >+ /** >+ * Tick handler for the broadcastTimer. >+ */ >+ private void broadcastTimerTick() { >+ // For each Player, send all per Player buffered >+ // DrawMessages, prefixing each DrawMessage with the player's >+ // lastReceuvedMessageId. >+ // Multiple messages are concatenated with "|". >+ >+ for (Player p : players) { >+ >+ StringBuilder sb = new StringBuilder(); >+ List<DrawMessage> drawMessages = p.getBufferedDrawMessages(); >+ >+ if (drawMessages.size() > 0) { >+ for (int i = 0; i < drawMessages.size(); i++) { >+ DrawMessage msg = drawMessages.get(i); >+ >+ String s = String.valueOf(p.getLastReceivedMessageId()) >+ + "," + msg.toString(); >+ if (i > 0) >+ sb.append("|"); >+ >+ sb.append(s); >+ } >+ drawMessages.clear(); >+ >+ p.sendRoomMessage(MessageType.DRAW_MESSAGE, sb.toString()); >+ } >+ } >+ } >+ >+ >+ >+ >+ /** >+ * Submits the given Runnable to the Room Executor. >+ * @param run >+ */ >+ public void invoke(Runnable task) { >+ roomExecutor.submit(task); >+ } >+ >+ /** >+ * Submits the given Runnable to the Room Executor and waits until it >+ * has been executed. >+ * @param task >+ * @throws InterruptedException if the current thread was interrupted >+ * while waiting >+ * @throws ExecutionException if the computation threw an exception >+ */ >+ public void invokeAndWait(Runnable task) >+ throws InterruptedException, ExecutionException { >+ Future<?> f = roomExecutor.submit(task); >+ f.get(); >+ } >+ >+ /** >+ * Shuts down the roomExecutor and the drawmessageBroadcastTimer. >+ */ >+ public void shutdown() { >+ roomExecutor.shutdown(); >+ drawmessageBroadcastTimer.cancel(); >+ } >+ >+ >+ >+ /** >+ * A Player participates in a Room. It is the interface between the >+ * {@link Room} and the {@link Client}.<br><br> >+ * >+ * Note: This means a player object is actually a join between Room and >+ * Endpoint. >+ */ >+ public final class Player { >+ >+ /** >+ * The room to which this player belongs. >+ */ >+ private Room room; >+ >+ /** >+ * The room buffers the last draw message ID that was received from >+ * this player. >+ */ >+ private long lastReceivedMessageId = 0; >+ >+ private final Client client; >+ >+ /** >+ * Buffered DrawMessages that will be sent by a Timer. >+ * TODO: This should be refactored to be in a Room-Player join class >+ * as this is room-specific. >+ */ >+ private final List<DrawMessage> bufferedDrawMessages = >+ new ArrayList<>(); >+ >+ private List<DrawMessage> getBufferedDrawMessages() { >+ return bufferedDrawMessages; >+ } >+ >+ >+ >+ private Player(Room room, Client client) { >+ this.room = room; >+ this.client = client; >+ } >+ >+ public Room getRoom() { >+ return room; >+ } >+ >+ public Client getClient() { >+ return client; >+ } >+ >+ /** >+ * Removes this player from its room, e.g. when >+ * the client disconnects. >+ */ >+ public void removeFromRoom() { >+ room.internalRemovePlayer(this); >+ room = null; >+ } >+ >+ >+ private long getLastReceivedMessageId() { >+ return lastReceivedMessageId; >+ } >+ private void setLastReceivedMessageId(long value) { >+ lastReceivedMessageId = value; >+ } >+ >+ >+ /** >+ * Handles the given DrawMessage by drawing it onto this Room's >+ * image and by broadcasting it to the connected players. >+ * @param sender >+ * @param msg >+ * @param msgId >+ */ >+ public void handleDrawMessage(DrawMessage msg, long msgId) { >+ room.internalHandleDrawMessage(this, msg, msgId); >+ } >+ >+ >+ /** >+ * Sends the given room message. >+ * @param type >+ * @param content >+ */ >+ private void sendRoomMessage(MessageType type, String content) { >+ if (content == null || type == null) >+ throw null; >+ >+ String completeMsg = String.valueOf(type.flag) + content; >+ >+ client.sendMessage(new StringWebsocketMessage(completeMsg)); >+ } >+ >+ >+ >+ } >+ >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (working copy) >@@ -0,0 +1,48 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import javax.servlet.ServletContextEvent; >+import javax.servlet.ServletContextListener; >+import javax.servlet.annotation.WebListener; >+import javax.websocket.DeploymentException; >+import javax.websocket.server.ServerContainer; >+import javax.websocket.server.ServerEndpointConfig; >+ >+@WebListener >+public final class WsConfigListener implements ServletContextListener { >+ >+ @Override >+ public void contextInitialized(ServletContextEvent sce) { >+ >+ ServerContainer sc = >+ (ServerContainer) sce.getServletContext().getAttribute( >+ "javax.websocket.server.ServerContainer"); >+ try { >+ sc.addEndpoint(ServerEndpointConfig.Builder.create( >+ DrawboardEndpoint.class, "/websocket/drawboard").build()); >+ } catch (DeploymentException e) { >+ throw new IllegalStateException(e); >+ } >+ } >+ >+ @Override >+ public void contextDestroyed(ServletContextEvent sce) { >+ // Shutdown our room. >+ DrawboardEndpoint.getRoom().shutdown(); >+ } >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (working copy) >@@ -0,0 +1,25 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+/** >+ * Abstract base class for Websocket Messages (binary or string) >+ * that can be buffered. >+ */ >+public abstract class AbstractWebsocketMessage { >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (working copy) >@@ -0,0 +1,34 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+import java.nio.ByteBuffer; >+ >+/** >+ * Represents a binary websocket message. >+ */ >+public final class BinaryWebsocketMessage extends AbstractWebsocketMessage { >+ private final ByteBuffer bytes; >+ >+ public BinaryWebsocketMessage(ByteBuffer bytes) { >+ this.bytes = bytes; >+ } >+ >+ public ByteBuffer getBytes() { >+ return bytes; >+ } >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (working copy) >@@ -0,0 +1,34 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+/** >+ * Represents a string websocket message. >+ * >+ */ >+public final class StringWebsocketMessage extends AbstractWebsocketMessage { >+ private final String string; >+ >+ public StringWebsocketMessage(String string) { >+ this.string = string; >+ } >+ >+ public String getString() { >+ return string; >+ } >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages (working copy) > >Property changes on: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages >___________________________________________________________________ >Added: bugtraq:url >## -0,0 +1 ## >+https://issues.apache.org/bugzilla/show_bug.cgi?id=%BUGID% >\ No newline at end of property >Added: bugtraq:message >## -0,0 +1 ## >+Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=%BUGID% >\ No newline at end of property >Added: bugtraq:append >## -0,0 +1 ## >+false >\ No newline at end of property >Added: bugtraq:label >## -0,0 +1 ## >+Bugzilla ID (optional) >\ No newline at end of property >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (working copy) >@@ -0,0 +1,210 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import java.io.EOFException; >+ >+import javax.websocket.CloseReason; >+import javax.websocket.Endpoint; >+import javax.websocket.EndpointConfig; >+import javax.websocket.MessageHandler; >+import javax.websocket.Session; >+ >+import org.apache.juli.logging.Log; >+import org.apache.juli.logging.LogFactory; >+ >+import websocket.drawboard.DrawMessage.ParseException; >+import websocket.drawboard.wsmessages.StringWebsocketMessage; >+ >+ >+public final class DrawboardEndpoint extends Endpoint { >+ >+ private static final Log log = >+ LogFactory.getLog(DrawboardEndpoint.class); >+ >+ >+ /** >+ * Our room where players can join. >+ */ >+ private static final Room room = new Room(); >+ >+ public static Room getRoom() { >+ return room; >+ } >+ >+ /** >+ * The player that is associated with this Endpoint and the current room. >+ * Note that this variable is only accessed from the Room Thread.<br><br> >+ * >+ * TODO: Currently, Tomcat uses an Endpoint instance once - however >+ * the java doc of endpoint says: >+ * "Each instance of a websocket endpoint is guaranteed not to be called by >+ * more than one thread at a time per active connection." >+ * This could mean that after calling onClose(), the instance >+ * could be reused for another connection so onOpen() will get called >+ * (possibly from another thread).<br> >+ * If this is the case, we would need a variable holder for the variables >+ * that are accessed by the Room thread, and read the reference to the holder >+ * at the beginning of onOpen, onMessage, onClose methods to ensure the room >+ * thread always gets the correct instance of the variable holder. >+ */ >+ private Room.Player player; >+ >+ >+ @Override >+ public void onOpen(Session session, EndpointConfig config) { >+ // Set maximum messages size to 10.000 bytes. >+ session.setMaxTextMessageBufferSize(10000); >+ session.addMessageHandler(stringHandler); >+ final Client client = new Client(session.getAsyncRemote()); >+ >+ room.invoke(new Runnable() { >+ @Override >+ public void run() { >+ try { >+ >+ // Create a new Player and add it to the room. >+ try { >+ player = room.createAndAddPlayer(client); >+ } catch (IllegalStateException ex) { >+ // Probably the max. number of players has been >+ // reached. >+ client.sendMessage(new StringWebsocketMessage( >+ "0" + ex.getLocalizedMessage())); >+ } >+ >+ } catch (RuntimeException ex) { >+ log.error("Unexpected exception: " + ex.toString(), ex); >+ } >+ } >+ }); >+ >+ } >+ >+ >+ @Override >+ public void onClose(Session session, CloseReason closeReason) { >+ room.invoke(new Runnable() { >+ @Override >+ public void run() { >+ try { >+ >+ // Player can be null if it couldn't enter the room >+ if (player != null) { >+ // Remove this player from the room. >+ player.removeFromRoom(); >+ } >+ >+ } catch (RuntimeException ex) { >+ log.error("Unexpected exception: " + ex.toString(), ex); >+ } >+ } >+ }); >+ >+ } >+ >+ >+ >+ @Override >+ public void onError(Session session, Throwable t) { >+ // Most likely cause is a user closing their browser. Check to see if >+ // the root cause is EOF and if it is ignore it. >+ // Protect against infinite loops. >+ int count = 0; >+ Throwable root = t; >+ while (root.getCause() != null && count < 20) { >+ root = root.getCause(); >+ count ++; >+ } >+ if (root instanceof EOFException) { >+ // Assume this is triggered by the user closing their browser and >+ // ignore it. >+ } else { >+ log.error("onError: " + t.toString(), t); >+ } >+ } >+ >+ >+ >+ private final MessageHandler.Whole<String> stringHandler = >+ new MessageHandler.Whole<String>() { >+ >+ @Override >+ public void onMessage(final String message) { >+ // Invoke handling of the message in the room. >+ room.invoke(new Runnable() { >+ @Override >+ public void run() { >+ try { >+ >+ // Currently, the only types of messages the client will send >+ // are draw messages prefixed by a Message ID >+ // (starting with char '1'), and pong messages (starting >+ // with char '0'). >+ // Draw messages should look like this: >+ // ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2 >+ >+ boolean dontSwallowException = false; >+ try { >+ char messageType = message.charAt(0); >+ switch (messageType) { >+ case '0': >+ // Pong message. >+ // Do nothing. >+ break; >+ >+ case '1': >+ // Draw message >+ int indexOfChar = message.indexOf('|'); >+ long msgId = Long.parseLong( >+ message.substring(0, indexOfChar)); >+ >+ DrawMessage msg = DrawMessage.parseFromString( >+ message.substring(indexOfChar + 1)); >+ >+ // Don't ingore RuntimeExceptions thrown by >+ // this method >+ // TODO: Find a better solution than this variable >+ dontSwallowException = true; >+ if (player != null) { >+ player.handleDrawMessage(msg, msgId); >+ } >+ dontSwallowException = false; >+ >+ break; >+ } >+ >+ } catch (RuntimeException|ParseException ex) { >+ // Client sent invalid data. >+ // Ignore, TODO: maybe close connection >+ if (dontSwallowException >+ && ex instanceof RuntimeException) { >+ throw (RuntimeException) ex; >+ } >+ } >+ >+ } catch (RuntimeException ex) { >+ log.error("Unexpected exception: " + ex.toString(), ex); >+ } >+ } >+ }); >+ >+ } >+ }; >+ >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (working copy) >@@ -0,0 +1,211 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import java.awt.BasicStroke; >+import java.awt.Color; >+import java.awt.Graphics2D; >+ >+/** >+ * A message that represents a drawing action. >+ * Note that we use primitive types instead of Point, Color etc. >+ * to reduce object allocation.<br><br> >+ * >+ * TODO: But a Color objects needs to be created anyway for drawing this >+ * onto a Graphics2D object, so this probably does not save much. >+ */ >+public final class DrawMessage { >+ >+ >+ private int type; >+ private byte colorR, colorG, colorB, colorA; >+ private double thickness; >+ private int x1, y1, x2, y2; >+ >+ /** >+ * The type. 1: Line. >+ * @return >+ */ >+ public int getType() { >+ return type; >+ } >+ public void setType(int type) { >+ this.type = type; >+ } >+ >+ public double getThickness() { >+ return thickness; >+ } >+ public void setThickness(double thickness) { >+ this.thickness = thickness; >+ } >+ >+ public byte getColorR() { >+ return colorR; >+ } >+ public void setColorR(byte colorR) { >+ this.colorR = colorR; >+ } >+ public byte getColorG() { >+ return colorG; >+ } >+ public void setColorG(byte colorG) { >+ this.colorG = colorG; >+ } >+ public byte getColorB() { >+ return colorB; >+ } >+ public void setColorB(byte colorB) { >+ this.colorB = colorB; >+ } >+ public byte getColorA() { >+ return colorA; >+ } >+ public void setColorA(byte colorA) { >+ this.colorA = colorA; >+ } >+ >+ public long getX1() { >+ return x1; >+ } >+ public void setX1(int x1) { >+ this.x1 = x1; >+ } >+ public int getX2() { >+ return x2; >+ } >+ public void setX2(int x2) { >+ this.x2 = x2; >+ } >+ public int getY1() { >+ return y1; >+ } >+ public void setY1(int y1) { >+ this.y1 = y1; >+ } >+ public int getY2() { >+ return y2; >+ } >+ public void setY2(int y2) { >+ this.y2 = y2; >+ } >+ >+ >+ >+ public DrawMessage(int type, byte colorR, byte colorG, byte colorB, >+ byte colorA, double thickness, int x1, int x2, int y1, int y2) { >+ >+ this.type = type; >+ this.colorR = colorR; >+ this.colorG = colorG; >+ this.colorB = colorB; >+ this.colorA = colorA; >+ this.thickness = thickness; >+ this.x1 = x1; >+ this.x2 = x2; >+ this.y1 = y1; >+ this.y2 = y2; >+ } >+ >+ >+ /** >+ * Draws this DrawMessage onto the given Graphics2D. >+ * @param g >+ */ >+ public void draw(Graphics2D g) { >+ switch (type) { >+ case 1: >+ // Draw a line. >+ g.setStroke(new BasicStroke((float) thickness, >+ BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)); >+ g.setColor(new Color(colorR & 0xFF, colorG & 0xFF, colorB & 0xFF, >+ colorA & 0xFF)); >+ g.drawLine(x1, y1, x2, y2); >+ break; >+ } >+ } >+ >+ /** >+ * Converts this message into a String representation that >+ * can be sent over WebSocket.<br> >+ * Since a DrawMessage consists only of numbers, >+ * we concatenate those numbers with a ",". >+ */ >+ @Override >+ public String toString() { >+ >+ return type + "," + (colorR & 0xFF) + "," + (colorG & 0xFF) + "," >+ + (colorB & 0xFF) + "," + (colorA & 0xFF) + "," + thickness >+ + "," + x1 + "," + y1 + "," + x2 + "," + y2; >+ } >+ >+ public static DrawMessage parseFromString(String str) >+ throws ParseException { >+ >+ int type; >+ byte[] colors = new byte[4]; >+ double thickness; >+ int[] coords = new int[4]; >+ >+ try { >+ String[] elements = str.split(","); >+ >+ type = Integer.parseInt(elements[0]); >+ if (type != 1) >+ throw new ParseException("Invalid type: " + type); >+ >+ for (int i = 0; i < colors.length; i++) { >+ colors[i] = (byte) Integer.parseInt(elements[1 + i]); >+ } >+ >+ thickness = Double.parseDouble(elements[5]); >+ if (Double.isNaN(thickness) || thickness < 0 || thickness > 100) >+ throw new ParseException("Invalid thickness: " + thickness); >+ >+ for (int i = 0; i < coords.length; i++) { >+ coords[i] = Integer.parseInt(elements[6 + i]); >+ if (coords[i] < -1000000L || coords[i] > 1000000L) >+ throw new ParseException("Invalid coordinate: " >+ + coords[i]); >+ } >+ >+ >+ } catch (RuntimeException ex) { >+ throw new ParseException(ex); >+ } >+ >+ DrawMessage m = new DrawMessage(type, colors[0], colors[1], >+ colors[2], colors[3], thickness, coords[0], coords[2], >+ coords[1], coords[3]); >+ >+ return m; >+ } >+ >+ public static class ParseException extends Exception { >+ private static final long serialVersionUID = -6651972769789842960L; >+ >+ public ParseException(Throwable root) { >+ super(root); >+ } >+ >+ public ParseException(String message) { >+ super(message); >+ } >+ } >+ >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (working copy) >@@ -0,0 +1,436 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import java.awt.Color; >+import java.awt.Graphics2D; >+import java.awt.RenderingHints; >+import java.awt.image.BufferedImage; >+import java.io.ByteArrayOutputStream; >+import java.io.IOException; >+import java.nio.ByteBuffer; >+import java.util.ArrayList; >+import java.util.List; >+import java.util.Timer; >+import java.util.TimerTask; >+import java.util.concurrent.ExecutionException; >+import java.util.concurrent.ExecutorService; >+import java.util.concurrent.Executors; >+import java.util.concurrent.Future; >+ >+import javax.imageio.ImageIO; >+ >+import websocket.drawboard.wsmessages.BinaryWebsocketMessage; >+import websocket.drawboard.wsmessages.StringWebsocketMessage; >+ >+/** >+ * A Room represents a drawboard where a number of >+ * users participate.<br><br> >+ * >+ * Each Room has its own "Room Thread" which manages all the actions >+ * to be done in this Room. Instance methods should only be invoked >+ * from this Room's thread by calling {@link #invoke(Runnable)} or >+ * {@link #invokeAndWait(Runnable)}. >+ */ >+public final class Room { >+ >+ /** >+ * Specifies the type of a room message that is sent to a client.<br> >+ * Note: Currently we are sending simple string messages - for production >+ * apps, a JSON lib should be used for object-level messages.<br><br> >+ * >+ * The number (single char) will be prefixed to the string when sending >+ * the message. >+ */ >+ public static enum MessageType { >+ /** >+ * '0': Error: contains error message. >+ */ >+ ERROR('0'), >+ /** >+ * '1': DrawMesssage: contains serialized DrawMessage(s) prefixed >+ * with the current Player's {@link Player#lastReceivedMessageId} >+ * and ",".<br> >+ * Multiple draw messages are concatenated with "|" as separator. >+ */ >+ DRAW_MESSAGE('1'), >+ /** >+ * '2': ImageMessage: Contains number of current players in this room. >+ * After this message a Binary Websocket message will follow, >+ * containing the current Room image as PNG.<br> >+ * This is the first message that a Room sends to a new Player. >+ */ >+ IMAGE_MESSAGE('2'), >+ /** >+ * '3': PlayerChanged: contains "+" or "-" which indicate a player >+ * was added or removed to this Room. >+ */ >+ PLAYER_CHANGED('3'); >+ >+ private final char flag; >+ >+ private MessageType(char flag) { >+ this.flag = flag; >+ } >+ >+ } >+ >+ >+ >+ /** >+ * If <code>true</code>, outgoing DrawMessages will be buffered until the >+ * drawmessageBroadcastTimer ticks. Otherwise they will be sent >+ * immediately. >+ */ >+ private static final boolean BUFFER_DRAW_MESSAGES = true; >+ >+ /** >+ * A single-threaded ExecutorService where tasks >+ * are scheduled that are to be run in the Room Thread. >+ */ >+ private final ExecutorService roomExecutor = >+ Executors.newSingleThreadExecutor(); >+ >+ /** >+ * A timer which sends buffered drawmessages to the client at once >+ * at a regular interval, to avoid sending a lot of very small >+ * messages which would cause TCP overhead and high CPU usage. >+ */ >+ private final Timer drawmessageBroadcastTimer = new Timer(); >+ >+ >+ /** >+ * The current image of the room drawboard. DrawMessages that are >+ * received from Players will be drawn onto this image. >+ */ >+ private final BufferedImage roomImage = >+ new BufferedImage(900, 600, BufferedImage.TYPE_INT_RGB); >+ private final Graphics2D roomGraphics = roomImage.createGraphics(); >+ >+ >+ /** >+ * The maximum number of players that can join this room. >+ */ >+ private static final int MAX_PLAYER_COUNT = 2; >+ >+ /** >+ * List of all currently joined players. >+ */ >+ private final List<Player> players = new ArrayList<>(); >+ >+ >+ >+ public Room() { >+ roomGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, >+ RenderingHints.VALUE_ANTIALIAS_ON); >+ >+ // Clear the image with white background. >+ roomGraphics.setBackground(Color.WHITE); >+ roomGraphics.clearRect(0, 0, roomImage.getWidth(), >+ roomImage.getHeight()); >+ >+ // Schedule a TimerTask that broadcasts draw messages. >+ drawmessageBroadcastTimer.schedule(new TimerTask() { >+ @Override >+ public void run() { >+ try { >+ invokeAndWait(new Runnable() { >+ @Override >+ public void run() { >+ broadcastTimerTick(); >+ } >+ }); >+ } catch (InterruptedException | ExecutionException e) { >+ // TODO >+ } >+ } >+ }, 30, 30); >+ } >+ >+ /** >+ * Creates a Player from the given Client and adds it to this room. >+ * @param c the client >+ * @return >+ */ >+ public Player createAndAddPlayer(Client client) { >+ if (players.size() >= MAX_PLAYER_COUNT) { >+ throw new IllegalStateException("MAX_PLAYER_COUNT has been reached."); >+ } >+ >+ Player p = new Player(this, client); >+ >+ // Broadcast to the other players that one player joined. >+ broadcastRoomMessage(MessageType.PLAYER_CHANGED, "+"); >+ >+ // Add the new player to the list. >+ players.add(p); >+ >+ // Send him the current number of players and the current room image. >+ String content = String.valueOf(players.size()); >+ p.sendRoomMessage(MessageType.IMAGE_MESSAGE, content); >+ >+ // Store image as PNG >+ ByteArrayOutputStream bout = new ByteArrayOutputStream(); >+ try { >+ ImageIO.write(roomImage, "PNG", bout); >+ } catch (IOException e) { /* Should never happen */ } >+ >+ >+ // Send the image as binary message. >+ BinaryWebsocketMessage msg = new BinaryWebsocketMessage( >+ ByteBuffer.wrap(bout.toByteArray())); >+ p.getClient().sendMessage(msg); >+ >+ return p; >+ >+ } >+ >+ /** >+ * @see Player#removeFromRoom() >+ * @param p >+ */ >+ private void internalRemovePlayer(Player p) { >+ players.remove(p); >+ >+ // Broadcast that one player is removed. >+ broadcastRoomMessage(MessageType.PLAYER_CHANGED, "-"); >+ } >+ >+ /** >+ * @see Player#handleDrawMessage(DrawMessage, long) >+ * @param p >+ * @param msg >+ * @param msgId >+ */ >+ private void internalHandleDrawMessage(Player p, DrawMessage msg, >+ long msgId) { >+ p.setLastReceivedMessageId(msgId); >+ >+ // Draw the RoomMessage onto our Room Image. >+ msg.draw(roomGraphics); >+ >+ // Broadcast the Draw Message. >+ broadcastDrawMessage(msg); >+ } >+ >+ >+ /** >+ * Broadcasts the given drawboard message to all connected players. >+ * Note: For DrawMessages, please use >+ * {@link #broadcastDrawMessage(DrawMessage)} >+ * as this method will buffer them and prefix them with the correct >+ * last received Message ID. >+ * @param type >+ * @param content >+ */ >+ private void broadcastRoomMessage(MessageType type, String content) { >+ for (Player p : players) { >+ p.sendRoomMessage(type, content); >+ } >+ } >+ >+ >+ /** >+ * Broadcast the given DrawMessage. This will buffer the message >+ * and the {@link #drawmessageBroadcastTimer} will broadcast them >+ * at a regular interval, prefixing them with the player's current >+ * {@link Player#lastReceivedMessageId}. >+ * @param msg >+ */ >+ private void broadcastDrawMessage(DrawMessage msg) { >+ if (!BUFFER_DRAW_MESSAGES) { >+ String msgStr = msg.toString(); >+ >+ for (Player p : players) { >+ String s = String.valueOf(p.getLastReceivedMessageId()) >+ + "," + msgStr; >+ p.sendRoomMessage(MessageType.DRAW_MESSAGE, s); >+ } >+ } else { >+ for (Player p : players) { >+ p.getBufferedDrawMessages().add(msg); >+ } >+ } >+ } >+ >+ >+ /** >+ * Tick handler for the broadcastTimer. >+ */ >+ private void broadcastTimerTick() { >+ // For each Player, send all per Player buffered >+ // DrawMessages, prefixing each DrawMessage with the player's >+ // lastReceuvedMessageId. >+ // Multiple messages are concatenated with "|". >+ >+ for (Player p : players) { >+ >+ StringBuilder sb = new StringBuilder(); >+ List<DrawMessage> drawMessages = p.getBufferedDrawMessages(); >+ >+ if (drawMessages.size() > 0) { >+ for (int i = 0; i < drawMessages.size(); i++) { >+ DrawMessage msg = drawMessages.get(i); >+ >+ String s = String.valueOf(p.getLastReceivedMessageId()) >+ + "," + msg.toString(); >+ if (i > 0) >+ sb.append("|"); >+ >+ sb.append(s); >+ } >+ drawMessages.clear(); >+ >+ p.sendRoomMessage(MessageType.DRAW_MESSAGE, sb.toString()); >+ } >+ } >+ } >+ >+ >+ >+ >+ /** >+ * Submits the given Runnable to the Room Executor. >+ * @param run >+ */ >+ public void invoke(Runnable task) { >+ roomExecutor.submit(task); >+ } >+ >+ /** >+ * Submits the given Runnable to the Room Executor and waits until it >+ * has been executed. >+ * @param task >+ * @throws InterruptedException if the current thread was interrupted >+ * while waiting >+ * @throws ExecutionException if the computation threw an exception >+ */ >+ public void invokeAndWait(Runnable task) >+ throws InterruptedException, ExecutionException { >+ Future<?> f = roomExecutor.submit(task); >+ f.get(); >+ } >+ >+ /** >+ * Shuts down the roomExecutor and the drawmessageBroadcastTimer. >+ */ >+ public void shutdown() { >+ roomExecutor.shutdown(); >+ drawmessageBroadcastTimer.cancel(); >+ } >+ >+ >+ >+ /** >+ * A Player participates in a Room. It is the interface between the >+ * {@link Room} and the {@link Client}.<br><br> >+ * >+ * Note: This means a player object is actually a join between Room and >+ * Endpoint. >+ */ >+ public final class Player { >+ >+ /** >+ * The room to which this player belongs. >+ */ >+ private Room room; >+ >+ /** >+ * The room buffers the last draw message ID that was received from >+ * this player. >+ */ >+ private long lastReceivedMessageId = 0; >+ >+ private final Client client; >+ >+ /** >+ * Buffered DrawMessages that will be sent by a Timer. >+ * TODO: This should be refactored to be in a Room-Player join class >+ * as this is room-specific. >+ */ >+ private final List<DrawMessage> bufferedDrawMessages = >+ new ArrayList<>(); >+ >+ private List<DrawMessage> getBufferedDrawMessages() { >+ return bufferedDrawMessages; >+ } >+ >+ >+ >+ private Player(Room room, Client client) { >+ this.room = room; >+ this.client = client; >+ } >+ >+ public Room getRoom() { >+ return room; >+ } >+ >+ public Client getClient() { >+ return client; >+ } >+ >+ /** >+ * Removes this player from its room, e.g. when >+ * the client disconnects. >+ */ >+ public void removeFromRoom() { >+ room.internalRemovePlayer(this); >+ room = null; >+ } >+ >+ >+ private long getLastReceivedMessageId() { >+ return lastReceivedMessageId; >+ } >+ private void setLastReceivedMessageId(long value) { >+ lastReceivedMessageId = value; >+ } >+ >+ >+ /** >+ * Handles the given DrawMessage by drawing it onto this Room's >+ * image and by broadcasting it to the connected players. >+ * @param sender >+ * @param msg >+ * @param msgId >+ */ >+ public void handleDrawMessage(DrawMessage msg, long msgId) { >+ room.internalHandleDrawMessage(this, msg, msgId); >+ } >+ >+ >+ /** >+ * Sends the given room message. >+ * @param type >+ * @param content >+ */ >+ private void sendRoomMessage(MessageType type, String content) { >+ if (content == null || type == null) >+ throw null; >+ >+ String completeMsg = String.valueOf(type.flag) + content; >+ >+ client.sendMessage(new StringWebsocketMessage(completeMsg)); >+ } >+ >+ >+ >+ } >+ >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (working copy) >@@ -0,0 +1,48 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard; >+ >+import javax.servlet.ServletContextEvent; >+import javax.servlet.ServletContextListener; >+import javax.servlet.annotation.WebListener; >+import javax.websocket.DeploymentException; >+import javax.websocket.server.ServerContainer; >+import javax.websocket.server.ServerEndpointConfig; >+ >+@WebListener >+public final class WsConfigListener implements ServletContextListener { >+ >+ @Override >+ public void contextInitialized(ServletContextEvent sce) { >+ >+ ServerContainer sc = >+ (ServerContainer) sce.getServletContext().getAttribute( >+ "javax.websocket.server.ServerContainer"); >+ try { >+ sc.addEndpoint(ServerEndpointConfig.Builder.create( >+ DrawboardEndpoint.class, "/websocket/drawboard").build()); >+ } catch (DeploymentException e) { >+ throw new IllegalStateException(e); >+ } >+ } >+ >+ @Override >+ public void contextDestroyed(ServletContextEvent sce) { >+ // Shutdown our room. >+ DrawboardEndpoint.getRoom().shutdown(); >+ } >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (working copy) >@@ -0,0 +1,25 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+/** >+ * Abstract base class for Websocket Messages (binary or string) >+ * that can be buffered. >+ */ >+public abstract class AbstractWebsocketMessage { >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (working copy) >@@ -0,0 +1,34 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+import java.nio.ByteBuffer; >+ >+/** >+ * Represents a binary websocket message. >+ */ >+public final class BinaryWebsocketMessage extends AbstractWebsocketMessage { >+ private final ByteBuffer bytes; >+ >+ public BinaryWebsocketMessage(ByteBuffer bytes) { >+ this.bytes = bytes; >+ } >+ >+ public ByteBuffer getBytes() { >+ return bytes; >+ } >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (working copy) >@@ -0,0 +1,34 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+/** >+ * Represents a string websocket message. >+ * >+ */ >+public final class StringWebsocketMessage extends AbstractWebsocketMessage { >+ private final String string; >+ >+ public StringWebsocketMessage(String string) { >+ this.string = string; >+ } >+ >+ public String getString() { >+ return string; >+ } >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages (working copy) > >Property changes on: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages >___________________________________________________________________ >Added: bugtraq:label >## -0,0 +1 ## >+Bugzilla ID (optional) >\ No newline at end of property >Added: bugtraq:url >## -0,0 +1 ## >+https://issues.apache.org/bugzilla/show_bug.cgi?id=%BUGID% >\ No newline at end of property >Added: bugtraq:message >## -0,0 +1 ## >+Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=%BUGID% >\ No newline at end of property >Added: bugtraq:append >## -0,0 +1 ## >+false >\ No newline at end of property >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (working copy) >@@ -0,0 +1,25 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+/** >+ * Abstract base class for Websocket Messages (binary or string) >+ * that can be buffered. >+ */ >+public abstract class AbstractWebsocketMessage { >+ >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (working copy) >@@ -0,0 +1,34 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+import java.nio.ByteBuffer; >+ >+/** >+ * Represents a binary websocket message. >+ */ >+public final class BinaryWebsocketMessage extends AbstractWebsocketMessage { >+ private final ByteBuffer bytes; >+ >+ public BinaryWebsocketMessage(ByteBuffer bytes) { >+ this.bytes = bytes; >+ } >+ >+ public ByteBuffer getBytes() { >+ return bytes; >+ } >+} >Index: webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java >=================================================================== >--- webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (revision 0) >+++ webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (working copy) >@@ -0,0 +1,34 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You 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. >+ */ >+package websocket.drawboard.wsmessages; >+ >+/** >+ * Represents a string websocket message. >+ * >+ */ >+public final class StringWebsocketMessage extends AbstractWebsocketMessage { >+ private final String string; >+ >+ public StringWebsocketMessage(String string) { >+ this.string = string; >+ } >+ >+ public String getString() { >+ return string; >+ } >+ >+} >Index: webapps/examples/websocket/drawboard.xhtml >=================================================================== >--- webapps/examples/websocket/drawboard.xhtml (revision 0) >+++ webapps/examples/websocket/drawboard.xhtml (working copy) >@@ -0,0 +1,611 @@ >+<?xml version="1.0" encoding="UTF-8"?> >+<!-- >+ Licensed to the Apache Software Foundation (ASF) under one or more >+ contributor license agreements. See the NOTICE file distributed with >+ this work for additional information regarding copyright ownership. >+ The ASF licenses this file to You 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. >+--> >+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> >+<head> >+ <title>Apache Tomcat WebSocket Examples: Drawboard</title> >+ <style type="text/css"><![CDATA[ >+ >+ body { >+ font-family: Arial, sans-serif; >+ font-size: 11pt; >+ background-color: #eeeeea; >+ padding: 10px; >+ } >+ >+ #console-container { >+ float: left; >+ background-color: #fff; >+ width: 250px; >+ } >+ >+ #console { >+ font-size: 10pt; >+ height: 600px; >+ overflow-y: scroll; >+ padding-left: 5px; >+ padding-right: 5px; >+ } >+ >+ #console p { >+ padding: 0; >+ margin: 0; >+ } >+ >+ #drawContainer { >+ float: left; >+ display: none; >+ margin-right: 25px; >+ } >+ >+ #drawContainer canvas { >+ display: block; >+ -ms-touch-action: none; /* Disable touch behaviors, like pan and zoom */ >+ } >+ >+ #labelContainer { >+ margin-bottom: 15px; >+ } >+ >+ #drawContainer, #console-container { >+ box-shadow: 0px 0px 8px 3px #bbb; >+ border: 1px solid #CCCCCC; >+ >+ } >+ >+ ]]></style> >+ <script type="application/javascript"><![CDATA[ >+ "use strict"; >+ >+ (function() { >+ >+ document.addEventListener("DOMContentLoaded", function() { >+ // Remove elements with "noscript" class - <noscript> is not >+ // allowed in XHTML >+ var noscripts = document.getElementsByClassName("noscript"); >+ for (var i = 0; i < noscripts.length; i++) { >+ noscripts[i].parentNode.removeChild(noscripts[i]); >+ } >+ >+ >+ var Console = {}; >+ >+ Console.log = (function() { >+ var consoleContainer = >+ document.getElementById("console-container"); >+ var console = document.createElement("div"); >+ console.setAttribute("id", "console"); >+ consoleContainer.appendChild(console); >+ >+ return function(message) { >+ var p = document.createElement('p'); >+ p.style.wordWrap = "break-word"; >+ p.appendChild(document.createTextNode(message)); >+ console.appendChild(p); >+ while (console.childNodes.length > 25) { >+ console.removeChild(console.firstChild); >+ } >+ console.scrollTop = console.scrollHeight; >+ } >+ })(); >+ >+ >+ function Room(drawContainer) { >+ >+ // The WebSocket object. >+ var socket; >+ // ID of the timer which sends ping messages. >+ var pingTimerId; >+ >+ var isStarted = false; >+ var playerCount = 0; >+ >+ // An array of PathIdContainer objects that the server >+ // did not yet handle. >+ // They are ordered by id (ascending). >+ var pathsNotHandled = []; >+ >+ var nextMsgId = 1; >+ >+ var canvasDisplay = document.createElement("canvas"); >+ var canvasBackground = document.createElement("canvas"); >+ var canvasServerImage = document.createElement("canvas"); >+ var canvasArray = [canvasDisplay, canvasBackground, >+ canvasServerImage]; >+ >+ var labelPlayerCount = document.createTextNode("0"); >+ var optionContainer = document.createElement("div"); >+ >+ >+ var canvasDisplayCtx = canvasDisplay.getContext("2d"); >+ var canvasBackgroundCtx = canvasBackground.getContext("2d"); >+ var canvasServerImageCtx = canvasServerImage.getContext("2d"); >+ >+ var mouseInWindow = false; >+ var mouseDown = false; >+ var currentMouseX = 0, currentMouseY = 0; >+ >+ var availableColors = []; >+ var currentColorIndex; >+ var colorContainers; >+ >+ var availableThicknesses = [2, 3, 6, 10, 16, 28, 50]; >+ var currentThicknessIndex; >+ var thicknessContainers; >+ >+ >+ var placeholder = document.createElement("div"); >+ placeholder.appendChild(document.createTextNode("Loading... ")); >+ var progressElem = document.createElement("progress"); >+ placeholder.appendChild(progressElem); >+ >+ labelContainer.appendChild(placeholder); >+ >+ function rgb(color) { >+ return "rgba(" + color[0] + "," + color[1] + "," >+ + color[2] + "," + color[3] + ")"; >+ } >+ >+ function PathIdContainer(path, id) { >+ this.path = path; >+ this.id = id; >+ } >+ >+ function Path(type, color, thickness, x1, y1, x2, y2) { >+ this.type = type; >+ this.color = color; >+ this.thickness = thickness; >+ this.x1 = x1; >+ this.y1 = y1; >+ this.x2 = x2; >+ this.y2 = y2; >+ >+ this.draw = function(ctx) { >+ ctx.beginPath(); >+ ctx.lineCap = "round"; >+ ctx.lineWidth = thickness; >+ var style = rgb(color); >+ ctx.strokeStyle = style; >+ >+ if (x1 == x2 && y1 == y2) { >+ // Always draw as arc to meet the behavior >+ // in Java2D. >+ ctx.fillStyle = style; >+ ctx.arc(x1, y1, thickness / 2.0, 0, >+ Math.PI * 2.0, false); >+ ctx.fill(); >+ } else { >+ if (type == 1) { >+ // Draw a line. >+ ctx.moveTo(x1, y1); >+ ctx.lineTo(x2, y2); >+ ctx.stroke(); >+ } >+ } >+ }; >+ } >+ >+ >+ function connect() { >+ var host = (window.location.protocol == "https:" >+ ? "wss://" : "ws://") + window.location.host >+ + "/examples/websocket/drawboard"; >+ socket = new WebSocket(host); >+ >+ socket.onopen = function () { >+ // Socket has opened. Now wait for the server to >+ // send us the initial packet. >+ Console.log("WebSocket connection opened."); >+ >+ // Set up a timer for pong messages. >+ pingTimerId = window.setInterval(function() { >+ socket.send("0"); >+ }, 30000); >+ } >+ >+ socket.onclose = function () { >+ Console.log("WebSocket connection closed."); >+ disableControls(); >+ >+ // Disable pong timer. >+ window.clearInterval(pingTimerId); >+ } >+ >+ socket.onmessage = function(message) { >+ >+ // Split joined message and process them >+ // invidividually. >+ var messages = message.data.split(";"); >+ for (var msgArrIdx = 0; msgArrIdx < messages.length; >+ msgArrIdx++) { >+ var msg = messages[msgArrIdx]; >+ var type = msg.substring(0, 1); >+ >+ if (type == "0") { >+ // Error message. >+ var error = msg.substring(1); >+ // Log it to the console and show an alert. >+ Console.log("Error: " + error); >+ alert(error); >+ >+ } else { >+ if (!isStarted) { >+ if (type == "2") { >+ // Initial message. It contains the >+ // number of players. >+ // After this message we will receive >+ // a binary message containing the current >+ // room image as PNG. >+ playerCount = parseInt(msg.substring(1)); >+ >+ refreshPlayerCount(); >+ >+ // The next message will be a binary >+ // message containing the room images >+ // as PNG. Therefore we temporarily swap >+ // the message handler. >+ var originalHandler = socket.onmessage; >+ socket.onmessage = function(message) { >+ // First, we restore the original handler. >+ socket.onmessage = originalHandler; >+ >+ // Read the image. >+ var blob = message.data; >+ // Create new blob with correct MIME type. >+ blob = new Blob([blob], {type : "image/png"}); >+ >+ var url = URL.createObjectURL(blob); >+ >+ var img = new Image(); >+ >+ // We must wait until the onload event is >+ // raised until we can draw the image onto >+ // the canvas. >+ >+ // TODO: I don't know if there is a guarantee >+ // that no WebSocket events (onmessage) will >+ // be raised until the onload event of this >+ // image is raised. Maybe we need to need to >+ // push websocket messages on a queue until >+ // this onload function is called. >+ img.onload = function() { >+ >+ // Release the object URL. >+ URL.revokeObjectURL(url); >+ >+ // Set the canvases to the correct size. >+ >+ for (var i = 0; i < canvasArray.length; i++) { >+ canvasArray[i].width = img.width; >+ canvasArray[i].height = img.height; >+ } >+ >+ // Now draw the image on the last canvas. >+ canvasServerImageCtx.clearRect(0, 0, >+ canvasServerImage.width, >+ canvasServerImage.height); >+ canvasServerImageCtx.drawImage(img, 0, 0); >+ >+ // Draw it on the background canvas. >+ canvasBackgroundCtx.drawImage(canvasServerImage, >+ 0, 0); >+ >+ // Refresh the display canvas. >+ refreshDisplayCanvas(); >+ >+ isStarted = true; >+ startControls(); >+ }; >+ >+ img.src = url; >+ }; >+ } >+ } else { >+ if (type == "3") { >+ // The number of players in this room changed. >+ var playerAdded = msg.substring(1) == "+"; >+ playerCount += playerAdded ? 1 : -1; >+ refreshPlayerCount(); >+ >+ Console.log("Player " + (playerAdded >+ ? "joined." : "left.")); >+ >+ } else if (type == "1") { >+ // We received a new DrawMessage. >+ var maxLastHandledId = -1; >+ var drawMessages = msg.substring(1).split("|"); >+ for (var i = 0; i < drawMessages.length; i++) { >+ var elements = drawMessages[i].split(","); >+ var lastHandledId = parseInt(elements[0]); >+ maxLastHandledId = Math.max(maxLastHandledId, >+ lastHandledId); >+ >+ var path = new Path( >+ parseInt(elements[1]), >+ [parseInt(elements[2]), >+ parseInt(elements[3]), >+ parseInt(elements[4]), >+ parseInt(elements[5]) / 255.0], >+ parseFloat(elements[6]), >+ parseInt(elements[7]), >+ parseInt(elements[8]), >+ parseInt(elements[9]), >+ parseInt(elements[10])); >+ >+ // Draw the path onto the last canvas. >+ path.draw(canvasServerImageCtx); >+ } >+ >+ // Draw the last canvas onto the background one. >+ canvasBackgroundCtx.drawImage(canvasServerImage, >+ 0, 0); >+ >+ // Now go through the pathsNotHandled array and >+ // remove the paths that were already handled by >+ // the server. >+ while (pathsNotHandled.length > 0 >+ && pathsNotHandled[0].id <= maxLastHandledId) >+ pathsNotHandled.shift(); >+ >+ // Now me must draw the remaining paths onto >+ // the background canvas. >+ for (var i = 0; i < pathsNotHandled.length; i++) { >+ pathsNotHandled[i].path.draw(canvasBackgroundCtx); >+ } >+ >+ refreshDisplayCanvas(); >+ } >+ } >+ } >+ } >+ }; >+ >+ } >+ >+ function refreshPlayerCount() { >+ labelPlayerCount.nodeValue = String(playerCount); >+ } >+ >+ function refreshDisplayCanvas() { >+ canvasDisplayCtx.drawImage(canvasBackground, 0, 0); >+ if (mouseInWindow && !mouseDown) { >+ canvasDisplayCtx.beginPath(); >+ var color = availableColors[currentColorIndex].slice(0); >+ color[3] = 0.5; >+ canvasDisplayCtx.fillStyle = rgb(color); >+ >+ canvasDisplayCtx.arc(currentMouseX, currentMouseY, availableThicknesses[currentThicknessIndex] / 2, 0, Math.PI * 2.0, true); >+ >+ canvasDisplayCtx.fill(); >+ } >+ } >+ >+ function startControls() { >+ var labelContainer = document.getElementById("labelContainer"); >+ labelContainer.removeChild(placeholder); >+ placeholder = undefined; >+ >+ labelContainer.appendChild( >+ document.createTextNode("Number of Players: ")); >+ labelContainer.appendChild(labelPlayerCount); >+ >+ >+ drawContainer.style.display = "block"; >+ drawContainer.appendChild(canvasDisplay); >+ >+ drawContainer.appendChild(optionContainer); >+ >+ canvasDisplay.onmousemove = function(e) { >+ mouseInWindow = true; >+ var oldMouseX = currentMouseX, oldMouseY = currentMouseY; >+ currentMouseX = e.pageX - canvasDisplay.offsetLeft; >+ currentMouseY = e.pageY - canvasDisplay.offsetTop; >+ >+ if (mouseDown) { >+ var path = new Path(1, availableColors[currentColorIndex], >+ availableThicknesses[currentThicknessIndex], >+ oldMouseX, oldMouseY, currentMouseX, >+ currentMouseY); >+ // Draw it on the background canvas. >+ path.draw(canvasBackgroundCtx); >+ >+ // Send it to the sever. >+ pushPath(path); >+ } >+ >+ refreshDisplayCanvas(); >+ }; >+ >+ canvasDisplay.onmousedown = function(e) { >+ currentMouseX = e.pageX - canvasDisplay.offsetLeft; >+ currentMouseY = e.pageY - canvasDisplay.offsetTop; >+ >+ if (e.button == 0) { >+ mouseDown = true; >+ >+ var path = new Path(1, availableColors[currentColorIndex], >+ availableThicknesses[currentThicknessIndex], >+ currentMouseX, currentMouseY, currentMouseX, >+ currentMouseY); >+ // Draw it on the background canvas. >+ path.draw(canvasBackgroundCtx); >+ >+ // Send it to the sever. >+ pushPath(path); >+ >+ refreshDisplayCanvas(); >+ >+ } else if (mouseDown) { >+ // Cancel drawing. >+ mouseDown = false; >+ >+ refreshDisplayCanvas(); >+ } >+ } >+ >+ canvasDisplay.onmouseup = function(e) { >+ if (e.button == 0) { >+ if (mouseDown) { >+ mouseDown = false; >+ >+ refreshDisplayCanvas(); >+ } >+ } >+ }; >+ >+ canvasDisplay.onmouseout = function() { >+ mouseInWindow = false; >+ refreshDisplayCanvas(); >+ }; >+ >+ >+ // Create color and thickness controls. >+ var colorContainersBox = document.createElement("div"); >+ colorContainersBox.setAttribute("style", >+ "margin: 4px; border: 1px solid #bbb; border-radius: 3px;"); >+ optionContainer.appendChild(colorContainersBox); >+ >+ >+ colorContainers = new Array(3 * 3 * 3); >+ for (var i = 0; i < colorContainers.length; i++) { >+ var colorContainer = colorContainers[i] = >+ document.createElement("div"); >+ var color = availableColors[i] = >+ [ >+ Math.floor((i % 3) * 255 / 2), >+ Math.floor((Math.floor(i / 3) % 3) * 255 / 2), >+ Math.floor((Math.floor(i / (3 * 3)) % 3) * 255 / 2), >+ 1.0 >+ ]; >+ colorContainer.setAttribute("style", >+ "margin: 3px; width: 18px; height: 18px; " >+ + "float: left; background-color: " + rgb(color)); >+ colorContainer.style.border = '2px solid #000'; >+ colorContainer.onmousedown = (function(ix) { >+ return function() { >+ setColor(ix); >+ }; >+ })(i); >+ >+ colorContainersBox.appendChild(colorContainer); >+ } >+ >+ var divClearLeft = document.createElement("div"); >+ divClearLeft.setAttribute("style", "clear: left;"); >+ colorContainersBox.appendChild(divClearLeft); >+ >+ var thicknessContainersBox = document.createElement("div"); >+ thicknessContainersBox.setAttribute("style", >+ "margin: 3px; border: 1px solid #bbb; border-radius: 3px;"); >+ optionContainer.appendChild(thicknessContainersBox); >+ >+ >+ thicknessContainers = new Array(availableThicknesses.length); >+ for (var i = 0; i < thicknessContainers.length; i++) { >+ var thicknessContainer = thicknessContainers[i] = >+ document.createElement("div"); >+ thicknessContainer.setAttribute("style", >+ "text-align: center; margin: 3px; width: 18px; " >+ + "height: 18px; float: left;"); >+ thicknessContainer.style.border = "2px solid #000"; >+ thicknessContainer.appendChild(document.createTextNode( >+ String(availableThicknesses[i]))); >+ thicknessContainer.onmousedown = (function(ix) { >+ return function() { >+ setThickness(ix); >+ }; >+ })(i); >+ >+ thicknessContainersBox.appendChild(thicknessContainer); >+ } >+ >+ divClearLeft = document.createElement("div"); >+ divClearLeft.setAttribute("style", "clear: left;"); >+ thicknessContainersBox.appendChild(divClearLeft); >+ >+ >+ setColor(0); >+ setThickness(0); >+ >+ } >+ >+ function disableControls() { >+ canvasDisplay.onmousemove = canvasDisplay.onmousedown = >+ undefined; >+ mouseInWindow = false; >+ refreshDisplayCanvas(); >+ } >+ >+ function pushPath(path) { >+ >+ // Push it into the pathsNotHandled array. >+ var container = new PathIdContainer(path, nextMsgId++); >+ pathsNotHandled.push(container); >+ >+ // Send the path to the server. >+ var message = container.id + "|" + path.type + "," >+ + path.color[0] + "," + path.color[1] + "," >+ + path.color[2] + "," >+ + Math.round(path.color[3] * 255.0) + "," >+ + path.thickness + "," + path.x1 + "," >+ + path.y1 + "," + path.x2 + "," + path.y2; >+ >+ socket.send("1" + message); >+ } >+ >+ function setThickness(thicknessIndex) { >+ if (typeof currentThicknessIndex !== "undefined") >+ thicknessContainers[currentThicknessIndex] >+ .style.borderColor = "#000"; >+ currentThicknessIndex = thicknessIndex; >+ thicknessContainers[currentThicknessIndex] >+ .style.borderColor = "#d08"; >+ } >+ >+ function setColor(colorIndex) { >+ if (typeof currentColorIndex !== "undefined") >+ colorContainers[currentColorIndex] >+ .style.borderColor = "#000"; >+ currentColorIndex = colorIndex; >+ colorContainers[currentColorIndex] >+ .style.borderColor = "#d08"; >+ } >+ >+ >+ connect(); >+ >+ } >+ >+ >+ // Initialize the room >+ var room = new Room(document.getElementById("drawContainer")); >+ >+ >+ }, false); >+ >+ })(); >+ ]]></script> >+</head> >+<body> >+ <div class="noscript"><h2 style="color: #ff0000;">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable >+ Javascript and reload this page!</h2></div> >+ <div id="labelContainer"/> >+ <div id="drawContainer"/> >+ <div id="console-container"/> >+ >+</body> >+</html> >\ No newline at end of file >Index: webapps/examples/websocket/index.xhtml >=================================================================== >--- webapps/examples/websocket/index.xhtml (revision 1529994) >+++ webapps/examples/websocket/index.xhtml (working copy) >@@ -25,6 +25,8 @@ > <li><a href="echo.xhtml">Echo example</a></li> > <li><a href="chat.xhtml">Chat example</a></li> > <li><a href="snake.xhtml">Multiplayer snake example</a></li> >+ <li><a href="drawboard.xhtml">Multiplayer drawboard example</a></li> >+ > </ul> > </body> > </html> >\ No newline at end of file
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
Attachments on
bug 55639
:
30909
|
30910