OSDN Git Service

XML処理の整理
[jindolf/JinArchiver.git] / src / main / java / jp / sourceforge / jindolf / archiver / XmlUtils.java
index b8b8d65..b4ada54 100644 (file)
-/*\r
- * XML utils\r
- *\r
- * License : The MIT License\r
- * Copyright(c) 2008 olyutorskii\r
- */\r
-\r
-package jp.sourceforge.jindolf.archiver;\r
-\r
-import java.io.BufferedOutputStream;\r
-import java.io.BufferedWriter;\r
-import java.io.File;\r
-import java.io.FileOutputStream;\r
-import java.io.IOException;\r
-import java.io.OutputStream;\r
-import java.io.OutputStreamWriter;\r
-import java.io.Writer;\r
-import java.text.MessageFormat;\r
-import java.util.Calendar;\r
-import java.util.GregorianCalendar;\r
-import java.util.List;\r
-import java.util.TimeZone;\r
-import jp.sourceforge.jindolf.corelib.LandDef;\r
-import jp.sourceforge.jindolf.parser.DecodeErrorInfo;\r
-import jp.sourceforge.jindolf.parser.DecodedContent;\r
-\r
-/**\r
- * XML用各種ユーティリティ。\r
- */\r
-public final class XmlUtils{\r
-\r
-    private static final String ORIG_DTD =\r
-            "http://jindolf.sourceforge.jp/xml/dtd/bbsArchive-110421.dtd";\r
-    private static final String ORIG_NS =\r
-            "http://jindolf.sourceforge.jp/xml/ns/501";\r
-    private static final String ORIG_SCHEME =\r
-            "http://jindolf.sourceforge.jp/xml/xsd/bbsArchive-110421.xsd";\r
-    private static final String SCHEMA_NS =\r
-            "http://www.w3.org/2001/XMLSchema-instance";\r
-\r
-    private static final String OUTPATH = "D:\\TEMP\\zxzx\\";\r
-\r
-    private static final char BS_CHAR = (char) 0x005c; // Backslash\r
-    private static final String INDENT_UNIT = "\u0020\u0020";\r
-\r
-    private static final TimeZone TZ_TOKYO =\r
-            TimeZone.getTimeZone("Asia/Tokyo");\r
-\r
-\r
-    /**\r
-     * 隠れコンストラクタ。\r
-     */\r
-    private XmlUtils(){\r
-        throw new Error();\r
-    }\r
-\r
-\r
-    /**\r
-     * DOCTYPE宣言を出力する。\r
-     * @param writer 出力先\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpDocType(Writer writer) throws IOException{\r
-        writer.append("<!DOCTYPE village SYSTEM ");\r
-        writer.append('"');\r
-        writer.append(ORIG_DTD);\r
-        writer.append('"');\r
-        writer.append(" >");\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * オリジナルNameSpace宣言を出力する。\r
-     * @param writer 出力先\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpNameSpaceDecl(Writer writer)\r
-            throws IOException{\r
-        attrOut(writer, "xmlns", ORIG_NS);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * スキーマNameSpace宣言を出力する。\r
-     * @param writer 出力先\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpSiNameSpaceDecl(Writer writer)\r
-            throws IOException{\r
-        attrOut(writer, "xmlns:xsi", SCHEMA_NS);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * スキーマ位置指定を出力する。\r
-     * @param writer 出力先\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpSchemeLocation(Writer writer)\r
-            throws IOException{\r
-        attrOut(writer,\r
-                "xsi:schemaLocation",\r
-                ORIG_NS + " " + ORIG_SCHEME);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * インデント用空白を出力する。\r
-     * ネスト単位は空白2文字\r
-     * @param writer 出力先\r
-     * @param level ネストレベル\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void indent(Writer writer, int level) throws IOException{\r
-        for(int ct = 1; ct <= level; ct++){\r
-            writer.append(INDENT_UNIT);\r
-        }\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * XML数値文字参照を出力する。\r
-     * @param writer 出力先\r
-     * @param chVal 出力文字\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void charRefOut(Writer writer, char chVal)\r
-            throws IOException{\r
-        if(chVal == '\u0020'){\r
-            writer.append("&#x20;");\r
-            return;\r
-        }\r
-\r
-        if(chVal == '\u0009'){\r
-            writer.append("&#x09;");\r
-            return;\r
-        }\r
-\r
-        int ival = 0xffff & ((int) chVal);\r
-        String hex = Integer.toHexString(ival);\r
-        if(hex.length() % 2 != 0) hex = "0" + hex;\r
-\r
-        writer.append("&#x");\r
-        writer.append(hex);\r
-        writer.append(";");\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 不正文字をXML出力する。\r
-     * @param writer 出力先\r
-     * @param chVal 不正文字\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpInvalidChar(Writer writer, char chVal)\r
-            throws IOException{\r
-        int hexVal;\r
-        hexVal = chVal & 0xff;\r
-        String hexBin = Integer.toHexString(hexVal);\r
-        if(hexBin.length() % 2 != 0) hexBin = "0" + hexBin;\r
-\r
-        char replaceChar = '\ufffd';\r
-        if('\u0000' <= chVal && chVal <= '\u001f'){\r
-            replaceChar = (char)( chVal + '\u2400' );\r
-        }\r
-\r
-        writer.append("<rawdata");\r
-\r
-        writer.append(' ');\r
-        attrOut(writer, "encoding", "Shift_JIS");\r
-\r
-        writer.append(' ');\r
-        attrOut(writer, "hexBin", hexBin);\r
-\r
-        writer.append(" >");\r
-        writer.append(replaceChar);\r
-        writer.append("</rawdata>");\r
-    }\r
-\r
-    /**\r
-     * 任意の文字がXML規格上のホワイトスペースに属するか判定する。\r
-     * @param chVal 文字\r
-     * @return ホワイトスペースならtrue\r
-     */\r
-    public static boolean isWhiteSpace(char chVal){\r
-        switch(chVal){\r
-        case '\u0020':\r
-        case '\t':\r
-        case '\n':\r
-        case '\r':\r
-            return true;\r
-        default:\r
-            break;\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     * 文字列を出力する。\r
-     * <ul>\r
-     * <li>先頭および末尾のホワイトスペースは強制的に文字参照化される。\r
-     * <li>連続したホワイトスペースの2文字目以降は文字参照化される。\r
-     * <li>スペースでないホワイトスペースは無条件に文字参照化される。\r
-     * <li>{@literal &, <, >, "}は無条件に文字参照化される。\r
-     * </ul>\r
-     * 参考:XML 1.0 規格 3.3.3節\r
-     * @param writer 出力先\r
-     * @param seq CDATA文字列\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void textOut(Writer writer, CharSequence seq)\r
-            throws IOException{\r
-        int len = seq.length();\r
-\r
-        boolean leadSpace = false;\r
-\r
-        for(int pos = 0; pos < len; pos++){\r
-            char chVal = seq.charAt(pos);\r
-\r
-            if(isWhiteSpace(chVal)){\r
-                if(pos == 0 || pos >= len - 1 || leadSpace){\r
-                    charRefOut(writer, chVal);\r
-                }else if(chVal != '\u0020'){\r
-                    charRefOut(writer, chVal);\r
-                }else{\r
-                    writer.append(chVal);\r
-                }\r
-                leadSpace = true;\r
-            }else{\r
-                if(chVal == '&'){\r
-                    writer.append("&amp;");\r
-                }else if(chVal == '<'){\r
-                    writer.append("&lt;");\r
-                }else if(chVal == '>'){\r
-                    writer.append("&gt;");\r
-                }else if(chVal == '"'){\r
-                    writer.append("&quot;");\r
-                }else if(chVal == '\''){\r
-                    writer.append("&apos;");\r
-                }else if(chVal == BS_CHAR){\r
-                    writer.append('\u00a5');\r
-                }else if(chVal == '\u007e'){\r
-                    writer.append('\u203e');\r
-                }else if(Character.isISOControl(chVal)){\r
-                    dumpInvalidChar(writer, chVal);\r
-                }else{\r
-                    writer.append(chVal);\r
-                }\r
-                leadSpace = false;\r
-            }\r
-        }\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 属性を出力する。\r
-     * @param writer 出力先\r
-     * @param name 属性名\r
-     * @param value 属性値\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void attrOut(Writer writer,\r
-                                CharSequence name,\r
-                                CharSequence value)\r
-            throws IOException{\r
-        StringBuilder newValue = new StringBuilder(value);\r
-        for(int pt = 0; pt < newValue.length(); pt++){\r
-            char chVal = newValue.charAt(pt);\r
-            if(chVal == '\n' || chVal == '\r' || chVal == '\t') continue;\r
-            if(Character.isISOControl(chVal)){\r
-                newValue.setCharAt(pt, (char)('\u2400' + chVal));\r
-            }\r
-        }\r
-\r
-        writer.append(name);\r
-        writer.append('=');\r
-        writer.append('"');\r
-        textOut(writer, newValue);\r
-        writer.append('"');\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * xsd:time形式の時刻属性を出力する。\r
-     * タイムゾーンは「+09:00」固定\r
-     * @param writer 出力先\r
-     * @param name 属性名\r
-     * @param hour 時間\r
-     * @param minute 分\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void timeAttrOut(Writer writer,\r
-                                     CharSequence name,\r
-                                     int hour, int minute)\r
-            throws IOException{\r
-        String cmtTime =\r
-                MessageFormat\r
-                .format("{0,number,#00}:{1,number,#00}:00+09:00",\r
-                        hour, minute);\r
-        attrOut(writer, name, cmtTime);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * xsd:gMonthDay形式の日付属性を出力する。\r
-     * タイムゾーンは「+09:00」固定\r
-     * @param writer 出力先\r
-     * @param name 属性名\r
-     * @param month 月\r
-     * @param day 日\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dateAttrOut(Writer writer,\r
-                                     CharSequence name,\r
-                                     int month, int day)\r
-            throws IOException{\r
-        String dateAttr =\r
-                MessageFormat.format("--{0,number,#00}-{1,number,#00}+09:00",\r
-                                     month, day);\r
-        attrOut(writer, name, dateAttr);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * xsd:dateTime形式の日付時刻属性を出力する。\r
-     * タイムゾーンは「+09:00」固定\r
-     * @param writer 出力先\r
-     * @param name 属性名\r
-     * @param epochMs エポック時刻\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dateTimeAttr(Writer writer,\r
-                                      CharSequence name,\r
-                                      long epochMs)\r
-            throws IOException{\r
-        Calendar calendar = new GregorianCalendar(TZ_TOKYO);\r
-\r
-        calendar.setTimeInMillis(epochMs);\r
-        int year = calendar.get(Calendar.YEAR);\r
-        int month = calendar.get(Calendar.MONTH) + 1;\r
-        int day = calendar.get(Calendar.DATE);\r
-        int hour = calendar.get(Calendar.HOUR_OF_DAY);\r
-        int minute = calendar.get(Calendar.MINUTE);\r
-        int sec = calendar.get(Calendar.SECOND);\r
-        int msec = calendar.get(Calendar.MILLISECOND);\r
-\r
-        String attrVal = MessageFormat.format(\r
-                 "{0,number,#0000}-{1,number,#00}-{2,number,#00}"\r
-                +"T{3,number,#00}:{4,number,#00}:{5,number,#00}"\r
-                +".{6,number,#000}+09:00",\r
-                year, month, day, hour, minute, sec, msec);\r
-\r
-        attrOut(writer, name, attrVal);\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * デコードエラー情報をrawdataタグで出力する。\r
-     * 文字列集合に関するエラーの場合、windows31jでのデコード出力を試みる。\r
-     * @param writer 出力先\r
-     * @param errorInfo デコードエラー\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpErrorInfo(Writer writer,\r
-                                       DecodeErrorInfo errorInfo)\r
-            throws IOException{\r
-        int hexVal;\r
-        hexVal = errorInfo.getRawByte1st() & 0xff;\r
-        if(errorInfo.has2nd()){\r
-            hexVal <<= 8;\r
-            hexVal |= errorInfo.getRawByte2nd() & 0xff;\r
-        }\r
-\r
-        String hexBin = Integer.toHexString(hexVal);\r
-        if(hexBin.length() % 2 != 0) hexBin = "0" + hexBin;\r
-\r
-        char replaceChar = Win31j.getWin31jChar(errorInfo);\r
-\r
-        writer.append("<rawdata");\r
-\r
-        writer.append(' ');\r
-        attrOut(writer, "encoding", "Shift_JIS");\r
-\r
-        writer.append(' ');\r
-        attrOut(writer, "hexBin", hexBin);\r
-\r
-        writer.append(" >");\r
-        writer.append(replaceChar);\r
-        writer.append("</rawdata>");\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * デコードエラー込みのテキストを出力する。\r
-     * @param writer 出力先\r
-     * @param content テキスト\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpDecodedContent(Writer writer,\r
-                                             DecodedContent content)\r
-            throws IOException{\r
-        if( ! content.hasDecodeError() ){\r
-            textOut(writer, content);\r
-            return;\r
-        }\r
-\r
-        int last = 0;\r
-\r
-        List<DecodeErrorInfo> errList = content.getDecodeErrorList();\r
-        for(DecodeErrorInfo err : errList){\r
-            int charPos = err.getCharPosition();\r
-            CharSequence line = content.subSequence(last, charPos);\r
-            textOut(writer, line);\r
-            dumpErrorInfo(writer, err);\r
-            last = charPos + 1;\r
-        }\r
-\r
-        CharSequence line = content.subSequence(last, content.length());\r
-        textOut(writer, line);\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 村情報をXML形式で出力する。\r
-     * @param writer 出力先\r
-     * @param villageData 村情報\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static void dumpVillageData(Writer writer,\r
-                                         VillageData villageData)\r
-            throws IOException{\r
-        writer.append("<?xml");\r
-        writer.append(' ');\r
-        attrOut(writer, "version", "1.0");\r
-        writer.append(' ');\r
-        attrOut(writer, "encoding", "UTF-8");\r
-        writer.append(" ?>\n\n");\r
-\r
-        writer.append("<!--\n");\r
-        writer.append("  人狼BBSアーカイブ\n");\r
-        writer.append("  http://jindolf.sourceforge.jp/\n");\r
-        writer.append("-->\n\n");\r
-\r
-        dumpDocType(writer);\r
-        writer.append("\n\n");\r
-\r
-        villageData.dumpXml(writer);\r
-\r
-        writer.append("\n<!-- EOF -->\n");\r
-\r
-        writer.flush();\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 村情報を反映した出力ファイル名を生成する。\r
-     * @param village 村情報\r
-     * @return XML出力ファイル名\r
-     */\r
-    public static String createOutFileName(VillageData village){\r
-        LandDef landDef = village.getLandDef();\r
-        String landId = landDef.getLandId();\r
-        int vid = village.getVillageId();\r
-\r
-        String fname =\r
-                MessageFormat.format(\r
-                "{0}jin_{1}_{2,number,#00000}.xml", OUTPATH, landId, vid);\r
-        return fname;\r
-    }\r
-\r
-    /**\r
-     * 村情報を反映した出力ファイルへの文字ストリームを生成する。\r
-     * @param village 村情報\r
-     * @return 出力先文字ストリーム\r
-     * @throws IOException 出力エラー\r
-     */\r
-    public static Writer createFileWriter(VillageData village)\r
-            throws IOException{\r
-        String fname = createOutFileName(village);\r
-        File file = new File(fname);\r
-\r
-        OutputStream ostream;\r
-        ostream = new FileOutputStream(file);\r
-        ostream = new BufferedOutputStream(ostream, 10000);\r
-        Writer writer;\r
-        writer = new OutputStreamWriter(ostream, "UTF-8");\r
-        writer = new BufferedWriter(writer, 10000);\r
-        return writer;\r
-    }\r
-\r
-}\r
+/*
+ * XML utils
+ *
+ * License : The MIT License
+ * Copyright(c) 2008 olyutorskii
+ */
+
+package jp.sourceforge.jindolf.archiver;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.text.MessageFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.TimeZone;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+import jp.sourceforge.jindolf.parser.DecodeErrorInfo;
+import jp.sourceforge.jindolf.parser.DecodedContent;
+import org.xml.sax.SAXException;
+
+/**
+ * XML用各種ユーティリティ。
+ */
+public final class XmlUtils{
+
+    private static final String ORIG_DTD =
+            "http://jindolf.sourceforge.jp/xml/dtd/bbsArchive-110421.dtd";
+    private static final String ORIG_NS =
+            "http://jindolf.sourceforge.jp/xml/ns/501";
+    private static final String ORIG_SCHEME =
+            "http://jindolf.sourceforge.jp/xml/xsd/bbsArchive-110421.xsd";
+    private static final String SCHEMA_NS =
+            "http://www.w3.org/2001/XMLSchema-instance";
+
+    private static final char BS_CHAR = (char) 0x005c; // Backslash
+    private static final String INDENT_UNIT = "\u0020\u0020";
+
+    private static final TimeZone TZ_TOKYO =
+            TimeZone.getTimeZone("Asia/Tokyo");
+
+
+    /**
+     * 隠れコンストラクタ。
+     */
+    private XmlUtils(){
+        throw new Error();
+    }
+
+
+    /**
+     * XML読み込み用DocumentBuilderを生成する。
+     * @return DocumentBuilder
+     * @throws ParserConfigurationException 実装が要求に応えられない。
+     */
+    public static DocumentBuilder createDocumentBuilder()
+            throws ParserConfigurationException {
+        DocumentBuilderFactory factory;
+        factory = DocumentBuilderFactory.newInstance();
+
+        DocumentBuilder builder;
+        builder = factory.newDocumentBuilder();
+
+        return builder;
+    }
+
+    /**
+     * バリデータを生成する。
+     * @return バリデータ
+     * @throws SAXException 実装が要求に応えられない。
+     */
+    public static Validator createValidator() throws SAXException{
+        SchemaFactory factory;
+        String nsuri = XMLConstants.W3C_XML_SCHEMA_NS_URI;
+        factory = SchemaFactory.newInstance(nsuri);
+
+        Schema schema;
+        schema = factory.newSchema();
+
+        Validator validator = schema.newValidator();
+
+        return validator;
+    }
+
+    /**
+     * DOCTYPE宣言を出力する。
+     * @param writer 出力先
+     * @throws IOException 出力エラー
+     */
+    public static void dumpDocType(Writer writer) throws IOException{
+        writer.append("<!DOCTYPE village SYSTEM ");
+        writer.append('"');
+        writer.append(ORIG_DTD);
+        writer.append('"');
+        writer.append(" >");
+        return;
+    }
+
+    /**
+     * オリジナルNameSpace宣言を出力する。
+     * @param writer 出力先
+     * @throws IOException 出力エラー
+     */
+    public static void dumpNameSpaceDecl(Writer writer)
+            throws IOException{
+        attrOut(writer, "xmlns", ORIG_NS);
+        return;
+    }
+
+    /**
+     * スキーマNameSpace宣言を出力する。
+     * @param writer 出力先
+     * @throws IOException 出力エラー
+     */
+    public static void dumpSiNameSpaceDecl(Writer writer)
+            throws IOException{
+        attrOut(writer, "xmlns:xsi", SCHEMA_NS);
+        return;
+    }
+
+    /**
+     * スキーマ位置指定を出力する。
+     * @param writer 出力先
+     * @throws IOException 出力エラー
+     */
+    public static void dumpSchemeLocation(Writer writer)
+            throws IOException{
+        attrOut(writer,
+                "xsi:schemaLocation",
+                ORIG_NS + " " + ORIG_SCHEME);
+        return;
+    }
+
+    /**
+     * インデント用空白を出力する。
+     * ネスト単位は空白2文字
+     * @param writer 出力先
+     * @param level ネストレベル
+     * @throws IOException 出力エラー
+     */
+    public static void indent(Writer writer, int level) throws IOException{
+        for(int ct = 1; ct <= level; ct++){
+            writer.append(INDENT_UNIT);
+        }
+        return;
+    }
+
+    /**
+     * XML数値文字参照を出力する。
+     * @param writer 出力先
+     * @param chVal 出力文字
+     * @throws IOException 出力エラー
+     */
+    public static void charRefOut(Writer writer, char chVal)
+            throws IOException{
+        if(chVal == '\u0020'){
+            writer.append("&#x20;");
+            return;
+        }
+
+        if(chVal == '\u0009'){
+            writer.append("&#x09;");
+            return;
+        }
+
+        int ival = 0xffff & ((int) chVal);
+        String hex = Integer.toHexString(ival);
+        if(hex.length() % 2 != 0) hex = "0" + hex;
+
+        writer.append("&#x");
+        writer.append(hex);
+        writer.append(";");
+
+        return;
+    }
+
+    /**
+     * 不正文字をXML出力する。
+     * @param writer 出力先
+     * @param chVal 不正文字
+     * @throws IOException 出力エラー
+     */
+    public static void dumpInvalidChar(Writer writer, char chVal)
+            throws IOException{
+        int hexVal;
+        hexVal = chVal & 0xff;
+        String hexBin = Integer.toHexString(hexVal);
+        if(hexBin.length() % 2 != 0) hexBin = "0" + hexBin;
+
+        char replaceChar = '\ufffd';
+        if('\u0000' <= chVal && chVal <= '\u001f'){
+            replaceChar = (char)( chVal + '\u2400' );
+        }
+
+        writer.append("<rawdata");
+
+        writer.append(' ');
+        attrOut(writer, "encoding", "Shift_JIS");
+
+        writer.append(' ');
+        attrOut(writer, "hexBin", hexBin);
+
+        writer.append(" >");
+        writer.append(replaceChar);
+        writer.append("</rawdata>");
+    }
+
+    /**
+     * 任意の文字がXML規格上のホワイトスペースに属するか判定する。
+     * @param chVal 文字
+     * @return ホワイトスペースならtrue
+     */
+    public static boolean isWhiteSpace(char chVal){
+        switch(chVal){
+        case '\u0020':
+        case '\t':
+        case '\n':
+        case '\r':
+            return true;
+        default:
+            break;
+        }
+
+        return false;
+    }
+
+    /**
+     * 文字列を出力する。
+     * <ul>
+     * <li>先頭および末尾のホワイトスペースは強制的に文字参照化される。
+     * <li>連続したホワイトスペースの2文字目以降は文字参照化される。
+     * <li>スペースでないホワイトスペースは無条件に文字参照化される。
+     * <li>{@literal &, <, >, "}は無条件に文字参照化される。
+     * </ul>
+     * 参考:XML 1.0 規格 3.3.3節
+     * @param writer 出力先
+     * @param seq CDATA文字列
+     * @throws IOException 出力エラー
+     */
+    public static void textOut(Writer writer, CharSequence seq)
+            throws IOException{
+        int len = seq.length();
+
+        boolean leadSpace = false;
+
+        for(int pos = 0; pos < len; pos++){
+            char chVal = seq.charAt(pos);
+
+            if(isWhiteSpace(chVal)){
+                if(pos == 0 || pos >= len - 1 || leadSpace){
+                    charRefOut(writer, chVal);
+                }else if(chVal != '\u0020'){
+                    charRefOut(writer, chVal);
+                }else{
+                    writer.append(chVal);
+                }
+                leadSpace = true;
+            }else{
+                if(chVal == '&'){
+                    writer.append("&amp;");
+                }else if(chVal == '<'){
+                    writer.append("&lt;");
+                }else if(chVal == '>'){
+                    writer.append("&gt;");
+                }else if(chVal == '"'){
+                    writer.append("&quot;");
+                }else if(chVal == '\''){
+                    writer.append("&apos;");
+                }else if(chVal == BS_CHAR){
+                    writer.append('\u00a5');
+                }else if(chVal == '\u007e'){
+                    writer.append('\u203e');
+                }else if(Character.isISOControl(chVal)){
+                    dumpInvalidChar(writer, chVal);
+                }else{
+                    writer.append(chVal);
+                }
+                leadSpace = false;
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * 属性を出力する。
+     * @param writer 出力先
+     * @param name 属性名
+     * @param value 属性値
+     * @throws IOException 出力エラー
+     */
+    public static void attrOut(Writer writer,
+                                CharSequence name,
+                                CharSequence value)
+            throws IOException{
+        StringBuilder newValue = new StringBuilder(value);
+        for(int pt = 0; pt < newValue.length(); pt++){
+            char chVal = newValue.charAt(pt);
+            if(chVal == '\n' || chVal == '\r' || chVal == '\t') continue;
+            if(Character.isISOControl(chVal)){
+                newValue.setCharAt(pt, (char)('\u2400' + chVal));
+            }
+        }
+
+        writer.append(name);
+        writer.append('=');
+        writer.append('"');
+        textOut(writer, newValue);
+        writer.append('"');
+        return;
+    }
+
+    /**
+     * xsd:time形式の時刻属性を出力する。
+     * タイムゾーンは「+09:00」固定
+     * @param writer 出力先
+     * @param name 属性名
+     * @param hour 時間
+     * @param minute 分
+     * @throws IOException 出力エラー
+     */
+    public static void timeAttrOut(Writer writer,
+                                     CharSequence name,
+                                     int hour, int minute)
+            throws IOException{
+        String cmtTime =
+                MessageFormat
+                .format("{0,number,#00}:{1,number,#00}:00+09:00",
+                        hour, minute);
+        attrOut(writer, name, cmtTime);
+        return;
+    }
+
+    /**
+     * xsd:gMonthDay形式の日付属性を出力する。
+     * タイムゾーンは「+09:00」固定
+     * @param writer 出力先
+     * @param name 属性名
+     * @param month 月
+     * @param day 日
+     * @throws IOException 出力エラー
+     */
+    public static void dateAttrOut(Writer writer,
+                                     CharSequence name,
+                                     int month, int day)
+            throws IOException{
+        String dateAttr =
+                MessageFormat.format("--{0,number,#00}-{1,number,#00}+09:00",
+                                     month, day);
+        attrOut(writer, name, dateAttr);
+        return;
+    }
+
+    /**
+     * xsd:dateTime形式の日付時刻属性を出力する。
+     * タイムゾーンは「+09:00」固定
+     * @param writer 出力先
+     * @param name 属性名
+     * @param epochMs エポック時刻
+     * @throws IOException 出力エラー
+     */
+    public static void dateTimeAttr(Writer writer,
+                                      CharSequence name,
+                                      long epochMs)
+            throws IOException{
+        Calendar calendar = new GregorianCalendar(TZ_TOKYO);
+
+        calendar.setTimeInMillis(epochMs);
+        int year = calendar.get(Calendar.YEAR);
+        int month = calendar.get(Calendar.MONTH) + 1;
+        int day = calendar.get(Calendar.DATE);
+        int hour = calendar.get(Calendar.HOUR_OF_DAY);
+        int minute = calendar.get(Calendar.MINUTE);
+        int sec = calendar.get(Calendar.SECOND);
+        int msec = calendar.get(Calendar.MILLISECOND);
+
+        String attrVal = MessageFormat.format(
+                 "{0,number,#0000}-{1,number,#00}-{2,number,#00}"
+                +"T{3,number,#00}:{4,number,#00}:{5,number,#00}"
+                +".{6,number,#000}+09:00",
+                year, month, day, hour, minute, sec, msec);
+
+        attrOut(writer, name, attrVal);
+
+        return;
+    }
+
+    /**
+     * デコードエラー情報をrawdataタグで出力する。
+     * 文字列集合に関するエラーの場合、windows31jでのデコード出力を試みる。
+     * @param writer 出力先
+     * @param errorInfo デコードエラー
+     * @throws IOException 出力エラー
+     */
+    public static void dumpErrorInfo(Writer writer,
+                                       DecodeErrorInfo errorInfo)
+            throws IOException{
+        int hexVal;
+        hexVal = errorInfo.getRawByte1st() & 0xff;
+        if(errorInfo.has2nd()){
+            hexVal <<= 8;
+            hexVal |= errorInfo.getRawByte2nd() & 0xff;
+        }
+
+        String hexBin = Integer.toHexString(hexVal);
+        if(hexBin.length() % 2 != 0) hexBin = "0" + hexBin;
+
+        char replaceChar = Win31j.getWin31jChar(errorInfo);
+
+        writer.append("<rawdata");
+
+        writer.append(' ');
+        attrOut(writer, "encoding", "Shift_JIS");
+
+        writer.append(' ');
+        attrOut(writer, "hexBin", hexBin);
+
+        writer.append(" >");
+        writer.append(replaceChar);
+        writer.append("</rawdata>");
+
+        return;
+    }
+
+    /**
+     * デコードエラー込みのテキストを出力する。
+     * @param writer 出力先
+     * @param content テキスト
+     * @throws IOException 出力エラー
+     */
+    public static void dumpDecodedContent(Writer writer,
+                                             DecodedContent content)
+            throws IOException{
+        if( ! content.hasDecodeError() ){
+            textOut(writer, content);
+            return;
+        }
+
+        int last = 0;
+
+        List<DecodeErrorInfo> errList = content.getDecodeErrorList();
+        for(DecodeErrorInfo err : errList){
+            int charPos = err.getCharPosition();
+            CharSequence line = content.subSequence(last, charPos);
+            textOut(writer, line);
+            dumpErrorInfo(writer, err);
+            last = charPos + 1;
+        }
+
+        CharSequence line = content.subSequence(last, content.length());
+        textOut(writer, line);
+
+        return;
+    }
+
+    /**
+     * 村情報をXML形式で出力する。
+     * @param writer 出力先
+     * @param villageData 村情報
+     * @throws IOException 出力エラー
+     */
+    public static void dumpVillageData(Writer writer,
+                                         VillageData villageData)
+            throws IOException{
+        writer.append("<?xml");
+        writer.append(' ');
+        attrOut(writer, "version", "1.0");
+        writer.append(' ');
+        attrOut(writer, "encoding", "UTF-8");
+        writer.append(" ?>\n\n");
+
+        writer.append("<!--\n");
+        writer.append("  人狼BBSアーカイブ\n");
+        writer.append("  http://jindolf.sourceforge.jp/\n");
+        writer.append("-->\n\n");
+
+        dumpDocType(writer);
+        writer.append("\n\n");
+
+        villageData.dumpXml(writer);
+
+        writer.append("\n<!-- EOF -->\n");
+
+        writer.flush();
+
+        return;
+    }
+
+}