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 java.util.regex.Matcher;
18 import java.util.regex.Pattern;
19 import javax.xml.bind.DatatypeConverter;
20
21 /**
22  * 各種XMLエクスポータの基本機能。
23  * UCS4は未サポート。
24  */
25 public class BasicXmlExporter {
26
27     /** デフォルトエンコーディング。 */
28     private static final Charset CS_UTF8 = Charset.forName("UTF-8");
29
30     /** デフォルトの改行文字列。 */
31     private static final String DEF_NL = "\n";       // 0x0a(LF)
32     /** デフォルトのインデント単位。 */
33     private static final String DEF_INDENT_UNIT = "\u0020\u0020"; // ␣␣
34
35     private static final char CH_SP     = '\u0020';    // ␣
36     private static final char CH_YEN    = '\u00a5';    // ¥
37     private static final char CH_BSLASH = (char)0x005c; // \
38     private static final char CH_DQ     = '\u0022';    // "
39     private static final char CH_SQ     = (char)0x0027; // '
40
41     private static final String COMM_START = "<!--";
42     private static final String COMM_END   =   "-->";
43     private static final String REF_HEX = "&#x";
44
45     private static final Pattern NUM_FUZZY =
46             Pattern.compile("([^.]*\\.[0-9][0-9]*?)0+");
47
48     private static final int HEX_EXP = 4;    // 2 ** 4 == 16
49     private static final int MASK_1HEX = (1 << HEX_EXP) - 1;  // 0b00001111
50     private static final int MAX_OCTET = (1 << Byte.SIZE) - 1;   // 0xff
51     private static final char[] HEXCHAR_TABLE = {
52         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
53         'A', 'B', 'C', 'D', 'E', 'F',
54     };
55
56
57     static{
58         assert HEX_EXP * 2 == Byte.SIZE;
59         assert HEXCHAR_TABLE.length == (1 << HEX_EXP);
60     }
61
62
63     private final Appendable appendable;
64
65     private String newline = DEF_NL;
66     private String indentUnit = DEF_INDENT_UNIT;
67
68     private int indentNest = 0;
69     private boolean basicLatinOnlyOut = true;
70
71
72     /**
73      * コンストラクタ。
74      * 文字エンコーディングはUTF-8が用いられる。
75      * @param stream 出力ストリーム
76      */
77     public BasicXmlExporter(OutputStream stream){
78         this(stream, CS_UTF8);
79         return;
80     }
81
82     /**
83      * コンストラクタ。
84      * @param stream 出力ストリーム
85      * @param charSet 文字エンコーディング指定
86      */
87     public BasicXmlExporter(OutputStream stream, Charset charSet){
88         this(
89             new BufferedWriter(
90                 new OutputStreamWriter(stream, charSet)
91             )
92         );
93         return;
94     }
95
96     /**
97      * コンストラクタ。
98      * @param appendable 文字列出力
99      */
100     public BasicXmlExporter(Appendable appendable){
101         super();
102         this.appendable = appendable;
103         return;
104     }
105
106
107     /**
108      * ASCIIコード相当(UCS:Basic-Latin)の文字か否か判定する。
109      * <p>※ Basic-Latinには各種制御文字も含まれる。
110      * @param ch 判定対象文字
111      * @return Basic-Latin文字ならtrue
112      */
113     public static boolean isBasicLatin(char ch){
114         if('\u0000' <= ch && ch <= '\u007f'){
115             return true;
116         }
117         return false;
118     }
119
120     /**
121      * 冗長な実数出力を抑止する。
122      * <p>DatatypeConverterにおけるJDK1.6系と1.7系の仕様変更を吸収する。
123      * <p>0.001fは"0.0010"ではなく"0.001"と出力される。
124      * <p>指数表記での冗長桁は無視する。
125      * @param numTxt 実数表記
126      * @return 冗長桁が抑止された実数表記
127      */
128     public static String chopFuzzyZero(String numTxt){
129         String result;
130
131         Matcher matcher = NUM_FUZZY.matcher(numTxt);
132         if(matcher.matches()){
133             result = matcher.group(1);
134         }else{
135             result = numTxt;
136         }
137
138         return result;
139     }
140
141
142     /**
143      * BasicLatin文字だけで出力するか設定する。
144      * <p>BasicLatin以外の文字(≒日本語)を、そのまま出力するか、
145      * 文字参照で出力するか、の設定が可能。
146      * <p>コメント部中身は対象外。
147      * @param bool BasicLatin文字だけで出力するならtrue
148      */
149     public void setBasicLatinOnlyOut(boolean bool){
150         this.basicLatinOnlyOut = bool;
151         return;
152     }
153
154     /**
155      * BasicLatin文字だけを出力する状態か判定する。
156      * <p>コメント部中身は対象外。
157      * @return BasicLatin文字だけで出力するならtrue
158      */
159     public boolean isBasicLatinOnlyOut(){
160         return this.basicLatinOnlyOut;
161     }
162
163     /**
164      * 改行文字列を設定する。
165      * @param newLine 改行文字列
166      * @throws NullPointerException 引数がnull
167      */
168     public void setNewLine(String newLine) throws NullPointerException{
169         if(newLine == null) throw new NullPointerException();
170         this.newline = newLine;
171         return;
172     }
173
174     /**
175      * 改行文字列を返す。
176      * @return 改行文字列
177      */
178     public String getNewLine(){
179         return this.newline;
180     }
181
182     /**
183      * インデント単位文字列を設定する。
184      * <p>デフォルトでは空白2個。
185      * @param indUnit インデント単位文字列。
186      * @throws NullPointerException 引数がnull
187      */
188     public void setIndentUnit(String indUnit) throws NullPointerException{
189         if(indUnit == null) throw new NullPointerException();
190         this.indentUnit = indUnit;
191         return;
192     }
193
194     /**
195      * インデント単位文字列を返す。
196      * @return インデント単位文字列
197      */
198     public String getIndentUnit(){
199         return this.indentUnit;
200     }
201
202     /**
203      * 可能であれば出力をフラッシュする。
204      * @throws IOException 出力エラー
205      */
206     public void flush() throws IOException{
207         if(this.appendable instanceof Flushable){
208             ((Flushable)this.appendable).flush();
209         }
210         return;
211     }
212
213     /**
214      * 可能であれば出力をクローズする。
215      * @throws IOException 出力エラー
216      */
217     public void close() throws IOException{
218         if(this.appendable instanceof Closeable){
219             ((Closeable)this.appendable).close();
220         }
221         return;
222     }
223
224     /**
225      * 1文字出力する。
226      * @param ch 文字
227      * @return this本体
228      * @throws IOException 出力エラー
229      */
230     public BasicXmlExporter putRawCh(char ch) throws IOException{
231         this.appendable.append(ch);
232         return this;
233     }
234
235     /**
236      * 文字列を出力する。
237      * @param seq 文字列
238      * @return this本体
239      * @throws IOException 出力エラー
240      */
241     public BasicXmlExporter putRawText(CharSequence seq) throws IOException{
242         this.appendable.append(seq);
243         return this;
244     }
245
246     /**
247      * 指定された文字を16進2桁の文字参照形式で出力する。
248      * 2桁で出力できない場合(>0x00ff)は4桁で出力する。
249      * @param ch 文字
250      * @return this本体
251      * @throws IOException 出力エラー
252      */
253     public BasicXmlExporter putCharRef2Hex(char ch) throws IOException{
254         if(ch > MAX_OCTET) return putCharRef4Hex(ch);
255
256         int ibits = ch;   // 常に正なので符号拡張なし
257
258         int idx4 = ibits & MASK_1HEX;
259         ibits >>= HEX_EXP;
260         int idx3 = ibits & MASK_1HEX;
261
262         char hex3 = HEXCHAR_TABLE[idx3];
263         char hex4 = HEXCHAR_TABLE[idx4];
264
265         putRawText(REF_HEX).putRawCh(hex3).putRawCh(hex4)
266                            .putRawCh(';');
267
268         return this;
269     }
270
271     /**
272      * 指定された文字を16進4桁の文字参照形式で出力する。
273      * UCS4に伴うサロゲートペアは未サポート
274      * @param ch 文字
275      * @return this本体
276      * @throws IOException 出力エラー
277      */
278     public BasicXmlExporter putCharRef4Hex(char ch) throws IOException{
279         int ibits = ch;   // 常に正なので符号拡張なし
280
281         int idx4 = ibits & MASK_1HEX;
282         ibits >>= HEX_EXP;
283         int idx3 = ibits & MASK_1HEX;
284         ibits >>= HEX_EXP;
285         int idx2 = ibits & MASK_1HEX;
286         ibits >>= HEX_EXP;
287         int idx1 = ibits & MASK_1HEX;
288
289         char hex1 = HEXCHAR_TABLE[idx1];
290         char hex2 = HEXCHAR_TABLE[idx2];
291         char hex3 = HEXCHAR_TABLE[idx3];
292         char hex4 = HEXCHAR_TABLE[idx4];
293
294         putRawText(REF_HEX).putRawCh(hex1).putRawCh(hex2)
295                            .putRawCh(hex3).putRawCh(hex4)
296                            .putRawCh(';');
297
298         return this;
299     }
300
301     /**
302      * 要素の中身および属性値中身を出力する。
303      * <p>XMLの構文規則を守る上で必要な各種エスケープ処理が行われる。
304      * @param ch 文字
305      * @return this本体
306      * @throws IOException 出力エラー
307      */
308     public BasicXmlExporter putCh(char ch) throws IOException{
309         if(Character.isISOControl(ch)){
310             putCharRef2Hex(ch);
311             return this;
312         }
313
314         String escTxt;
315         switch(ch){
316         case '&':   escTxt = "&amp;";  break;
317         case '<':   escTxt = "&lt;";   break;
318         case '>':   escTxt = "&gt;";   break;
319         case CH_DQ: escTxt = "&quot;"; break;
320         case CH_SQ: escTxt = "&apos;"; break;
321         default:    escTxt = null;     break;
322         }
323
324         if(escTxt != null){
325             putRawText(escTxt);
326         }else{
327             putRawCh(ch);
328         }
329
330         return this;
331     }
332
333     /**
334      * 要素の中身および属性値中身を出力する。
335      * <p>必要に応じてXML定義済み実体文字が割り振られた文字、
336      * コントロールコード、および非BasicLatin文字がエスケープされる。
337      * <p>半角通貨記号U+00A5はバックスラッシュU+005Cに置換される。
338      * <p>連続するスペースU+0020の2文字目以降は文字参照化される。
339      * <p>全角スペースその他空白文字は無条件に文字参照化される。
340      * @param content 内容
341      * @return this本体
342      * @throws IOException 出力エラー
343      */
344     public BasicXmlExporter putContent(CharSequence content)
345             throws IOException{
346         int length = content.length();
347
348         char prev = '\0';
349         for(int pos = 0; pos < length; pos++){
350             char ch = content.charAt(pos);
351
352             if( isBasicLatinOnlyOut() && ! isBasicLatin(ch) ){
353                 putCharRef4Hex(ch);
354             }else if(ch == CH_YEN){
355                 putRawCh(CH_BSLASH);
356             }else if(Character.isSpaceChar(ch)){
357                 if(ch == CH_SP && prev != CH_SP){
358                     putRawCh(ch);
359                 }else{
360                     putCharRef2Hex(ch);
361                 }
362             }else{
363                 putCh(ch);
364             }
365
366             prev = ch;
367         }
368
369         return this;
370     }
371
372     /**
373      * 改行を出力する。
374      * @return this本体
375      * @throws IOException 出力エラー
376      */
377     public BasicXmlExporter ln() throws IOException{
378         this.appendable.append(this.newline);
379         return this;
380     }
381
382     /**
383      * 改行を指定回数出力する。
384      * @param count 改行回数。0以下の場合は何も出力しない。
385      * @return this本体
386      * @throws IOException 出力エラー
387      */
388     public BasicXmlExporter ln(int count) throws IOException{
389         for(int ct = 1; ct <= count; ct++){
390             this.appendable.append(this.newline);
391         }
392         return this;
393     }
394
395     /**
396      * 空白を出力する。
397      * @return this本体
398      * @throws IOException 出力エラー
399      */
400     public BasicXmlExporter sp() throws IOException{
401         this.appendable.append(CH_SP);
402         return this;
403     }
404
405     /**
406      * 空白を指定回数出力する。
407      * @param count 空白回数。0以下の場合は何も出力しない。
408      * @return this本体
409      * @throws IOException 出力エラー
410      */
411     public BasicXmlExporter sp(int count) throws IOException{
412         for(int ct = 1; ct <= count; ct++){
413             this.appendable.append(CH_SP);
414         }
415         return this;
416     }
417
418     /**
419      * インデントを出力する。
420      * インデント単位文字列をネストレベル回数分出力する。
421      * @return this本体
422      * @throws IOException 出力エラー
423      */
424     public BasicXmlExporter ind() throws IOException{
425         for(int ct = 1; ct <= this.indentNest; ct++){
426             putRawText(this.indentUnit);
427         }
428         return this;
429     }
430
431     /**
432      * インデントレベルを一段下げる。
433      */
434     public void pushNest(){
435         this.indentNest++;
436         return;
437     }
438
439     /**
440      * インデントレベルを一段上げる。
441      * インデントレベル0の状態をさらに上げようとした場合、何も起こらない。
442      */
443     public void popNest(){
444         this.indentNest--;
445         if(this.indentNest < 0) this.indentNest = 0;
446         return;
447     }
448
449     /**
450      * int値をXMLスキーマ準拠の形式で出力する。
451      * @param iVal int値
452      * @return this本体
453      * @throws IOException 出力エラー
454      * @see java.lang.Integer#toString(int)
455      */
456     public BasicXmlExporter putXsdInt(int iVal) throws IOException{
457         String value = DatatypeConverter.printInt(iVal);
458         this.appendable.append(value);
459         return this;
460     }
461
462     /**
463      * float値をXMLスキーマ準拠の形式で出力する。
464      * @param fVal float値
465      * @return this本体
466      * @throws IOException 出力エラー
467      * @see java.lang.Float#toString(float)
468      */
469     public BasicXmlExporter putXsdFloat(float fVal) throws IOException{
470         String value = DatatypeConverter.printFloat(fVal);
471         this.appendable.append(value);
472         return this;
473     }
474
475     /**
476      * 属性値を出力する。
477      * @param attrName 属性名
478      * @param content 属性内容
479      * @return this本体
480      * @throws IOException 出力エラー
481      */
482     public BasicXmlExporter putAttr(CharSequence attrName,
483                                      CharSequence content)
484             throws IOException{
485         putRawText(attrName).putRawCh('=');
486
487         putRawCh('"');
488         putContent(content);
489         putRawCh('"');
490
491         return this;
492     }
493
494     /**
495      * int型属性値を出力する。
496      * @param attrName 属性名
497      * @param iVal int値
498      * @return this本体
499      * @throws IOException 出力エラー
500      */
501     public BasicXmlExporter putIntAttr(CharSequence attrName,
502                                         int iVal)
503             throws IOException{
504         String attrValue = DatatypeConverter.printInt(iVal);
505         putAttr(attrName, attrValue);
506         return this;
507     }
508
509     /**
510      * float型属性値を出力する。
511      * @param attrName 属性名
512      * @param fVal float値
513      * @return this本体
514      * @throws IOException 出力エラー
515      */
516     public BasicXmlExporter putFloatAttr(CharSequence attrName,
517                                            float fVal)
518             throws IOException{
519         String attrValue = DatatypeConverter.printFloat(fVal);
520         attrValue = chopFuzzyZero(attrValue);
521         putAttr(attrName, attrValue);
522         return this;
523     }
524
525     /**
526      * コメントの内容を出力する。
527      * <p>コメント中の'\n'記号出現に伴い、
528      * あらかじめ指定された改行文字が出力される。
529      * <p>コメント中の'\n'以外のコントロールコードは
530      * Control Pictures(U+2400〜)で代替される。
531      * <p>それ以外の非BasicLatin文字はそのまま出力される。
532      * <p>連続するハイフン(-)記号間には強制的にスペースが挿入される。
533      * @param comment コメント内容
534      * @return this本体
535      * @throws IOException 出力エラー
536      */
537     public BasicXmlExporter putCommentContent(CharSequence comment)
538             throws IOException{
539         int length = comment.length();
540
541         char prev = '\0';
542         for(int pos = 0; pos < length; pos++){
543             char ch = comment.charAt(pos);
544
545             if(ch == '\n'){
546                 ln();
547             }else if('\u0000' <= ch && ch <= '\u001f'){
548                 putRawCh((char)('\u2400' + ch));
549             }else if(ch == '\u007f'){
550                 putRawCh('\u2421');
551             }else if(prev == '-' && ch == '-'){
552                 sp().putRawCh(ch);
553             }else{
554                 putRawCh(ch);
555             }
556
557             prev = ch;
558         }
559
560         return this;
561     }
562
563     /**
564      * 1行コメントを出力する。
565      * コメント内部の頭及び末尾に空白が1つ挿入される。
566      * @param comment コメント内容
567      * @return this本体
568      * @throws IOException 出力エラー
569      */
570     public BasicXmlExporter putLineComment(CharSequence comment)
571             throws IOException{
572         putRawText(COMM_START).sp();
573         putCommentContent(comment);
574         sp().putRawText(COMM_END);
575         return this;
576     }
577
578     /**
579      * ブロックコメントを出力する。
580      * <p>コメント内部の頭の前に改行が出力される。
581      * <p>コメント内部の末尾が改行でない場合、改行が挿入される。
582      * <p>ブロックコメント末尾は改行で終わる。
583      * <p>インデント設定は無視される。
584      * @param comment コメント内容
585      * @return this本体
586      * @throws IOException 出力エラー
587      */
588     public BasicXmlExporter putBlockComment(CharSequence comment)
589             throws IOException{
590         putRawText(COMM_START).ln();
591
592         putCommentContent(comment);
593
594         int commentLength = comment.length();
595         if(commentLength > 0){
596             char lastCh = comment.charAt(commentLength - 1);
597             if(lastCh != '\n'){
598                 ln();
599             }
600         }
601
602         putRawText(COMM_END).ln();
603
604         return this;
605     }
606
607 }