OSDN Git Service

Appendable対応
[mikutoga/TogaGem.git] / src / main / java / jp / sourceforge / mikutoga / xml / AbstractXmlExporter.java
1 /*
2  * abstract xml exporter
3  *
4  * License : The MIT License
5  * Copyright(c) 2013 MikuToga Partners
6  */
7
8 package jp.sourceforge.mikutoga.xml;
9
10 import java.io.IOException;
11 import java.util.regex.Matcher;
12 import java.util.regex.Pattern;
13 import javax.xml.bind.DatatypeConverter;
14
15 /**
16  * Appendable実装に依存したXMLエクスポータの半実装。
17  * UCS4は未サポート。
18  */
19 abstract class AbstractXmlExporter implements XmlExporter{
20
21     /** デフォルトの改行文字列。 */
22     private static final String DEF_NL = "\n";       // 0x0a(LF)
23     /** デフォルトのインデント単位。 */
24     private static final String DEF_INDENT_UNIT = "\u0020\u0020"; // ␣␣
25
26     private static final char CH_SP     = '\u0020';    // ␣
27     private static final char CH_YEN    = '\u00a5';    // ¥
28     private static final char CH_BSLASH = (char)0x005c; // \
29     private static final char CH_DQ     = '\u0022';    // "
30     private static final char CH_SQ     = (char)0x0027; // '
31     private static final char CH_EQ     = '=';          // =
32     private static final char CH_LT     = '<';
33     private static final char CH_GT     = '>';
34
35     private static final String COMM_START = "<!--";
36     private static final String COMM_END   =   "-->";
37
38     private static final Pattern NUM_FUZZY =
39             Pattern.compile("([^.]*\\.[0-9][0-9]*?)0+");
40
41     private static final String REF_HEX = "&#x";
42     private static final int HEX_EXP = 4;    // 2 ** 4 == 16
43     private static final int MASK_1HEX = (1 << HEX_EXP) - 1;  // 0b00001111
44     private static final int MAX_OCTET = (1 << Byte.SIZE) - 1;   // 0xff
45     private static final char[] HEXCHAR_TABLE = {
46         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
47         'A', 'B', 'C', 'D', 'E', 'F',
48     };
49
50     static{
51         assert HEX_EXP * 2 == Byte.SIZE;
52         assert HEXCHAR_TABLE.length == (1 << HEX_EXP);
53     }
54
55
56     private boolean basicLatinOnlyOut = true;
57     private String newline = DEF_NL;
58     private String indentUnit = DEF_INDENT_UNIT;
59     private int indentNest = 0;
60
61
62     /**
63      * コンストラクタ。
64      */
65     protected AbstractXmlExporter(){
66         super();
67         return;
68     }
69
70
71     /**
72      * ASCIIコード相当(UCS:Basic-Latin)の文字か否か判定する。
73      * <p>※ Basic-Latinには各種制御文字も含まれる。
74      * @param ch 判定対象文字
75      * @return Basic-Latin文字ならtrue
76      * <a href="http://www.unicode.org/charts/PDF/U0000.pdf">
77      * Unicode 6.2 Controls and Basic Latin
78      * </a>
79      */
80     protected static boolean isBasicLatin(char ch){
81         if('\u0000' <= ch && ch <= '\u007f'){
82             return true;
83         }
84         return false;
85     }
86
87     /**
88      * 冗長な実数出力を抑止する。
89      * <p>DatatypeConverterにおけるJDK1.6系と1.7系の仕様変更を吸収する。
90      * <p>0.001fは"0.0010"ではなく"0.001"と出力される。
91      * <p>指数表記での冗長桁は無視する。
92      * @param numTxt 実数表記
93      * @return 冗長桁が抑止された実数表記
94      * @see javax.xml.bind.DatatypeConverter
95      */
96     protected static String chopFuzzyZero(String numTxt){
97         String result;
98
99         Matcher matcher = NUM_FUZZY.matcher(numTxt);
100         if(matcher.matches()){
101             result = matcher.group(1);
102         }else{
103             result = numTxt;
104         }
105
106         return result;
107     }
108
109
110     /**
111      * {@inheritDoc}
112      * @param ch {@inheritDoc}
113      * @return {@inheritDoc}
114      * @throws IOException {@inheritDoc}
115      */
116     @Override
117     public abstract Appendable append(char ch) throws IOException;
118
119     /**
120      * {@inheritDoc}
121      * @param seq {@inheritDoc}
122      * @return {@inheritDoc}
123      * @throws IOException {@inheritDoc}
124      */
125     @Override
126     public abstract Appendable append(CharSequence seq) throws IOException;
127
128     /**
129      * {@inheritDoc}
130      * @param seq {@inheritDoc}
131      * @param start {@inheritDoc}
132      * @param end {@inheritDoc}
133      * @return {@inheritDoc}
134      * @throws IOException {@inheritDoc}
135      */
136     @Override
137     public abstract Appendable append(CharSequence seq, int start, int end)
138             throws IOException;
139
140     /**
141      * {@inheritDoc}
142      * @throws IOException {@inheritDoc}
143      */
144     @Override
145     public abstract void flush() throws IOException;
146
147     /**
148      * {@inheritDoc}
149      * @throws IOException {@inheritDoc}
150      */
151     @Override
152     public abstract void close() throws IOException;
153
154
155     /**
156      * {@inheritDoc}
157      * @param ch {@inheritDoc}
158      * @return {@inheritDoc}
159      * @throws IOException {@inheritDoc}
160      */
161     @Override
162     public XmlExporter putRawCh(char ch) throws IOException{
163         append(ch);
164         return this;
165     }
166
167     /**
168      * {@inheritDoc}
169      * @param seq {@inheritDoc}
170      * @return {@inheritDoc}
171      * @throws IOException {@inheritDoc}
172      */
173     @Override
174     public XmlExporter putRawText(CharSequence seq)
175             throws IOException{
176         append(seq);
177         return this;
178     }
179
180     /**
181      * {@inheritDoc}
182      * @return {@inheritDoc}
183      * @throws IOException {@inheritDoc}
184      */
185     @Override
186     public XmlExporter sp() throws IOException{
187         putRawCh(CH_SP);
188         return this;
189     }
190
191     /**
192      * {@inheritDoc}
193      * @param count {@inheritDoc}
194      * @return {@inheritDoc}
195      * @throws IOException {@inheritDoc}
196      */
197     @Override
198     public XmlExporter sp(int count) throws IOException{
199         for(int ct = 1; ct <= count; ct++){
200             sp();
201         }
202         return this;
203     }
204
205     /**
206      * {@inheritDoc}
207      * @return {@inheritDoc}
208      */
209     @Override
210     public String getNewLine(){
211         return this.newline;
212     }
213
214     /**
215      * {@inheritDoc}
216      * @param newLine {@inheritDoc}
217      * @throws NullPointerException {@inheritDoc}
218      */
219     @Override
220     public void setNewLine(String newLine) throws NullPointerException{
221         if(newLine == null) throw new NullPointerException();
222         this.newline = newLine;
223         return;
224     }
225
226     /**
227      * {@inheritDoc}
228      * @return {@inheritDoc}
229      * @throws IOException {@inheritDoc}
230      */
231     @Override
232     public XmlExporter ln() throws IOException{
233         putRawText(getNewLine());
234         return this;
235     }
236
237     /**
238      * {@inheritDoc}
239      * @param count {@inheritDoc}
240      * @return {@inheritDoc}
241      * @throws IOException {@inheritDoc}
242      */
243     @Override
244     public XmlExporter ln(int count) throws IOException{
245         for(int ct = 1; ct <= count; ct++){
246             ln();
247         }
248         return this;
249     }
250
251     /**
252      * {@inheritDoc}
253      * @return {@inheritDoc}
254      */
255     @Override
256     public String getIndentUnit(){
257         return this.indentUnit;
258     }
259
260     /**
261      * {@inheritDoc}
262      * @param indUnit {@inheritDoc}
263      * @throws NullPointerException {@inheritDoc}
264      */
265     @Override
266     public void setIndentUnit(String indUnit) throws NullPointerException{
267         if(indUnit == null) throw new NullPointerException();
268         this.indentUnit = indUnit;
269         return;
270     }
271
272     /**
273      * {@inheritDoc}
274      */
275     @Override
276     public void pushNest(){
277         this.indentNest++;
278         return;
279     }
280
281     /**
282      * {@inheritDoc}
283      */
284     @Override
285     public void popNest(){
286         this.indentNest--;
287         if(this.indentNest < 0) this.indentNest = 0;
288         return;
289     }
290
291     /**
292      * {@inheritDoc}
293      * @return {@inheritDoc}
294      */
295     @Override
296     public int getIndentLevel(){
297         return this.indentNest;
298     }
299
300     /**
301      * {@inheritDoc}
302      * @return {@inheritDoc}
303      * @throws IOException {@inheritDoc}
304      */
305     @Override
306     public XmlExporter ind() throws IOException{
307         int level = getIndentLevel();
308         for(int ct = 1; ct <= level; ct++){
309             putRawText(getIndentUnit());
310         }
311         return this;
312     }
313
314     /**
315      * {@inheritDoc}
316      * @return {@inheritDoc}
317      */
318     @Override
319     public boolean isBasicLatinOnlyOut(){
320         return this.basicLatinOnlyOut;
321     }
322
323     /**
324      * {@inheritDoc}
325      * @param bool {@inheritDoc}
326      */
327     @Override
328     public void setBasicLatinOnlyOut(boolean bool){
329         this.basicLatinOnlyOut = bool;
330         return;
331     }
332
333     /**
334      * {@inheritDoc}
335      * @param ch {@inheritDoc}
336      * @return {@inheritDoc}
337      * @throws IOException {@inheritDoc}
338      */
339     @Override
340     public XmlExporter putCharRef2Hex(char ch) throws IOException{
341         if(ch > MAX_OCTET) return putCharRef4Hex(ch);
342
343         int ibits = ch;   // 常に正なので符号拡張なし
344
345         int idx4 = ibits & MASK_1HEX;
346         ibits >>= HEX_EXP;
347         int idx3 = ibits & MASK_1HEX;
348
349         char hex3 = HEXCHAR_TABLE[idx3];
350         char hex4 = HEXCHAR_TABLE[idx4];
351
352         putRawText(REF_HEX).putRawCh(hex3).putRawCh(hex4)
353                            .putRawCh(';');
354
355         return this;
356     }
357
358     /**
359      * {@inheritDoc}
360      * @param ch {@inheritDoc}
361      * @return {@inheritDoc}
362      * @throws IOException {@inheritDoc}
363      */
364     @Override
365     public XmlExporter putCharRef4Hex(char ch) throws IOException{
366         int ibits = ch;   // 常に正なので符号拡張なし
367
368         int idx4 = ibits & MASK_1HEX;
369         ibits >>= HEX_EXP;
370         int idx3 = ibits & MASK_1HEX;
371         ibits >>= HEX_EXP;
372         int idx2 = ibits & MASK_1HEX;
373         ibits >>= HEX_EXP;
374         int idx1 = ibits & MASK_1HEX;
375
376         char hex1 = HEXCHAR_TABLE[idx1];
377         char hex2 = HEXCHAR_TABLE[idx2];
378         char hex3 = HEXCHAR_TABLE[idx3];
379         char hex4 = HEXCHAR_TABLE[idx4];
380
381         putRawText(REF_HEX).putRawCh(hex1).putRawCh(hex2)
382                            .putRawCh(hex3).putRawCh(hex4)
383                            .putRawCh(';');
384
385         return this;
386     }
387
388     /**
389      * {@inheritDoc}
390      * @param ch {@inheritDoc}
391      * @return {@inheritDoc}
392      * @throws IOException {@inheritDoc}
393      */
394     @Override
395     public XmlExporter putCh(char ch) throws IOException{
396         if(Character.isISOControl(ch)){
397             putCharRef2Hex(ch);
398             return this;
399         }
400
401         String escTxt;
402         switch(ch){
403         case '&':   escTxt = "&amp;";  break;
404         case CH_LT: escTxt = "&lt;";   break;
405         case CH_GT: escTxt = "&gt;";   break;
406         case CH_DQ: escTxt = "&quot;"; break;
407         case CH_SQ: escTxt = "&apos;"; break;
408         default:    escTxt = null;     break;
409         }
410
411         if(escTxt != null){
412             putRawText(escTxt);
413         }else{
414             putRawCh(ch);
415         }
416
417         return this;
418     }
419
420     /**
421      * {@inheritDoc}
422      * @param content {@inheritDoc}
423      * @return {@inheritDoc}
424      * @throws IOException {@inheritDoc}
425      */
426     @Override
427     public XmlExporter putContent(CharSequence content)
428             throws IOException{
429         int length = content.length();
430
431         char prev = '\0';
432         for(int pos = 0; pos < length; pos++){
433             char ch = content.charAt(pos);
434
435             if( isBasicLatinOnlyOut() && ! isBasicLatin(ch) ){
436                 putCharRef4Hex(ch);
437             }else if(ch == CH_YEN){
438                 putRawCh(CH_BSLASH);
439             }else if(Character.isSpaceChar(ch)){
440                 if(ch == CH_SP && prev != CH_SP){
441                     putRawCh(ch);
442                 }else{
443                     putCharRef2Hex(ch);
444                 }
445             }else{
446                 putCh(ch);
447             }
448
449             prev = ch;
450         }
451
452         return this;
453     }
454
455     /**
456      * {@inheritDoc}
457      * @param comment {@inheritDoc}
458      * @return {@inheritDoc}
459      * @throws IOException {@inheritDoc}
460      */
461     @Override
462     public XmlExporter putCommentContent(CharSequence comment)
463             throws IOException{
464         int length = comment.length();
465
466         char prev = '\0';
467         for(int pos = 0; pos < length; pos++){
468             char ch = comment.charAt(pos);
469
470             if(ch == '\n'){
471                 ln();
472             }else if('\u0000' <= ch && ch <= '\u001f'){
473                 putRawCh((char)('\u2400' + ch));
474             }else if(ch == '\u007f'){
475                 putRawCh('\u2421');
476             }else if(prev == '-' && ch == '-'){
477                 sp().putRawCh(ch);
478             }else{
479                 putRawCh(ch);
480             }
481
482             prev = ch;
483         }
484
485         return this;
486     }
487
488     /**
489      * {@inheritDoc}
490      * @param comment {@inheritDoc}
491      * @return {@inheritDoc}
492      * @throws IOException {@inheritDoc}
493      */
494     @Override
495     public XmlExporter putLineComment(CharSequence comment)
496             throws IOException{
497         putRawText(COMM_START).sp();
498         putCommentContent(comment);
499         sp().putRawText(COMM_END);
500         return this;
501     }
502
503     /**
504      * {@inheritDoc}
505      * @param comment {@inheritDoc}
506      * @return {@inheritDoc}
507      * @throws IOException {@inheritDoc}
508      */
509     @Override
510     public XmlExporter putBlockComment(CharSequence comment)
511             throws IOException{
512         putRawText(COMM_START).ln();
513
514         putCommentContent(comment);
515
516         int commentLength = comment.length();
517         if(commentLength > 0){
518             char lastCh = comment.charAt(commentLength - 1);
519             if(lastCh != '\n'){
520                 ln();
521             }
522         }
523
524         putRawText(COMM_END).ln();
525
526         return this;
527     }
528
529     /**
530      * {@inheritDoc}
531      * @param tagName {@inheritDoc}
532      * @return {@inheritDoc}
533      * @throws IOException {@inheritDoc}
534      */
535     @Override
536     public XmlExporter putOpenSTag(CharSequence tagName)
537             throws IOException{
538         putRawCh(CH_LT);
539         putRawText(tagName);
540         return this;
541     }
542
543     /**
544      * {@inheritDoc}
545      * @return {@inheritDoc}
546      * @throws IOException {@inheritDoc}
547      */
548     @Override
549     public XmlExporter putCloseSTag()
550             throws IOException{
551         putRawCh(CH_GT);
552         return this;
553     }
554
555     /**
556      * {@inheritDoc}
557      * @param tagName {@inheritDoc}
558      * @return {@inheritDoc}
559      * @throws IOException {@inheritDoc}
560      */
561     @Override
562     public XmlExporter putSimpleSTag(CharSequence tagName)
563             throws IOException{
564         putRawCh(CH_LT);
565         putRawText(tagName);
566         putRawCh(CH_GT);
567         return this;
568     }
569
570     /**
571      * {@inheritDoc}
572      * @param tagName {@inheritDoc}
573      * @return {@inheritDoc}
574      * @throws IOException {@inheritDoc}
575      */
576     @Override
577     public XmlExporter putETag(CharSequence tagName)
578             throws IOException{
579         putRawText("</");
580         putRawText(tagName);
581         putRawCh(CH_GT);
582         return this;
583     }
584
585     /**
586      * {@inheritDoc}
587      * @param tagName {@inheritDoc}
588      * @return {@inheritDoc}
589      * @throws IOException {@inheritDoc}
590      */
591     @Override
592     public XmlExporter putSimpleEmpty(CharSequence tagName)
593             throws IOException{
594         putRawCh(CH_LT);
595         putRawText(tagName).sp();
596         putCloseEmpty();
597         return this;
598     }
599
600     /**
601      * {@inheritDoc}
602      * @return {@inheritDoc}
603      * @throws IOException {@inheritDoc}
604      */
605     @Override
606     public XmlExporter putCloseEmpty()
607             throws IOException{
608         putRawText("/>");
609         return this;
610     }
611
612     /**
613      * {@inheritDoc}
614      * @param iVal {@inheritDoc}
615      * @return {@inheritDoc}
616      * @throws IOException {@inheritDoc}
617      */
618     @Override
619     public XmlExporter putXsdInt(int iVal) throws IOException{
620         String value = DatatypeConverter.printInt(iVal);
621         putRawText(value);
622         return this;
623     }
624
625     /**
626      * {@inheritDoc}
627      * @param fVal {@inheritDoc}
628      * @return {@inheritDoc}
629      * @throws IOException {@inheritDoc}
630      */
631     @Override
632     public XmlExporter putXsdFloat(float fVal) throws IOException{
633         String value = DatatypeConverter.printFloat(fVal);
634         value = chopFuzzyZero(value);
635         putRawText(value);
636         return this;
637     }
638
639     /**
640      * {@inheritDoc}
641      * @param attrName {@inheritDoc}
642      * @param iVal {@inheritDoc}
643      * @return {@inheritDoc}
644      * @throws IOException {@inheritDoc}
645      */
646     @Override
647     public XmlExporter putIntAttr(CharSequence attrName,
648                                         int iVal)
649             throws IOException{
650         putRawText(attrName).putRawCh(CH_EQ);
651
652         putRawCh(CH_DQ);
653         putXsdInt(iVal);
654         putRawCh(CH_DQ);
655
656         return this;
657     }
658
659     /**
660      * {@inheritDoc}
661      * @param attrName {@inheritDoc}
662      * @param fVal {@inheritDoc}
663      * @return {@inheritDoc}
664      * @throws IOException {@inheritDoc}
665      */
666     @Override
667     public XmlExporter putFloatAttr(CharSequence attrName,
668                                            float fVal)
669             throws IOException{
670         putRawText(attrName).putRawCh(CH_EQ);
671
672         putRawCh(CH_DQ);
673         putXsdFloat(fVal);
674         putRawCh(CH_DQ);
675
676         return this;
677     }
678
679     /**
680      * {@inheritDoc}
681      * @param attrName {@inheritDoc}
682      * @param content {@inheritDoc}
683      * @return {@inheritDoc}
684      * @throws IOException {@inheritDoc}
685      */
686     @Override
687     public XmlExporter putAttr(CharSequence attrName,
688                                      CharSequence content)
689             throws IOException{
690         putRawText(attrName).putRawCh(CH_EQ);
691
692         putRawCh(CH_DQ);
693         putContent(content);
694         putRawCh(CH_DQ);
695
696         return this;
697     }
698
699 }