/*
* MegaMek - Copyright (C) 2003 Ben Mazur (bmazur@sev.org)
*
* This program 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.
*
* This program 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.
*/
package megamek.common.xml;
import gd.xml.ParseException;
import gd.xml.tiny.ParsedXML;
import gd.xml.tiny.TinyParser;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Enumeration;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import megamek.common.Board;
import megamek.common.IBoard;
import megamek.common.IGame;
import megamek.common.net.Packet;
import com.Ostermiller.util.Base64;
/**
* Objects of this class can encode a <code>Packet</code> object as XML into
* an output writer and decode one from a parsed XML node. It is used when
* saving games into a version- neutral format.
*
* @author James Damour <suvarov454@users.sourceforge.net>
*/
public class PacketEncoder {
/**
* Helper function to encode the packet's data.
*
* @param packet - the <code>Packet</code> to be encoded. This value must
* not be <code>null</code>.
* @param out - the <code>Writer</code> that will receive the XML. This
* value must not be <code>null</code>.
* @throws <code>IllegalArgumentException</code> if the node is
* <code>null</code>.
* @throws <code>IOException</code> if there's any error on write.
*/
private static void encodeData(Packet packet, Writer out)
throws IOException {
// Packet data encoding is based on data type.
Object[] data = packet.getData();
for (int loop = 0; loop < data.length; loop++) {
if (null == data[loop]) {
out.write("<null />");
} else if (data[loop].getClass().equals(Integer.class)) {
out.write("<integer value=\"");
out.write(data[loop].toString());
out.write("\" />");
} else if (data[loop].getClass().equals(IBoard.class)) {
BoardEncoder.encode((Board) data[loop], out);
}
}
}
/**
* Helper function to decode the packet's data.
*
* @param node - the <code>ParsedXML</code> node for this object. This
* value must not be <code>null</code>.
* @param game - the <code>IGame</code> the decoded object belongs to.
* @return the <code>Object</code> corresponding to the node. This value
* may be <code>null</code>.
* @throws <code>IllegalArgumentException</code> if the node is
* <code>null</code>.
* @throws <code>IllegalStateException</code> if the node does not contain
* a valid <code>Packet</code>.
* @throws <code>NumberFormatException</code> if the value of a numeric
* data element is not in a valid format.
*/
private static Object decodeData(ParsedXML node, IGame game) {
Object retval = null;
// Decoding is base the node's name.
if (node.getName().equals("integer")) {
retval = new Integer(node.getAttribute("value"));
} else if (node.getName().equals("board")) {
retval = BoardEncoder.decode(node, game);
}
return retval;
}
/**
* Encode a <code>Packet</code> object to an output writer.
*
* @param packet - the <code>Packet</code> to be encoded. This value must
* not be <code>null</code>.
* @param out - the <code>Writer</code> that will receive the XML. This
* value must not be <code>null</code>.
* @throws <code>IllegalArgumentException</code> if the node is
* <code>null</code>.
* @throws <code>IOException</code> if there's any error on write.
*/
public static void encode(Packet packet, Writer out) throws IOException {
// First, validate our input.
if (null == packet) {
throw new IllegalArgumentException("The packet is null.");
}
if (null == out) {
throw new IllegalArgumentException("The writer is null.");
}
// Encode the packet object to the stream.
out.write("<packet type=\"");
out.write(Integer.toString(packet.getCommand()));
out.write("\" >");
// Is the packet zipped?
// boolean zipped = packet.isZipped();
boolean zipped = false;
// Do we have any data in this packet?
// N.B. this action will unzip the packet.
Object[] data = packet.getData();
if (null != data) {
// Should we compress the data?
if (zipped) {
// XML encode the data and GZIP it.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Writer zipOut = new BufferedWriter(new OutputStreamWriter(
new GZIPOutputStream(baos)));
PacketEncoder.encodeData(packet, zipOut);
zipOut.close();
// Base64 encode the commpressed data.
// Please note, I couldn't get anything other than a
// straight stream-to-stream encoding to work.
byte[] zipData = baos.toByteArray();
ByteArrayOutputStream base64 = new ByteArrayOutputStream(
(4 * zipData.length + 2) / 3);
Base64.encode(new ByteArrayInputStream(zipData), base64, false);
/***************************************************************
* begin debug code int loop; for ( loop = 0; loop <
* zipData.length; loop++ ) { System.out.print( (char)
* zipData[loop] ); } System.out.println( "" ); String zipStr =
* baos.toString(); for ( loop = 0; loop < zipStr.length();
* loop++ ) { System.out.print( zipStr.charAt(loop) ); }
* System.out.println( "" ); zipStr = base64.toString(); for (
* loop = 0; loop < zipStr.length(); loop++ ) {
* System.out.print( zipStr.charAt(loop) ); }
* System.out.println( "" ); end debug code *
**************************************************************/
// Save the compressed data as the packetData CDATA.
out.write("<packetData count=\"");
out.write(Integer.toString(data.length));
out.write("\" isGzipped=\"true\" >");
out.write(base64.toString());
out.write("</packetData>");
} // End packet-is-zipped
else {
// Don't compress the XML.
out.write("<packetData count=\"");
out.write(Integer.toString(data.length));
out.write("\" isGzipped=\"false\" >");
PacketEncoder.encodeData(packet, out);
out.write("</packetData>");
}
} // End have-data
// Finish off the packet.
out.write("</packet>");
}
/**
* Decode a <code>Packet</code> object from the passed node.
*
* @param node - the <code>ParsedXML</code> node for this object. This
* value must not be <code>null</code>.
* @param game - the <code>IGame</code> the decoded object belongs to.
* @return the <code>Packet</code> object based on the node.
* @throws <code>IllegalArgumentException</code> if the node is
* <code>null</code>.
* @throws <code>IllegalStateException</code> if the node does not contain
* a valid <code>Packet</code>.
* @throws <code>NumberFormatException</code> if the value of a numeric
* data element is not in a valid format.
*/
public static Packet decode(ParsedXML node, IGame game) {
Packet packet = null;
int command = 0;
Object[] data = null;
// Make sure we got a valid packet.
if (null == node) {
throw new IllegalArgumentException("The passed node is null.");
}
if (!node.getName().equals("packet")) {
throw new IllegalStateException(
"The passed node is not for a packet.");
}
// Figure out what type of packet this is.
String commandStr = node.getAttribute("type");
if (null == commandStr) {
throw new IllegalStateException(
"Could not determine the packet type.");
}
command = Integer.parseInt(commandStr);
// TODO : perform version checking.
// Walk the packet node's children. Try to find a "packetData" node.
Enumeration<?> children = node.elements();
while (children.hasMoreElements()) {
ParsedXML subNode = (ParsedXML) children.nextElement();
if (subNode.getName().equals("packetData")) {
// How many data elements are in the packet data?
final int count = Integer.parseInt(subNode
.getAttribute("count"));
data = new Object[count];
// Do we need to unzip the data elements?
Enumeration<?> dataElements = null;
if (subNode.getAttribute("isGzipped").equals("true")) {
// Try to find the zipped content.
String cdata = subNode.getContent();
if (null == cdata) {
Enumeration<?> cdataEnum = subNode.elements();
while (cdataEnum.hasMoreElements() && null == cdata) {
final ParsedXML cdataNode = (ParsedXML) cdataEnum
.nextElement();
if (cdataNode.getTypeName().equals("text")) {
cdata = cdataNode.getContent();
} else if (cdataNode.getTypeName().equals("cdata")) {
cdata = cdataNode.getContent();
}
}
} // End look-for-cdata-nodes
// Did we find the zipped content?
if (null == cdata) {
throw new IllegalStateException(
"Could not find CDATA for packetData.");
}
// Yup. Unencode the data from Base64.
byte[] unBase64 = Base64.decodeToBytes(cdata);
InputStream parseStream;
try {
// Unzip the data.
parseStream = new GZIPInputStream(
new ByteArrayInputStream(unBase64));
} catch (IOException ioErr) {
StringBuffer iobuf = new StringBuffer();
iobuf.append("Could not unzip data elements: ").append(
ioErr.getMessage());
throw new IllegalStateException(iobuf.toString());
}
try {
/*******************************************************
* BEGIN debug code try { parseStream.mark(64000); int
* inChar = 0; while ( -1 != (inChar =
* parseStream.read()) ) { System.out.print( (char)
* inChar ); } System.out.println( "" );
* parseStream.reset(); } catch ( IOException debugErr ) {
* debugErr.printStackTrace(); } END debug code *
******************************************************/
// Parse the XML.
ParsedXML dummyNode = TinyParser.parseXML(parseStream);
dataElements = dummyNode.elements();
} catch (ParseException parseErr) {
StringBuffer parsebuf = new StringBuffer();
parsebuf.append("Could not parse data elements: ")
.append(parseErr.getMessage());
throw new IllegalStateException(parsebuf.toString());
}
} else {
// Nope. Just return the data elements.
dataElements = subNode.elements();
}
// Walk the children, and decode them into the data array.
for (int loop = 0; dataElements.hasMoreElements(); loop++) {
data[loop] = PacketEncoder.decodeData(
(ParsedXML) dataElements.nextElement(), game);
}
} // End found-packetData-element
} // Check the next child of the packet node.
// Create and return the packet.
if (null != data) {
packet = new Packet(command, data);
} else {
packet = new Packet(command);
}
return packet;
}
}