/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * FreeCol is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.common.networking; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import net.sf.freecol.common.FreeColException; import org.xml.sax.SAXException; /** * The thread that checks for incoming messages. */ final class ReceivingThread extends Thread { private static final Logger logger = Logger.getLogger(ReceivingThread.class.getName()); /** * Input stream for buffering the data from the network. * * This is just a buffered input stream that signals end-of-stream * when a given token {@link #END_OF_STREAM} is encountered. In * order to continue receiving data, the method {@link #enable} * has to be called. Calls to <code>close()</code> have no effect, * the underlying input stream has to be closed directly. */ private class FreeColNetworkInputStream extends InputStream { private static final int BUFFER_SIZE = 8192; private static final char END_OF_STREAM = '\n'; private final InputStream in; private byte[] buffer = new byte[BUFFER_SIZE]; private int bStart = 0; private int bEnd = 0; private boolean empty = true; private boolean wait = false; /** * Creates a new <code>FreeColNetworkInputStream</code>. * * @param in The input stream in which this object should get the data * from. */ FreeColNetworkInputStream(InputStream in) { this.in = in; } /** * Fills the buffer with data. * * @return <i>true</i> if the buffer has been filled with data, and * <i>false</i> if an error occured. * @exception IOException if thrown by the underlying stream. */ private boolean fill() throws IOException { int r; if (bStart < bEnd || empty && bStart == bEnd) { if (empty) { bStart = 0; bEnd = 0; } r = in.read(buffer, bEnd, BUFFER_SIZE - bEnd); } else if (bStart == bEnd) { throw new IllegalStateException(); } else { r = in.read(buffer, bEnd, bStart - bEnd); } if (r <= 0) { logger.fine("Could not read data from stream."); return false; } empty = false; bEnd += r; if (bEnd == BUFFER_SIZE) { bEnd = 0; } return true; } /** * Prepares the input stream for a new message. * Makes the subsequent calls to <code>read</code> return the data * instead of <code>-1</code>. */ public void enable() { wait = false; } /** * Reads a single byte. * * @see #read(byte[], int, int) */ public int read() throws IOException { if (wait) return -1; if (empty) { if (!fill()) { wait = true; return -1; } } if (buffer[bStart] == END_OF_STREAM) { bStart++; if (bStart == BUFFER_SIZE) { bStart = 0; } if (bStart == bEnd) { empty = true; } wait = true; return -1; } else { bStart++; if (bStart == bEnd || bEnd == 0 && bStart == BUFFER_SIZE) { empty = true; } if (bStart == BUFFER_SIZE) { bStart = 0; return buffer[BUFFER_SIZE - 1]; } else { return buffer[bStart - 1]; } } } /** * Reads from the buffer and returns the data. * * @param b The place where the data will be put. * @param off The offset to use when writing the data to * <code>b</code>. * @param len Number of bytes to read. * @return The actual number of bytes read and <code>-1</code> if the * message has ended, that is; if the token * {@link #END_OF_STREAM} is encountered. */ public int read(byte[] b, int off, int len) throws IOException { if (wait) return -1; if (empty) { if (!fill()) { wait = true; return -1; } } int r = 0; for (; r < len; r++) { if (buffer[bStart] == END_OF_STREAM) { bStart++; if (bStart == BUFFER_SIZE) { bStart = 0; } if (bStart == bEnd) { empty = true; } wait = true; return r; } b[r + off] = buffer[bStart]; bStart++; if (bStart == bEnd || bEnd == 0 && bStart == BUFFER_SIZE) { empty = true; if (!fill()) { wait = true; return r + 1; } } if (bStart == BUFFER_SIZE) bStart = 0; } return len; } } /** Maximum number of retries before closing the connection. */ private static final int MAXIMUM_RETRIES = 5; /** A map of network ids to the corresponding waiting thread. */ private final Map<Integer, NetworkReplyObject> waitingThreads = Collections.synchronizedMap(new HashMap<Integer, NetworkReplyObject>()); /** The wrapped version of the input stream. */ private final FreeColNetworkInputStream in; /** The connection to receive on. */ private final Connection connection; /** Whether the thread should run. */ private boolean shouldRun; /** A counter for reply ids. */ private int nextNetworkReplyId; /** * The constructor to use. * * @param connection The <code>Connection</code> this * <code>ReceivingThread</code> belongs to. * @param in The stream to read from. */ ReceivingThread(Connection connection, InputStream in, String threadName) { super(threadName + "ReceivingThread - " + connection.toString()); this.in = new FreeColNetworkInputStream(in); this.connection = connection; this.shouldRun = true; this.nextNetworkReplyId = 1; } /** * Gets the next network reply identifier that will be used when * identifing a network message. * * @return The next available network reply identifier. */ public synchronized int getNextNetworkReplyId() { return nextNetworkReplyId++; } /** * Creates and registers a new <code>NetworkReplyObject</code> with the * specified ID. * * @param networkReplyId The id of the message the calling thread should * wait for. * @return The <code>NetworkReplyObject</code> containing the network * message. */ public NetworkReplyObject waitForNetworkReply(int networkReplyId) { NetworkReplyObject nro = new NetworkReplyObject(networkReplyId); waitingThreads.put(networkReplyId, nro); return nro; } /** * Checks if this thread should run. */ private synchronized boolean shouldRun() { return shouldRun; } /** * Tells this thread that it does not need to do any more work. */ public synchronized void askToStop() { shouldRun = false; for (NetworkReplyObject o : waitingThreads.values()) { o.interrupt(); } } /** * Disconnects this thread. */ private void disconnect(String reason) { if (connection.getMessageHandler() != null) { try { connection.getMessageHandler().handle(connection, DOMMessage.createMessage("disconnect", "reason", reason)); } catch (FreeColException e) { logger.log(Level.WARNING, "Rx disconnect", e); } } askToStop(); } /** * Listens to the InputStream and calls the MessageHandler for * each message received. * * @throws IOException If thrown by the {@link FreeColNetworkInputStream}. * @throws SAXException if a problem occured during parsing. * @throws XMLStreamException if a problem occured during parsing. */ private void listen() throws IOException, SAXException, XMLStreamException { final int LOOK_AHEAD = 500; BufferedInputStream bis = new BufferedInputStream(in, LOOK_AHEAD); in.enable(); bis.mark(LOOK_AHEAD); if (!shouldRun()) return; XMLInputFactory xif = XMLInputFactory.newInstance(); XMLStreamReader xmlIn = xif.createXMLStreamReader(bis); xmlIn.nextTag(); String tag = xmlIn.getLocalName(); if ("disconnect".equals(tag)) { askToStop(); } else if ("reply".equals(tag)) { String id = xmlIn.getAttributeValue(null, "networkReplyId"); NetworkReplyObject nro = waitingThreads.remove(Integer.valueOf(id)); if (nro == null) { // while (xmlIn.hasNext()) xmlIn.next(); logger.warning("Could not find networkReplyId: " + id); } else { bis.reset(); nro.setResponse(new DOMMessage(bis)); } } else { bis.reset(); try { connection.handleAndSendReply(bis); } catch (IOException ioe) { logger.log(Level.WARNING, "IO error", ioe); } } xmlIn.close(); } /** * Receives messages from the network in a loop. This method is * invoked when the thread starts and the thread will stop when * this method returns. */ public void run() { int timesFailed = 0; try { while (shouldRun()) { try { listen(); timesFailed = 0; } catch (XMLStreamException e) { timesFailed++; if (shouldRun && timesFailed > MAXIMUM_RETRIES) { disconnect("XML failure: " + e.getMessage()); } } catch (SAXException e) { timesFailed++; if (shouldRun && timesFailed > MAXIMUM_RETRIES) { disconnect("SAX failure: " + e.getMessage()); } } catch (IOException e) { if (shouldRun) { disconnect("IO failure: " + e.getMessage()); } } } } finally { askToStop(); } } }