// XmlWriter.java
package net.sf.gogui.xml;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.EnumSet;
import java.util.Map;
import net.sf.gogui.game.ConstGameInfo;
import net.sf.gogui.game.ConstGameTree;
import net.sf.gogui.game.ConstNode;
import net.sf.gogui.game.ConstSgfProperties;
import net.sf.gogui.game.NodeUtil;
import net.sf.gogui.game.MarkType;
import net.sf.gogui.game.SgfProperties;
import net.sf.gogui.game.StringInfo;
import net.sf.gogui.game.StringInfoColor;
import net.sf.gogui.game.TimeSettings;
import net.sf.gogui.go.GoColor;
import static net.sf.gogui.go.GoColor.BLACK;
import static net.sf.gogui.go.GoColor.EMPTY;
import static net.sf.gogui.go.GoColor.WHITE;
import net.sf.gogui.go.ConstPointList;
import net.sf.gogui.go.GoPoint;
import net.sf.gogui.go.Move;
import net.sf.gogui.sgf.SgfUtil;
import net.sf.gogui.util.XmlUtil;
/** Write a game or board position in XML style to a stream.
This class uses Jago's XML format, see http://www.rene-grothmann.de/jago
It writes files that are valid XML documents according to the go.dtd
from the Jago webpage (10/2007), see also the appendix "XML Format"
of the GoGui documentation. */
public class XmlWriter
{
/** Construct writer and write tree. */
public XmlWriter(OutputStream out, ConstGameTree tree, String application)
{
try
{
m_out = new PrintStream(out, false, "UTF-8");
m_out.print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
}
catch (UnsupportedEncodingException e)
{
// Every Java implementation is required to support UTF-8
assert false;
m_out = new PrintStream(out, false);
m_out.print("<?xml version='1.0'?>\n");
}
ConstNode root = tree.getRootConst();
ConstGameInfo info = tree.getGameInfoConst(root);
// Game name is not supported in game.GameInformation, but XmlReader
// puts it into the SGF-Proprty "GN"
String gameNameAtt = "";
ConstSgfProperties sgfProperties = root.getSgfPropertiesConst();
if (sgfProperties != null && sgfProperties.hasKey("GN")
&& sgfProperties.getNumberValues("GN") > 0)
gameNameAtt = " name=\""
+ XmlUtil.escapeAttr(sgfProperties.getValue("GN", 0)) + "\"";
m_out.print("<Go>\n" +
"<GoGame" + gameNameAtt + ">\n");
m_boardSize = tree.getBoardSize();
printGameInfo(application, info);
m_out.print("<Nodes>\n");
printNode(root, true);
m_out.print("</Nodes>\n" +
"</GoGame>\n" +
"</Go>\n");
m_out.close();
}
private int m_boardSize;
private PrintStream m_out;
private String getSgfPoint(GoPoint p)
{
if (p == null)
return "";
int x = 'a' + p.getX();
int y = 'a' + (m_boardSize - p.getY() - 1);
return "" + (char)x + (char)y;
}
private void printElementWithParagraphs(String element, String value)
{
if (value == null)
return;
StringReader reader = new StringReader(value);
m_out.print("<" + element + ">\n");
boolean endsWithNewline = false;
while (true)
{
String line = readLine(reader);
if (line.equals(""))
break;
endsWithNewline = line.endsWith("\n");
if (endsWithNewline)
line = line.substring(0, line.length() - 1);
if (line.equals(""))
m_out.print("<P/>\n");
else
printElementLine("P", line);
}
if (endsWithNewline)
m_out.print("<P/>\n");
m_out.print("</" + element + ">\n");
}
private void printElementLine(String element, String text)
{
m_out.print("<" + element + ">" + XmlUtil.escapeText(text)
+ "</" + element + ">\n");
}
private void printGameInfo(String application, ConstGameInfo info)
{
m_out.print("<Information>\n");
if (application != null)
printElementLine("Application", application);
printElementLine("BoardSize", Integer.toString(m_boardSize));
printInfo("WhitePlayer", info.get(StringInfoColor.NAME, WHITE));
printInfo("BlackPlayer", info.get(StringInfoColor.NAME, BLACK));
printInfo("WhiteRank", info.get(StringInfoColor.RANK, WHITE));
printInfo("BlackRank", info.get(StringInfoColor.RANK, BLACK));
printInfo("Date", info.get(StringInfo.DATE));
printInfo("Rules", info.get(StringInfo.RULES));
if (info.getHandicap() > 0)
printInfo("Handicap", Integer.toString(info.getHandicap()));
if (info.getKomi() != null)
printInfo("Komi", info.getKomi().toString());
if (info.getTimeSettings() != null)
{
long time = info.getTimeSettings().getPreByoyomi() / 1000L;
printInfo("Time", Long.toString(time));
}
printInfo("Result", info.get(StringInfo.RESULT));
printInfo("WhiteTeam", info.get(StringInfoColor.TEAM, WHITE));
printInfo("BlackTeam", info.get(StringInfoColor.TEAM, BLACK));
printInfo("User", info.get(StringInfo.USER));
printInfo("Annotation", info.get(StringInfo.ANNOTATION));
printInfo("Source", info.get(StringInfo.SOURCE));
printInfo("Round", info.get(StringInfo.ROUND));
printElementWithParagraphs("Copyright", info.get(StringInfo.COPYRIGHT));
m_out.print("</Information>\n");
}
private void printInfo(String element, String value)
{
if (value == null)
return;
printElementLine(element, value);
}
private void printMarkup(ConstNode node)
{
printMarkup(node, MarkType.MARK, "");
printMarkup(node, MarkType.CIRCLE, " type=\"circle\"");
printMarkup(node, MarkType.SQUARE, " type=\"square\"");
printMarkup(node, MarkType.TRIANGLE, " type=\"triangle\"");
printMarkup(node, MarkType.TERRITORY_BLACK, " territory=\"black\"");
printMarkup(node, MarkType.TERRITORY_WHITE, " territory=\"white\"");
ConstPointList pointList = node.getMarkedConst(MarkType.SELECT);
if (pointList != null)
// There is no type select in the Mark element -> use SGF/SL
for (GoPoint p : pointList)
m_out.print("<SGF type=\"SL\"><Arg>" + getSgfPoint(p)
+ "</Arg></SGF>\n");
Map<GoPoint,String> labels = node.getLabelsUnmodifiable();
if (labels != null)
for (Map.Entry<GoPoint,String> e : labels.entrySet())
m_out.print("<Mark at=\"" + e.getKey() + "\" label=\""
+ XmlUtil.escapeAttr(e.getValue()) + "\"/>\n");
}
private void printMarkup(ConstNode node, MarkType type, String attributes)
{
ConstPointList pointList = node.getMarkedConst(type);
if (pointList == null)
return;
for (GoPoint p : pointList)
m_out.print("<Mark at=\"" + p + "\"" + attributes + "/>\n");
}
private void printMove(ConstNode node)
{
Move move = node.getMove();
if (move == null)
return;
GoPoint p = move.getPoint();
String at = (p == null ? "" : p.toString());
GoColor c = move.getColor();
int number = NodeUtil.getMoveNumber(node);
String timeLeftAtt = "";
double timeLeft = node.getTimeLeft(c);
if (! Double.isNaN(timeLeft))
timeLeftAtt = " timeleft=\"" + timeLeft + "\"";
if (c == BLACK)
m_out.print("<Black number=\"" + number + "\" at=\"" + at
+ "\"" + timeLeftAtt + "/>\n");
else if (c == WHITE)
m_out.print("<White number=\"" + number + "\" at=\"" + at
+ "\"" + timeLeftAtt + "/>\n");
int movesLeft = node.getMovesLeft(c);
// There is no movesleft attribute in Black/White -> use SGF/OW,OB
if (movesLeft >= 0)
{
if (c == BLACK)
m_out.print("<SGF type=\"OB\"><Arg>" + movesLeft
+ "</Arg></SGF>\n");
else if (c == WHITE)
m_out.print("<SGF type=\"OW\"><Arg>" + movesLeft
+ "</Arg></SGF>\n");
}
}
private void printNode(ConstNode node, boolean isRoot)
{
Move move = node.getMove();
String comment = node.getComment();
SgfProperties sgfProps = NodeUtil.cleanSgfProps(node);
// Game name is not supported in game.GameInformation, but XmlReader
// puts it into the SGF-Proprty "N"
String nameAtt = "";
if (sgfProps.hasKey("N") && sgfProps.getNumberValues("N") > 0)
{
nameAtt = " name=\""
+ XmlUtil.escapeAttr(sgfProps.getValue("N", 0)) + "\"";
sgfProps.remove("N");
}
// Preserve time left that cannot be written as timeleft attribute
// of move element as SGF element
if (! Double.isNaN(node.getTimeLeft(BLACK))
&& (move == null || move.getColor() != BLACK))
sgfProps.add("BL", Double.toString(node.getTimeLeft(BLACK)));
if (! Double.isNaN(node.getTimeLeft(WHITE))
&& (move == null || move.getColor() != WHITE))
sgfProps.add("WL", Double.toString(node.getTimeLeft(WHITE)));
ConstGameInfo info = node.getGameInfoConst();
// Write overtime information as SGF element (no XML element exists)
if (isRoot)
{
TimeSettings timeSettings = info.getTimeSettings();
if (timeSettings != null)
{
String overtime = SgfUtil.getOvertime(timeSettings);
if (overtime != null)
sgfProps.add("OT", overtime);
}
}
Map<GoPoint,String> labels = node.getLabelsUnmodifiable();
boolean hasMarkup = (labels != null && ! labels.isEmpty());
if (! hasMarkup)
for (MarkType type : EnumSet.allOf(MarkType.class))
{
ConstPointList pointList = node.getMarkedConst(type);
if (pointList != null && ! node.getMarkedConst(type).isEmpty())
{
hasMarkup = true;
break;
}
}
boolean hasSetup = node.hasSetup() || node.getPlayer() != null;
// Moves left are currently written as SGF element, which needs a Node
boolean hasMovesLeft =
(move != null && node.getMovesLeft(move.getColor()) != -1);
boolean hasNonRootGameInfo = (info != null && ! isRoot);
// Root is considered empty, even if it has game info, because
// this is written in Information element
boolean isEmptyButMoveOrComment
= (sgfProps.isEmpty()
&& ! hasSetup && ! hasMarkup && ! hasMovesLeft
&& ! hasNonRootGameInfo);
// Is a node element needed? (not if only move and comment)
boolean needsNode = (! isEmptyButMoveOrComment || ! nameAtt.equals("")
|| (move == null && comment != null));
boolean isEmpty =
(isEmptyButMoveOrComment && comment == null && move == null);
if (isEmpty)
m_out.print("<Node" + nameAtt + "/>\n");
else
{
if (needsNode)
m_out.print("<Node" + nameAtt + ">\n");
printMove(node);
printSetup(node);
printMarkup(node);
printElementWithParagraphs("Comment", comment);
if (hasNonRootGameInfo)
putGameInfoSgf(info, sgfProps);
printSgfProperties(sgfProps);
if (needsNode)
m_out.print("</Node>\n");
}
ConstNode father = node.getFatherConst();
if (father != null && father.getChildConst() == node)
{
int numberSiblings = father.getNumberChildren();
for (int i = 1; i < numberSiblings; ++i)
{
m_out.print("<Variation>\n");
printNode(father.getChildConst(i), false);
m_out.print("</Variation>\n");
}
}
ConstNode child = node.getChildConst();
if (child != null)
printNode(child, false);
}
private void printSgfProperties(ConstSgfProperties sgfProps)
{
for (String key : sgfProps.getKeys())
{
m_out.print("<SGF type=\"" + key + "\">");
int numberValues = sgfProps.getNumberValues(key);
for (int i = 0; i < numberValues; ++i)
m_out.print("<Arg>" +
XmlUtil.escapeText(sgfProps.getValue(key, i))
+ "</Arg>");
m_out.print("</SGF>\n");
}
}
private void printSetup(ConstNode node)
{
for (GoPoint p : node.getSetup(BLACK))
m_out.print("<AddBlack at=\"" + p + "\"/>\n");
for (GoPoint p : node.getSetup(WHITE))
m_out.print("<AddWhite at=\"" + p + "\"/>\n");
for (GoPoint p : node.getSetup(EMPTY))
m_out.print("<Delete at=\"" + p + "\"/>\n");
GoColor player = node.getPlayer();
// The BlackToPlay, WhiteToPlay elements in the DTD are not usable:
// they don't have a legal parent and it is not clear why they
// have a text content -> save player with a SGF property
if (BLACK.equals(player))
m_out.print("<SGF type=\"PL\"><Arg>B</Arg></SGF>\n");
else if (WHITE.equals(player))
m_out.print("<SGF type=\"PL\"><Arg>W</Arg></SGF>\n");
}
/** Put game information for non-root nodes into SGF properties.
Game information for non-root nodes Not supported directly in XML. */
private void putGameInfoSgf(ConstGameInfo info, SgfProperties sgfProps)
{
putGameInfoSgf(info, sgfProps, "PB", StringInfoColor.NAME, BLACK);
putGameInfoSgf(info, sgfProps, "PW", StringInfoColor.NAME, WHITE);
putGameInfoSgf(info, sgfProps, "BR", StringInfoColor.RANK, BLACK);
putGameInfoSgf(info, sgfProps, "WR", StringInfoColor.RANK, WHITE);
putGameInfoSgf(info, sgfProps, "BT", StringInfoColor.TEAM, BLACK);
putGameInfoSgf(info, sgfProps, "WT", StringInfoColor.TEAM, WHITE);
putGameInfoSgf(info, sgfProps, "DT", StringInfo.DATE);
putGameInfoSgf(info, sgfProps, "RE", StringInfo.RESULT);
putGameInfoSgf(info, sgfProps, "RO", StringInfo.ROUND);
putGameInfoSgf(info, sgfProps, "RU", StringInfo.RULES);
putGameInfoSgf(info, sgfProps, "US", StringInfo.USER);
putGameInfoSgf(info, sgfProps, "CP", StringInfo.COPYRIGHT);
putGameInfoSgf(info, sgfProps, "AN", StringInfo.ANNOTATION);
if (info.getHandicap() > 0)
putGameInfoSgf(sgfProps, "HA",
Integer.toString(info.getHandicap()));
if (info.getKomi() != null)
putGameInfoSgf(sgfProps, "KM", info.getKomi().toString());
if (info.getTimeSettings() != null)
putGameInfoSgf(sgfProps, "TM", info.getTimeSettings().toString());
}
private void putGameInfoSgf(SgfProperties sgfProps, String key,
String value)
{
sgfProps.add(key, value);
}
private void putGameInfoSgf(ConstGameInfo info, SgfProperties sgfProps,
String key, StringInfo type)
{
String value = info.get(type);
if (value == null)
return;
sgfProps.add(key, value);
}
private void putGameInfoSgf(ConstGameInfo info, SgfProperties sgfProps,
String key, StringInfoColor type, GoColor c)
{
String value = info.get(type, c);
if (value == null)
return;
sgfProps.add(key, value);
}
/** Reads a line without trimming the trailing newline. */
private static String readLine(StringReader reader)
{
StringBuilder result = new StringBuilder();
try
{
int c;
do
{
c = reader.read();
if (c == -1)
break;
result.append((char)c);
}
while ((char)c != '\n');
}
catch (IOException e)
{
assert false;
}
return result.toString();
}
}