OSDN Git Service

不要ソースの削除整理
[jindolf/JinArchiver.git] / src / main / java / jp / sourceforge / jindolf / archiver / XmlUtils.java
1 /*
2  * XML utils
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sourceforge.jindolf.archiver;
9
10 import java.io.IOException;
11 import java.io.Writer;
12 import java.text.MessageFormat;
13 import java.util.Calendar;
14 import java.util.GregorianCalendar;
15 import java.util.List;
16 import java.util.TimeZone;
17 import jp.sourceforge.jindolf.parser.DecodeErrorInfo;
18 import jp.sourceforge.jindolf.parser.DecodedContent;
19
20 /**
21  * XML用各種ユーティリティ。
22  */
23 public final class XmlUtils{
24
25     private static final String ORIG_DTD =
26             "http://jindolf.sourceforge.jp/xml/dtd/bbsArchive-110421.dtd";
27     private static final String ORIG_NS =
28             "http://jindolf.sourceforge.jp/xml/ns/501";
29     private static final String ORIG_SCHEME =
30             "http://jindolf.sourceforge.jp/xml/xsd/bbsArchive-110421.xsd";
31     private static final String SCHEMA_NS =
32             "http://www.w3.org/2001/XMLSchema-instance";
33
34     private static final char BS_CHAR = (char) 0x005c; // Backslash
35     private static final String INDENT_UNIT = "\u0020\u0020";
36
37     private static final TimeZone TZ_TOKYO =
38             TimeZone.getTimeZone("Asia/Tokyo");
39
40
41     /**
42      * 隠れコンストラクタ。
43      */
44     private XmlUtils(){
45         throw new Error();
46     }
47
48
49     /**
50      * DOCTYPE宣言を出力する。
51      * @param writer 出力先
52      * @throws IOException 出力エラー
53      */
54     public static void dumpDocType(Writer writer) throws IOException{
55         writer.append("<!DOCTYPE village SYSTEM ");
56         writer.append('"');
57         writer.append(ORIG_DTD);
58         writer.append('"');
59         writer.append(" >");
60         return;
61     }
62
63     /**
64      * オリジナルNameSpace宣言を出力する。
65      * @param writer 出力先
66      * @throws IOException 出力エラー
67      */
68     public static void dumpNameSpaceDecl(Writer writer)
69             throws IOException{
70         attrOut(writer, "xmlns", ORIG_NS);
71         return;
72     }
73
74     /**
75      * スキーマNameSpace宣言を出力する。
76      * @param writer 出力先
77      * @throws IOException 出力エラー
78      */
79     public static void dumpSiNameSpaceDecl(Writer writer)
80             throws IOException{
81         attrOut(writer, "xmlns:xsi", SCHEMA_NS);
82         return;
83     }
84
85     /**
86      * スキーマ位置指定を出力する。
87      * @param writer 出力先
88      * @throws IOException 出力エラー
89      */
90     public static void dumpSchemeLocation(Writer writer)
91             throws IOException{
92         attrOut(writer,
93                 "xsi:schemaLocation",
94                 ORIG_NS + " " + ORIG_SCHEME);
95         return;
96     }
97
98     /**
99      * インデント用空白を出力する。
100      * ネスト単位は空白2文字
101      * @param writer 出力先
102      * @param level ネストレベル
103      * @throws IOException 出力エラー
104      */
105     public static void indent(Writer writer, int level) throws IOException{
106         for(int ct = 1; ct <= level; ct++){
107             writer.append(INDENT_UNIT);
108         }
109         return;
110     }
111
112     /**
113      * XML数値文字参照を出力する。
114      * @param writer 出力先
115      * @param chVal 出力文字
116      * @throws IOException 出力エラー
117      */
118     public static void charRefOut(Writer writer, char chVal)
119             throws IOException{
120         if(chVal == '\u0020'){
121             writer.append("&#x20;");
122             return;
123         }
124
125         if(chVal == '\u0009'){
126             writer.append("&#x09;");
127             return;
128         }
129
130         int ival = 0xffff & ((int) chVal);
131         String hex = Integer.toHexString(ival);
132         if(hex.length() % 2 != 0) hex = "0" + hex;
133
134         writer.append("&#x");
135         writer.append(hex);
136         writer.append(";");
137
138         return;
139     }
140
141     /**
142      * 不正文字をXML出力する。
143      * @param writer 出力先
144      * @param chVal 不正文字
145      * @throws IOException 出力エラー
146      */
147     public static void dumpInvalidChar(Writer writer, char chVal)
148             throws IOException{
149         int hexVal;
150         hexVal = chVal & 0xff;
151         String hexBin = Integer.toHexString(hexVal);
152         if(hexBin.length() % 2 != 0) hexBin = "0" + hexBin;
153
154         char replaceChar = '\ufffd';
155         if('\u0000' <= chVal && chVal <= '\u001f'){
156             replaceChar = (char)( chVal + '\u2400' );
157         }
158
159         writer.append("<rawdata");
160
161         writer.append(' ');
162         attrOut(writer, "encoding", "Shift_JIS");
163
164         writer.append(' ');
165         attrOut(writer, "hexBin", hexBin);
166
167         writer.append(" >");
168         writer.append(replaceChar);
169         writer.append("</rawdata>");
170     }
171
172     /**
173      * 任意の文字がXML規格上のホワイトスペースに属するか判定する。
174      * @param chVal 文字
175      * @return ホワイトスペースならtrue
176      */
177     public static boolean isWhiteSpace(char chVal){
178         switch(chVal){
179         case '\u0020':
180         case '\t':
181         case '\n':
182         case '\r':
183             return true;
184         default:
185             break;
186         }
187
188         return false;
189     }
190
191     /**
192      * 文字列を出力する。
193      * <ul>
194      * <li>先頭および末尾のホワイトスペースは強制的に文字参照化される。
195      * <li>連続したホワイトスペースの2文字目以降は文字参照化される。
196      * <li>スペースでないホワイトスペースは無条件に文字参照化される。
197      * <li>{@literal &, <, >, "}は無条件に文字参照化される。
198      * </ul>
199      * 参考:XML 1.0 規格 3.3.3節
200      * @param writer 出力先
201      * @param seq CDATA文字列
202      * @throws IOException 出力エラー
203      */
204     public static void textOut(Writer writer, CharSequence seq)
205             throws IOException{
206         int len = seq.length();
207
208         boolean leadSpace = false;
209
210         for(int pos = 0; pos < len; pos++){
211             char chVal = seq.charAt(pos);
212
213             if(isWhiteSpace(chVal)){
214                 if(pos == 0 || pos >= len - 1 || leadSpace){
215                     charRefOut(writer, chVal);
216                 }else if(chVal != '\u0020'){
217                     charRefOut(writer, chVal);
218                 }else{
219                     writer.append(chVal);
220                 }
221                 leadSpace = true;
222             }else{
223                 if(chVal == '&'){
224                     writer.append("&amp;");
225                 }else if(chVal == '<'){
226                     writer.append("&lt;");
227                 }else if(chVal == '>'){
228                     writer.append("&gt;");
229                 }else if(chVal == '"'){
230                     writer.append("&quot;");
231                 }else if(chVal == '\''){
232                     writer.append("&apos;");
233                 }else if(chVal == BS_CHAR){
234                     writer.append('\u00a5');
235                 }else if(chVal == '\u007e'){
236                     writer.append('\u203e');
237                 }else if(Character.isISOControl(chVal)){
238                     dumpInvalidChar(writer, chVal);
239                 }else{
240                     writer.append(chVal);
241                 }
242                 leadSpace = false;
243             }
244         }
245
246         return;
247     }
248
249     /**
250      * 属性を出力する。
251      * @param writer 出力先
252      * @param name 属性名
253      * @param value 属性値
254      * @throws IOException 出力エラー
255      */
256     public static void attrOut(Writer writer,
257                                 CharSequence name,
258                                 CharSequence value)
259             throws IOException{
260         StringBuilder newValue = new StringBuilder(value);
261         for(int pt = 0; pt < newValue.length(); pt++){
262             char chVal = newValue.charAt(pt);
263             if(chVal == '\n' || chVal == '\r' || chVal == '\t') continue;
264             if(Character.isISOControl(chVal)){
265                 newValue.setCharAt(pt, (char)('\u2400' + chVal));
266             }
267         }
268
269         writer.append(name);
270         writer.append('=');
271         writer.append('"');
272         textOut(writer, newValue);
273         writer.append('"');
274         return;
275     }
276
277     /**
278      * xsd:time形式の時刻属性を出力する。
279      * タイムゾーンは「+09:00」固定
280      * @param writer 出力先
281      * @param name 属性名
282      * @param hour 時間
283      * @param minute 分
284      * @throws IOException 出力エラー
285      */
286     public static void timeAttrOut(Writer writer,
287                                      CharSequence name,
288                                      int hour, int minute)
289             throws IOException{
290         String cmtTime =
291                 MessageFormat
292                 .format("{0,number,#00}:{1,number,#00}:00+09:00",
293                         hour, minute);
294         attrOut(writer, name, cmtTime);
295         return;
296     }
297
298     /**
299      * xsd:gMonthDay形式の日付属性を出力する。
300      * タイムゾーンは「+09:00」固定
301      * @param writer 出力先
302      * @param name 属性名
303      * @param month 月
304      * @param day 日
305      * @throws IOException 出力エラー
306      */
307     public static void dateAttrOut(Writer writer,
308                                      CharSequence name,
309                                      int month, int day)
310             throws IOException{
311         String dateAttr =
312                 MessageFormat.format("--{0,number,#00}-{1,number,#00}+09:00",
313                                      month, day);
314         attrOut(writer, name, dateAttr);
315         return;
316     }
317
318     /**
319      * xsd:dateTime形式の日付時刻属性を出力する。
320      * タイムゾーンは「+09:00」固定
321      * @param writer 出力先
322      * @param name 属性名
323      * @param epochMs エポック時刻
324      * @throws IOException 出力エラー
325      */
326     public static void dateTimeAttr(Writer writer,
327                                       CharSequence name,
328                                       long epochMs)
329             throws IOException{
330         Calendar calendar = new GregorianCalendar(TZ_TOKYO);
331
332         calendar.setTimeInMillis(epochMs);
333         int year = calendar.get(Calendar.YEAR);
334         int month = calendar.get(Calendar.MONTH) + 1;
335         int day = calendar.get(Calendar.DATE);
336         int hour = calendar.get(Calendar.HOUR_OF_DAY);
337         int minute = calendar.get(Calendar.MINUTE);
338         int sec = calendar.get(Calendar.SECOND);
339         int msec = calendar.get(Calendar.MILLISECOND);
340
341         String attrVal = MessageFormat.format(
342                  "{0,number,#0000}-{1,number,#00}-{2,number,#00}"
343                 +"T{3,number,#00}:{4,number,#00}:{5,number,#00}"
344                 +".{6,number,#000}+09:00",
345                 year, month, day, hour, minute, sec, msec);
346
347         attrOut(writer, name, attrVal);
348
349         return;
350     }
351
352     /**
353      * デコードエラー情報をrawdataタグで出力する。
354      * 文字列集合に関するエラーの場合、windows31jでのデコード出力を試みる。
355      * @param writer 出力先
356      * @param errorInfo デコードエラー
357      * @throws IOException 出力エラー
358      */
359     public static void dumpErrorInfo(Writer writer,
360                                        DecodeErrorInfo errorInfo)
361             throws IOException{
362         int hexVal;
363         hexVal = errorInfo.getRawByte1st() & 0xff;
364         if(errorInfo.has2nd()){
365             hexVal <<= 8;
366             hexVal |= errorInfo.getRawByte2nd() & 0xff;
367         }
368
369         String hexBin = Integer.toHexString(hexVal);
370         if(hexBin.length() % 2 != 0) hexBin = "0" + hexBin;
371
372         char replaceChar = Win31j.getWin31jChar(errorInfo);
373
374         writer.append("<rawdata");
375
376         writer.append(' ');
377         attrOut(writer, "encoding", "Shift_JIS");
378
379         writer.append(' ');
380         attrOut(writer, "hexBin", hexBin);
381
382         writer.append(" >");
383         writer.append(replaceChar);
384         writer.append("</rawdata>");
385
386         return;
387     }
388
389     /**
390      * デコードエラー込みのテキストを出力する。
391      * @param writer 出力先
392      * @param content テキスト
393      * @throws IOException 出力エラー
394      */
395     public static void dumpDecodedContent(Writer writer,
396                                              DecodedContent content)
397             throws IOException{
398         if( ! content.hasDecodeError() ){
399             textOut(writer, content);
400             return;
401         }
402
403         int last = 0;
404
405         List<DecodeErrorInfo> errList = content.getDecodeErrorList();
406         for(DecodeErrorInfo err : errList){
407             int charPos = err.getCharPosition();
408             CharSequence line = content.subSequence(last, charPos);
409             textOut(writer, line);
410             dumpErrorInfo(writer, err);
411             last = charPos + 1;
412         }
413
414         CharSequence line = content.subSequence(last, content.length());
415         textOut(writer, line);
416
417         return;
418     }
419
420     /**
421      * 村情報をXML形式で出力する。
422      * @param writer 出力先
423      * @param villageData 村情報
424      * @throws IOException 出力エラー
425      */
426     public static void dumpVillageData(Writer writer,
427                                          VillageData villageData)
428             throws IOException{
429         writer.append("<?xml");
430         writer.append(' ');
431         attrOut(writer, "version", "1.0");
432         writer.append(' ');
433         attrOut(writer, "encoding", "UTF-8");
434         writer.append(" ?>\n\n");
435
436         writer.append("<!--\n");
437         writer.append("  人狼BBSアーカイブ\n");
438         writer.append("  http://jindolf.sourceforge.jp/\n");
439         writer.append("-->\n\n");
440
441         dumpDocType(writer);
442         writer.append("\n\n");
443
444         villageData.dumpXml(writer);
445
446         writer.append("\n<!-- EOF -->\n");
447
448         writer.flush();
449
450         return;
451     }
452
453 }