View | Details | Raw Unified | Return to bug 55639
Collapse All | Expand All

(-)webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java (+124 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.util.LinkedList;
20
21
import javax.websocket.RemoteEndpoint;
22
import javax.websocket.SendHandler;
23
import javax.websocket.SendResult;
24
25
import websocket.drawboard.wsmessages.AbstractWebsocketMessage;
26
import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
27
import websocket.drawboard.wsmessages.StringWebsocketMessage;
28
29
/**
30
 * Represents a client with methods to send messages.
31
 */
32
public class Client {
33
34
    private final RemoteEndpoint.Async async;
35
36
    /**
37
     * Contains the messages wich are buffered until the previous
38
     * send operation has finished.
39
     */
40
    private final LinkedList<AbstractWebsocketMessage> messagesToSend =
41
            new LinkedList<>();
42
    /**
43
     * If this client is currently sending a messages asynchronously.
44
     */
45
    private volatile boolean isSendingMessage = false;
46
47
    public Client(RemoteEndpoint.Async async) {
48
        this.async = async;
49
    }
50
51
52
    /**
53
     * Sends the given message asynchronously to the client.
54
     * If there is already a async sending in progress, then the message
55
     * will be buffered and sent when possible.<br><br>
56
     * 
57
     * This method can be called from multiple threads.
58
     * @param msg
59
     */
60
    public void sendMessage(AbstractWebsocketMessage msg) {
61
        synchronized (messagesToSend) {
62
            if (isSendingMessage) {
63
                // TODO: Check if the buffered messages exceed
64
                // a specific amount - in that case, disconnect the client
65
                // to prevent DoS.
66
67
                // TODO: Check if the last message is a
68
                // String message - in that case we should concatenate them
69
                // to reduce TCP overhead (using ";" as separator).
70
71
                messagesToSend.add(msg);
72
            } else {
73
                isSendingMessage = true;
74
                internalSendMessageAsync(msg);
75
            }
76
77
78
        }
79
    }
80
81
    /**
82
     * Internally sends the messages asynchronously.
83
     * @param msg
84
     */
85
    private void internalSendMessageAsync(AbstractWebsocketMessage msg) {
86
        try {
87
            if (msg instanceof StringWebsocketMessage) {
88
                StringWebsocketMessage sMsg = (StringWebsocketMessage) msg;
89
                async.sendText(sMsg.getString(), sendHandler);
90
91
            } else if (msg instanceof BinaryWebsocketMessage) {
92
                BinaryWebsocketMessage bMsg = (BinaryWebsocketMessage) msg;
93
                async.sendBinary(bMsg.getBytes(), sendHandler);
94
            }
95
        } catch (IllegalStateException ex) {
96
            // Trying to write to the client when the session has
97
            // already been closed.
98
            // Ignore
99
        }
100
    }
101
102
103
104
    /**
105
     * SendHandler that will continue to send buffered messages.
106
     */
107
    private final SendHandler sendHandler = new SendHandler() {
108
        @Override
109
        public void onResult(SendResult result) {
110
            synchronized (messagesToSend) {
111
112
                if (!messagesToSend.isEmpty()) {
113
                    AbstractWebsocketMessage msg = messagesToSend.remove();
114
                    internalSendMessageAsync(msg);
115
116
                } else {
117
                    isSendingMessage = false;
118
                }
119
120
            }
121
        }
122
    };
123
124
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (+211 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.awt.BasicStroke;
20
import java.awt.Color;
21
import java.awt.Graphics2D;
22
23
/**
24
 * A message that represents a drawing action.
25
 * Note that we use primitive types instead of Point, Color etc.
26
 * to reduce object allocation.<br><br>
27
 * 
28
 * TODO: But a Color objects needs to be created anyway for drawing this
29
 * onto a Graphics2D object, so this probably does not save much.
30
 */
31
public final class DrawMessage {
32
33
34
    private int type;
35
    private byte colorR, colorG, colorB, colorA;
36
    private double thickness;
37
    private int x1, y1, x2, y2;
38
39
    /**
40
     * The type. 1: Line.
41
     * @return
42
     */
43
    public int getType() {
44
        return type;
45
    }
46
    public void setType(int type) {
47
        this.type = type;
48
    }
49
50
    public double getThickness() {
51
        return thickness;
52
    }
53
    public void setThickness(double thickness) {
54
        this.thickness = thickness;
55
    }
56
57
    public byte getColorR() {
58
        return colorR;
59
    }
60
    public void setColorR(byte colorR) {
61
        this.colorR = colorR;
62
    }
63
    public byte getColorG() {
64
        return colorG;
65
    }
66
    public void setColorG(byte colorG) {
67
        this.colorG = colorG;
68
    }
69
    public byte getColorB() {
70
        return colorB;
71
    }
72
    public void setColorB(byte colorB) {
73
        this.colorB = colorB;
74
    }
75
    public byte getColorA() {
76
        return colorA;
77
    }
78
    public void setColorA(byte colorA) {
79
        this.colorA = colorA;
80
    }
81
82
    public long getX1() {
83
        return x1;
84
    }
85
    public void setX1(int x1) {
86
        this.x1 = x1;
87
    }
88
    public int getX2() {
89
        return x2;
90
    }
91
    public void setX2(int x2) {
92
        this.x2 = x2;
93
    }
94
    public int getY1() {
95
        return y1;
96
    }
97
    public void setY1(int y1) {
98
        this.y1 = y1;
99
    }
100
    public int getY2() {
101
        return y2;
102
    }
103
    public void setY2(int y2) {
104
        this.y2 = y2;
105
    }
106
107
108
109
    public DrawMessage(int type, byte colorR, byte colorG, byte colorB,
110
            byte colorA, double thickness, int x1, int x2, int y1, int y2) {
111
112
        this.type = type;
113
        this.colorR = colorR;
114
        this.colorG = colorG;
115
        this.colorB = colorB;
116
        this.colorA = colorA;
117
        this.thickness = thickness;
118
        this.x1 = x1;
119
        this.x2 = x2;
120
        this.y1 = y1;
121
        this.y2 = y2;
122
    }
123
124
125
    /**
126
     * Draws this DrawMessage onto the given Graphics2D.
127
     * @param g
128
     */
129
    public void draw(Graphics2D g) {
130
        switch (type) {
131
        case 1:
132
            // Draw a line.
133
            g.setStroke(new BasicStroke((float) thickness,
134
                    BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
135
            g.setColor(new Color(colorR & 0xFF, colorG & 0xFF, colorB & 0xFF,
136
                    colorA & 0xFF));
137
            g.drawLine(x1, y1, x2, y2);
138
            break;
139
        }
140
    }
141
142
    /**
143
     * Converts this message into a String representation that
144
     * can be sent over WebSocket.<br>
145
     * Since a DrawMessage consists only of numbers,
146
     * we concatenate those numbers with a ",".
147
     */
148
    @Override
149
    public String toString() {
150
151
        return type + "," + (colorR & 0xFF) + "," + (colorG & 0xFF) + ","
152
                + (colorB & 0xFF) + "," + (colorA & 0xFF) + "," + thickness
153
                + "," + x1 + "," + y1 + "," + x2 + "," + y2;
154
    }
155
156
    public static DrawMessage parseFromString(String str)
157
            throws ParseException {
158
159
        int type; 
160
        byte[] colors = new byte[4];
161
        double thickness;
162
        int[] coords = new int[4];
163
164
        try {
165
            String[] elements = str.split(",");
166
167
            type = Integer.parseInt(elements[0]);
168
            if (type != 1)
169
                throw new ParseException("Invalid type: " + type);
170
171
            for (int i = 0; i < colors.length; i++) {
172
                colors[i] = (byte) Integer.parseInt(elements[1 + i]);
173
            }
174
175
            thickness = Double.parseDouble(elements[5]);
176
            if (Double.isNaN(thickness) || thickness < 0 || thickness > 100)
177
                throw new ParseException("Invalid thickness: " + thickness);
178
179
            for (int i = 0; i < coords.length; i++) {
180
                coords[i] = Integer.parseInt(elements[6 + i]);
181
                if (coords[i] < -1000000L || coords[i] > 1000000L)
182
                    throw new ParseException("Invalid coordinate: "
183
                            + coords[i]);
184
            }
185
186
187
        } catch (RuntimeException ex) {
188
            throw new ParseException(ex);
189
        }
190
191
        DrawMessage m = new DrawMessage(type, colors[0], colors[1],
192
                colors[2], colors[3], thickness, coords[0], coords[2],
193
                coords[1], coords[3]);
194
195
        return m;
196
    }
197
198
    public static class ParseException extends Exception {
199
        private static final long serialVersionUID = -6651972769789842960L;
200
201
        public ParseException(Throwable root) {
202
            super(root);
203
        }
204
205
        public ParseException(String message) {
206
            super(message);
207
        }
208
    }
209
210
211
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (+210 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.io.EOFException;
20
21
import javax.websocket.CloseReason;
22
import javax.websocket.Endpoint;
23
import javax.websocket.EndpointConfig;
24
import javax.websocket.MessageHandler;
25
import javax.websocket.Session;
26
27
import org.apache.juli.logging.Log;
28
import org.apache.juli.logging.LogFactory;
29
30
import websocket.drawboard.DrawMessage.ParseException;
31
import websocket.drawboard.wsmessages.StringWebsocketMessage;
32
33
34
public final class DrawboardEndpoint extends Endpoint {
35
36
    private static final Log log =
37
            LogFactory.getLog(DrawboardEndpoint.class);
38
39
40
    /**
41
     * Our room where players can join.
42
     */
43
    private static final Room room = new Room();
44
45
    public static Room getRoom() {
46
        return room;
47
    }
48
49
    /**
50
     * The player that is associated with this Endpoint and the current room.
51
     * Note that this variable is only accessed from the Room Thread.<br><br>
52
     * 
53
     * TODO: Currently, Tomcat uses an Endpoint instance once - however
54
     * the java doc of endpoint says:
55
     * "Each instance of a websocket endpoint is guaranteed not to be called by
56
     * more than one thread at a time per active connection."
57
     * This could mean that after calling onClose(), the instance
58
     * could be reused for another connection so onOpen() will get called
59
     * (possibly from another thread).<br>
60
     * If this is the case, we would need a variable holder for the variables
61
     * that are accessed by the Room thread, and read the reference to the holder
62
     * at the beginning of onOpen, onMessage, onClose methods to ensure the room
63
     * thread always gets the correct instance of the variable holder.
64
     */
65
    private Room.Player player;
66
67
68
    @Override
69
    public void onOpen(Session session, EndpointConfig config) {
70
        // Set maximum messages size to 10.000 bytes.
71
        session.setMaxTextMessageBufferSize(10000);
72
        session.addMessageHandler(stringHandler);
73
        final Client client = new Client(session.getAsyncRemote());
74
75
        room.invoke(new Runnable() {
76
            @Override
77
            public void run() {
78
                try {
79
80
                    // Create a new Player and add it to the room.
81
                    try {
82
                        player = room.createAndAddPlayer(client);
83
                    } catch (IllegalStateException ex) {
84
                        // Probably the max. number of players has been
85
                        // reached.
86
                        client.sendMessage(new StringWebsocketMessage(
87
                                "0" + ex.getLocalizedMessage()));
88
                    }
89
90
                } catch (RuntimeException ex) {
91
                    log.error("Unexpected exception: " + ex.toString(), ex);
92
                }
93
            }
94
        });
95
96
    }
97
98
99
    @Override
100
    public void onClose(Session session, CloseReason closeReason) {
101
        room.invoke(new Runnable() {
102
            @Override
103
            public void run() {
104
                try {
105
106
                    // Player can be null if it couldn't enter the room
107
                    if (player != null) {
108
                        // Remove this player from the room.
109
                        player.removeFromRoom();
110
                    }
111
112
                } catch (RuntimeException ex) {
113
                    log.error("Unexpected exception: " + ex.toString(), ex);
114
                }
115
            }
116
        });
117
118
    }
119
120
121
122
    @Override
123
    public void onError(Session session, Throwable t) {
124
        // Most likely cause is a user closing their browser. Check to see if
125
        // the root cause is EOF and if it is ignore it.
126
        // Protect against infinite loops.
127
        int count = 0;
128
        Throwable root = t;
129
        while (root.getCause() != null && count < 20) {
130
            root = root.getCause();
131
            count ++;
132
        }
133
        if (root instanceof EOFException) {
134
            // Assume this is triggered by the user closing their browser and
135
            // ignore it.
136
        } else {
137
            log.error("onError: " + t.toString(), t);
138
        }
139
    }
140
141
142
143
    private final MessageHandler.Whole<String> stringHandler =
144
            new MessageHandler.Whole<String>() {
145
146
        @Override
147
        public void onMessage(final String message) {
148
            // Invoke handling of the message in the room.
149
            room.invoke(new Runnable() {
150
                @Override
151
                public void run() {
152
                    try {
153
154
                        // Currently, the only types of messages the client will send
155
                        // are draw messages prefixed by a Message ID
156
                        // (starting with char '1'), and pong messages (starting
157
                        // with char '0').
158
                        // Draw messages should look like this:
159
                        // ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2
160
161
                        boolean dontSwallowException = false;
162
                        try {
163
                            char messageType = message.charAt(0);
164
                            switch (messageType) {
165
                            case '0':
166
                                // Pong message.
167
                                // Do nothing.
168
                                break;
169
170
                            case '1':
171
                                // Draw message
172
                                int indexOfChar = message.indexOf('|');
173
                                long msgId = Long.parseLong(
174
                                        message.substring(0, indexOfChar));
175
176
                                DrawMessage msg = DrawMessage.parseFromString(
177
                                        message.substring(indexOfChar + 1));
178
179
                                // Don't ingore RuntimeExceptions thrown by
180
                                // this method
181
                                // TODO: Find a better solution than this variable
182
                                dontSwallowException = true;
183
                                if (player != null) {
184
                                    player.handleDrawMessage(msg, msgId);
185
                                }
186
                                dontSwallowException = false;
187
188
                                break;
189
                            }
190
191
                        } catch (RuntimeException|ParseException ex) {
192
                            // Client sent invalid data.
193
                            // Ignore, TODO: maybe close connection
194
                            if (dontSwallowException 
195
                                    && ex instanceof RuntimeException) {
196
                                throw (RuntimeException) ex;
197
                            }
198
                        }
199
200
                    } catch (RuntimeException ex) {
201
                        log.error("Unexpected exception: " + ex.toString(), ex);
202
                    }
203
                }
204
            });
205
206
        }
207
    };
208
209
210
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (+436 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.awt.Color;
20
import java.awt.Graphics2D;
21
import java.awt.RenderingHints;
22
import java.awt.image.BufferedImage;
23
import java.io.ByteArrayOutputStream;
24
import java.io.IOException;
25
import java.nio.ByteBuffer;
26
import java.util.ArrayList;
27
import java.util.List;
28
import java.util.Timer;
29
import java.util.TimerTask;
30
import java.util.concurrent.ExecutionException;
31
import java.util.concurrent.ExecutorService;
32
import java.util.concurrent.Executors;
33
import java.util.concurrent.Future;
34
35
import javax.imageio.ImageIO;
36
37
import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
38
import websocket.drawboard.wsmessages.StringWebsocketMessage;
39
40
/**
41
 * A Room represents a drawboard where a number of
42
 * users participate.<br><br>
43
 * 
44
 * Each Room has its own "Room Thread" which manages all the actions
45
 * to be done in this Room. Instance methods should only be invoked
46
 * from this Room's thread by calling {@link #invoke(Runnable)} or
47
 * {@link #invokeAndWait(Runnable)}.
48
 */
49
public final class Room {
50
51
    /**
52
     * Specifies the type of a room message that is sent to a client.<br>
53
     * Note: Currently we are sending simple string messages - for production
54
     * apps, a JSON lib should be used for object-level messages.<br><br>
55
     * 
56
     * The number (single char) will be prefixed to the string when sending
57
     * the message.
58
     */
59
    public static enum MessageType {
60
        /**
61
         * '0': Error: contains error message.
62
         */
63
        ERROR('0'),
64
        /**
65
         * '1': DrawMesssage: contains serialized DrawMessage(s) prefixed
66
         *      with the current Player's {@link Player#lastReceivedMessageId}
67
         *      and ",".<br>
68
         *      Multiple draw messages are concatenated with "|" as separator.
69
         */
70
        DRAW_MESSAGE('1'),
71
        /**
72
         * '2': ImageMessage: Contains number of current players in this room.
73
         *      After this message a Binary Websocket message will follow,
74
         *      containing the current Room image as PNG.<br>
75
         *      This is the first message that a Room sends to a new Player.
76
         */
77
        IMAGE_MESSAGE('2'),
78
        /**
79
         * '3': PlayerChanged: contains "+" or "-" which indicate a player
80
         *      was added or removed to this Room.
81
         */
82
        PLAYER_CHANGED('3');
83
84
        private final char flag;
85
86
        private MessageType(char flag) {
87
            this.flag = flag;
88
        }
89
90
    }
91
92
93
94
    /**
95
     * If <code>true</code>, outgoing DrawMessages will be buffered until the
96
     * drawmessageBroadcastTimer ticks. Otherwise they will be sent
97
     * immediately.
98
     */
99
    private static final boolean BUFFER_DRAW_MESSAGES = true; 
100
101
    /**
102
     * A single-threaded ExecutorService where tasks
103
     * are scheduled that are to be run in the Room Thread.
104
     */
105
    private final ExecutorService roomExecutor =
106
            Executors.newSingleThreadExecutor();
107
108
    /**
109
     * A timer which sends buffered drawmessages to the client at once
110
     * at a regular interval, to avoid sending a lot of very small
111
     * messages which would cause TCP overhead and high CPU usage.
112
     */
113
    private final Timer drawmessageBroadcastTimer = new Timer();
114
115
116
    /**
117
     * The current image of the room drawboard. DrawMessages that are
118
     * received from Players will be drawn onto this image.
119
     */
120
    private final BufferedImage roomImage =
121
            new BufferedImage(900, 600, BufferedImage.TYPE_INT_RGB);
122
    private final Graphics2D roomGraphics = roomImage.createGraphics();
123
124
125
    /**
126
     * The maximum number of players that can join this room.
127
     */
128
    private static final int MAX_PLAYER_COUNT = 2;
129
130
    /**
131
     * List of all currently joined players.
132
     */
133
    private final List<Player> players = new ArrayList<>();
134
135
136
137
    public Room() {
138
        roomGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
139
                RenderingHints.VALUE_ANTIALIAS_ON);
140
141
        // Clear the image with white background.
142
        roomGraphics.setBackground(Color.WHITE);
143
        roomGraphics.clearRect(0, 0, roomImage.getWidth(),
144
                roomImage.getHeight());
145
146
        // Schedule a TimerTask that broadcasts draw messages.
147
        drawmessageBroadcastTimer.schedule(new TimerTask() {
148
            @Override
149
            public void run() {
150
                try {
151
                    invokeAndWait(new Runnable() {
152
                        @Override
153
                        public void run() {
154
                            broadcastTimerTick();
155
                        }
156
                    });
157
                } catch (InterruptedException | ExecutionException e) {
158
                    // TODO
159
                }
160
            }
161
        }, 30, 30);
162
    }
163
164
    /**
165
     * Creates a Player from the given Client and adds it to this room.
166
     * @param c the client
167
     * @return
168
     */
169
    public Player createAndAddPlayer(Client client) {
170
        if (players.size() >= MAX_PLAYER_COUNT) {
171
            throw new IllegalStateException("MAX_PLAYER_COUNT has been reached.");
172
        }
173
174
        Player p = new Player(this, client);
175
176
        // Broadcast to the other players that one player joined.
177
        broadcastRoomMessage(MessageType.PLAYER_CHANGED, "+");
178
179
        // Add the new player to the list.
180
        players.add(p);
181
182
        // Send him the current number of players and the current room image.
183
        String content = String.valueOf(players.size());
184
        p.sendRoomMessage(MessageType.IMAGE_MESSAGE, content);
185
186
        // Store image as PNG
187
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
188
        try {
189
            ImageIO.write(roomImage, "PNG", bout);
190
        } catch (IOException e) { /* Should never happen */ }
191
192
193
        // Send the image as binary message.
194
        BinaryWebsocketMessage msg = new BinaryWebsocketMessage(
195
                ByteBuffer.wrap(bout.toByteArray()));
196
        p.getClient().sendMessage(msg);
197
198
        return p;
199
200
    }
201
202
    /**
203
     * @see Player#removeFromRoom()
204
     * @param p
205
     */
206
    private void internalRemovePlayer(Player p) {
207
        players.remove(p);
208
209
        // Broadcast that one player is removed.
210
        broadcastRoomMessage(MessageType.PLAYER_CHANGED, "-");
211
    }
212
213
    /**
214
     * @see Player#handleDrawMessage(DrawMessage, long)
215
     * @param p
216
     * @param msg
217
     * @param msgId
218
     */
219
    private void internalHandleDrawMessage(Player p, DrawMessage msg,
220
            long msgId) {
221
        p.setLastReceivedMessageId(msgId);
222
223
        // Draw the RoomMessage onto our Room Image.
224
        msg.draw(roomGraphics);
225
226
        // Broadcast the Draw Message.
227
        broadcastDrawMessage(msg);
228
    }
229
230
231
    /**
232
     * Broadcasts the given drawboard message to all connected players.
233
     * Note: For DrawMessages, please use
234
     * {@link #broadcastDrawMessage(DrawMessage)}
235
     * as this method will buffer them and prefix them with the correct
236
     * last received Message ID.
237
     * @param type
238
     * @param content
239
     */
240
    private void broadcastRoomMessage(MessageType type, String content) {
241
        for (Player p : players) {
242
            p.sendRoomMessage(type, content);
243
        }
244
    }
245
246
247
    /**
248
     * Broadcast the given DrawMessage. This will buffer the message
249
     * and the {@link #drawmessageBroadcastTimer} will broadcast them
250
     * at a regular interval, prefixing them with the player's current
251
     * {@link Player#lastReceivedMessageId}.
252
     * @param msg
253
     */
254
    private void broadcastDrawMessage(DrawMessage msg) {
255
        if (!BUFFER_DRAW_MESSAGES) {
256
            String msgStr = msg.toString();
257
258
            for (Player p : players) {
259
                String s = String.valueOf(p.getLastReceivedMessageId())
260
                        + "," + msgStr;
261
                p.sendRoomMessage(MessageType.DRAW_MESSAGE, s);
262
            }
263
        } else {
264
            for (Player p : players) {
265
                p.getBufferedDrawMessages().add(msg);
266
            }
267
        }
268
    }
269
270
271
    /**
272
     * Tick handler for the broadcastTimer.
273
     */
274
    private void broadcastTimerTick() {
275
        // For each Player, send all per Player buffered
276
        // DrawMessages, prefixing each DrawMessage with the player's
277
        // lastReceuvedMessageId.
278
        // Multiple messages are concatenated with "|".
279
280
        for (Player p : players) {
281
282
            StringBuilder sb = new StringBuilder();
283
            List<DrawMessage> drawMessages = p.getBufferedDrawMessages();
284
285
            if (drawMessages.size() > 0) {
286
                for (int i = 0; i < drawMessages.size(); i++) {
287
                    DrawMessage msg = drawMessages.get(i);
288
289
                    String s = String.valueOf(p.getLastReceivedMessageId())
290
                            + "," + msg.toString();
291
                    if (i > 0)
292
                        sb.append("|");
293
294
                    sb.append(s);
295
                }
296
                drawMessages.clear();            
297
298
                p.sendRoomMessage(MessageType.DRAW_MESSAGE, sb.toString());
299
            }
300
        }
301
    }
302
303
304
305
306
    /**
307
     * Submits the given Runnable to the Room Executor.
308
     * @param run
309
     */
310
    public void invoke(Runnable task) {
311
        roomExecutor.submit(task);
312
    }
313
314
    /**
315
     * Submits the given Runnable to the Room Executor and waits until it
316
     * has been executed.
317
     * @param task
318
     * @throws InterruptedException if the current thread was interrupted
319
     * while waiting
320
     * @throws ExecutionException if the computation threw an exception 
321
     */
322
    public void invokeAndWait(Runnable task)
323
            throws InterruptedException, ExecutionException {
324
        Future<?> f = roomExecutor.submit(task);
325
        f.get();
326
    }
327
328
    /**
329
     * Shuts down the roomExecutor and the drawmessageBroadcastTimer.
330
     */
331
    public void shutdown() {
332
        roomExecutor.shutdown();
333
        drawmessageBroadcastTimer.cancel();
334
    }
335
336
337
338
    /**
339
     * A Player participates in a Room. It is the interface between the
340
     * {@link Room} and the {@link Client}.<br><br>
341
     * 
342
     * Note: This means a player object is actually a join between Room and
343
     * Endpoint.
344
     */
345
    public final class Player {
346
347
        /**
348
         * The room to which this player belongs.
349
         */
350
        private Room room;
351
352
        /**
353
         * The room buffers the last draw message ID that was received from
354
         * this player.
355
         */
356
        private long lastReceivedMessageId = 0;
357
358
        private final Client client;
359
360
        /**
361
         * Buffered DrawMessages that will be sent by a Timer.
362
         * TODO: This should be refactored to be in a Room-Player join class
363
         * as this is room-specific.
364
         */
365
        private final List<DrawMessage> bufferedDrawMessages =
366
                new ArrayList<>();
367
368
        private List<DrawMessage> getBufferedDrawMessages() {
369
            return bufferedDrawMessages;
370
        }
371
372
373
374
        private Player(Room room, Client client) {
375
            this.room = room;
376
            this.client = client;
377
        }
378
379
        public Room getRoom() {
380
            return room;
381
        }
382
383
        public Client getClient() {
384
            return client;
385
        }
386
387
        /**
388
         * Removes this player from its room, e.g. when
389
         * the client disconnects.
390
         */
391
        public void removeFromRoom() {
392
            room.internalRemovePlayer(this);
393
            room = null;
394
        }
395
396
397
        private long getLastReceivedMessageId() {
398
            return lastReceivedMessageId;
399
        }
400
        private void setLastReceivedMessageId(long value) {
401
            lastReceivedMessageId = value;
402
        }
403
404
405
        /**
406
         * Handles the given DrawMessage by drawing it onto this Room's
407
         * image and by broadcasting it to the connected players.
408
         * @param sender
409
         * @param msg
410
         * @param msgId
411
         */
412
        public void handleDrawMessage(DrawMessage msg, long msgId) {
413
            room.internalHandleDrawMessage(this, msg, msgId);
414
        }
415
416
417
        /**
418
         * Sends the given room message.
419
         * @param type
420
         * @param content
421
         */
422
        private void sendRoomMessage(MessageType type, String content) {
423
            if (content == null || type == null)
424
                throw null;
425
426
            String completeMsg = String.valueOf(type.flag) + content;
427
428
            client.sendMessage(new StringWebsocketMessage(completeMsg));
429
        }
430
431
432
433
    }
434
435
436
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (+48 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import javax.servlet.ServletContextEvent;
20
import javax.servlet.ServletContextListener;
21
import javax.servlet.annotation.WebListener;
22
import javax.websocket.DeploymentException;
23
import javax.websocket.server.ServerContainer;
24
import javax.websocket.server.ServerEndpointConfig;
25
26
@WebListener
27
public final class WsConfigListener implements ServletContextListener {
28
29
    @Override
30
    public void contextInitialized(ServletContextEvent sce) {
31
32
        ServerContainer sc =
33
                (ServerContainer) sce.getServletContext().getAttribute(
34
                        "javax.websocket.server.ServerContainer");
35
        try {
36
            sc.addEndpoint(ServerEndpointConfig.Builder.create(
37
                    DrawboardEndpoint.class, "/websocket/drawboard").build());
38
        } catch (DeploymentException e) {
39
            throw new IllegalStateException(e);
40
        }
41
    }
42
43
    @Override
44
    public void contextDestroyed(ServletContextEvent sce) {
45
        // Shutdown our room.
46
        DrawboardEndpoint.getRoom().shutdown();
47
    }
48
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (+25 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
/**
20
 * Abstract base class for Websocket Messages (binary or string)
21
 * that can be buffered.
22
 */
23
public abstract class AbstractWebsocketMessage {
24
25
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (+34 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
import java.nio.ByteBuffer;
20
21
/**
22
 * Represents a binary websocket message.
23
 */
24
public final class BinaryWebsocketMessage extends AbstractWebsocketMessage {
25
    private final ByteBuffer bytes;
26
27
    public BinaryWebsocketMessage(ByteBuffer bytes) {
28
        this.bytes = bytes;
29
    }
30
31
    public ByteBuffer getBytes() {
32
        return bytes;
33
    }
34
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (+34 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
/**
20
 * Represents a string websocket message.
21
 *
22
 */
23
public final class StringWebsocketMessage extends AbstractWebsocketMessage {
24
    private final String string;
25
26
    public StringWebsocketMessage(String string) {
27
        this.string = string;
28
    }
29
30
    public String getString() {
31
        return string;
32
    }
33
34
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java (+124 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.util.LinkedList;
20
21
import javax.websocket.RemoteEndpoint;
22
import javax.websocket.SendHandler;
23
import javax.websocket.SendResult;
24
25
import websocket.drawboard.wsmessages.AbstractWebsocketMessage;
26
import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
27
import websocket.drawboard.wsmessages.StringWebsocketMessage;
28
29
/**
30
 * Represents a client with methods to send messages.
31
 */
32
public class Client {
33
34
    private final RemoteEndpoint.Async async;
35
36
    /**
37
     * Contains the messages wich are buffered until the previous
38
     * send operation has finished.
39
     */
40
    private final LinkedList<AbstractWebsocketMessage> messagesToSend =
41
            new LinkedList<>();
42
    /**
43
     * If this client is currently sending a messages asynchronously.
44
     */
45
    private volatile boolean isSendingMessage = false;
46
47
    public Client(RemoteEndpoint.Async async) {
48
        this.async = async;
49
    }
50
51
52
    /**
53
     * Sends the given message asynchronously to the client.
54
     * If there is already a async sending in progress, then the message
55
     * will be buffered and sent when possible.<br><br>
56
     * 
57
     * This method can be called from multiple threads.
58
     * @param msg
59
     */
60
    public void sendMessage(AbstractWebsocketMessage msg) {
61
        synchronized (messagesToSend) {
62
            if (isSendingMessage) {
63
                // TODO: Check if the buffered messages exceed
64
                // a specific amount - in that case, disconnect the client
65
                // to prevent DoS.
66
67
                // TODO: Check if the last message is a
68
                // String message - in that case we should concatenate them
69
                // to reduce TCP overhead (using ";" as separator).
70
71
                messagesToSend.add(msg);
72
            } else {
73
                isSendingMessage = true;
74
                internalSendMessageAsync(msg);
75
            }
76
77
78
        }
79
    }
80
81
    /**
82
     * Internally sends the messages asynchronously.
83
     * @param msg
84
     */
85
    private void internalSendMessageAsync(AbstractWebsocketMessage msg) {
86
        try {
87
            if (msg instanceof StringWebsocketMessage) {
88
                StringWebsocketMessage sMsg = (StringWebsocketMessage) msg;
89
                async.sendText(sMsg.getString(), sendHandler);
90
91
            } else if (msg instanceof BinaryWebsocketMessage) {
92
                BinaryWebsocketMessage bMsg = (BinaryWebsocketMessage) msg;
93
                async.sendBinary(bMsg.getBytes(), sendHandler);
94
            }
95
        } catch (IllegalStateException ex) {
96
            // Trying to write to the client when the session has
97
            // already been closed.
98
            // Ignore
99
        }
100
    }
101
102
103
104
    /**
105
     * SendHandler that will continue to send buffered messages.
106
     */
107
    private final SendHandler sendHandler = new SendHandler() {
108
        @Override
109
        public void onResult(SendResult result) {
110
            synchronized (messagesToSend) {
111
112
                if (!messagesToSend.isEmpty()) {
113
                    AbstractWebsocketMessage msg = messagesToSend.remove();
114
                    internalSendMessageAsync(msg);
115
116
                } else {
117
                    isSendingMessage = false;
118
                }
119
120
            }
121
        }
122
    };
123
124
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (+210 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.io.EOFException;
20
21
import javax.websocket.CloseReason;
22
import javax.websocket.Endpoint;
23
import javax.websocket.EndpointConfig;
24
import javax.websocket.MessageHandler;
25
import javax.websocket.Session;
26
27
import org.apache.juli.logging.Log;
28
import org.apache.juli.logging.LogFactory;
29
30
import websocket.drawboard.DrawMessage.ParseException;
31
import websocket.drawboard.wsmessages.StringWebsocketMessage;
32
33
34
public final class DrawboardEndpoint extends Endpoint {
35
36
    private static final Log log =
37
            LogFactory.getLog(DrawboardEndpoint.class);
38
39
40
    /**
41
     * Our room where players can join.
42
     */
43
    private static final Room room = new Room();
44
45
    public static Room getRoom() {
46
        return room;
47
    }
48
49
    /**
50
     * The player that is associated with this Endpoint and the current room.
51
     * Note that this variable is only accessed from the Room Thread.<br><br>
52
     * 
53
     * TODO: Currently, Tomcat uses an Endpoint instance once - however
54
     * the java doc of endpoint says:
55
     * "Each instance of a websocket endpoint is guaranteed not to be called by
56
     * more than one thread at a time per active connection."
57
     * This could mean that after calling onClose(), the instance
58
     * could be reused for another connection so onOpen() will get called
59
     * (possibly from another thread).<br>
60
     * If this is the case, we would need a variable holder for the variables
61
     * that are accessed by the Room thread, and read the reference to the holder
62
     * at the beginning of onOpen, onMessage, onClose methods to ensure the room
63
     * thread always gets the correct instance of the variable holder.
64
     */
65
    private Room.Player player;
66
67
68
    @Override
69
    public void onOpen(Session session, EndpointConfig config) {
70
        // Set maximum messages size to 10.000 bytes.
71
        session.setMaxTextMessageBufferSize(10000);
72
        session.addMessageHandler(stringHandler);
73
        final Client client = new Client(session.getAsyncRemote());
74
75
        room.invoke(new Runnable() {
76
            @Override
77
            public void run() {
78
                try {
79
80
                    // Create a new Player and add it to the room.
81
                    try {
82
                        player = room.createAndAddPlayer(client);
83
                    } catch (IllegalStateException ex) {
84
                        // Probably the max. number of players has been
85
                        // reached.
86
                        client.sendMessage(new StringWebsocketMessage(
87
                                "0" + ex.getLocalizedMessage()));
88
                    }
89
90
                } catch (RuntimeException ex) {
91
                    log.error("Unexpected exception: " + ex.toString(), ex);
92
                }
93
            }
94
        });
95
96
    }
97
98
99
    @Override
100
    public void onClose(Session session, CloseReason closeReason) {
101
        room.invoke(new Runnable() {
102
            @Override
103
            public void run() {
104
                try {
105
106
                    // Player can be null if it couldn't enter the room
107
                    if (player != null) {
108
                        // Remove this player from the room.
109
                        player.removeFromRoom();
110
                    }
111
112
                } catch (RuntimeException ex) {
113
                    log.error("Unexpected exception: " + ex.toString(), ex);
114
                }
115
            }
116
        });
117
118
    }
119
120
121
122
    @Override
123
    public void onError(Session session, Throwable t) {
124
        // Most likely cause is a user closing their browser. Check to see if
125
        // the root cause is EOF and if it is ignore it.
126
        // Protect against infinite loops.
127
        int count = 0;
128
        Throwable root = t;
129
        while (root.getCause() != null && count < 20) {
130
            root = root.getCause();
131
            count ++;
132
        }
133
        if (root instanceof EOFException) {
134
            // Assume this is triggered by the user closing their browser and
135
            // ignore it.
136
        } else {
137
            log.error("onError: " + t.toString(), t);
138
        }
139
    }
140
141
142
143
    private final MessageHandler.Whole<String> stringHandler =
144
            new MessageHandler.Whole<String>() {
145
146
        @Override
147
        public void onMessage(final String message) {
148
            // Invoke handling of the message in the room.
149
            room.invoke(new Runnable() {
150
                @Override
151
                public void run() {
152
                    try {
153
154
                        // Currently, the only types of messages the client will send
155
                        // are draw messages prefixed by a Message ID
156
                        // (starting with char '1'), and pong messages (starting
157
                        // with char '0').
158
                        // Draw messages should look like this:
159
                        // ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2
160
161
                        boolean dontSwallowException = false;
162
                        try {
163
                            char messageType = message.charAt(0);
164
                            switch (messageType) {
165
                            case '0':
166
                                // Pong message.
167
                                // Do nothing.
168
                                break;
169
170
                            case '1':
171
                                // Draw message
172
                                int indexOfChar = message.indexOf('|');
173
                                long msgId = Long.parseLong(
174
                                        message.substring(0, indexOfChar));
175
176
                                DrawMessage msg = DrawMessage.parseFromString(
177
                                        message.substring(indexOfChar + 1));
178
179
                                // Don't ingore RuntimeExceptions thrown by
180
                                // this method
181
                                // TODO: Find a better solution than this variable
182
                                dontSwallowException = true;
183
                                if (player != null) {
184
                                    player.handleDrawMessage(msg, msgId);
185
                                }
186
                                dontSwallowException = false;
187
188
                                break;
189
                            }
190
191
                        } catch (RuntimeException|ParseException ex) {
192
                            // Client sent invalid data.
193
                            // Ignore, TODO: maybe close connection
194
                            if (dontSwallowException 
195
                                    && ex instanceof RuntimeException) {
196
                                throw (RuntimeException) ex;
197
                            }
198
                        }
199
200
                    } catch (RuntimeException ex) {
201
                        log.error("Unexpected exception: " + ex.toString(), ex);
202
                    }
203
                }
204
            });
205
206
        }
207
    };
208
209
210
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (+211 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.awt.BasicStroke;
20
import java.awt.Color;
21
import java.awt.Graphics2D;
22
23
/**
24
 * A message that represents a drawing action.
25
 * Note that we use primitive types instead of Point, Color etc.
26
 * to reduce object allocation.<br><br>
27
 * 
28
 * TODO: But a Color objects needs to be created anyway for drawing this
29
 * onto a Graphics2D object, so this probably does not save much.
30
 */
31
public final class DrawMessage {
32
33
34
    private int type;
35
    private byte colorR, colorG, colorB, colorA;
36
    private double thickness;
37
    private int x1, y1, x2, y2;
38
39
    /**
40
     * The type. 1: Line.
41
     * @return
42
     */
43
    public int getType() {
44
        return type;
45
    }
46
    public void setType(int type) {
47
        this.type = type;
48
    }
49
50
    public double getThickness() {
51
        return thickness;
52
    }
53
    public void setThickness(double thickness) {
54
        this.thickness = thickness;
55
    }
56
57
    public byte getColorR() {
58
        return colorR;
59
    }
60
    public void setColorR(byte colorR) {
61
        this.colorR = colorR;
62
    }
63
    public byte getColorG() {
64
        return colorG;
65
    }
66
    public void setColorG(byte colorG) {
67
        this.colorG = colorG;
68
    }
69
    public byte getColorB() {
70
        return colorB;
71
    }
72
    public void setColorB(byte colorB) {
73
        this.colorB = colorB;
74
    }
75
    public byte getColorA() {
76
        return colorA;
77
    }
78
    public void setColorA(byte colorA) {
79
        this.colorA = colorA;
80
    }
81
82
    public long getX1() {
83
        return x1;
84
    }
85
    public void setX1(int x1) {
86
        this.x1 = x1;
87
    }
88
    public int getX2() {
89
        return x2;
90
    }
91
    public void setX2(int x2) {
92
        this.x2 = x2;
93
    }
94
    public int getY1() {
95
        return y1;
96
    }
97
    public void setY1(int y1) {
98
        this.y1 = y1;
99
    }
100
    public int getY2() {
101
        return y2;
102
    }
103
    public void setY2(int y2) {
104
        this.y2 = y2;
105
    }
106
107
108
109
    public DrawMessage(int type, byte colorR, byte colorG, byte colorB,
110
            byte colorA, double thickness, int x1, int x2, int y1, int y2) {
111
112
        this.type = type;
113
        this.colorR = colorR;
114
        this.colorG = colorG;
115
        this.colorB = colorB;
116
        this.colorA = colorA;
117
        this.thickness = thickness;
118
        this.x1 = x1;
119
        this.x2 = x2;
120
        this.y1 = y1;
121
        this.y2 = y2;
122
    }
123
124
125
    /**
126
     * Draws this DrawMessage onto the given Graphics2D.
127
     * @param g
128
     */
129
    public void draw(Graphics2D g) {
130
        switch (type) {
131
        case 1:
132
            // Draw a line.
133
            g.setStroke(new BasicStroke((float) thickness,
134
                    BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
135
            g.setColor(new Color(colorR & 0xFF, colorG & 0xFF, colorB & 0xFF,
136
                    colorA & 0xFF));
137
            g.drawLine(x1, y1, x2, y2);
138
            break;
139
        }
140
    }
141
142
    /**
143
     * Converts this message into a String representation that
144
     * can be sent over WebSocket.<br>
145
     * Since a DrawMessage consists only of numbers,
146
     * we concatenate those numbers with a ",".
147
     */
148
    @Override
149
    public String toString() {
150
151
        return type + "," + (colorR & 0xFF) + "," + (colorG & 0xFF) + ","
152
                + (colorB & 0xFF) + "," + (colorA & 0xFF) + "," + thickness
153
                + "," + x1 + "," + y1 + "," + x2 + "," + y2;
154
    }
155
156
    public static DrawMessage parseFromString(String str)
157
            throws ParseException {
158
159
        int type; 
160
        byte[] colors = new byte[4];
161
        double thickness;
162
        int[] coords = new int[4];
163
164
        try {
165
            String[] elements = str.split(",");
166
167
            type = Integer.parseInt(elements[0]);
168
            if (type != 1)
169
                throw new ParseException("Invalid type: " + type);
170
171
            for (int i = 0; i < colors.length; i++) {
172
                colors[i] = (byte) Integer.parseInt(elements[1 + i]);
173
            }
174
175
            thickness = Double.parseDouble(elements[5]);
176
            if (Double.isNaN(thickness) || thickness < 0 || thickness > 100)
177
                throw new ParseException("Invalid thickness: " + thickness);
178
179
            for (int i = 0; i < coords.length; i++) {
180
                coords[i] = Integer.parseInt(elements[6 + i]);
181
                if (coords[i] < -1000000L || coords[i] > 1000000L)
182
                    throw new ParseException("Invalid coordinate: "
183
                            + coords[i]);
184
            }
185
186
187
        } catch (RuntimeException ex) {
188
            throw new ParseException(ex);
189
        }
190
191
        DrawMessage m = new DrawMessage(type, colors[0], colors[1],
192
                colors[2], colors[3], thickness, coords[0], coords[2],
193
                coords[1], coords[3]);
194
195
        return m;
196
    }
197
198
    public static class ParseException extends Exception {
199
        private static final long serialVersionUID = -6651972769789842960L;
200
201
        public ParseException(Throwable root) {
202
            super(root);
203
        }
204
205
        public ParseException(String message) {
206
            super(message);
207
        }
208
    }
209
210
211
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (+436 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import java.awt.Color;
20
import java.awt.Graphics2D;
21
import java.awt.RenderingHints;
22
import java.awt.image.BufferedImage;
23
import java.io.ByteArrayOutputStream;
24
import java.io.IOException;
25
import java.nio.ByteBuffer;
26
import java.util.ArrayList;
27
import java.util.List;
28
import java.util.Timer;
29
import java.util.TimerTask;
30
import java.util.concurrent.ExecutionException;
31
import java.util.concurrent.ExecutorService;
32
import java.util.concurrent.Executors;
33
import java.util.concurrent.Future;
34
35
import javax.imageio.ImageIO;
36
37
import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
38
import websocket.drawboard.wsmessages.StringWebsocketMessage;
39
40
/**
41
 * A Room represents a drawboard where a number of
42
 * users participate.<br><br>
43
 * 
44
 * Each Room has its own "Room Thread" which manages all the actions
45
 * to be done in this Room. Instance methods should only be invoked
46
 * from this Room's thread by calling {@link #invoke(Runnable)} or
47
 * {@link #invokeAndWait(Runnable)}.
48
 */
49
public final class Room {
50
51
    /**
52
     * Specifies the type of a room message that is sent to a client.<br>
53
     * Note: Currently we are sending simple string messages - for production
54
     * apps, a JSON lib should be used for object-level messages.<br><br>
55
     * 
56
     * The number (single char) will be prefixed to the string when sending
57
     * the message.
58
     */
59
    public static enum MessageType {
60
        /**
61
         * '0': Error: contains error message.
62
         */
63
        ERROR('0'),
64
        /**
65
         * '1': DrawMesssage: contains serialized DrawMessage(s) prefixed
66
         *      with the current Player's {@link Player#lastReceivedMessageId}
67
         *      and ",".<br>
68
         *      Multiple draw messages are concatenated with "|" as separator.
69
         */
70
        DRAW_MESSAGE('1'),
71
        /**
72
         * '2': ImageMessage: Contains number of current players in this room.
73
         *      After this message a Binary Websocket message will follow,
74
         *      containing the current Room image as PNG.<br>
75
         *      This is the first message that a Room sends to a new Player.
76
         */
77
        IMAGE_MESSAGE('2'),
78
        /**
79
         * '3': PlayerChanged: contains "+" or "-" which indicate a player
80
         *      was added or removed to this Room.
81
         */
82
        PLAYER_CHANGED('3');
83
84
        private final char flag;
85
86
        private MessageType(char flag) {
87
            this.flag = flag;
88
        }
89
90
    }
91
92
93
94
    /**
95
     * If <code>true</code>, outgoing DrawMessages will be buffered until the
96
     * drawmessageBroadcastTimer ticks. Otherwise they will be sent
97
     * immediately.
98
     */
99
    private static final boolean BUFFER_DRAW_MESSAGES = true; 
100
101
    /**
102
     * A single-threaded ExecutorService where tasks
103
     * are scheduled that are to be run in the Room Thread.
104
     */
105
    private final ExecutorService roomExecutor =
106
            Executors.newSingleThreadExecutor();
107
108
    /**
109
     * A timer which sends buffered drawmessages to the client at once
110
     * at a regular interval, to avoid sending a lot of very small
111
     * messages which would cause TCP overhead and high CPU usage.
112
     */
113
    private final Timer drawmessageBroadcastTimer = new Timer();
114
115
116
    /**
117
     * The current image of the room drawboard. DrawMessages that are
118
     * received from Players will be drawn onto this image.
119
     */
120
    private final BufferedImage roomImage =
121
            new BufferedImage(900, 600, BufferedImage.TYPE_INT_RGB);
122
    private final Graphics2D roomGraphics = roomImage.createGraphics();
123
124
125
    /**
126
     * The maximum number of players that can join this room.
127
     */
128
    private static final int MAX_PLAYER_COUNT = 2;
129
130
    /**
131
     * List of all currently joined players.
132
     */
133
    private final List<Player> players = new ArrayList<>();
134
135
136
137
    public Room() {
138
        roomGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
139
                RenderingHints.VALUE_ANTIALIAS_ON);
140
141
        // Clear the image with white background.
142
        roomGraphics.setBackground(Color.WHITE);
143
        roomGraphics.clearRect(0, 0, roomImage.getWidth(),
144
                roomImage.getHeight());
145
146
        // Schedule a TimerTask that broadcasts draw messages.
147
        drawmessageBroadcastTimer.schedule(new TimerTask() {
148
            @Override
149
            public void run() {
150
                try {
151
                    invokeAndWait(new Runnable() {
152
                        @Override
153
                        public void run() {
154
                            broadcastTimerTick();
155
                        }
156
                    });
157
                } catch (InterruptedException | ExecutionException e) {
158
                    // TODO
159
                }
160
            }
161
        }, 30, 30);
162
    }
163
164
    /**
165
     * Creates a Player from the given Client and adds it to this room.
166
     * @param c the client
167
     * @return
168
     */
169
    public Player createAndAddPlayer(Client client) {
170
        if (players.size() >= MAX_PLAYER_COUNT) {
171
            throw new IllegalStateException("MAX_PLAYER_COUNT has been reached.");
172
        }
173
174
        Player p = new Player(this, client);
175
176
        // Broadcast to the other players that one player joined.
177
        broadcastRoomMessage(MessageType.PLAYER_CHANGED, "+");
178
179
        // Add the new player to the list.
180
        players.add(p);
181
182
        // Send him the current number of players and the current room image.
183
        String content = String.valueOf(players.size());
184
        p.sendRoomMessage(MessageType.IMAGE_MESSAGE, content);
185
186
        // Store image as PNG
187
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
188
        try {
189
            ImageIO.write(roomImage, "PNG", bout);
190
        } catch (IOException e) { /* Should never happen */ }
191
192
193
        // Send the image as binary message.
194
        BinaryWebsocketMessage msg = new BinaryWebsocketMessage(
195
                ByteBuffer.wrap(bout.toByteArray()));
196
        p.getClient().sendMessage(msg);
197
198
        return p;
199
200
    }
201
202
    /**
203
     * @see Player#removeFromRoom()
204
     * @param p
205
     */
206
    private void internalRemovePlayer(Player p) {
207
        players.remove(p);
208
209
        // Broadcast that one player is removed.
210
        broadcastRoomMessage(MessageType.PLAYER_CHANGED, "-");
211
    }
212
213
    /**
214
     * @see Player#handleDrawMessage(DrawMessage, long)
215
     * @param p
216
     * @param msg
217
     * @param msgId
218
     */
219
    private void internalHandleDrawMessage(Player p, DrawMessage msg,
220
            long msgId) {
221
        p.setLastReceivedMessageId(msgId);
222
223
        // Draw the RoomMessage onto our Room Image.
224
        msg.draw(roomGraphics);
225
226
        // Broadcast the Draw Message.
227
        broadcastDrawMessage(msg);
228
    }
229
230
231
    /**
232
     * Broadcasts the given drawboard message to all connected players.
233
     * Note: For DrawMessages, please use
234
     * {@link #broadcastDrawMessage(DrawMessage)}
235
     * as this method will buffer them and prefix them with the correct
236
     * last received Message ID.
237
     * @param type
238
     * @param content
239
     */
240
    private void broadcastRoomMessage(MessageType type, String content) {
241
        for (Player p : players) {
242
            p.sendRoomMessage(type, content);
243
        }
244
    }
245
246
247
    /**
248
     * Broadcast the given DrawMessage. This will buffer the message
249
     * and the {@link #drawmessageBroadcastTimer} will broadcast them
250
     * at a regular interval, prefixing them with the player's current
251
     * {@link Player#lastReceivedMessageId}.
252
     * @param msg
253
     */
254
    private void broadcastDrawMessage(DrawMessage msg) {
255
        if (!BUFFER_DRAW_MESSAGES) {
256
            String msgStr = msg.toString();
257
258
            for (Player p : players) {
259
                String s = String.valueOf(p.getLastReceivedMessageId())
260
                        + "," + msgStr;
261
                p.sendRoomMessage(MessageType.DRAW_MESSAGE, s);
262
            }
263
        } else {
264
            for (Player p : players) {
265
                p.getBufferedDrawMessages().add(msg);
266
            }
267
        }
268
    }
269
270
271
    /**
272
     * Tick handler for the broadcastTimer.
273
     */
274
    private void broadcastTimerTick() {
275
        // For each Player, send all per Player buffered
276
        // DrawMessages, prefixing each DrawMessage with the player's
277
        // lastReceuvedMessageId.
278
        // Multiple messages are concatenated with "|".
279
280
        for (Player p : players) {
281
282
            StringBuilder sb = new StringBuilder();
283
            List<DrawMessage> drawMessages = p.getBufferedDrawMessages();
284
285
            if (drawMessages.size() > 0) {
286
                for (int i = 0; i < drawMessages.size(); i++) {
287
                    DrawMessage msg = drawMessages.get(i);
288
289
                    String s = String.valueOf(p.getLastReceivedMessageId())
290
                            + "," + msg.toString();
291
                    if (i > 0)
292
                        sb.append("|");
293
294
                    sb.append(s);
295
                }
296
                drawMessages.clear();            
297
298
                p.sendRoomMessage(MessageType.DRAW_MESSAGE, sb.toString());
299
            }
300
        }
301
    }
302
303
304
305
306
    /**
307
     * Submits the given Runnable to the Room Executor.
308
     * @param run
309
     */
310
    public void invoke(Runnable task) {
311
        roomExecutor.submit(task);
312
    }
313
314
    /**
315
     * Submits the given Runnable to the Room Executor and waits until it
316
     * has been executed.
317
     * @param task
318
     * @throws InterruptedException if the current thread was interrupted
319
     * while waiting
320
     * @throws ExecutionException if the computation threw an exception 
321
     */
322
    public void invokeAndWait(Runnable task)
323
            throws InterruptedException, ExecutionException {
324
        Future<?> f = roomExecutor.submit(task);
325
        f.get();
326
    }
327
328
    /**
329
     * Shuts down the roomExecutor and the drawmessageBroadcastTimer.
330
     */
331
    public void shutdown() {
332
        roomExecutor.shutdown();
333
        drawmessageBroadcastTimer.cancel();
334
    }
335
336
337
338
    /**
339
     * A Player participates in a Room. It is the interface between the
340
     * {@link Room} and the {@link Client}.<br><br>
341
     * 
342
     * Note: This means a player object is actually a join between Room and
343
     * Endpoint.
344
     */
345
    public final class Player {
346
347
        /**
348
         * The room to which this player belongs.
349
         */
350
        private Room room;
351
352
        /**
353
         * The room buffers the last draw message ID that was received from
354
         * this player.
355
         */
356
        private long lastReceivedMessageId = 0;
357
358
        private final Client client;
359
360
        /**
361
         * Buffered DrawMessages that will be sent by a Timer.
362
         * TODO: This should be refactored to be in a Room-Player join class
363
         * as this is room-specific.
364
         */
365
        private final List<DrawMessage> bufferedDrawMessages =
366
                new ArrayList<>();
367
368
        private List<DrawMessage> getBufferedDrawMessages() {
369
            return bufferedDrawMessages;
370
        }
371
372
373
374
        private Player(Room room, Client client) {
375
            this.room = room;
376
            this.client = client;
377
        }
378
379
        public Room getRoom() {
380
            return room;
381
        }
382
383
        public Client getClient() {
384
            return client;
385
        }
386
387
        /**
388
         * Removes this player from its room, e.g. when
389
         * the client disconnects.
390
         */
391
        public void removeFromRoom() {
392
            room.internalRemovePlayer(this);
393
            room = null;
394
        }
395
396
397
        private long getLastReceivedMessageId() {
398
            return lastReceivedMessageId;
399
        }
400
        private void setLastReceivedMessageId(long value) {
401
            lastReceivedMessageId = value;
402
        }
403
404
405
        /**
406
         * Handles the given DrawMessage by drawing it onto this Room's
407
         * image and by broadcasting it to the connected players.
408
         * @param sender
409
         * @param msg
410
         * @param msgId
411
         */
412
        public void handleDrawMessage(DrawMessage msg, long msgId) {
413
            room.internalHandleDrawMessage(this, msg, msgId);
414
        }
415
416
417
        /**
418
         * Sends the given room message.
419
         * @param type
420
         * @param content
421
         */
422
        private void sendRoomMessage(MessageType type, String content) {
423
            if (content == null || type == null)
424
                throw null;
425
426
            String completeMsg = String.valueOf(type.flag) + content;
427
428
            client.sendMessage(new StringWebsocketMessage(completeMsg));
429
        }
430
431
432
433
    }
434
435
436
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (+48 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard;
18
19
import javax.servlet.ServletContextEvent;
20
import javax.servlet.ServletContextListener;
21
import javax.servlet.annotation.WebListener;
22
import javax.websocket.DeploymentException;
23
import javax.websocket.server.ServerContainer;
24
import javax.websocket.server.ServerEndpointConfig;
25
26
@WebListener
27
public final class WsConfigListener implements ServletContextListener {
28
29
    @Override
30
    public void contextInitialized(ServletContextEvent sce) {
31
32
        ServerContainer sc =
33
                (ServerContainer) sce.getServletContext().getAttribute(
34
                        "javax.websocket.server.ServerContainer");
35
        try {
36
            sc.addEndpoint(ServerEndpointConfig.Builder.create(
37
                    DrawboardEndpoint.class, "/websocket/drawboard").build());
38
        } catch (DeploymentException e) {
39
            throw new IllegalStateException(e);
40
        }
41
    }
42
43
    @Override
44
    public void contextDestroyed(ServletContextEvent sce) {
45
        // Shutdown our room.
46
        DrawboardEndpoint.getRoom().shutdown();
47
    }
48
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (+25 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
/**
20
 * Abstract base class for Websocket Messages (binary or string)
21
 * that can be buffered.
22
 */
23
public abstract class AbstractWebsocketMessage {
24
25
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (+34 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
import java.nio.ByteBuffer;
20
21
/**
22
 * Represents a binary websocket message.
23
 */
24
public final class BinaryWebsocketMessage extends AbstractWebsocketMessage {
25
    private final ByteBuffer bytes;
26
27
    public BinaryWebsocketMessage(ByteBuffer bytes) {
28
        this.bytes = bytes;
29
    }
30
31
    public ByteBuffer getBytes() {
32
        return bytes;
33
    }
34
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (+34 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
/**
20
 * Represents a string websocket message.
21
 *
22
 */
23
public final class StringWebsocketMessage extends AbstractWebsocketMessage {
24
    private final String string;
25
26
    public StringWebsocketMessage(String string) {
27
        this.string = string;
28
    }
29
30
    public String getString() {
31
        return string;
32
    }
33
34
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (+25 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
/**
20
 * Abstract base class for Websocket Messages (binary or string)
21
 * that can be buffered.
22
 */
23
public abstract class AbstractWebsocketMessage {
24
25
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (+34 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
import java.nio.ByteBuffer;
20
21
/**
22
 * Represents a binary websocket message.
23
 */
24
public final class BinaryWebsocketMessage extends AbstractWebsocketMessage {
25
    private final ByteBuffer bytes;
26
27
    public BinaryWebsocketMessage(ByteBuffer bytes) {
28
        this.bytes = bytes;
29
    }
30
31
    public ByteBuffer getBytes() {
32
        return bytes;
33
    }
34
}
(-)webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (+34 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package websocket.drawboard.wsmessages;
18
19
/**
20
 * Represents a string websocket message.
21
 *
22
 */
23
public final class StringWebsocketMessage extends AbstractWebsocketMessage {
24
    private final String string;
25
26
    public StringWebsocketMessage(String string) {
27
        this.string = string;
28
    }
29
30
    public String getString() {
31
        return string;
32
    }
33
34
}
(-)webapps/examples/websocket/drawboard.xhtml (+611 lines)
Line 0 Link Here
1
<?xml version="1.0" encoding="UTF-8"?>
2
<!--
3
  Licensed to the Apache Software Foundation (ASF) under one or more
4
  contributor license agreements.  See the NOTICE file distributed with
5
  this work for additional information regarding copyright ownership.
6
  The ASF licenses this file to You under the Apache License, Version 2.0
7
  (the "License"); you may not use this file except in compliance with
8
  the License.  You may obtain a copy of the License at
9
10
      http://www.apache.org/licenses/LICENSE-2.0
11
12
  Unless required by applicable law or agreed to in writing, software
13
  distributed under the License is distributed on an "AS IS" BASIS,
14
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
  See the License for the specific language governing permissions and
16
  limitations under the License.
17
-->
18
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
19
<head>
20
    <title>Apache Tomcat WebSocket Examples: Drawboard</title>
21
    <style type="text/css"><![CDATA[
22
23
        body {
24
            font-family: Arial, sans-serif;
25
            font-size: 11pt;
26
            background-color: #eeeeea;
27
            padding: 10px;
28
        }
29
30
        #console-container {
31
            float: left;
32
            background-color: #fff;
33
            width: 250px;
34
        }
35
36
        #console {
37
            font-size: 10pt;
38
            height: 600px;
39
            overflow-y: scroll;
40
            padding-left: 5px;
41
            padding-right: 5px;
42
        }
43
44
        #console p {
45
            padding: 0;
46
            margin: 0;
47
        }
48
49
        #drawContainer {
50
            float: left;
51
            display: none;
52
            margin-right: 25px;
53
        }
54
55
        #drawContainer canvas {
56
            display: block;
57
            -ms-touch-action: none; /* Disable touch behaviors, like pan and zoom */
58
        }
59
60
        #labelContainer {
61
            margin-bottom: 15px;
62
        }
63
64
        #drawContainer, #console-container {
65
            box-shadow: 0px 0px 8px 3px #bbb;
66
            border: 1px solid #CCCCCC;
67
            
68
        }
69
70
    ]]></style>
71
    <script type="application/javascript"><![CDATA[
72
    "use strict";
73
74
    (function() {
75
76
        document.addEventListener("DOMContentLoaded", function() {
77
            // Remove elements with "noscript" class - <noscript> is not
78
            // allowed in XHTML
79
            var noscripts = document.getElementsByClassName("noscript");
80
            for (var i = 0; i < noscripts.length; i++) {
81
                noscripts[i].parentNode.removeChild(noscripts[i]);
82
            }
83
84
85
            var Console = {};
86
87
            Console.log = (function() {
88
                var consoleContainer =
89
                    document.getElementById("console-container");
90
                var console = document.createElement("div");
91
                console.setAttribute("id", "console");
92
                consoleContainer.appendChild(console);
93
94
                return function(message) {
95
                    var p = document.createElement('p');
96
                    p.style.wordWrap = "break-word";
97
                    p.appendChild(document.createTextNode(message));
98
                    console.appendChild(p);
99
                    while (console.childNodes.length > 25) {
100
                        console.removeChild(console.firstChild);
101
                    }
102
                    console.scrollTop = console.scrollHeight;
103
                }
104
            })();
105
106
107
            function Room(drawContainer) {
108
109
                // The WebSocket object.
110
                var socket;
111
                // ID of the timer which sends ping messages.
112
                var pingTimerId;
113
114
                var isStarted = false;
115
                var playerCount = 0;
116
117
                // An array of PathIdContainer objects that the server
118
                // did not yet handle.
119
                // They are ordered by id (ascending).
120
                var pathsNotHandled = [];
121
122
                var nextMsgId = 1;
123
124
                var canvasDisplay = document.createElement("canvas");
125
                var canvasBackground = document.createElement("canvas");
126
                var canvasServerImage = document.createElement("canvas");
127
                var canvasArray = [canvasDisplay, canvasBackground,
128
                    canvasServerImage];
129
130
                var labelPlayerCount = document.createTextNode("0");
131
                var optionContainer = document.createElement("div");
132
133
134
                var canvasDisplayCtx = canvasDisplay.getContext("2d");
135
                var canvasBackgroundCtx = canvasBackground.getContext("2d");
136
                var canvasServerImageCtx = canvasServerImage.getContext("2d");
137
138
                var mouseInWindow = false;
139
                var mouseDown = false;
140
                var currentMouseX = 0, currentMouseY = 0;
141
142
                var availableColors = [];
143
                var currentColorIndex;
144
                var colorContainers;
145
146
                var availableThicknesses = [2, 3, 6, 10, 16, 28, 50];
147
                var currentThicknessIndex;
148
                var thicknessContainers;
149
150
151
                var placeholder = document.createElement("div");
152
                placeholder.appendChild(document.createTextNode("Loading... "));
153
                var progressElem = document.createElement("progress");
154
                placeholder.appendChild(progressElem);
155
156
                labelContainer.appendChild(placeholder);
157
158
                function rgb(color) {
159
                       return "rgba(" + color[0] + "," + color[1] + ","
160
                               + color[2] + "," + color[3] + ")";
161
                   }
162
163
                function PathIdContainer(path, id) {
164
                    this.path = path;
165
                    this.id = id;
166
                }
167
168
                function Path(type, color, thickness, x1, y1, x2, y2) {
169
                    this.type = type;
170
                    this.color = color;
171
                    this.thickness = thickness;
172
                    this.x1 = x1;
173
                    this.y1 = y1;
174
                    this.x2 = x2;
175
                    this.y2 = y2;
176
177
                    this.draw = function(ctx) {
178
                        ctx.beginPath();
179
                        ctx.lineCap = "round";
180
                        ctx.lineWidth = thickness;
181
                        var style = rgb(color);
182
                        ctx.strokeStyle = style;
183
                        
184
                        if (x1 == x2 && y1 == y2) {
185
                            // Always draw as arc to meet the behavior
186
                            // in Java2D.
187
                            ctx.fillStyle = style;
188
                            ctx.arc(x1, y1, thickness / 2.0, 0,
189
                                    Math.PI * 2.0, false);
190
                            ctx.fill();
191
                        } else {
192
                            if (type == 1) {
193
                                // Draw a line.
194
                                ctx.moveTo(x1, y1);
195
                                ctx.lineTo(x2, y2);
196
                                ctx.stroke();
197
                            }
198
                        }
199
                    };
200
                }
201
202
203
                function connect() {
204
                    var host = (window.location.protocol == "https:"
205
                            ? "wss://" : "ws://") + window.location.host
206
                            + "/examples/websocket/drawboard";
207
                    socket = new WebSocket(host);
208
209
                    socket.onopen = function () {
210
                        // Socket has opened. Now wait for the server to
211
                        // send us the initial packet.
212
                        Console.log("WebSocket connection opened.");
213
214
                        // Set up a timer for pong messages.
215
                        pingTimerId = window.setInterval(function() {
216
                            socket.send("0");
217
                        }, 30000);
218
                    }
219
220
                    socket.onclose = function () {
221
                        Console.log("WebSocket connection closed.");
222
                        disableControls();
223
224
                        // Disable pong timer.
225
                        window.clearInterval(pingTimerId);
226
                    }
227
228
                    socket.onmessage = function(message) {
229
230
                        // Split joined message and process them
231
                        // invidividually.
232
                        var messages = message.data.split(";");
233
                        for (var msgArrIdx = 0; msgArrIdx < messages.length;
234
                                msgArrIdx++) {
235
                            var msg = messages[msgArrIdx];
236
                            var type = msg.substring(0, 1);
237
238
                            if (type == "0") {
239
                                // Error message.
240
                                var error = msg.substring(1);
241
                                // Log it to the console and show an alert.
242
                                Console.log("Error: " + error);
243
                                alert(error);
244
245
                            } else {
246
                                if (!isStarted) {
247
                                    if (type == "2") {
248
                                        // Initial message. It contains the
249
                                        // number of players.
250
                                        // After this message we will receive
251
                                        // a binary message containing the current
252
                                        // room image as PNG.
253
                                        playerCount = parseInt(msg.substring(1));
254
255
                                        refreshPlayerCount();
256
257
                                        // The next message will be a binary
258
                                        // message containing the room images
259
                                        // as PNG. Therefore we temporarily swap
260
                                        // the message handler.
261
                                        var originalHandler = socket.onmessage;
262
                                        socket.onmessage = function(message) {
263
                                            // First, we restore the original handler.
264
                                            socket.onmessage = originalHandler;
265
266
                                            // Read the image.
267
                                            var blob = message.data;
268
                                            // Create new blob with correct MIME type.
269
                                            blob = new Blob([blob], {type : "image/png"});
270
271
                                            var url = URL.createObjectURL(blob);
272
273
                                            var img = new Image(); 
274
275
                                            // We must wait until the onload event is
276
                                            // raised until we can draw the image onto
277
                                            // the canvas.
278
                                            
279
                                            // TODO: I don't know if there is a guarantee
280
                                            // that no WebSocket events (onmessage) will
281
                                            // be raised until the onload event of this
282
                                            // image is raised. Maybe we need to need to
283
                                            // push websocket messages on a queue until
284
                                            // this onload function is called.
285
                                            img.onload = function() {
286
287
                                                // Release the object URL.
288
                                                URL.revokeObjectURL(url);
289
290
                                                // Set the canvases to the correct size.
291
            
292
                                                for (var i = 0; i < canvasArray.length; i++) {
293
                                                    canvasArray[i].width = img.width;
294
                                                    canvasArray[i].height = img.height;
295
                                                }
296
297
                                                // Now draw the image on the last canvas.
298
                                                canvasServerImageCtx.clearRect(0, 0,
299
                                                        canvasServerImage.width,
300
                                                        canvasServerImage.height);
301
                                                canvasServerImageCtx.drawImage(img, 0, 0);
302
303
                                                // Draw it on the background canvas.
304
                                                canvasBackgroundCtx.drawImage(canvasServerImage,
305
                                                        0, 0);
306
307
                                                // Refresh the display canvas.
308
                                                refreshDisplayCanvas();
309
310
                                                isStarted = true;
311
                                                startControls();
312
                                            };
313
314
                                            img.src = url;
315
                                        };
316
                                    }
317
                                } else {
318
                                    if (type == "3") {
319
                                        // The number of players in this room changed.
320
                                        var playerAdded = msg.substring(1) == "+";
321
                                        playerCount += playerAdded ? 1 : -1;
322
                                        refreshPlayerCount();
323
324
                                        Console.log("Player " + (playerAdded
325
                                                ? "joined." : "left."));
326
327
                                    } else if (type == "1") {
328
                                        // We received a new DrawMessage.
329
                                        var maxLastHandledId = -1;
330
                                        var drawMessages = msg.substring(1).split("|");
331
                                        for (var i = 0; i < drawMessages.length; i++) {
332
                                            var elements = drawMessages[i].split(",");
333
                                            var lastHandledId = parseInt(elements[0]);
334
                                               maxLastHandledId = Math.max(maxLastHandledId,
335
                                                       lastHandledId);
336
337
                                            var path = new Path(
338
                                                    parseInt(elements[1]),
339
                                                    [parseInt(elements[2]),
340
                                                    parseInt(elements[3]),
341
                                                    parseInt(elements[4]),
342
                                                    parseInt(elements[5]) / 255.0],
343
                                                    parseFloat(elements[6]),
344
                                                    parseInt(elements[7]),
345
                                                    parseInt(elements[8]),
346
                                                    parseInt(elements[9]),
347
                                                    parseInt(elements[10]));
348
349
                                            // Draw the path onto the last canvas.
350
                                            path.draw(canvasServerImageCtx);
351
                                        }
352
353
                                        // Draw the last canvas onto the background one.
354
                                        canvasBackgroundCtx.drawImage(canvasServerImage,
355
                                                0, 0);
356
357
                                        // Now go through the pathsNotHandled array and
358
                                        // remove the paths that were already handled by
359
                                        // the server.
360
                                        while (pathsNotHandled.length > 0 
361
                                                && pathsNotHandled[0].id <= maxLastHandledId)
362
                                            pathsNotHandled.shift();
363
364
                                        // Now me must draw the remaining paths onto
365
                                        // the background canvas.
366
                                        for (var i = 0; i < pathsNotHandled.length; i++) {
367
                                            pathsNotHandled[i].path.draw(canvasBackgroundCtx);
368
                                        }
369
370
                                        refreshDisplayCanvas();
371
                                    } 
372
                                }
373
                            }
374
                        }
375
                    };
376
377
                }
378
379
                function refreshPlayerCount() {
380
                    labelPlayerCount.nodeValue = String(playerCount);
381
                }
382
383
                function refreshDisplayCanvas() {
384
                    canvasDisplayCtx.drawImage(canvasBackground, 0, 0);
385
                    if (mouseInWindow && !mouseDown) {
386
                        canvasDisplayCtx.beginPath();
387
                        var color = availableColors[currentColorIndex].slice(0);
388
                        color[3] = 0.5;
389
                        canvasDisplayCtx.fillStyle = rgb(color);
390
                        
391
                        canvasDisplayCtx.arc(currentMouseX, currentMouseY, availableThicknesses[currentThicknessIndex] / 2, 0, Math.PI * 2.0, true);
392
                        
393
                        canvasDisplayCtx.fill();
394
                    }
395
                }
396
397
                function startControls() {
398
                    var labelContainer = document.getElementById("labelContainer");
399
                    labelContainer.removeChild(placeholder);
400
                    placeholder = undefined;
401
                    
402
                    labelContainer.appendChild(
403
                            document.createTextNode("Number of Players: "));
404
                    labelContainer.appendChild(labelPlayerCount);
405
406
407
                    drawContainer.style.display = "block";
408
                    drawContainer.appendChild(canvasDisplay);
409
410
                    drawContainer.appendChild(optionContainer);
411
412
                    canvasDisplay.onmousemove = function(e) {
413
                        mouseInWindow = true;
414
                        var oldMouseX = currentMouseX, oldMouseY = currentMouseY;
415
                        currentMouseX = e.pageX - canvasDisplay.offsetLeft;
416
                        currentMouseY = e.pageY - canvasDisplay.offsetTop;
417
418
                        if (mouseDown) {
419
                            var path = new Path(1, availableColors[currentColorIndex],
420
                                    availableThicknesses[currentThicknessIndex],
421
                                    oldMouseX, oldMouseY, currentMouseX,
422
                                    currentMouseY);
423
                            // Draw it on the background canvas.
424
                            path.draw(canvasBackgroundCtx);
425
426
                            // Send it to the sever.
427
                            pushPath(path);
428
                        }
429
430
                        refreshDisplayCanvas();
431
                    };
432
433
                    canvasDisplay.onmousedown = function(e) {
434
                        currentMouseX = e.pageX - canvasDisplay.offsetLeft;
435
                        currentMouseY = e.pageY - canvasDisplay.offsetTop;
436
437
                        if (e.button == 0) {
438
                            mouseDown = true;
439
440
                            var path = new Path(1, availableColors[currentColorIndex],
441
                                    availableThicknesses[currentThicknessIndex],
442
                                    currentMouseX, currentMouseY, currentMouseX,
443
                                    currentMouseY);
444
                            // Draw it on the background canvas.
445
                            path.draw(canvasBackgroundCtx);
446
447
                            // Send it to the sever.
448
                            pushPath(path);
449
450
                            refreshDisplayCanvas();
451
452
                        } else if (mouseDown) {
453
                            // Cancel drawing.
454
                            mouseDown = false;
455
456
                            refreshDisplayCanvas();
457
                        }
458
                    }
459
460
                    canvasDisplay.onmouseup = function(e) {
461
                        if (e.button == 0) {
462
                            if (mouseDown) {
463
                                mouseDown = false;
464
465
                                refreshDisplayCanvas();
466
                            }
467
                        }
468
                    };
469
470
                    canvasDisplay.onmouseout = function() {
471
                        mouseInWindow = false;
472
                        refreshDisplayCanvas();
473
                    };
474
475
476
                    // Create color and thickness controls.
477
                    var colorContainersBox = document.createElement("div");
478
                    colorContainersBox.setAttribute("style",
479
                            "margin: 4px; border: 1px solid #bbb; border-radius: 3px;");
480
                    optionContainer.appendChild(colorContainersBox);
481
482
483
                    colorContainers = new Array(3 * 3 * 3);
484
                    for (var i = 0; i < colorContainers.length; i++) {
485
                        var colorContainer = colorContainers[i] =
486
                            document.createElement("div");
487
                        var color = availableColors[i] = 
488
                            [
489
                                Math.floor((i % 3) * 255 / 2),
490
                                Math.floor((Math.floor(i / 3) % 3) * 255 / 2),
491
                                Math.floor((Math.floor(i / (3 * 3)) % 3) * 255 / 2), 
492
                                1.0
493
                            ];
494
                        colorContainer.setAttribute("style",
495
                                "margin: 3px; width: 18px; height: 18px; "
496
                                + "float: left; background-color: " + rgb(color));
497
                        colorContainer.style.border = '2px solid #000';
498
                        colorContainer.onmousedown = (function(ix) {
499
                            return function() {
500
                                setColor(ix);
501
                            };
502
                        })(i);
503
504
                        colorContainersBox.appendChild(colorContainer);
505
                    }
506
507
                    var divClearLeft = document.createElement("div");
508
                    divClearLeft.setAttribute("style", "clear: left;");
509
                    colorContainersBox.appendChild(divClearLeft);
510
511
                    var thicknessContainersBox = document.createElement("div");
512
                    thicknessContainersBox.setAttribute("style",
513
                            "margin: 3px; border: 1px solid #bbb; border-radius: 3px;");
514
                    optionContainer.appendChild(thicknessContainersBox);
515
516
517
                    thicknessContainers = new Array(availableThicknesses.length);
518
                    for (var i = 0; i < thicknessContainers.length; i++) {
519
                        var thicknessContainer = thicknessContainers[i] =
520
                            document.createElement("div");
521
                        thicknessContainer.setAttribute("style",
522
                                "text-align: center; margin: 3px; width: 18px; "
523
                                + "height: 18px; float: left;");
524
                        thicknessContainer.style.border = "2px solid #000";
525
                        thicknessContainer.appendChild(document.createTextNode(
526
                                String(availableThicknesses[i])));
527
                        thicknessContainer.onmousedown = (function(ix) {
528
                            return function() {
529
                                setThickness(ix);
530
                            };
531
                        })(i);
532
533
                        thicknessContainersBox.appendChild(thicknessContainer);
534
                    }
535
536
                    divClearLeft = document.createElement("div");
537
                    divClearLeft.setAttribute("style", "clear: left;");
538
                    thicknessContainersBox.appendChild(divClearLeft);
539
540
541
                    setColor(0);
542
                    setThickness(0);
543
544
                }
545
546
                function disableControls() {
547
                    canvasDisplay.onmousemove = canvasDisplay.onmousedown =
548
                        undefined;
549
                    mouseInWindow = false;
550
                    refreshDisplayCanvas();
551
                }
552
553
                function pushPath(path) {
554
555
                    // Push it into the pathsNotHandled array.
556
                    var container = new PathIdContainer(path, nextMsgId++);
557
                    pathsNotHandled.push(container);
558
559
                    // Send the path to the server.
560
                    var message = container.id + "|" + path.type + ","
561
                            + path.color[0] + "," + path.color[1] + ","
562
                            + path.color[2] + ","
563
                            + Math.round(path.color[3] * 255.0) + ","
564
                            + path.thickness + "," + path.x1 + "," 
565
                            + path.y1 + "," + path.x2 + "," + path.y2;
566
                    
567
                    socket.send("1" + message);
568
                }
569
570
                function setThickness(thicknessIndex) {
571
                    if (typeof currentThicknessIndex !== "undefined")
572
                        thicknessContainers[currentThicknessIndex]
573
                            .style.borderColor = "#000";
574
                    currentThicknessIndex = thicknessIndex;
575
                    thicknessContainers[currentThicknessIndex]
576
                        .style.borderColor = "#d08";
577
                }
578
                
579
                function setColor(colorIndex) {
580
                    if (typeof currentColorIndex !== "undefined")
581
                        colorContainers[currentColorIndex]
582
                            .style.borderColor = "#000";
583
                    currentColorIndex = colorIndex;
584
                    colorContainers[currentColorIndex]
585
                        .style.borderColor = "#d08";
586
                }
587
588
589
                connect();
590
591
            }
592
593
594
            // Initialize the room
595
            var room = new Room(document.getElementById("drawContainer"));
596
597
598
        }, false);
599
600
    })();
601
    ]]></script>
602
</head>
603
<body>
604
    <div class="noscript"><h2 style="color: #ff0000;">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
605
    Javascript and reload this page!</h2></div>
606
    <div id="labelContainer"/>
607
    <div id="drawContainer"/>
608
    <div id="console-container"/>
609
610
</body>
611
</html>
(-)webapps/examples/websocket/index.xhtml (+2 lines)
Lines 25-30 Link Here
25
    <li><a href="echo.xhtml">Echo example</a></li>
25
    <li><a href="echo.xhtml">Echo example</a></li>
26
    <li><a href="chat.xhtml">Chat example</a></li>
26
    <li><a href="chat.xhtml">Chat example</a></li>
27
    <li><a href="snake.xhtml">Multiplayer snake example</a></li>
27
    <li><a href="snake.xhtml">Multiplayer snake example</a></li>
28
    <li><a href="drawboard.xhtml">Multiplayer drawboard example</a></li>
29
28
</ul>
30
</ul>
29
</body>
31
</body>
30
</html>
32
</html>

Return to bug 55639