/**
* 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamWriter;
import net.sf.freecol.FreeCol;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Class for parsing raw message data into an XML-tree and for creating new
* XML-trees.
*/
public class Message {
protected static final Logger logger = Logger.getLogger(Message.class.getName());
private static final String FREECOL_PROTOCOL_VERSION = "0.1.6";
private static final String INVALID_MESSAGE = "invalid";
/** The actual Message data. */
protected Document document;
protected Message() {
// empty constructor
}
/**
* Constructs a new Message with data from the given String. The constructor
* to use if this is an INCOMING message.
*
* @param msg The raw message data.
* @exception IOException should not be thrown.
* @exception SAXException if thrown during parsing.
*/
public Message(String msg) throws SAXException, IOException {
this(new InputSource(new StringReader(msg)));
}
/**
* Constructs a new Message with data from the given InputStream. The
* constructor to use if this is an INCOMING message.
*
* @param inputStream The <code>InputStream</code> to get the XML-data
* from.
* @exception IOException if thrown by the <code>InputStream</code>.
* @exception SAXException if thrown during parsing.
*/
public Message(InputStream inputStream) throws SAXException, IOException {
this(new InputSource(inputStream));
}
/**
* Constructs a new Message with data from the given InputSource. The
* constructor to use if this is an INCOMING message.
*
* @param inputSource The <code>InputSource</code> to get the XML-data
* from.
* @exception IOException if thrown by the <code>InputSource</code>.
* @exception SAXException if thrown during parsing.
*/
private Message(InputSource inputSource) throws SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document tempDocument = null;
boolean dumpMsgOnError = true; // FreeCol.isInDebugMode();
if (dumpMsgOnError) {
/*
* inputSource.setByteStream( new
* ReplayableInputStream(inputSource.getByteStream()) );
*
*/
inputSource.setByteStream(new BufferedInputStream(inputSource.getByteStream()));
inputSource.getByteStream().mark(1000000);
}
try {
DocumentBuilder builder = factory.newDocumentBuilder();
tempDocument = builder.parse(inputSource);
} catch (ParserConfigurationException pce) {
// Parser with specified options can't be built
logger.log(Level.WARNING, "Parser error", pce);
} catch (SAXException se) {
throw se;
} catch (IOException ie) {
throw ie;
} catch (ArrayIndexOutOfBoundsException e) {
// Xerces throws ArrayIndexOutOfBoundsException when it barfs on
// some FreeCol messages. I'd like to see the messages upon which
// it barfs
if (dumpMsgOnError) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
inputSource.getByteStream().reset();
while (true) {
int i = inputSource.getByteStream().read();
if (-1 == i) {
break;
}
baos.write(i);
}
logger.severe(baos.toString());
}
throw e;
}
document = tempDocument;
}
/**
* Constructs a new Message with data from the given XML-document.
*
* @param document The document representing an XML-message.
*/
public Message(Document document) {
this.document = document;
}
/**
* Gets the current version of the FreeCol protocol.
*
* @return The version of the FreeCol protocol.
*/
public static String getFreeColProtocolVersion() {
return FREECOL_PROTOCOL_VERSION;
}
/**
* Creates and returns a new XML-document.
*
* @return the new XML-document.
*/
public static Document createNewDocument() {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.newDocument();
} catch (ParserConfigurationException pce) {
// Parser with specified options can't be built
pce.printStackTrace();
return null;
}
}
/**
* Creates a new root element. This is done by creating a new document and
* using this document to create the root element.
*
* @param tagName The tag name of the root element beeing created,
* @return the new root element.
*/
public static Element createNewRootElement(String tagName) {
return createNewDocument().createElement(tagName);
}
/**
* Creates an error message.
*
* @param messageID Identifies the "i18n"-keyname. Not specified in the
* message if <i>null</i>.
* @param message The error in plain text. Not specified in the message if
* <i>null</i>.
* @return The root <code>Element</code> of the error message.
*/
public static Element createError(String messageID, String message) {
Element errorElement = createNewRootElement("error");
if (messageID != null && !messageID.equals("")) {
errorElement.setAttribute("messageID", messageID);
}
if (message != null && !message.equals("")) {
errorElement.setAttribute("message", message);
}
return errorElement;
}
/**
* Creates an error message.
*
* @param out The output stream for the message.
* @param messageID Identifies the "i18n"-keyname. Not specified in the
* message if <i>null</i>.
* @param message The error in plain text. Not specified in the message if
* <i>null</i>.
*/
public static void createError(XMLStreamWriter out, String messageID, String message) {
try {
out.writeStartElement("error");
if (messageID != null && !messageID.equals("")) {
out.writeAttribute("messageID", messageID);
}
if (message != null && !message.equals("")) {
out.writeAttribute("message", message);
}
out.writeEndElement();
} catch (XMLStreamException e) {
logger.warning("Could not send error message.");
}
}
/**
* Creates an error message in response to bad client data.
*
* @param message The error in plain text.
* @return The root <code>Element</code> of the error message.
*/
public static Element clientError(String message) {
if (FreeCol.isInDebugMode()) {
try {
throw new IllegalStateException(message);
} catch (Exception e) {
logger.log(Level.WARNING, "Client error", e);
}
}
Element errorElement = createNewRootElement("error");
errorElement.setAttribute("messageID", "server.reject");
errorElement.setAttribute("message", message);
return errorElement;
}
/**
* Gets the <code>Document</code> holding the message data.
*
* @return The <code>Document</code> holding the message data.
*/
public Document getDocument() {
return document;
}
/**
* Gets the type of this Message.
*
* @return The type of this Message.
*/
public String getType() {
if (document != null && document.getDocumentElement() != null) {
return document.getDocumentElement().getTagName();
}
return INVALID_MESSAGE;
}
/**
* Checks if this message is of a given type.
*
* @param type The type you wish to test against.
* @return <code>true</code> if the type of this message equals the given
* type and <code>false</code> otherwise.
*/
public boolean isType(String type) {
return getType().equals(type);
}
/**
* Sets an attribute on the root element.
*
* @param key The key of the attribute.
* @param value The value of the attribute.
*/
public void setAttribute(String key, String value) {
document.getDocumentElement().setAttribute(key, value);
}
/**
* Sets an attribute on the root element.
*
* @param key The key of the attribute.
* @param value The value of the attribute.
*/
public void setAttribute(String key, int value) {
document.getDocumentElement().setAttribute(key, (new Integer(value)).toString());
}
/**
* Gets an attribute from the root element.
*
* @param key The key of the attribute.
* @return The value of the attribute with the given key.
*/
public String getAttribute(String key) {
return document.getDocumentElement().getAttribute(key);
}
/**
* Checks if an attribute is set on the root element.
*
* @param attribute The attribute in which to verify the existence of.
* @return <code>true</code> if the root element has the given attribute.
*/
public boolean hasAttribute(String attribute) {
return document.getDocumentElement().hasAttribute(attribute);
}
/**
* Inserts <code>newRoot</code> as the new root element and appends the
* old root element.
*
* @param newRoot The new root element.
*/
public void insertAsRoot(Element newRoot) {
Element oldRoot = document.getDocumentElement();
if (oldRoot != null) {
document.removeChild(oldRoot);
newRoot.appendChild(oldRoot);
}
document.appendChild(newRoot);
}
/**
* Convenience method: returns the first child element with the specified
* tagname.
*
* @param element The <code>Element</code> to search for the child
* element.
* @param tagName The tag name of the child element to be found.
* @return The first child element with the given name.
*/
public static Element getChildElement(Element element, String tagName) {
NodeList n = element.getChildNodes();
for (int i = 0; i < n.getLength(); i++) {
if (n.item(i) instanceof Element && ((Element) n.item(i)).getTagName().equals(tagName)) {
return (Element) n.item(i);
}
}
return null;
}
public Element toXMLElement() {
// do nothing
return null;
}
/**
* Returns the <code>String</code> representation of the message. This is
* what actually gets transmitted to the other peer.
*
* @return The <code>String</code> representation of the message.
*/
@Override
public String toString() {
return document.getDocumentElement().toString();
}
}