OSDN Git Service

92cab4c645dc0604d18523c8d587cde4c2733fa3
[mikutoga/TogaGem.git] / src / main / java / jp / sourceforge / mikutoga / xml / BasicXmlExporter.java
1 /*
2  * basic xml exporter
3  *
4  * License : The MIT License
5  * Copyright(c) 2010 MikuToga Partners
6  */
7
8 package jp.sourceforge.mikutoga.xml;
9
10 import java.io.BufferedWriter;
11 import java.io.Closeable;
12 import java.io.Flushable;
13 import java.io.IOException;
14 import java.io.OutputStream;
15 import java.io.OutputStreamWriter;
16 import java.nio.charset.Charset;
17 import javax.xml.bind.DatatypeConverter;
18
19 /**
20  * 各種XMLエクスポータの基本機能。
21  * UCS4は未サポート。
22  */
23 public class BasicXmlExporter {
24
25     /** デフォルトエンコーディング。 */
26     private static final Charset CS_UTF8 = Charset.forName("UTF-8");
27
28     /** デフォルトの改行文字列。 */
29     private static final String LF = "\n";       // 0x0a
30     /** デフォルトのインデント単位。 */
31     private static final String DEFAULT_INDENT_UNIT = "\u0020\u0020";
32
33     private static final char CH_SP     = '\u0020';       //
34     private static final char CH_YEN    = '\u00a5';       // ¥
35     private static final char CH_BSLASH = '\u005c\u005c'; // \
36
37     private static final char[] HEXCHAR_TABLE = {
38         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
39         'A', 'B', 'C', 'D', 'E', 'F',
40     };
41
42     private static final String COMM_START = "<!--";
43     private static final String COMM_END   =     "-->";
44
45     private static final int MASK_BIT8  = 0x000f;
46     private static final int MASK_BIT16 = 0x00ff;
47
48     static{
49         assert HEXCHAR_TABLE.length == 16;
50     }
51
52
53     private final Appendable appendable;
54
55     private String newline = LF;
56     private String indentUnit = DEFAULT_INDENT_UNIT;
57
58     private int indentNest = 0;
59     private boolean basicLatinOnlyOut = true;
60
61
62     /**
63      * コンストラクタ。
64      * 文字エンコーディングはUTF-8が用いられる。
65      * @param stream 出力ストリーム
66      */
67     public BasicXmlExporter(OutputStream stream){
68         this(stream, CS_UTF8);
69         return;
70     }
71
72     /**
73      * コンストラクタ。
74      * @param stream 出力ストリーム
75      * @param charSet 文字エンコーディング指定
76      */
77     public BasicXmlExporter(OutputStream stream, Charset charSet){
78         this(
79             new BufferedWriter(
80                 new OutputStreamWriter(stream, charSet)
81             )
82         );
83         return;
84     }
85
86     /**
87      * コンストラクタ。
88      * @param appendable 文字列出力
89      */
90     public BasicXmlExporter(Appendable appendable){
91         super();
92         this.appendable = appendable;
93         return;
94     }
95
96     /**
97      * ASCIIコード相当(UCS:Basic-Latin)の文字か否か判定する。
98      * @param ch 判定対象文字
99      * @return Basic-Latin文字ならtrue
100      */
101     public static boolean isBasicLatin(char ch){
102         Character.UnicodeBlock block = Character.UnicodeBlock.of(ch);
103         if(block == Character.UnicodeBlock.BASIC_LATIN) return true;
104         return false;
105     }
106
107     /**
108      * 改行文字列を設定する。
109      * @param newLine 改行文字列
110      * @throws NullPointerException 引数がnull
111      */
112     public void setNewLine(String newLine) throws NullPointerException{
113         if(newLine == null) throw new NullPointerException();
114         this.newline = newLine;
115         return;
116     }
117
118     /**
119      * BasicLatin文字だけで出力するか設定する。
120      * BasicLatin以外の文字(≒日本語)をそのまま出力するか
121      * 文字参照で出力するかの設定が可能。
122      * コメント部中身は対象外。
123      * @param bool BasicLatin文字だけで出力するならtrue
124      */
125     public void setBasicLatinOnlyOut(boolean bool){
126         this.basicLatinOnlyOut = bool;
127         return;
128     }
129
130     /**
131      * BasicLatin文字だけを出力する状態か判定する。
132      * コメント部中身は対象外。
133      * @return BasicLatin文字だけで出力するならtrue
134      */
135     public boolean isBasicLatinOnlyOut(){
136         return this.basicLatinOnlyOut;
137     }
138
139     /**
140      * 改行文字列を設定する。
141      * デフォルトではLF(0x0a)\nが用いられる。
142      * @param seq 改行文字列。nullは空文字列""と解釈される。
143      */
144     public void setNewLine(CharSequence seq){
145         if(seq == null) this.newline = "";
146         else            this.newline = seq.toString();
147         return;
148     }
149
150     /**
151      * インデント単位文字列を設定する。
152      * デフォルトでは空白2個。
153      * @param seq インデント単位文字列。nullは空文字列""と解釈される。
154      */
155     public void setIndentUnit(CharSequence seq){
156         if(seq == null) this.indentUnit = "";
157         else            this.indentUnit = seq.toString();
158     }
159
160     /**
161      * 可能であれば出力をフラッシュする。
162      * @throws IOException 出力エラー
163      */
164     public void flush() throws IOException{
165         if(this.appendable instanceof Flushable){
166             ((Flushable)this.appendable).flush();
167         }
168         return;
169     }
170
171     /**
172      * 可能であれば出力をクローズする。
173      * @throws IOException 出力エラー
174      */
175     public void close() throws IOException{
176         if(this.appendable instanceof Closeable){
177             ((Closeable)this.appendable).close();
178         }
179         return;
180     }
181
182     /**
183      * 1文字出力する。
184      * @param ch 文字
185      * @return this本体
186      * @throws IOException 出力エラー
187      */
188     public BasicXmlExporter put(char ch) throws IOException{
189         this.appendable.append(ch);
190         return this;
191     }
192
193     /**
194      * 文字列を出力する。
195      * @param seq 文字列
196      * @return this本体
197      * @throws IOException 出力エラー
198      */
199     public BasicXmlExporter put(CharSequence seq) throws IOException{
200         this.appendable.append(seq);
201         return this;
202     }
203
204     /**
205      * int値を出力する。
206      * @param iVal int値
207      * @return this本体
208      * @throws IOException 出力エラー
209      * @see java.lang.Integer#toString(int)
210      */
211     public BasicXmlExporter put(int iVal) throws IOException{
212         String value = DatatypeConverter.printInt(iVal);
213         this.appendable.append(value);
214         return this;
215     }
216
217     /**
218      * float値を出力する。
219      * @param fVal float値
220      * @return this本体
221      * @throws IOException 出力エラー
222      * @see java.lang.Float#toString(float)
223      */
224     public BasicXmlExporter put(float fVal) throws IOException{
225         String value = DatatypeConverter.printFloat(fVal);
226         this.appendable.append(value);
227         return this;
228     }
229
230     /**
231      * 改行を出力する。
232      * @return this本体
233      * @throws IOException 出力エラー
234      */
235     public BasicXmlExporter ln() throws IOException{
236         this.appendable.append(this.newline);
237         return this;
238     }
239
240     /**
241      * 改行を指定回数出力する。
242      * @param count 改行回数。0以下の場合は何も出力しない。
243      * @return this本体
244      * @throws IOException 出力エラー
245      */
246     public BasicXmlExporter ln(int count) throws IOException{
247         for(int ct = 1; ct <= count; ct++){
248             this.appendable.append(this.newline);
249         }
250         return this;
251     }
252
253     /**
254      * 空白を出力する。
255      * @return this本体
256      * @throws IOException 出力エラー
257      */
258     public BasicXmlExporter sp() throws IOException{
259         this.appendable.append(CH_SP);
260         return this;
261     }
262
263     /**
264      * 空白を指定回数出力する。
265      * @param count 空白回数。0以下の場合は何も出力しない。
266      * @return this本体
267      * @throws IOException 出力エラー
268      */
269     public BasicXmlExporter sp(int count) throws IOException{
270         for(int ct = 1; ct <= count; ct++){
271             this.appendable.append(CH_SP);
272         }
273         return this;
274     }
275
276     /**
277      * インデントを出力する。
278      * インデント単位文字列をネストレベル回数分出力する。
279      * @return this本体
280      * @throws IOException 出力エラー
281      */
282     public BasicXmlExporter ind() throws IOException{
283         for(int ct = 1; ct <= this.indentNest; ct++){
284             put(this.indentUnit);
285         }
286         return this;
287     }
288
289     /**
290      * インデントレベルを一段下げる。
291      */
292     public void pushNest(){
293         this.indentNest++;
294         return;
295     }
296
297     /**
298      * インデントレベルを一段上げる。
299      * インデントレベル0の状態をさらに上げようとした場合、何も起こらない。
300      */
301     public void popNest(){
302         this.indentNest--;
303         if(this.indentNest < 0) this.indentNest = 0;
304         return;
305     }
306
307     /**
308      * 指定された文字を16進2桁の文字参照形式で出力する。
309      * 2桁で出力できない場合は4桁で出力する。
310      * @param ch 文字
311      * @return this本体
312      * @throws IOException 出力エラー
313      */
314     public BasicXmlExporter putCharRef2Hex(char ch) throws IOException{
315         if(ch > MASK_BIT16) return putCharRef4Hex(ch);
316
317         char hex3 = HEXCHAR_TABLE[(ch >> 4) & MASK_BIT8];
318         char hex4 = HEXCHAR_TABLE[(ch >> 0) & MASK_BIT8];
319
320         put("&#x").put(hex3).put(hex4).put(';');
321
322         return this;
323     }
324
325     /**
326      * 指定された文字を16進4桁の文字参照形式で出力する。
327      * UCS4に伴うサロゲートペアは未サポート
328      * @param ch 文字
329      * @return this本体
330      * @throws IOException 出力エラー
331      */
332     public BasicXmlExporter putCharRef4Hex(char ch) throws IOException{
333         char hex1 = HEXCHAR_TABLE[(ch >> 12) & MASK_BIT8];
334         char hex2 = HEXCHAR_TABLE[(ch >>  8) & MASK_BIT8];
335         char hex3 = HEXCHAR_TABLE[(ch >>  4) & MASK_BIT8];
336         char hex4 = HEXCHAR_TABLE[(ch >>  0) & MASK_BIT8];
337
338         put("&#x").put(hex1).put(hex2).put(hex3).put(hex4).put(';');
339
340         return this;
341     }
342
343     /**
344      * 要素の中身および属性値中身を出力する。
345      * <p>必要に応じてXML定義済み実体文字が割り振られた文字、
346      * コントロールコード、および非BasicLatin文字がエスケープされる。
347      * <p>半角通貨記号U+00A5はバックスラッシュU+005Cに置換される。
348      * @param content 内容
349      * @return this本体
350      * @throws IOException 出力エラー
351      */
352     public BasicXmlExporter putContent(CharSequence content)
353             throws IOException{
354         int length = content.length();
355
356         char prev = '\0';
357         for(int pos = 0; pos < length; pos++){
358             char ch = content.charAt(pos);
359
360             if(Character.isISOControl(ch)){
361                 putCharRef2Hex(ch);
362             }else if( ! isBasicLatin(ch) && isBasicLatinOnlyOut()){
363                 putCharRef4Hex(ch);
364             }else if(ch == CH_SP){
365                 if(prev == CH_SP){
366                     putCharRef2Hex(ch);
367                 }else{
368                     put(ch);
369                 }
370             }else if(Character.isSpaceChar(ch)){
371                 // 全角スペースその他
372                 putCharRef2Hex(ch);
373             }else if(ch == CH_YEN){
374                 put(CH_BSLASH);
375             }else{
376                 switch(ch){
377                 case '&':    put("&amp;");    break;
378                 case '<':    put("&lt;");     break;
379                 case '>':    put("&gt;");     break;
380                 case '"':    put("&quot;");   break;
381                 case '\'':   put("&apos;");   break;
382                 default:     put(ch);         break;
383                 }
384             }
385
386             prev = ch;
387         }
388
389         return this;
390     }
391
392     /**
393      * 属性値を出力する。
394      * @param attrName 属性名
395      * @param content 属性内容
396      * @return this本体
397      * @throws IOException 出力エラー
398      */
399     public BasicXmlExporter putAttr(CharSequence attrName,
400                                      CharSequence content)
401             throws IOException{
402         put(attrName).put('=').put('"').putContent(content).put('"');
403         return this;
404     }
405
406     /**
407      * int型属性値を出力する。
408      * @param attrName 属性名
409      * @param iVal int値
410      * @return this本体
411      * @throws IOException 出力エラー
412      */
413     public BasicXmlExporter putIntAttr(CharSequence attrName,
414                                            int iVal)
415             throws IOException{
416         put(attrName).put('=').put('"').put(iVal).put('"');
417         return this;
418     }
419
420     /**
421      * float型属性値を出力する。
422      * @param attrName 属性名
423      * @param fVal float値
424      * @return this本体
425      * @throws IOException 出力エラー
426      */
427     public BasicXmlExporter putFloatAttr(CharSequence attrName,
428                                               float fVal)
429             throws IOException{
430         put(attrName).put('=').put('"').put(fVal).put('"');
431         return this;
432     }
433
434     /**
435      * コメントの内容を出力する。
436      * <p>コメント中の'\n'記号出現に伴い、
437      * あらかじめ指定された改行文字が出力される。
438      * <p>コメント中の'\n'以外のコントロールコードは
439      * Control Pictures(U+2400〜)で代替される。
440      * <p>それ以外の非BasicLatin文字はそのまま出力される。
441      * <p>連続するハイフン(-)記号間には強制的にスペースが挿入される。
442      * @param comment コメント内容
443      * @return this本体
444      * @throws IOException 出力エラー
445      */
446     public BasicXmlExporter putCommentContent(CharSequence comment)
447             throws IOException{
448         int length = comment.length();
449
450         char prev = '\0';
451         for(int pos = 0; pos < length; pos++){
452             char ch = comment.charAt(pos);
453
454             if(ch == '\n'){
455                 ln();
456             }else if('\u0000' <= ch && ch <= '\u001f'){
457                 put((char)('\u2400' + ch));
458             }else if(ch == '\u007f'){
459                 put('\u2421');
460             }else if(prev == '-' && ch == '-'){
461                 sp().put(ch);
462             }else{
463                 put(ch);
464             }
465
466             prev = ch;
467         }
468
469         return this;
470     }
471
472     /**
473      * 1行コメントを出力する。
474      * コメント内部の頭及び末尾に空白が1つ挿入される。
475      * @param comment コメント内容
476      * @return this本体
477      * @throws IOException 出力エラー
478      */
479     public BasicXmlExporter putLineComment(CharSequence comment)
480             throws IOException{
481         put(COMM_START).sp();
482         putCommentContent(comment);
483         sp().put(COMM_END);
484         return this;
485     }
486
487     /**
488      * ブロックコメントを出力する。
489      * <p>コメント内部の頭の前に改行が出力される。
490      * <p>コメント内部の末尾が改行でない場合、改行が挿入される。
491      * <p>ブロックコメント末尾は改行で終わる。
492      * <p>インデント設定は無視される。
493      * @param comment コメント内容
494      * @return this本体
495      * @throws IOException 出力エラー
496      */
497     public BasicXmlExporter putBlockComment(CharSequence comment)
498             throws IOException{
499         put(COMM_START).ln();
500
501         putCommentContent(comment);
502
503         int commentLength = comment.length();
504         if(commentLength > 0){
505             char lastCh = comment.charAt(commentLength - 1);
506             if(lastCh != '\n'){
507                 ln();
508             }
509         }
510
511         put(COMM_END).ln();
512
513         return this;
514     }
515
516 }