/** * 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.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.util.logging.Level; import java.util.logging.Logger; import android.util.Log; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import net.sf.freecol.FreeCol; import org.freecolandroid.debug.DebugOutputStream; import org.freecolandroid.xml.stream.XMLInputFactory; import org.freecolandroid.xml.stream.XMLOutputFactory; import org.freecolandroid.xml.stream.XMLStreamReader; import org.freecolandroid.xml.stream.XMLStreamWriter; import org.w3c.dom.Element; /** * A network connection. Responsible for both sending and receiving network * messages. * * @see #send(Element) * @see #sendAndWait(Element) * @see #ask(Element) */ public class Connection { private static final Logger logger = Logger.getLogger(Connection.class.getName()); private static final int TIMEOUT = 5000; private final OutputStream out; private final InputStream in; private final Socket socket; private final Transformer xmlTransformer; private final ReceivingThread thread; private final XMLOutputFactory xof = XMLOutputFactory.newInstance(); private MessageHandler messageHandler; private XMLStreamWriter xmlOut = null; private int currentQuestionID = -1; private String name = null; /** * Trivial constructor for DummyConnection to use. */ protected Connection(String name) { out = null; in = null; socket = null; thread = null; xmlTransformer = null; this.name = name; } /** * Sets up a new socket with specified host and port and uses * {@link #Connection(Socket, MessageHandler, String)}. * * @param host The host to connect to. * @param port The port to connect to. * @param messageHandler The MessageHandler to call for each message * received. * @throws IOException */ public Connection(String host, int port, MessageHandler messageHandler, String name) throws IOException { this(createSocket(host, port), messageHandler, name); } /** * Creates a new <code>Connection</code> with the specified * <code>Socket</code> and {@link MessageHandler}. * * @param socket The socket to the client. * @param messageHandler The MessageHandler to call for each message * received. * @throws IOException */ public Connection(Socket socket, MessageHandler messageHandler, String name) throws IOException { this.messageHandler = messageHandler; this.socket = socket; this.name = name; out = socket.getOutputStream(); in = socket.getInputStream(); Transformer myTransformer; try { TransformerFactory factory = TransformerFactory.newInstance(); myTransformer = factory.newTransformer(); myTransformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); } catch (TransformerException e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); logger.log(Level.WARNING, "Failed to install transformer!", e); myTransformer = null; } xmlTransformer = myTransformer; thread = new ReceivingThread(this, in, name); thread.start(); } private static Socket createSocket(String host, int port) throws IOException { Socket socket = new Socket(); SocketAddress addr = new InetSocketAddress(host, port); socket.connect(addr, TIMEOUT); return socket; } /** * Gets the socket. * * @return The <code>Socket</code> used while communicating with the other * peer. */ public Socket getSocket() { return socket; } /** * Gets the connection name. * * @return The connection name. */ public String getName() { return name; } /** * Sends a "disconnect"-message and closes this connection. * * @throws IOException */ public void close() throws IOException { Element disconnectElement = DOMMessage.createNewRootElement("disconnect"); send(disconnectElement); reallyClose(); } /** * Closes this connection. * * @throws IOException */ public void reallyClose() throws IOException { if (thread != null) { thread.askToStop(); } if (out != null) { out.close(); } if (socket != null) { socket.close(); } if (in != null) { in.close(); } logger.info("Connection closed."); } /** * Sends the given message over this Connection. * * @param element The element (root element in a DOM-parsed XML tree) that * holds all the information * @throws IOException If an error occur while sending the message. * @see #sendAndWait(Element) * @see #ask(Element) */ public void send(Element element) throws IOException { System.out.println("Connection.send()"); Log.d("Connection", "Sending " + element.getTagName()); // Note - waits for question but does not install a new value. // Must hold out for entire call. synchronized (out) { while (currentQuestionID != -1) { try { if (logger.isLoggable(Level.FINE)) { logger.fine("Waiting to send element " + element.getTagName() + "..."); } out.wait(); } catch (InterruptedException e) { } } if (logger.isLoggable(Level.FINE)) { logger.fine("Sending element " + element.getTagName() + "..."); } try { xmlTransformer.transform(new DOMSource(element), new StreamResult(out)); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); logger.log(Level.WARNING, "Failed to transform and send element!", e); } out.write('\n'); out.flush(); // Just in case others are waiting out.notifyAll(); } } /** * Sends a message to the other peer and returns the reply. * * @param element The question for the other peer. * @return The reply from the other peer. * @throws IOException If an error occur while sending the message. * @see #send(Element) * @see #sendAndWait(Element) */ public Element ask(Element element) throws IOException { System.out.println("Connection.ask() - " + element.getTagName()); int networkReplyId = thread.getNextNetworkReplyId(); Element questionElement = element.getOwnerDocument().createElement("question"); questionElement.setAttribute("networkReplyId", Integer.toString(networkReplyId)); questionElement.appendChild(element); if (Thread.currentThread() == thread) { logger.warning("Attempt to 'wait()' the ReceivingThread for sending " + element.getTagName()); throw new IOException("Attempt to 'wait()' the ReceivingThread."); } else { NetworkReplyObject nro = thread.waitForNetworkReply(networkReplyId); send(questionElement); DOMMessage response = (DOMMessage) nro.getResponse(); if (response == null) return null; Element rootElement = response.getDocument().getDocumentElement(); return (Element) rootElement.getFirstChild(); } } /** * Starts a session for asking a question using streaming. There is also a * simpler method for sending data using {@link #ask(Element) XML Elements} * that can be used when streaming is not required (that is: when the * messages to be transmitted are small). * * <br> * <br> * * <b>Example:</b> * * <PRE> * * try { XMLStreamWriter out = ask(); // Write XML here XMLStreamReader in = * connection.getReply(); // Read XML here connection.endTransmission(in); } * catch (IOException e) { logger.warning("Could not send XML."); } * * </PRE> * * @return The <code>XMLStreamWriter</code> for sending the question. The * method {@link #getReply()} should be called when the message has * been written and the reply is required. * @throws IOException if thrown by the underlying network stream. * @see #getReply() * @see #endTransmission(XMLStreamReader) */ public XMLStreamWriter ask() throws IOException { System.out.println("Connection.ask()"); waitForAndSetNewQuestionId(); try { // xmlOut = xof.createXMLStreamWriter(out); xmlOut = xof.createXMLStreamWriter(new DebugOutputStream(out)); xmlOut.writeStartElement("question"); xmlOut.writeAttribute("networkReplyId", Integer.toString(currentQuestionID)); return xmlOut; } catch (Exception e) { logger.log(Level.WARNING, "Failed to ask question (" + currentQuestionID + ")", e); releaseQuestionId(); throw new IOException(e.toString()); } } /** * Release a previously obtained question id. This is absolutely necessary * as other questions will be blocked as long as the old id is in place. * * @see #waitForAndSetNewQuestionId() */ private void releaseQuestionId() { System.out.println("Connection.releaseQuestionId()"); synchronized (out) { if (logger.isLoggable(Level.FINE)) { logger.fine(toString() + " released question id " + currentQuestionID); } currentQuestionID = -1; out.notifyAll(); } } /** * Wait until the previous question has been released, then install a new * question id. The caller is then free to send. */ private void waitForAndSetNewQuestionId() { System.out.println("Connection.waitForAndSetNewQuestionId()"); synchronized (out) { while (currentQuestionID != -1) { try { if (logger.isLoggable(Level.FINE)) { logger.fine(toString() + " waiting for question id..."); } out.wait(); } catch (InterruptedException e) { logger.log(Level.WARNING, "Interrupted waiting for question id!", e); } } currentQuestionID = thread.getNextNetworkReplyId(); if (logger.isLoggable(Level.FINE)) { logger.fine(toString() + " installed new question id " + currentQuestionID); } } } /** * Starts a session for sending a message using streaming. There is also a * simpler method for sending data using {@link #send(Element) XML Elements} * that can be used when streaming is not required (that is: when the * messages to be transmitted are small). * * <br> * <br> * * <b>Example:</b> * * <PRE> * * try { XMLStreamWriter out = send(); // Write XML here * connection.endTransmission(in); } catch (IOException e) { * logger.warning("Could not send XML."); } * * </PRE> * * @return The <code>XMLStreamWriter</code> for sending the question. The * method {@link #endTransmission(XMLStreamReader)} should be called * when the message has been written. * @throws IOException if thrown by the underlying network stream. * @see #getReply() * @see #endTransmission(XMLStreamReader) */ public XMLStreamWriter send() throws IOException { System.out.println("Connection.send()"); waitForAndSetNewQuestionId(); try { xmlOut = xof.createXMLStreamWriter(out); return xmlOut; } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); logger.log(Level.WARNING, "Failed to send message", e); releaseQuestionId(); throw new IOException(e.toString()); } } /** * Gets the reply being received after sending a question. * * @return An <code>XMLStreamReader</code> for reading the incoming data. * @throws IOException if thrown by the underlying network stream. * @see #ask() */ public XMLStreamReader getReply() throws IOException { System.out.println("Connection.getReply()"); try { NetworkReplyObject nro = thread.waitForStreamedNetworkReply(currentQuestionID); xmlOut.writeEndElement(); xmlOut.writeCharacters("\n"); xmlOut.flush(); xmlOut.close(); xmlOut = null; XMLStreamReader in = (XMLStreamReader) nro.getResponse(); in.nextTag(); return in; } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); System.out.println("Failed to get reply"); logger.log(Level.WARNING, toString() + " failed to get reply (" + currentQuestionID + ")", e); throw new IOException(e.toString()); } } /** * Ends the transmission of a message or a ask/get-reply session. * * @throws IOException if thrown by the underlying network stream. * @see #ask() * @see #send() */ public void endTransmission(XMLStreamReader in) throws IOException { System.out.println("Connection.endTransmission()"); try { if (in != null) { while (in.hasNext()) { in.next(); } thread.unlock(); in.close(); } else { xmlOut.writeCharacters("\n"); xmlOut.flush(); xmlOut.close(); xmlOut = null; } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); logger.log(Level.WARNING, toString() + " failed to end transmission", e); throw new IOException(e.toString()); } finally { // Unless the question id is released, can we ever recover? releaseQuestionId(); } } /** * Sends the given message over this <code>Connection</code> and waits for * confirmation of receiveval before returning. * * @param element The element (root element in a DOM-parsed XML tree) that * holds all the information * @throws IOException If an error occur while sending the message. * @see #send(Element) * @see #ask(Element) */ public void sendAndWait(Element element) throws IOException { System.out.println("Connection.sendAndWait()"); ask(element); } /** * Sets the MessageHandler for this Connection. * * @param mh The new MessageHandler for this Connection. */ public void setMessageHandler(MessageHandler mh) { System.out.println("Connection.setMessageHandler()"); messageHandler = mh; } /** * Gets the MessageHandler for this Connection. * * @return The MessageHandler for this Connection. */ public MessageHandler getMessageHandler() { System.out.println("Connection.getMessageHandler()"); return messageHandler; } /** * Handles a message using the registered <code>MessageHandler</code>. * * @param in The stream containing the message. */ public void handleAndSendReply(final BufferedInputStream in) { System.out.println("Connection.handleAndSendReply()"); try { in.mark(200); final XMLInputFactory xif = XMLInputFactory.newInstance(); final XMLStreamReader xmlIn = xif.createXMLStreamReader(in); xmlIn.nextTag(); final String networkReplyId = xmlIn.getAttributeValue(null, "networkReplyId"); final boolean question = xmlIn.getLocalName().equals("question"); boolean messagedConsumed = false; if (messageHandler instanceof StreamedMessageHandler) { StreamedMessageHandler smh = (StreamedMessageHandler) messageHandler; if (question) { xmlIn.nextTag(); } if (smh.accepts(xmlIn.getLocalName())) { XMLStreamWriter xmlOut = null; if (question) { xmlOut = send(); xmlOut.writeStartElement("reply"); xmlOut.writeAttribute("networkReplyId", networkReplyId); } smh.handle(this, xmlIn, xmlOut); if (question) { xmlOut.writeEndElement(); endTransmission(null); } thread.unlock(); messagedConsumed = true; } } if (!messagedConsumed) { xmlIn.close(); in.reset(); final DOMMessage msg = new DOMMessage(in); final Connection connection = this; Thread t = new Thread(msg.getType()) { @Override public void run() { try { Element element = msg.getDocument().getDocumentElement(); if (question) { Element reply = messageHandler.handle(connection, (Element) element.getFirstChild()); if (reply == null) { reply = DOMMessage.createNewRootElement("reply"); reply.setAttribute("networkReplyId", networkReplyId); logger.finest("reply == null"); } else { Element replyHeader = reply.getOwnerDocument().createElement("reply"); replyHeader.setAttribute("networkReplyId", networkReplyId); replyHeader.appendChild(reply); reply = replyHeader; } connection.send(reply); } else { Element reply = messageHandler.handle(connection, element); if (reply != null) { connection.send(reply); } } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); logger.log(Level.WARNING, "Message handler failed!", e); logger.warning(msg.getDocument().getDocumentElement().toString()); } } }; t.setName(name + "MessageHandler:" + t.getName()); t.start(); } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); logger.log(Level.WARNING, "Failed to handle and send reply", e); } } /** * Handles a message using the registered <code>MessageHandler</code>. * * @param element The message as a DOM-parsed XML-tree. */ /* * public void handleAndSendReply(Element element) { try { if * (element.getTagName().equals("question")) { String networkReplyId = * element.getAttribute("networkReplyId"); * * Element reply = messageHandler.handle(this, (Element) * element.getFirstChild()); * * if (reply == null) { reply = Message.createNewRootElement("reply"); * reply.setAttribute("networkReplyId", networkReplyId); logger.info("reply == * null"); } else { Element replyHeader = * reply.getOwnerDocument().createElement("reply"); * replyHeader.setAttribute("networkReplyId", networkReplyId); * replyHeader.appendChild(reply); reply = replyHeader; } * * send(reply); } else { Element reply = messageHandler.handle(this, * element); * * if (reply != null) { send(reply); } } } catch (FreeColException e) { * StringWriter sw = new StringWriter(); e.printStackTrace(new * PrintWriter(sw)); logger.warning(sw.toString()); } catch (IOException e) { * StringWriter sw = new StringWriter(); e.printStackTrace(new * PrintWriter(sw)); logger.warning(sw.toString()); } } */ /** * Dumping version of ask(). * Dumps to System.err with a faked-XML prefix so the whole line can * be fed to an XML-pretty printer if required. * * @param request The <code>Element</code> to send. * @return The reply element. * @exception Throws IOException if ask() fails. */ public Element askDumping(Element request) throws IOException { System.out.println("Connection.askDumping()"); boolean dump = FreeCol.getDebugLevel() >= FreeCol.DEBUG_FULL_COMMS; if (dump) { try { System.err.println("<" + getName() + "-request>" + DOMMessage.elementToString(request) + "\n"); } catch (Exception e) {} } Element reply; if (dump) { try { reply = ask(request); try { System.err.println("<" + getName() + "-reply>" + ((reply == null) ? "" : DOMMessage.elementToString(reply)) + "\n"); } catch (Exception x) {} } catch (IOException e) { try { System.err.println("<" + getName() + "-reply><exception " + e.getMessage() + "\n"); } catch (Exception x) {} throw e; } } else { reply = ask(request); } return reply; } /** * Override the default and return socket details. * * @return human-readable description of connection. */ @Override public String toString() { return "Connection[" + getSocket() + "]"; } }