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

(-)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/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