/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard;
import java.io.EOFException;
import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import websocket.drawboard.DrawMessage.ParseException;
import websocket.drawboard.wsmessages.StringWebsocketMessage;
public final class DrawboardEndpoint extends Endpoint {
private static final Log log =
LogFactory.getLog(DrawboardEndpoint.class);
/**
* Our room where players can join.
*/
private static volatile Room room = null;
private static final Object roomLock = new Object();
public static Room getRoom(boolean create) {
if (create) {
if (room == null) {
synchronized (roomLock) {
if (room == null) {
room = new Room();
}
}
}
return room;
} else {
return room;
}
}
/**
* The player that is associated with this Endpoint and the current room.
* Note that this variable is only accessed from the Room Thread.
*
* TODO: Currently, Tomcat uses an Endpoint instance once - however
* the java doc of endpoint says:
* "Each instance of a websocket endpoint is guaranteed not to be called by
* more than one thread at a time per active connection."
* This could mean that after calling onClose(), the instance
* could be reused for another connection so onOpen() will get called
* (possibly from another thread).
* If this is the case, we would need a variable holder for the variables
* that are accessed by the Room thread, and read the reference to the holder
* at the beginning of onOpen, onMessage, onClose methods to ensure the room
* thread always gets the correct instance of the variable holder.
*/
private Room.Player player;
@Override
public void onOpen(Session session, EndpointConfig config) {
// Set maximum messages size to 10.000 bytes.
session.setMaxTextMessageBufferSize(10000);
session.addMessageHandler(stringHandler);
final Client client = new Client(session);
final Room room = getRoom(true);
room.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
// Create a new Player and add it to the room.
try {
player = room.createAndAddPlayer(client);
} catch (IllegalStateException ex) {
// Probably the max. number of players has been
// reached.
client.sendMessage(new StringWebsocketMessage(
"0" + ex.getLocalizedMessage()));
// Close the connection.
client.close();
}
} catch (RuntimeException ex) {
log.error("Unexpected exception: " + ex.toString(), ex);
}
}
});
}
@Override
public void onClose(Session session, CloseReason closeReason) {
Room room = getRoom(false);
if (room != null) {
room.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
// Player can be null if it couldn't enter the room
if (player != null) {
// Remove this player from the room.
player.removeFromRoom();
// Set player to null to prevent NPEs when onMessage events
// are processed (from other threads) after onClose has been
// called from different thread which closed the Websocket session.
player = null;
}
} catch (RuntimeException ex) {
log.error("Unexpected exception: " + ex.toString(), ex);
}
}
});
}
}
@Override
public void onError(Session session, Throwable t) {
// Most likely cause is a user closing their browser. Check to see if
// the root cause is EOF and if it is ignore it.
// Protect against infinite loops.
int count = 0;
Throwable root = t;
while (root.getCause() != null && count < 20) {
root = root.getCause();
count ++;
}
if (root instanceof EOFException) {
// Assume this is triggered by the user closing their browser and
// ignore it.
} else {
log.error("onError: " + t.toString(), t);
}
}
private final MessageHandler.Whole stringHandler =
new MessageHandler.Whole() {
@Override
public void onMessage(final String message) {
// Invoke handling of the message in the room.
room.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
// Currently, the only types of messages the client will send
// are draw messages prefixed by a Message ID
// (starting with char '1'), and pong messages (starting
// with char '0').
// Draw messages should look like this:
// ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2,lastInChain
boolean dontSwallowException = false;
try {
char messageType = message.charAt(0);
String messageContent = message.substring(1);
switch (messageType) {
case '0':
// Pong message.
// Do nothing.
break;
case '1':
// Draw message
int indexOfChar = messageContent.indexOf('|');
long msgId = Long.parseLong(
messageContent.substring(0, indexOfChar));
DrawMessage msg = DrawMessage.parseFromString(
messageContent.substring(indexOfChar + 1));
// Don't ingore RuntimeExceptions thrown by
// this method
// TODO: Find a better solution than this variable
dontSwallowException = true;
if (player != null) {
player.handleDrawMessage(msg, msgId);
}
dontSwallowException = false;
break;
}
} catch (RuntimeException ex) {
// Client sent invalid data.
// Ignore, TODO: maybe close connection
if (dontSwallowException) {
throw ex;
}
} catch (ParseException ex) {
// Client sent invalid data.
// Ignore, TODO: maybe close connection
}
} catch (RuntimeException ex) {
log.error("Unexpected exception: " + ex.toString(), ex);
}
}
});
}
};
}