You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by kp...@apache.org on 2013/10/08 16:41:33 UTC

svn commit: r1530296 - in /tomcat/trunk/webapps: docs/ examples/WEB-INF/classes/websocket/drawboard/ examples/WEB-INF/classes/websocket/drawboard/wsmessages/ examples/websocket/

Author: kpreisser
Date: Tue Oct  8 14:41:32 2013
New Revision: 1530296

URL: http://svn.apache.org/r1530296
Log:
Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=55639
Add a Drawboard WebSocket example.

Added:
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java
    tomcat/trunk/webapps/examples/websocket/drawboard.xhtml
Modified:
    tomcat/trunk/webapps/docs/changelog.xml
    tomcat/trunk/webapps/examples/websocket/index.xhtml

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1530296&r1=1530295&r2=1530296&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Tue Oct  8 14:41:32 2013
@@ -260,6 +260,9 @@
         the documentation web application. Patches provided by Konstantin
         Preißer. (markt)
       </update>
+	  <add>
+	    <bug>55639</bug>: Add a Drawboard WebSocket example. (kpreisser)
+	  </add>
     </changelog>
   </subsection>
   <subsection name="Tribes">

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java Tue Oct  8 14:41:32 2013
@@ -0,0 +1,124 @@
+/*
+ *  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.util.LinkedList;
+
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+import websocket.drawboard.wsmessages.AbstractWebsocketMessage;
+import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
+import websocket.drawboard.wsmessages.StringWebsocketMessage;
+
+/**
+ * Represents a client with methods to send messages.
+ */
+public class Client {
+
+    private final RemoteEndpoint.Async async;
+
+    /**
+     * Contains the messages wich are buffered until the previous
+     * send operation has finished.
+     */
+    private final LinkedList<AbstractWebsocketMessage> messagesToSend =
+            new LinkedList<>();
+    /**
+     * If this client is currently sending a messages asynchronously.
+     */
+    private volatile boolean isSendingMessage = false;
+
+    public Client(RemoteEndpoint.Async async) {
+        this.async = async;
+    }
+
+
+    /**
+     * Sends the given message asynchronously to the client.
+     * If there is already a async sending in progress, then the message
+     * will be buffered and sent when possible.<br><br>
+     * 
+     * This method can be called from multiple threads.
+     * @param msg
+     */
+    public void sendMessage(AbstractWebsocketMessage msg) {
+        synchronized (messagesToSend) {
+            if (isSendingMessage) {
+                // TODO: Check if the buffered messages exceed
+                // a specific amount - in that case, disconnect the client
+                // to prevent DoS.
+
+                // TODO: Check if the last message is a
+                // String message - in that case we should concatenate them
+                // to reduce TCP overhead (using ";" as separator).
+
+                messagesToSend.add(msg);
+            } else {
+                isSendingMessage = true;
+                internalSendMessageAsync(msg);
+            }
+
+
+        }
+    }
+
+    /**
+     * Internally sends the messages asynchronously.
+     * @param msg
+     */
+    private void internalSendMessageAsync(AbstractWebsocketMessage msg) {
+        try {
+            if (msg instanceof StringWebsocketMessage) {
+                StringWebsocketMessage sMsg = (StringWebsocketMessage) msg;
+                async.sendText(sMsg.getString(), sendHandler);
+
+            } else if (msg instanceof BinaryWebsocketMessage) {
+                BinaryWebsocketMessage bMsg = (BinaryWebsocketMessage) msg;
+                async.sendBinary(bMsg.getBytes(), sendHandler);
+            }
+        } catch (IllegalStateException ex) {
+            // Trying to write to the client when the session has
+            // already been closed.
+            // Ignore
+        }
+    }
+
+
+
+    /**
+     * SendHandler that will continue to send buffered messages.
+     */
+    private final SendHandler sendHandler = new SendHandler() {
+        @Override
+        public void onResult(SendResult result) {
+            synchronized (messagesToSend) {
+
+                if (!messagesToSend.isEmpty()) {
+                    AbstractWebsocketMessage msg = messagesToSend.remove();
+                    internalSendMessageAsync(msg);
+
+                } else {
+                    isSendingMessage = false;
+                }
+
+            }
+        }
+    };
+
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java Tue Oct  8 14:41:32 2013
@@ -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);
+        }
+    }
+
+
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java Tue Oct  8 14:41:32 2013
@@ -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);
+                    }
+                }
+            });
+
+        }
+    };
+
+
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java Tue Oct  8 14:41:32 2013
@@ -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));
+        }
+
+
+
+    }
+
+
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java Tue Oct  8 14:41:32 2013
@@ -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();
+    }
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java Tue Oct  8 14:41:32 2013
@@ -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 {
+
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java Tue Oct  8 14:41:32 2013
@@ -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;
+    }
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java Tue Oct  8 14:41:32 2013
@@ -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;
+    }
+
+}

Added: tomcat/trunk/webapps/examples/websocket/drawboard.xhtml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/drawboard.xhtml?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/websocket/drawboard.xhtml (added)
+++ tomcat/trunk/webapps/examples/websocket/drawboard.xhtml Tue Oct  8 14:41:32 2013
@@ -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

Modified: tomcat/trunk/webapps/examples/websocket/index.xhtml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/index.xhtml?rev=1530296&r1=1530295&r2=1530296&view=diff
==============================================================================
--- tomcat/trunk/webapps/examples/websocket/index.xhtml (original)
+++ tomcat/trunk/webapps/examples/websocket/index.xhtml Tue Oct  8 14:41:32 2013
@@ -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



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org