OSDN Git Service

Merge release/v1.101.104
[jovsonz/Jovsonz.git] / src / main / java / jp / sourceforge / jovsonz / JsString.java
1 /*
2  * JSON string value
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.sourceforge.jovsonz;
9
10 import java.io.IOException;
11
12 /**
13  * JSON STRING型Valueを表す。
14  * Unicode文字列データを反映する。
15  * <h1>表記例</h1>
16  * <pre>
17  * "xyz"
18  * "漢"
19  * "foo\nbar"
20  * "{@literal \}u304a"
21  * ""
22  * </pre>
23  */
24 public class JsString
25         implements JsValue, CharSequence, Comparable<JsString> {
26
27     private static final int HEX_BASE = 16;
28     private static final int NIBBLE_WIDE = 4;
29     private static final int NIBBLES_CHAR = Character.SIZE / NIBBLE_WIDE;
30
31     private static final String ERRMSG_INVESC = "invalid escape character";
32     private static final String ERRMSG_INVCTR = "invalid control character";
33
34     private final String rawText;
35
36     /**
37      * コンストラクタ。
38      * 長さ0の空文字が設定される。
39      */
40     public JsString(){
41         this("");
42         return;
43     }
44
45     /**
46      * コンストラクタ。
47      * 引数はJSON書式ではない生文字列。
48      * @param rawSeq 生文字列
49      * @throws NullPointerException 引数がnull
50      */
51     public JsString(CharSequence rawSeq) throws NullPointerException{
52         super();
53         if(rawSeq == null) throw new NullPointerException();
54         this.rawText = rawSeq.toString();
55         return;
56     }
57
58     /**
59      * FFFF形式4桁で16進エスケープされた文字列を読み、
60      * char1文字にデコードする。
61      * @param source 文字列ソース
62      * @return 文字
63      * @throws IOException 入力エラー
64      * @throws JsParseException 不正表記もしくは意図しない入力終了
65      */
66     static char parseHexChar(JsonSource source)
67             throws IOException, JsParseException{
68         char hex1Ch = source.readOrDie();
69         char hex2Ch = source.readOrDie();
70         char hex3Ch = source.readOrDie();
71         char hex4Ch = source.readOrDie();
72
73         int digit1 = Character.digit(hex1Ch, HEX_BASE);
74         int digit2 = Character.digit(hex2Ch, HEX_BASE);
75         int digit3 = Character.digit(hex3Ch, HEX_BASE);
76         int digit4 = Character.digit(hex4Ch, HEX_BASE);
77
78         if(   digit1 < 0
79            || digit2 < 0
80            || digit3 < 0
81            || digit4 < 0 ){
82             throw new JsParseException(ERRMSG_INVESC, source.getLineNumber());
83         }
84
85         int digit = 0;
86         digit += digit1;
87         digit <<= NIBBLE_WIDE;
88         digit += digit2;
89         digit <<= NIBBLE_WIDE;
90         digit += digit3;
91         digit <<= NIBBLE_WIDE;
92         digit += digit4;
93
94         char result = (char) digit;
95
96         return result;
97     }
98
99     /**
100      * '\'に続くスペシャルキャラの読み込みを行う。
101      * @param source 文字列ソース
102      * @param app スペシャルキャラ格納文字列
103      * @throws IOException 入出力エラー
104      * @throws JsParseException "\z"などの不正なスペシャルキャラ
105      * もしくは意図しない入力終了
106      */
107     private static void parseSpecial(JsonSource source, Appendable app)
108             throws IOException, JsParseException{
109         char special;
110
111         char chData = source.readOrDie();
112         switch(chData){
113         case '"':  special = '"';  break;
114         case '\\': special = '\\'; break;
115         case '/':  special = '/';  break;
116         case 'b':  special = '\b'; break;
117         case 'f':  special = '\f'; break;
118         case 'n':  special = '\n'; break;
119         case 'r':  special = '\r'; break;
120         case 't':  special = '\t'; break;
121         case 'u':  special = parseHexChar(source); break;
122         default:
123             throw new JsParseException(ERRMSG_INVESC, source.getLineNumber());
124         }
125
126         app.append(special);
127
128         return;
129     }
130
131     /**
132      * JSON文字列ソースからSTRING型Valueを読み込む。
133      * 別型の可能性のある先頭文字を読み込んだ場合、
134      * ソースに文字を読み戻した後nullが返される。
135      * @param source 文字列ソース
136      * @return STRING型Value。別型の可能性がある場合はnull。
137      * @throws IOException 入力エラー
138      * @throws JsParseException 不正な表記もしくは意図しない入力終了
139      */
140     static JsString parseString(JsonSource source)
141             throws IOException, JsParseException{
142         char charHead = source.readOrDie();
143         if(charHead != '"'){
144             source.unread(charHead);
145             return null;
146         }
147
148         StringBuilder text = new StringBuilder();
149
150         for(;;){
151             char chData = source.readOrDie();
152             if(chData == '"') break;
153
154             if(chData == '\\'){
155                 parseSpecial(source, text);
156             }else if(Character.isISOControl(chData)){
157                 throw new JsParseException(ERRMSG_INVCTR,
158                                            source.getLineNumber());
159             }else{
160                 text.append(chData);
161             }
162         }
163
164         JsString result = new JsString(text);
165
166         return result;
167     }
168
169     /**
170      * 任意の文字からエスケープ出力用シンボルを得る。
171      * このシンボルは'\'に続けて用いられる1文字である。
172      * 'u'を返す事はありえない。
173      * @param ch 任意の文字
174      * @return エスケープ出力用シンボル。
175      * 1文字エスケープの必要がない場合は'\0'
176      */
177     private static char escapeSymbol(char ch){
178         char result;
179         switch(ch){
180         case '"' : result = '"';  break;
181         case '\\': result = '\\'; break;
182         case '/' : result = '/';  break;
183         case '\b': result = 'b';  break;
184         case '\f': result = 'f';  break;
185         case '\n': result = 'n';  break;
186         case '\r': result = 'r';  break;
187         case '\t': result = 't';  break;
188         default:   result = '\0'; break;
189         }
190         return result;
191     }
192
193     /**
194      * 特殊文字をエスケープ出力する。
195      * 特殊文字でなければなにもしない。
196      * @param appout 出力先
197      * @param ch 文字
198      * @return 特殊文字出力がエスケープされた時にtrue
199      * @throws IOException 出力エラー
200      */
201     private static boolean dumpSpecialChar(Appendable appout, char ch)
202             throws IOException{
203         char esc1ch = escapeSymbol(ch);
204
205         if(esc1ch != '\0'){
206             appout.append('\\').append(esc1ch);
207         }else if(Character.isISOControl(ch)){
208             // TODO さらなる高速化が必要
209             String hex = "0000" + Integer.toHexString(ch);
210             hex = hex.substring(hex.length() - NIBBLES_CHAR);
211             appout.append("\\u").append(hex);
212         }else{
213             return false;
214         }
215
216         return true;
217     }
218
219     /**
220      * JSON STRING型Value形式で文字列を出力する。
221      * @param appout 文字出力
222      * @param seq 文字列
223      * @throws IOException 出力エラー
224      */
225     public static void dumpString(Appendable appout, CharSequence seq)
226             throws IOException{
227         appout.append('"');
228
229         int length = seq.length();
230         for(int pos = 0; pos < length; pos++){
231             char ch = seq.charAt(pos);
232             if( ! dumpSpecialChar(appout, ch) ){
233                 appout.append(ch);
234             }
235         }
236
237         appout.append('"');
238
239         return;
240     }
241
242     /**
243      * JSON STRING型Value形式の文字列を返す。
244      * @param seq 生文字列
245      * @return STRING型表記に変換された文字列
246      */
247     // TODO いらない
248     public static StringBuilder escapeText(CharSequence seq){
249         StringBuilder result = new StringBuilder();
250         try{
251             dumpString(result, seq);
252         }catch(IOException e){
253             assert false;
254             throw new AssertionError(e);
255         }
256         return result;
257     }
258
259     /**
260      * {@inheritDoc}
261      * 常に{@link JsTypes#STRING}を返す。
262      * @return {@inheritDoc}
263      */
264     @Override
265     public JsTypes getJsTypes(){
266         return JsTypes.STRING;
267     }
268
269     /**
270      * 各種構造の出現をビジターに通知する。
271      * この実装ではthisの出現のみを通知する。
272      * @param visitor {@inheritDoc}
273      * @throws JsVisitException {@inheritDoc}
274      */
275     @Override
276     public void traverse(ValueVisitor visitor)
277             throws JsVisitException{
278         visitor.visitValue(this);
279         return;
280     }
281
282     /**
283      * {@inheritDoc}
284      * ハッシュ値を返す。
285      * @return {@inheritDoc}
286      */
287     @Override
288     public int hashCode(){
289         return this.rawText.hashCode();
290     }
291
292     /**
293      * {@inheritDoc}
294      * 等価判定を行う。
295      * {@link java.lang.String#equals(Object)}に準ずる。
296      * @param obj {@inheritDoc}
297      * @return {@inheritDoc}
298      */
299     @Override
300     public boolean equals(Object obj){
301         if(this == obj) return true;
302
303         if( ! (obj instanceof JsString) ) return false;
304         JsString string = (JsString) obj;
305
306         return this.rawText.equals(string.rawText);
307     }
308
309     /**
310      * {@inheritDoc}
311      * STRING型Valueを昇順に順序付ける。
312      * {@link java.lang.String#compareTo(String)}に準ずる。
313      * @param value {@inheritDoc}
314      * @return {@inheritDoc}
315      */
316     @Override
317     public int compareTo(JsString value){
318         if(this == value) return 0;
319         if(value == null) return +1;
320         return this.rawText.compareTo(value.rawText);
321     }
322
323     /**
324      * {@inheritDoc}
325      * 指定位置の文字を返す。
326      * @param index {@inheritDoc}
327      * @return {@inheritDoc}
328      * @throws IndexOutOfBoundsException {@inheritDoc}
329      */
330     @Override
331     public char charAt(int index)
332             throws IndexOutOfBoundsException{
333         return this.rawText.charAt(index);
334     }
335
336     /**
337      * {@inheritDoc}
338      * 文字列長(char値総数)を返す。
339      * @return {@inheritDoc}
340      */
341     @Override
342     public int length(){
343         return this.rawText.length();
344     }
345
346     /**
347      * {@inheritDoc}
348      * 部分文字列を返す。
349      * @param start {@inheritDoc}
350      * @param end {@inheritDoc}
351      * @return {@inheritDoc}
352      * @throws IndexOutOfBoundsException {@inheritDoc}
353      */
354     @Override
355     public CharSequence subSequence(int start, int end)
356             throws IndexOutOfBoundsException{
357         return this.rawText.subSequence(start, end);
358     }
359
360     /**
361      * クォーテーションやエスケープ処理の施されていない生の文字列を返す。
362      * @return 生の文字列
363      */
364     public String toRawString(){
365         return this.rawText;
366     }
367
368     /**
369      * {@inheritDoc}
370      * クォーテーションとエスケープ処理の施された文字列表記を生成する。
371      * JSON表記の一部としての利用も可能。
372      * @return {@inheritDoc}
373      */
374     @Override
375     public String toString(){
376         StringBuilder string = escapeText(this.rawText);
377         return string.toString();
378     }
379
380 }