Moved most of the message handling to other classes.

I'm finding that this code is being used multiple times, so inheritance seems to be warranted.
This commit is contained in:
Markil3
2021-07-29 09:11:33 -07:00
parent 6cc001af70
commit 1321549ef3
10 changed files with 926 additions and 350 deletions

View File

@@ -0,0 +1,260 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.browserCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
*
*/
public class MessageHandler implements Runnable, MessageSerializer
{
private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class);
private final InputStream input;
private final OutputStream output;
private final ExecutorService executor;
private final LinkedList<MessageListener> listeners = new LinkedList<>();
private final HashMap<Integer, Future<Object>> messageResponses = new HashMap<>();
/**
* Creates a message handler.
*
* @param input - The input from our external source.
* @param output - The output to the external source.
*/
public MessageHandler(InputStream input, OutputStream output)
{
this.input = input;
this.output = output;
this.executor = Executors.newCachedThreadPool();
}
@Override
public void run()
{
BufferedInputStream browserIn = null;
BufferedOutputStream browserOut = null;
boolean running = true;
byte[][] message;
byte[] messageByte;
Object messageOb;
Future<Object> messageResponse;
ByteBuffer numBuffer = ByteBuffer.allocate(4);
HashSet<Integer> toRemove = new HashSet<>();
try
{
browserIn = new BufferedInputStream(this.input);
browserOut = new BufferedOutputStream(this.output);
while (running)
{
try
{
if (browserIn.available() > 0)
{
message = this.readMessage(browserIn);
if (message == null)
{
logger.info("Connection closed.");
running = false;
}
else
{
numBuffer.clear();
numBuffer.put(message[0]);
messageOb = this.deserializeObject(message[1]);
messageResponse = this.triggerListeners(messageOb);
synchronized (this.messageResponses)
{
this.messageResponses.put(numBuffer.getInt(), messageResponse);
}
}
}
}
catch (IOException | ClassNotFoundException e)
{
logger.error("Could not retrieve message", e);
}
/*
* Returns any finished responses.
*/
synchronized (this.messageResponses)
{
if (this.messageResponses.size() > 0)
{
for (Map.Entry<Integer, Future<Object>> responses : this.messageResponses.entrySet())
{
try
{
if (responses.getValue().isDone())
{
toRemove.add(responses.getKey());
messageByte = serializeObject(responses.getValue());
writeMessage(browserOut, responses.getKey(), messageByte);
}
}
catch (IOException e)
{
logger.error("Could not send response message for " + responses.getKey(), e);
}
}
toRemove.forEach(this.messageResponses::remove);
toRemove.clear();
}
}
}
}
finally
{
/*
* Release locks for any messages still waiting.
*/
synchronized (this.messageResponses)
{
this.messageResponses.values().forEach(future -> future.cancel(false));
}
/*
* Close the streams.
*/
try
{
if (browserIn != null)
{
browserIn.close();
}
}
catch (IOException e1)
{
logger.error("Could not close browser input", e1);
}
finally
{
try
{
if (browserOut != null)
{
browserOut.close();
}
}
catch (IOException e1)
{
logger.error("Could not close browser output", e1);
}
}
}
}
/**
* Triggers the message listeners.
*
* @param message - The message to send.
* @return The value returned by the listeners.
*/
protected Future<Object> triggerListeners(Object message)
{
MessageListener[] listeners;
Future<Object> returnVal;
/*
* Copies the list of listeners, so we have an unchanging array without
* holding onto the list for the whole time of execution.
*/
synchronized (this.listeners)
{
listeners = this.listeners.toArray(MessageListener[]::new);
}
returnVal = this.executor.submit(() -> {
Object returnValue = null;
for (MessageListener listener : listeners)
{
returnValue = listener.onMessage(message, returnValue);
}
return returnValue;
});
return returnVal;
}
/**
* Adds a message listener
*
* @param listener - A callback for when a message arrives. A value needs to
* be returned from one of the listeners.
*/
public void addListener(MessageListener listener)
{
synchronized (this.listeners)
{
this.listeners.add(listener);
}
}
/**
* Checks to see if a listener has been added to the list.
*
* @param listener - A callback for when a message arrives.
* @return True if the provided listener is part of the list, false otherwise.
*/
public boolean hasListener(MessageListener listener)
{
synchronized (this.listeners)
{
return this.listeners.contains(listener);
}
}
/**
* Removes a message listener from the list.
*
* @param listener - A callback for when a message arrives.
*/
public void removeListener(MessageListener listener)
{
synchronized (this.listeners)
{
this.listeners.remove(listener);
}
}
/**
* A message listener is called when a message is sent from the remote.
*
* @see #addListener(MessageListener)
* @see #hasListener(MessageListener)
* @see #removeListener(MessageListener)
*/
public interface MessageListener
{
/**
* Called when the remote sends a message and wants a response.
*
* @param providedValue - The message sent from the remote.
* @param previousReturn The value that was returned by the previous
* listener called. A value must be returned to
* the remote. The listener either has the option
* of returning this value, or returning a new
* value. If null, then this is either the first
* listener called or none of the other listeners
* have generated return values.
* @return The value to be returned to the remote.
*/
Object onMessage(Object providedValue, Object previousReturn);
}
}

