OSDN Git Service

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