OSDN Git Service

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