/**
* 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 javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
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.common.debug.FreeColDebugger;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
/**
* 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 XMLOutputFactory xof = XMLOutputFactory.newInstance();
private InputStream in;
private Socket socket;
private OutputStream out;
private Transformer xmlTransformer;
private ReceivingThread thread;
private MessageHandler messageHandler;
private String name;
protected static boolean dump
= FreeColDebugger.getDebugLevel() >= FreeColDebugger.DEBUG_FULL_COMMS;
/**
* Trivial constructor.
*
* @param name The name of the connection.
*/
protected Connection(String name) {
this.in = null;
this.socket = null;
this.out = null;
this.xmlTransformer = null;
this.thread = null;
this.messageHandler = 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(name);
this.in = socket.getInputStream();
this.socket = socket;
this.out = socket.getOutputStream();
Transformer myTransformer = null;
try {
TransformerFactory factory = TransformerFactory.newInstance();
myTransformer = factory.newTransformer();
myTransformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
"yes");
} catch (TransformerException e) {
logger.log(Level.WARNING, "Failed to install transformer!", e);
}
this.xmlTransformer = myTransformer;
this.thread = new ReceivingThread(this, in, name);
this.messageHandler = messageHandler;
this.name = name;
thread.start();
}
/**
* Creates a socket to communication with a given host, port pair.
*
* @param host The host to connect to.
* @param port The port to connect to.
* @return A new socket.
*/
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;
}
/**
* Sets the MessageHandler for this Connection.
*
* @param mh The new MessageHandler for this Connection.
*/
public void setMessageHandler(MessageHandler mh) {
messageHandler = mh;
}
/**
* Gets the MessageHandler for this Connection.
*
* @return The MessageHandler for this Connection.
*/
public MessageHandler getMessageHandler() {
return messageHandler;
}
/**
* 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() {
try {
sendDumping(DOMMessage.createMessage("disconnect"));
reallyClose();
} catch (IOException e) {
logger.log(Level.WARNING, "Error closing " + this.toString(), e);
}
}
/**
* Really 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.fine("Connection really closed.");
}
/**
* Fundamental routine to send a message over this Connection.
*
* @param element The <code>Element</code> (root element in a
* DOM-parsed XML tree) that holds all the information
* @param logOK Log the send if true.
* @throws IOException If an error occur while sending the message.
*/
private void send(Element element, boolean logOK) throws IOException {
synchronized (out) {
try {
xmlTransformer.transform(new DOMSource(element),
new StreamResult(out));
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to transform and send!", e);
}
out.write('\n');
out.flush();
out.notifyAll(); // Just in case others are waiting
}
if (logOK) logger.fine("Send: " + element.getTagName());
}
/**
* 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 {
send(element, logger.isLoggable(Level.FINE));
}
/**
* Dumping wrapper for send().
*
* @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 sendDumping(Element element) throws IOException {
if (dump) {
String x = getName() + "-send";
try {
System.err.println("<" + x + ">"
+ DOMMessage.elementToString(element)
+ "</" + x + ">\n");
} catch (Exception e) {}
}
send(element);
}
/**
* 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 {
askDumping(element);
}
/**
* 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 {
int networkReplyId = thread.getNextNetworkReplyId();
String tag = element.getTagName();
if (Thread.currentThread() == thread) {
throw new IOException("wait(ReceivingThread) for: " + tag);
}
Element question = element.getOwnerDocument()
.createElement("question");
question.setAttribute("networkReplyId",
Integer.toString(networkReplyId));
question.appendChild(element);
NetworkReplyObject nro = thread.waitForNetworkReply(networkReplyId);
send(question, false);
DOMMessage response = (DOMMessage)nro.getResponse();
Element reply = (response == null) ? null
: response.getDocument().getDocumentElement();
if (logger.isLoggable(Level.FINE)) {
logger.fine("Ask(" + networkReplyId + "): " + tag + ", reply: "
+ ((reply == null) ? "null" : reply.getTagName()));
}
return (reply == null) ? null : (Element)reply.getFirstChild();
}
/**
* Dumping wrapper for 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 {
if (dump) {
String x = getName() + "-request";
try {
System.err.println("<" + x + ">"
+ DOMMessage.elementToString(request)
+ "</" + x + ">\n");
} catch (Exception e) {}
}
Element reply;
if (dump) {
try {
reply = ask(request);
try {
String x = getName() + "-reply";
System.err.println("<" + x + ">"
+ DOMMessage.elementToString(reply)
+ "</" + x + ">\n");
} catch (Exception x) {}
} catch (IOException e) {
try {
System.err.println("<" + getName() + "-exception e=\""
+ e.getMessage() + "\" />\n");
} catch (Exception x) {}
throw e;
}
} else {
reply = ask(request);
}
return reply;
}
/**
* Handles a message using the registered <code>MessageHandler</code>.
*
* @param in The stream containing the message.
*/
public void handleAndSendReply(final BufferedInputStream in)
throws IOException {
// Peek at the reply id and tag.
in.mark(200);
final XMLInputFactory xif = XMLInputFactory.newInstance();
final String networkReplyId;
final boolean question;
try {
final XMLStreamReader xmlIn = xif.createXMLStreamReader(in);
xmlIn.nextTag();
networkReplyId = xmlIn.getAttributeValue(null, "networkReplyId");
question = xmlIn.getLocalName().equals("question");
xmlIn.close();
} catch (XMLStreamException xme) {
logger.log(Level.WARNING, "XML stream failure", xme);
return;
}
// Reset and build a message.
final DOMMessage msg;
in.reset();
try {
msg = new DOMMessage(in);
} catch (SAXException e) {
logger.log(Level.WARNING, "Unable to read message.", e);
return;
}
// Process the message in its own thread.
final Connection conn = this;
Thread t = new Thread(msg.getType()) {
@Override
public void run() {
Element reply, element = msg.getDocument()
.getDocumentElement();
try {
if (question) {
reply = messageHandler.handle(conn,
(Element)element.getFirstChild());
if (reply == null) {
reply = DOMMessage.createMessage("reply",
"networkReplyId", networkReplyId);
} else {
Element header = reply.getOwnerDocument()
.createElement("reply");
header.setAttribute("networkReplyId",
networkReplyId);
header.appendChild(reply);
reply = header;
}
} else {
reply = messageHandler.handle(conn, element);
}
if (reply != null) conn.sendDumping(reply);
} catch (Exception e) {
logger.log(Level.WARNING, "Handler failed: "
+ element.toString(), e);
}
}
};
t.setName(name + "-MessageHandler-" + t.getName());
t.start();
}
/**
* Override the default and return socket details.
*
* @return human-readable description of connection.
*/
@Override
public String toString() {
return "[Connection " + name + " (" + socket + ") ]";
}
}