View File

@@ -0,0 +1,372 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.browserCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* This class serves as a general inter-process object messaging system.
*/
public abstract class MessageRunner implements Runnable, MessageSerializer
{
private static final Logger logger = LoggerFactory.getLogger(MessageRunner.class);
private final InputStream input;
private final OutputStream output;
private final Object readLock = new Object();
/**
* This queue serves as a cache for objects we need to send.
*/
private final LinkedList<MessagePacket> sendQueue = new LinkedList<>();
/**
* This queue serves as a cache for objects that are waiting for a response.
*/
private final HashMap<Integer, MessagePacket> sentQueue = new HashMap<>();
private int messagesSent = 0;
/**
* Creates a message runner.
*
* @param input - The input from our external source.
* @param output - The output to the external source.
*/
public MessageRunner(InputStream input, OutputStream output)
{
this.input = input;
this.output = output;
}
@Override
public void run()
{
BufferedInputStream browserIn = null;
BufferedOutputStream browserOut = null;
MessagePacket packet;
boolean running = true;
byte[][] returnMessage;
ByteBuffer numBuffer = ByteBuffer.allocate(4);
try
{
browserIn = new BufferedInputStream(this.input);
browserOut = new BufferedOutputStream(this.output);
while (running)
{
/*
* Sends a messages
*/
synchronized (this.sendQueue)
{
packet = this.sendQueue.poll();
}
if (packet != null)
{
try
{
packet.returnValue.index = this.messagesSent;
writeMessage(browserOut, this.messagesSent, packet.message);
synchronized (this.sentQueue)
{
this.sentQueue.put(this.messagesSent, packet);
this.messagesSent++;
}
}
catch (IOException e)
{
logger.error("Could not send message " + new String(packet.message, StandardCharsets.UTF_8), e);
}
}
/*
* Reads any available messages
*/
try
{
if (!this.sentQueue.isEmpty())
{
if (browserIn.available() > 0)
{
/*
* Wait for a response from the browser.
*/
returnMessage = this.readMessage(browserIn);
if (returnMessage == null)
{
logger.info("Connection closed.");
running = false;
}
else
{
numBuffer.clear();
numBuffer.put(returnMessage[0]);
numBuffer.clear();
packet = this.sentQueue.get(numBuffer.getInt());
if (packet == null)
{
numBuffer.clear();
logger.warn("Received message {} for nonexistant packet", numBuffer.getInt());
}
else
{
packet.returnMessage = returnMessage[1];
numBuffer.clear();
logger.debug("Reading message {} {}", numBuffer.getInt(), packet.returnMessage);
synchronized (this.readLock)
{
logger.trace("Received message {}, notifying futures.", packet.returnValue.index);
this.readLock.notifyAll();
}
}
}
}
}
}
catch (IOException e)
{
logger.error("Could not retrieve message", e);
}
}
}
finally
{
/*
* Release locks for any messages still waiting.
*/
synchronized (this.sendQueue)
{
while (this.sendQueue.size() > 0)
{
packet = this.sendQueue.poll();
packet.returnMessage = null;
}
}
synchronized (this.readLock)
{
this.readLock.notifyAll();
}
/*
* Close the streams.
*/
try
{
if (browserIn != null)
{
browserIn.close();
}
}
catch (IOException e1)
{
logger.error("Could not close browser input", e1);
}
finally
{
try
{
if (browserOut != null)
{
browserOut.close();
}
}
catch (IOException e1)
{
logger.error("Could not close browser output", e1);
}
}
}
}
/**
* Sends an object to the remote.
*
* @param message - The message to send.
* @return A future for whatever the remote may return.
* @throws IOException Should an error occur in serializing the object.
*/
public Future<Object> sendObject(Object message) throws IOException
{
byte[] messageData = this.serializeObject(message);
MessagePacket packetEntry = new MessagePacket(messageData);
MessageFuture future = new MessageFuture(packetEntry);
synchronized (this.sendQueue)
{
sendQueue.add(packetEntry);
}
return future;
}
/**
* This represents a sent message in storage.
*/
public static class MessagePacket
{
public final byte[] message;
public byte[] returnMessage;
public MessageFuture returnValue;
public MessagePacket(byte[] message)
{
this(message, null);
}
public MessagePacket(byte[] message, MessageFuture returnValue)
{
this.message = message;
this.returnValue = returnValue;
}
}
/**
* This future waits for a return message from the runner.
*/
public class MessageFuture implements Future<Object>
{
private final MessagePacket packetEntry;
private int index;
private boolean canceled = false;
private MessageFuture(MessagePacket packet)
{
this.packetEntry = packet;
packet.returnValue = this;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
synchronized (sendQueue)
{
if (sendQueue.remove(this.packetEntry))
{
this.canceled = true;
if (mayInterruptIfRunning)
{
logger.trace("Canceling future, notifying self futures.");
synchronized (sentQueue)
{
sentQueue.remove(this.index);
}
synchronized (readLock)
{
readLock.notifyAll();
}
}
else
{
logger.trace("Canceling future.");
}
return true;
}
else
{
return false;
}
}
}
@Override
public boolean isCancelled()
{
return this.canceled;
}
@Override
public boolean isDone()
{
return this.packetEntry.returnMessage != null;
}
@Override
public Object get() throws InterruptedException, ExecutionException
{
if (this.isDone())
{
try
{
return deserializeObject(this.packetEntry.returnMessage);
}
catch (IOException | ClassNotFoundException e)
{
throw new ExecutionException("Error parsing object", e);
}
}
synchronized (readLock)
{
while (!this.canceled && packetEntry.returnMessage == null)
{
logger.trace("Future {} waiting for message", this.index);
readLock.wait();
}
}
if (packetEntry.returnMessage == null)
{
throw new InterruptedException();
}
logger.trace("Future {} has received message", this.index);
try
{
return deserializeObject(this.packetEntry.returnMessage);
}
catch (IOException | ClassNotFoundException e)
{
throw new ExecutionException("Error parsing object", e);
}
}
@Override
public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
if (this.isDone())
{
try
{
return deserializeObject(this.packetEntry.returnMessage);
}
catch (IOException | ClassNotFoundException e)
{
throw new ExecutionException("Error parsing object", e);
}
}
synchronized (readLock)
{
while (!this.canceled && packetEntry.returnMessage == null)
{
logger.trace("Future {} waiting for message", this.index);
readLock.wait(unit.toMillis(timeout));
}
}
if (packetEntry.returnMessage == null)
{
throw new InterruptedException();
}
logger.trace("Future {} has received message", this.index);
try
{
return deserializeObject(this.packetEntry.returnMessage);
}
catch (IOException | ClassNotFoundException e)
{
throw new ExecutionException("Error parsing object", e);
}
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.browserCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.ByteBuffer;
public interface MessageSerializer
{
Logger logger = LoggerFactory.getLogger(MessageSerializer.class);
/**
* Converts an object into a form that can be sent.
*
* @param message - The message to send.
* @return The byte array representation of that object.
* @throws IOException Should a serialization error occur.
*/
default byte[] serializeObject(Object message) throws IOException
{
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream())
{
try (ObjectOutputStream stream = new ObjectOutputStream(byteStream))
{
stream.writeObject(message);
}
return byteStream.toByteArray();
}
}
/**
* Converts a byte stream into an object.
*
* @param message - The message received.
* @return An object.
* @throws IOException If there is an error in parsing the message.
* @throws ClassNotFoundException If the object is not recognized
*/
default Object deserializeObject(byte[] message) throws IOException, ClassNotFoundException
{
try (ByteArrayInputStream byteStream = new ByteArrayInputStream(message))
{
try (ObjectInputStream stream = new ObjectInputStream(byteStream))
{
return stream.readObject();
}
}
}
/**
* Writes a message to the output stream.
*
* @param out - The output stream to write to.
* @param messageNum - The ID of the message being sent. This will help keep track of responses.
* @param message - The actual message contents to write.
* @throws IOException Thrown when an exception occures
*/
default void writeMessage(OutputStream out, int messageNum, byte[] message) throws IOException
{
ByteBuffer lengthBuffer = ByteBuffer.allocate(4);
logger.debug("Writing message {} {}", messageNum, message);
/*
* Writes the message number.
*/
lengthBuffer.clear();
lengthBuffer.putInt(messageNum);
out.write(lengthBuffer.array());
/*
* Writes the message length
*/
/*
* Wipe out the buffer
*/
lengthBuffer.clear();
lengthBuffer.put(new byte[4]);
lengthBuffer.clear();
lengthBuffer.putInt(message.length);
out.write(lengthBuffer.array());
/*
* Writes the message
*/
out.write(message);
out.flush();
}
/**
* Reads a message from the input stream
*
* @param in - The input stream to read from.
* @return Two byte arrays, each with their own value encoded. The first is
* a 4-byte representation of the message number this response corresponds
* to. The second array is the message itself. If the stream has been
* closed, null is returned.
* @throws IOException Thrown when an exception occurs reading a message.
*/
default byte[][] readMessage(InputStream in) throws IOException
{
int readLength;
ByteBuffer lengthBuffer = ByteBuffer.allocate(4);
byte[] messageNum = new byte[4];
byte[] message;
/*
* Reads the message number.
*/
readLength = in.read(messageNum);
if (readLength == 0)
{
logger.debug("Input stream closed, no more messages.");
return null;
}
/*
* Reads the message length
*/
lengthBuffer.clear();
readLength = in.read(lengthBuffer.array());
if (readLength == 0)
{
logger.error("Malformed message, could not get message length.");
return null;
}
message = new byte[lengthBuffer.getInt()];
/*
* Writes the message
*/
readLength = in.read(message);
if (readLength < message.length)
{
logger.warn("Message shorter than reported (expected {} bytes, got {} bytes)", message.length, readLength);
}
logger.debug("Reading message");
return new byte[][] {messageNum, message};
}
}