237 lines
9.1 KiB
Java
237 lines
9.1 KiB
Java
/*
|
|
* 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 java.io.IOException;
|
|
|
|
import jakarta.websocket.CloseReason;
|
|
import jakarta.websocket.Endpoint;
|
|
import jakarta.websocket.EndpointConfig;
|
|
import jakarta.websocket.MessageHandler;
|
|
import jakarta.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.<br><br>
|
|
*
|
|
* TODO: Currently, Tomcat uses an Endpoint instance once - however
|
|
* the java doc of endpoint says:
|
|
* "Each instance of a websocket endpoint is guaranteed not to be called by
|
|
* more than one thread at a time per active connection."
|
|
* This could mean that after calling onClose(), the instance
|
|
* could be reused for another connection so onOpen() will get called
|
|
* (possibly from another thread).<br>
|
|
* If this is the case, we would need a variable holder for the variables
|
|
* that are accessed by the Room thread, and read the reference to the holder
|
|
* at the beginning of onOpen, onMessage, onClose methods to ensure the room
|
|
* thread always gets the correct instance of the variable holder.
|
|
*/
|
|
private Room.Player player;
|
|
|
|
|
|
@Override
|
|
public void onOpen(Session session, EndpointConfig config) {
|
|
// Set maximum messages size to 10.000 bytes.
|
|
session.setMaxTextMessageBufferSize(10000);
|
|
session.addMessageHandler(stringHandler);
|
|
final Client client = new Client(session);
|
|
|
|
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 if (!session.isOpen() && root instanceof IOException) {
|
|
// IOException after close. Assume this is a variation of the user
|
|
// closing their browser (or refreshing very quickly) and ignore it.
|
|
} else {
|
|
log.error("onError: " + t.toString(), t);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private final MessageHandler.Whole<String> 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 ignore 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 (ParseException e) {
|
|
// Client sent invalid data
|
|
// Ignore, TODO: maybe close connection
|
|
} catch (RuntimeException e) {
|
|
// Client sent invalid data.
|
|
// Ignore, TODO: maybe close connection
|
|
if (dontSwallowException) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
} catch (RuntimeException ex) {
|
|
log.error("Unexpected exception: " + ex.toString(), ex);
|
|
}
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
|
|
|
|
}
|