4 * License : The MIT License
5 * Copyright(c) 2010 MikuToga Partners
8 package jp.sourceforge.mikutoga.xml;
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;
25 public class BasicXmlExporter {
28 private static final Charset CS_UTF8 = Charset.forName("UTF-8");
31 private static final String DEF_NL = "\n"; // 0x0a(LF)
33 private static final String DEF_INDENT_UNIT = "\u0020\u0020"; // ␣␣
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; // '
41 private static final String COMM_START = "<!--";
42 private static final String COMM_END = "-->";
43 private static final String REF_HEX = "&#x";
45 private static final Pattern NUM_FUZZY =
46 Pattern.compile("([^.]*\\.[0-9][0-9]*?)0+");
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',
58 assert HEX_EXP * 2 == Byte.SIZE;
59 assert HEXCHAR_TABLE.length == (1 << HEX_EXP);
63 private final Appendable appendable;
65 private String newline = DEF_NL;
66 private String indentUnit = DEF_INDENT_UNIT;
68 private int indentNest = 0;
69 private boolean basicLatinOnlyOut = true;
74 * 文字エンコーディングはUTF-8が用いられる。
75 * @param stream 出力ストリーム
77 public BasicXmlExporter(OutputStream stream){
78 this(stream, CS_UTF8);
84 * @param stream 出力ストリーム
85 * @param charSet 文字エンコーディング指定
87 public BasicXmlExporter(OutputStream stream, Charset charSet){
90 new OutputStreamWriter(stream, charSet)
98 * @param appendable 文字列出力
100 public BasicXmlExporter(Appendable appendable){
102 this.appendable = appendable;
108 * ASCIIコード相当(UCS:Basic-Latin)の文字か否か判定する。
109 * <p>※ Basic-Latinには各種制御文字も含まれる。
111 * @return Basic-Latin文字ならtrue
113 public static boolean isBasicLatin(char ch){
114 if('\u0000' <= ch && ch <= '\u007f'){
122 * <p>DatatypeConverterにおけるJDK1.6系と1.7系の仕様変更を吸収する。
123 * <p>0.001fは"0.0010"ではなく"0.001"と出力される。
126 * @return 冗長桁が抑止された実数表記
128 public static String chopFuzzyZero(String numTxt){
131 Matcher matcher = NUM_FUZZY.matcher(numTxt);
132 if(matcher.matches()){
133 result = matcher.group(1);
143 * BasicLatin文字だけで出力するか設定する。
144 * <p>BasicLatin以外の文字(≒日本語)を、そのまま出力するか、
147 * @param bool BasicLatin文字だけで出力するならtrue
149 public void setBasicLatinOnlyOut(boolean bool){
150 this.basicLatinOnlyOut = bool;
155 * BasicLatin文字だけを出力する状態か判定する。
157 * @return BasicLatin文字だけで出力するならtrue
159 public boolean isBasicLatinOnlyOut(){
160 return this.basicLatinOnlyOut;
165 * @param newLine 改行文字列
166 * @throws NullPointerException 引数がnull
168 public void setNewLine(String newLine) throws NullPointerException{
169 if(newLine == null) throw new NullPointerException();
170 this.newline = newLine;
178 public String getNewLine(){
185 * @param indUnit インデント単位文字列。
186 * @throws NullPointerException 引数がnull
188 public void setIndentUnit(String indUnit) throws NullPointerException{
189 if(indUnit == null) throw new NullPointerException();
190 this.indentUnit = indUnit;
198 public String getIndentUnit(){
199 return this.indentUnit;
204 * @throws IOException 出力エラー
206 public void flush() throws IOException{
207 if(this.appendable instanceof Flushable){
208 ((Flushable)this.appendable).flush();
215 * @throws IOException 出力エラー
217 public void close() throws IOException{
218 if(this.appendable instanceof Closeable){
219 ((Closeable)this.appendable).close();
228 * @throws IOException 出力エラー
230 public BasicXmlExporter putRawCh(char ch) throws IOException{
231 this.appendable.append(ch);
239 * @throws IOException 出力エラー
241 public BasicXmlExporter putRawText(CharSequence seq) throws IOException{
242 this.appendable.append(seq);
247 * 指定された文字を16進2桁の文字参照形式で出力する。
248 * 2桁で出力できない場合(>0x00ff)は4桁で出力する。
251 * @throws IOException 出力エラー
253 public BasicXmlExporter putCharRef2Hex(char ch) throws IOException{
254 if(ch > MAX_OCTET) return putCharRef4Hex(ch);
256 int ibits = ch; // 常に正なので符号拡張なし
258 int idx4 = ibits & MASK_1HEX;
260 int idx3 = ibits & MASK_1HEX;
262 char hex3 = HEXCHAR_TABLE[idx3];
263 char hex4 = HEXCHAR_TABLE[idx4];
265 putRawText(REF_HEX).putRawCh(hex3).putRawCh(hex4)
272 * 指定された文字を16進4桁の文字参照形式で出力する。
273 * UCS4に伴うサロゲートペアは未サポート
276 * @throws IOException 出力エラー
278 public BasicXmlExporter putCharRef4Hex(char ch) throws IOException{
279 int ibits = ch; // 常に正なので符号拡張なし
281 int idx4 = ibits & MASK_1HEX;
283 int idx3 = ibits & MASK_1HEX;
285 int idx2 = ibits & MASK_1HEX;
287 int idx1 = ibits & MASK_1HEX;
289 char hex1 = HEXCHAR_TABLE[idx1];
290 char hex2 = HEXCHAR_TABLE[idx2];
291 char hex3 = HEXCHAR_TABLE[idx3];
292 char hex4 = HEXCHAR_TABLE[idx4];
294 putRawText(REF_HEX).putRawCh(hex1).putRawCh(hex2)
295 .putRawCh(hex3).putRawCh(hex4)
302 * 要素の中身および属性値中身を出力する。
303 * <p>XMLの構文規則を守る上で必要な各種エスケープ処理が行われる。
306 * @throws IOException 出力エラー
308 public BasicXmlExporter putCh(char ch) throws IOException{
309 if(Character.isISOControl(ch)){
316 case '&': escTxt = "&"; break;
317 case '<': escTxt = "<"; break;
318 case '>': escTxt = ">"; break;
319 case CH_DQ: escTxt = """; break;
320 case CH_SQ: escTxt = "'"; break;
321 default: escTxt = null; break;
334 * 要素の中身および属性値中身を出力する。
335 * <p>必要に応じてXML定義済み実体文字が割り振られた文字、
336 * コントロールコード、および非BasicLatin文字がエスケープされる。
337 * <p>半角通貨記号U+00A5はバックスラッシュU+005Cに置換される。
338 * <p>連続するスペースU+0020の2文字目以降は文字参照化される。
339 * <p>全角スペースその他空白文字は無条件に文字参照化される。
342 * @throws IOException 出力エラー
344 public BasicXmlExporter putContent(CharSequence content)
346 int length = content.length();
349 for(int pos = 0; pos < length; pos++){
350 char ch = content.charAt(pos);
352 if( isBasicLatinOnlyOut() && ! isBasicLatin(ch) ){
354 }else if(ch == CH_YEN){
356 }else if(Character.isSpaceChar(ch)){
357 if(ch == CH_SP && prev != CH_SP){
375 * @throws IOException 出力エラー
377 public BasicXmlExporter ln() throws IOException{
378 this.appendable.append(this.newline);
384 * @param count 改行回数。0以下の場合は何も出力しない。
386 * @throws IOException 出力エラー
388 public BasicXmlExporter ln(int count) throws IOException{
389 for(int ct = 1; ct <= count; ct++){
390 this.appendable.append(this.newline);
398 * @throws IOException 出力エラー
400 public BasicXmlExporter sp() throws IOException{
401 this.appendable.append(CH_SP);
407 * @param count 空白回数。0以下の場合は何も出力しない。
409 * @throws IOException 出力エラー
411 public BasicXmlExporter sp(int count) throws IOException{
412 for(int ct = 1; ct <= count; ct++){
413 this.appendable.append(CH_SP);
420 * インデント単位文字列をネストレベル回数分出力する。
422 * @throws IOException 出力エラー
424 public BasicXmlExporter ind() throws IOException{
425 for(int ct = 1; ct <= this.indentNest; ct++){
426 putRawText(this.indentUnit);
434 public void pushNest(){
441 * インデントレベル0の状態をさらに上げようとした場合、何も起こらない。
443 public void popNest(){
445 if(this.indentNest < 0) this.indentNest = 0;
450 * int値をXMLスキーマ準拠の形式で出力する。
453 * @throws IOException 出力エラー
454 * @see java.lang.Integer#toString(int)
456 public BasicXmlExporter putXsdInt(int iVal) throws IOException{
457 String value = DatatypeConverter.printInt(iVal);
458 this.appendable.append(value);
463 * float値をXMLスキーマ準拠の形式で出力する。
466 * @throws IOException 出力エラー
467 * @see java.lang.Float#toString(float)
469 public BasicXmlExporter putXsdFloat(float fVal) throws IOException{
470 String value = DatatypeConverter.printFloat(fVal);
471 this.appendable.append(value);
477 * @param attrName 属性名
478 * @param content 属性内容
480 * @throws IOException 出力エラー
482 public BasicXmlExporter putAttr(CharSequence attrName,
483 CharSequence content)
485 putRawText(attrName).putRawCh('=');
496 * @param attrName 属性名
499 * @throws IOException 出力エラー
501 public BasicXmlExporter putIntAttr(CharSequence attrName,
504 String attrValue = DatatypeConverter.printInt(iVal);
505 putAttr(attrName, attrValue);
511 * @param attrName 属性名
514 * @throws IOException 出力エラー
516 public BasicXmlExporter putFloatAttr(CharSequence attrName,
519 String attrValue = DatatypeConverter.printFloat(fVal);
520 attrValue = chopFuzzyZero(attrValue);
521 putAttr(attrName, attrValue);
527 * <p>コメント中の'\n'記号出現に伴い、
528 * あらかじめ指定された改行文字が出力される。
529 * <p>コメント中の'\n'以外のコントロールコードは
530 * Control Pictures(U+2400〜)で代替される。
531 * <p>それ以外の非BasicLatin文字はそのまま出力される。
532 * <p>連続するハイフン(-)記号間には強制的にスペースが挿入される。
533 * @param comment コメント内容
535 * @throws IOException 出力エラー
537 public BasicXmlExporter putCommentContent(CharSequence comment)
539 int length = comment.length();
542 for(int pos = 0; pos < length; pos++){
543 char ch = comment.charAt(pos);
547 }else if('\u0000' <= ch && ch <= '\u001f'){
548 putRawCh((char)('\u2400' + ch));
549 }else if(ch == '\u007f'){
551 }else if(prev == '-' && ch == '-'){
565 * コメント内部の頭及び末尾に空白が1つ挿入される。
566 * @param comment コメント内容
568 * @throws IOException 出力エラー
570 public BasicXmlExporter putLineComment(CharSequence comment)
572 putRawText(COMM_START).sp();
573 putCommentContent(comment);
574 sp().putRawText(COMM_END);
580 * <p>コメント内部の頭の前に改行が出力される。
581 * <p>コメント内部の末尾が改行でない場合、改行が挿入される。
582 * <p>ブロックコメント末尾は改行で終わる。
584 * @param comment コメント内容
586 * @throws IOException 出力エラー
588 public BasicXmlExporter putBlockComment(CharSequence comment)
590 putRawText(COMM_START).ln();
592 putCommentContent(comment);
594 int commentLength = comment.length();
595 if(commentLength > 0){
596 char lastCh = comment.charAt(commentLength - 1);
602 putRawText(COMM_END).ln();