package rescuecore2.connection; import static rescuecore2.misc.EncodingTools.writeInt32; import static rescuecore2.misc.EncodingTools.readMessage; import static rescuecore2.misc.EncodingTools.writeMessage; import rescuecore2.messages.Message; import rescuecore2.misc.WorkerThread; import rescuecore2.registry.Registry; import rescuecore2.log.Logger; import java.util.List; import java.util.ArrayList; import java.util.LinkedList; import java.util.Collection; import java.util.Collections; import java.io.ByteArrayOutputStream; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.IOException; /** Abstract base class for Connection implementations. */ public abstract class AbstractConnection implements Connection { private static final int BROADCAST_WAIT = 10000; private List<ConnectionListener> listeners; private List<Message> toSend; private MessageBroadcastThread broadcast; private Registry registry; private boolean logBytes; private String name; private volatile State state; private final Object stateLock = new Object(); /** Construct an abstract connection. */ protected AbstractConnection() { listeners = new ArrayList<ConnectionListener>(); toSend = new LinkedList<Message>(); logBytes = false; state = State.NOT_STARTED; registry = Registry.SYSTEM_REGISTRY; name = toString(); } @Override public void setLogBytes(boolean enabled) { logBytes = enabled; } @Override public final void startup() { synchronized (stateLock) { if (state == State.NOT_STARTED) { Registry old = Registry.getCurrentRegistry(); Registry.setCurrentRegistry(registry); try { broadcast = new MessageBroadcastThread(); broadcast.start(); startupImpl(); state = State.STARTED; } finally { Registry.setCurrentRegistry(old); } } } } @Override public final void shutdown() { synchronized (stateLock) { if (state == State.STARTED) { try { broadcast.kill(); } catch (InterruptedException e) { Logger.error("AbstractConnection interrupted while shutting down broadcast thread", e); } shutdownImpl(); state = State.SHUTDOWN; } } } @Override public boolean isAlive() { boolean result; synchronized (stateLock) { result = state == State.STARTED; } return result; } @Override public void addConnectionListener(ConnectionListener l) { synchronized (listeners) { listeners.add(l); } } @Override public void removeConnectionListener(ConnectionListener l) { synchronized (listeners) { listeners.remove(l); } } @Override public void sendMessage(Message msg) throws ConnectionException { if (msg == null) { throw new IllegalArgumentException("Message cannot be null"); } sendMessages(Collections.singleton(msg)); } @Override public void sendMessages(Collection<? extends Message> messages) throws ConnectionException { if (messages == null) { throw new IllegalArgumentException("Messages cannot be null"); } synchronized (stateLock) { if (state == State.NOT_STARTED) { throw new ConnectionException("Connection has not been started"); } if (state == State.SHUTDOWN) { throw new ConnectionException("Connection has been shut down"); } if (!isAlive()) { throw new ConnectionException("Connection is dead"); } } try { ByteArrayOutputStream out = new ByteArrayOutputStream(); for (Message next : messages) { writeMessage(next, out); } // Add a zero to indicate no more messages writeInt32(0, out); // Send the bytes if (logBytes) { ByteLogger.log(out.toByteArray()); } sendBytes(out.toByteArray()); } catch (IOException e) { throw new ConnectionException(e); } } @Override public void setRegistry(Registry r) { this.registry = r; } @Override public Registry getRegistry() { return registry; } @Override public String getName() { return name; } @Override public void setName(String newName) { this.name = newName; } @Override public String toString() { if (name == null) { return super.toString(); } return name; } /** Send some bytes to the other end of the connection. @param b The bytes to send. @throws IOException If the data cannot be sent. */ protected abstract void sendBytes(byte[] b) throws IOException; /** Perform startup actions. This will only ever be called once. */ protected abstract void startupImpl(); /** Perform shutdown actions. This will only ever be called once. */ protected abstract void shutdownImpl(); /** Process some bytes that were received. The default implementation will use the Registry to decode all messages in the buffer and send them to listeners. @param b The received bytes. */ protected void bytesReceived(byte[] b) { InputStream decode = new ByteArrayInputStream(b); Message m = null; try { do { m = readMessage(decode); if (m != null) { fireMessageReceived(m); } } while (m != null); } catch (IOException e) { Logger.error("AbstractConnection error reading message", e); ByteLogger.log(b, this.toString()); } // CHECKSTYLE:OFF:IllegalCatch catch (RuntimeException e) { Logger.error("AbstractConnection error reading message", e); ByteLogger.log(b, this.toString()); throw e; } catch (Error e) { Logger.error("AbstractConnection error reading message", e); ByteLogger.log(b, this.toString()); throw e; } // CHECKSTYLE:ON:IllegalCatch } /** Fire a messageReceived event to all registered listeners. @param m The message that was received. */ protected void fireMessageReceived(Message m) { synchronized (toSend) { toSend.add(m); toSend.notifyAll(); } } /** The state of this connection: either not yet started, started or shut down. */ protected enum State { /** CHECKSTYLE:OFF:JavadocVariableCheck. */ NOT_STARTED, STARTED, SHUTDOWN; /** CHECKSTYLE:ON:JavadocVariableCheck. */ } /** Worker thread that broadcasts messages to listeners. */ private class MessageBroadcastThread extends WorkerThread { @Override protected boolean work() throws InterruptedException { Message m = null; synchronized (toSend) { if (toSend.isEmpty()) { toSend.wait(BROADCAST_WAIT); return true; } else { m = toSend.remove(0); } } if (m == null) { return true; } ConnectionListener[] l; synchronized (listeners) { l = new ConnectionListener[listeners.size()]; listeners.toArray(l); } for (ConnectionListener next : l) { next.messageReceived(AbstractConnection.this, m); } return true; } } }