OSDN Git Service

8344b4cb2205093e2e4f9d049b6e9a05d0310c9c
[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  * 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      * @throws IOException {@inheritDoc}
113      */
114     @Override
115     public abstract void flush() throws IOException;
116
117     /**
118      * {@inheritDoc}
119      * @throws IOException {@inheritDoc}
120      */
121     @Override
122     public abstract void close() throws IOException;
123
124     /**
125      * {@inheritDoc}
126      * @param ch {@inheritDoc}
127      * @return {@inheritDoc}
128      * @throws IOException {@inheritDoc}
129      */
130     @Override
131     public abstract XmlExporter putRawCh(char ch)
132             throws IOException;
133
134     /**
135      * {@inheritDoc}
136      * @param seq {@inheritDoc}
137      * @return {@inheritDoc}
138      * @throws IOException {@inheritDoc}
139      */
140     @Override
141     public abstract XmlExporter putRawText(CharSequence seq)
142             throws IOException;
143
144
145     /**
146      * {@inheritDoc}
147      * @return {@inheritDoc}
148      * @throws IOException {@inheritDoc}
149      */
150     @Override
151     public XmlExporter sp() throws IOException{
152         putRawCh(CH_SP);
153         return this;
154     }
155
156     /**
157      * {@inheritDoc}
158      * @param count {@inheritDoc}
159      * @return {@inheritDoc}
160      * @throws IOException {@inheritDoc}
161      */
162     @Override
163     public XmlExporter sp(int count) throws IOException{
164         for(int ct = 1; ct <= count; ct++){
165             sp();
166         }
167         return this;
168     }
169
170     /**
171      * {@inheritDoc}
172      * @return {@inheritDoc}
173      */
174     @Override
175     public String getNewLine(){
176         return this.newline;
177     }
178
179     /**
180      * {@inheritDoc}
181      * @param newLine {@inheritDoc}
182      * @throws NullPointerException {@inheritDoc}
183      */
184     @Override
185     public void setNewLine(String newLine) throws NullPointerException{
186         if(newLine == null) throw new NullPointerException();
187         this.newline = newLine;
188         return;
189     }
190
191     /**
192      * {@inheritDoc}
193      * @return {@inheritDoc}
194      * @throws IOException {@inheritDoc}
195      */
196     @Override
197     public XmlExporter ln() throws IOException{
198         putRawText(getNewLine());
199         return this;
200     }
201
202     /**
203      * {@inheritDoc}
204      * @param count {@inheritDoc}
205      * @return {@inheritDoc}
206      * @throws IOException {@inheritDoc}
207      */
208     @Override
209     public XmlExporter ln(int count) throws IOException{
210         for(int ct = 1; ct <= count; ct++){
211             ln();
212         }
213         return this;
214     }
215
216     /**
217      * {@inheritDoc}
218      * @return {@inheritDoc}
219      */
220     @Override
221     public String getIndentUnit(){
222         return this.indentUnit;
223     }
224
225     /**
226      * {@inheritDoc}
227      * @param indUnit {@inheritDoc}
228      * @throws NullPointerException {@inheritDoc}
229      */
230     @Override
231     public void setIndentUnit(String indUnit) throws NullPointerException{
232         if(indUnit == null) throw new NullPointerException();
233         this.indentUnit = indUnit;
234         return;
235     }
236
237     /**
238      * {@inheritDoc}
239      */
240     @Override
241     public void pushNest(){
242         this.indentNest++;
243         return;
244     }
245
246     /**
247      * {@inheritDoc}
248      */
249     @Override
250     public void popNest(){
251         this.indentNest--;
252         if(this.indentNest < 0) this.indentNest = 0;
253         return;
254     }
255
256     /**
257      * {@inheritDoc}
258      * @return {@inheritDoc}
259      */
260     @Override
261     public int getIndentLevel(){
262         return this.indentNest;
263     }
264
265     /**
266      * {@inheritDoc}
267      * @return {@inheritDoc}
268      * @throws IOException {@inheritDoc}
269      */
270     @Override
271     public XmlExporter ind() throws IOException{
272         int level = getIndentLevel();
273         for(int ct = 1; ct <= level; ct++){
274             putRawText(getIndentUnit());
275         }
276         return this;
277     }
278
279     /**
280      * {@inheritDoc}
281      * @return {@inheritDoc}
282      */
283     @Override
284     public boolean isBasicLatinOnlyOut(){
285         return this.basicLatinOnlyOut;
286     }
287
288     /**
289      * {@inheritDoc}
290      * @param bool {@inheritDoc}
291      */
292     @Override
293     public void setBasicLatinOnlyOut(boolean bool){
294         this.basicLatinOnlyOut = bool;
295         return;
296     }
297
298     /**
299      * {@inheritDoc}
300      * @param ch {@inheritDoc}
301      * @return {@inheritDoc}
302      * @throws IOException {@inheritDoc}
303      */
304     @Override
305     public XmlExporter putCharRef2Hex(char ch) throws IOException{
306         if(ch > MAX_OCTET) return putCharRef4Hex(ch);
307
308         int ibits = ch;   // 常に正なので符号拡張なし
309
310         int idx4 = ibits & MASK_1HEX;
311         ibits >>= HEX_EXP;
312         int idx3 = ibits & MASK_1HEX;
313
314         char hex3 = HEXCHAR_TABLE[idx3];
315         char hex4 = HEXCHAR_TABLE[idx4];
316
317         putRawText(REF_HEX).putRawCh(hex3).putRawCh(hex4)
318                            .putRawCh(';');
319
320         return this;
321     }
322
323     /**
324      * {@inheritDoc}
325      * @param ch {@inheritDoc}
326      * @return {@inheritDoc}
327      * @throws IOException {@inheritDoc}
328      */
329     @Override
330     public XmlExporter putCharRef4Hex(char ch) throws IOException{
331         int ibits = ch;   // 常に正なので符号拡張なし
332
333         int idx4 = ibits & MASK_1HEX;
334         ibits >>= HEX_EXP;
335         int idx3 = ibits & MASK_1HEX;
336         ibits >>= HEX_EXP;
337         int idx2 = ibits & MASK_1HEX;
338         ibits >>= HEX_EXP;
339         int idx1 = ibits & MASK_1HEX;
340
341         char hex1 = HEXCHAR_TABLE[idx1];
342         char hex2 = HEXCHAR_TABLE[idx2];
343         char hex3 = HEXCHAR_TABLE[idx3];
344         char hex4 = HEXCHAR_TABLE[idx4];
345
346         putRawText(REF_HEX).putRawCh(hex1).putRawCh(hex2)
347                            .putRawCh(hex3).putRawCh(hex4)
348                            .putRawCh(';');
349
350         return this;
351     }
352
353     /**
354      * {@inheritDoc}
355      * @param ch {@inheritDoc}
356      * @return {@inheritDoc}
357      * @throws IOException {@inheritDoc}
358      */
359     @Override
360     public XmlExporter putCh(char ch) throws IOException{
361         if(Character.isISOControl(ch)){
362             putCharRef2Hex(ch);
363             return this;
364         }
365
366         String escTxt;
367         switch(ch){
368         case '&':   escTxt = "&amp;";  break;
369         case CH_LT: escTxt = "&lt;";   break;
370         case CH_GT: escTxt = "&gt;";   break;
371         case CH_DQ: escTxt = "&quot;"; break;
372         case CH_SQ: escTxt = "&apos;"; break;
373         default:    escTxt = null;     break;
374         }
375
376         if(escTxt != null){
377             putRawText(escTxt);
378         }else{
379             putRawCh(ch);
380         }
381
382         return this;
383     }
384
385     /**
386      * {@inheritDoc}
387      * @param content {@inheritDoc}
388      * @return {@inheritDoc}
389      * @throws IOException {@inheritDoc}
390      */
391     @Override
392     public XmlExporter putContent(CharSequence content)
393             throws IOException{
394         int length = content.length();
395
396         char prev = '\0';
397         for(int pos = 0; pos < length; pos++){
398             char ch = content.charAt(pos);
399
400             if( isBasicLatinOnlyOut() && ! isBasicLatin(ch) ){
401                 putCharRef4Hex(ch);
402             }else if(ch == CH_YEN){
403                 putRawCh(CH_BSLASH);
404             }else if(Character.isSpaceChar(ch)){
405                 if(ch == CH_SP && prev != CH_SP){
406                     putRawCh(ch);
407                 }else{
408                     putCharRef2Hex(ch);
409                 }
410             }else{
411                 putCh(ch);
412             }
413
414             prev = ch;
415         }
416
417         return this;
418     }
419
420     /**
421      * {@inheritDoc}
422      * @param comment {@inheritDoc}
423      * @return {@inheritDoc}
424      * @throws IOException {@inheritDoc}
425      */
426     @Override
427     public XmlExporter 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
435             if(ch == '\n'){
436                 ln();
437             }else if('\u0000' <= ch && ch <= '\u001f'){
438                 putRawCh((char)('\u2400' + ch));
439             }else if(ch == '\u007f'){
440                 putRawCh('\u2421');
441             }else if(prev == '-' && ch == '-'){
442                 sp().putRawCh(ch);
443             }else{
444                 putRawCh(ch);
445             }
446
447             prev = ch;
448         }
449
450         return this;
451     }
452
453     /**
454      * {@inheritDoc}
455      * @param comment {@inheritDoc}
456      * @return {@inheritDoc}
457      * @throws IOException {@inheritDoc}
458      */
459     @Override
460     public XmlExporter putLineComment(CharSequence comment)
461             throws IOException{
462         putRawText(COMM_START).sp();
463         putCommentContent(comment);
464         sp().putRawText(COMM_END);
465         return this;
466     }
467
468     /**
469      * {@inheritDoc}
470      * @param comment {@inheritDoc}
471      * @return {@inheritDoc}
472      * @throws IOException {@inheritDoc}
473      */
474     @Override
475     public XmlExporter putBlockComment(CharSequence comment)
476             throws IOException{
477         putRawText(COMM_START).ln();
478
479         putCommentContent(comment);
480
481         int commentLength = comment.length();
482         if(commentLength > 0){
483             char lastCh = comment.charAt(commentLength - 1);
484             if(lastCh != '\n'){
485                 ln();
486             }
487         }
488
489         putRawText(COMM_END).ln();
490
491         return this;
492     }
493
494     /**
495      * {@inheritDoc}
496      * @param tagName {@inheritDoc}
497      * @return {@inheritDoc}
498      * @throws IOException {@inheritDoc}
499      */
500     @Override
501     public XmlExporter putOpenSTag(CharSequence tagName)
502             throws IOException{
503         putRawCh(CH_LT);
504         putRawText(tagName);
505         return this;
506     }
507
508     /**
509      * {@inheritDoc}
510      * @return {@inheritDoc}
511      * @throws IOException {@inheritDoc}
512      */
513     @Override
514     public XmlExporter putCloseSTag()
515             throws IOException{
516         putRawCh(CH_GT);
517         return this;
518     }
519
520     /**
521      * {@inheritDoc}
522      * @param tagName {@inheritDoc}
523      * @return {@inheritDoc}
524      * @throws IOException {@inheritDoc}
525      */
526     @Override
527     public XmlExporter putSimpleSTag(CharSequence tagName)
528             throws IOException{
529         putRawCh(CH_LT);
530         putRawText(tagName);
531         putRawCh(CH_GT);
532         return this;
533     }
534
535     /**
536      * {@inheritDoc}
537      * @param tagName {@inheritDoc}
538      * @return {@inheritDoc}
539      * @throws IOException {@inheritDoc}
540      */
541     @Override
542     public XmlExporter putETag(CharSequence tagName)
543             throws IOException{
544         putRawText("</");
545         putRawText(tagName);
546         putRawCh(CH_GT);
547         return this;
548     }
549
550     /**
551      * {@inheritDoc}
552      * @param tagName {@inheritDoc}
553      * @return {@inheritDoc}
554      * @throws IOException {@inheritDoc}
555      */
556     @Override
557     public XmlExporter putSimpleEmpty(CharSequence tagName)
558             throws IOException{
559         putRawCh(CH_LT);
560         putRawText(tagName).sp();
561         putCloseEmpty();
562         return this;
563     }
564
565     /**
566      * {@inheritDoc}
567      * @return {@inheritDoc}
568      * @throws IOException {@inheritDoc}
569      */
570     @Override
571     public XmlExporter putCloseEmpty()
572             throws IOException{
573         putRawText("/>");
574         return this;
575     }
576
577     /**
578      * {@inheritDoc}
579      * @param iVal {@inheritDoc}
580      * @return {@inheritDoc}
581      * @throws IOException {@inheritDoc}
582      */
583     @Override
584     public XmlExporter putXsdInt(int iVal) throws IOException{
585         String value = DatatypeConverter.printInt(iVal);
586         putRawText(value);
587         return this;
588     }
589
590     /**
591      * {@inheritDoc}
592      * @param fVal {@inheritDoc}
593      * @return {@inheritDoc}
594      * @throws IOException {@inheritDoc}
595      */
596     @Override
597     public XmlExporter putXsdFloat(float fVal) throws IOException{
598         String value = DatatypeConverter.printFloat(fVal);
599         value = chopFuzzyZero(value);
600         putRawText(value);
601         return this;
602     }
603
604     /**
605      * {@inheritDoc}
606      * @param attrName {@inheritDoc}
607      * @param iVal {@inheritDoc}
608      * @return {@inheritDoc}
609      * @throws IOException {@inheritDoc}
610      */
611     @Override
612     public XmlExporter putIntAttr(CharSequence attrName,
613                                         int iVal)
614             throws IOException{
615         putRawText(attrName).putRawCh(CH_EQ);
616
617         putRawCh(CH_DQ);
618         putXsdInt(iVal);
619         putRawCh(CH_DQ);
620
621         return this;
622     }
623
624     /**
625      * {@inheritDoc}
626      * @param attrName {@inheritDoc}
627      * @param fVal {@inheritDoc}
628      * @return {@inheritDoc}
629      * @throws IOException {@inheritDoc}
630      */
631     @Override
632     public XmlExporter putFloatAttr(CharSequence attrName,
633                                            float fVal)
634             throws IOException{
635         putRawText(attrName).putRawCh(CH_EQ);
636
637         putRawCh(CH_DQ);
638         putXsdFloat(fVal);
639         putRawCh(CH_DQ);
640
641         return this;
642     }
643
644     /**
645      * {@inheritDoc}
646      * @param attrName {@inheritDoc}
647      * @param content {@inheritDoc}
648      * @return {@inheritDoc}
649      * @throws IOException {@inheritDoc}
650      */
651     @Override
652     public XmlExporter putAttr(CharSequence attrName,
653                                      CharSequence content)
654             throws IOException{
655         putRawText(attrName).putRawCh(CH_EQ);
656
657         putRawCh(CH_DQ);
658         putContent(content);
659         putRawCh(CH_DQ);
660
661         return this;
662     }
663
664 }