OSDN Git Service

230005b06a61bb3db6dbb833eb7533bdbd20c4b0
[jindolf/JinParser.git] / src / main / java / jp / osdn / jindolf / parser / content / DecodedContent.java
1 /*
2  * decoded source
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.osdn.jindolf.parser.content;
9
10 import java.util.ArrayList;
11 import java.util.Collections;
12 import java.util.List;
13 import java.util.RandomAccess;
14
15 /**
16  * デコードエラー情報と共に追記可能な可変文字列。
17  *
18  * <p>デコードエラーを起こした箇所は代替文字{@link #ALTCHAR}で置き換えられる。
19  *
20  * <p>マルチスレッドには非対応。
21  */
22 public class DecodedContent
23         implements CharSequence,
24                    Appendable {
25
26     /**
27      * 代替文字。
28      *
29      * <p>{@literal HTMLで使うなら < や > や & や " や ' は避けた方が無難}
30      */
31     public static final char ALTCHAR = '?';
32
33     private static final List<DecodeErrorInfo> EMPTY_LIST;
34     private static final String NULLTEXT = "null";
35
36     static{
37         List<DecodeErrorInfo> emptyList;
38         emptyList = Collections.emptyList();
39         emptyList = Collections.unmodifiableList(emptyList);
40         EMPTY_LIST = emptyList;
41
42         assert createErrorList() instanceof RandomAccess;
43     }
44
45
46     private final StringBuilder rawContent = new StringBuilder();
47     private List<DecodeErrorInfo> errList;
48
49
50     /**
51      * コンストラクタ。
52      *
53      * <p>長さ0の文字列が反映され、デコードエラー総数は0件となる。
54      */
55     public DecodedContent(){
56         super();
57         initImpl();
58         return;
59     }
60
61     /**
62      * コンストラクタ。
63      *
64      * <p>引数の文字列が反映され、デコードエラー総数は0件となる。
65      *
66      * <p>nullが渡されると文字列"null"として解釈される。
67      *
68      * @param seq 初期化文字列
69      */
70     public DecodedContent(CharSequence seq){
71         super();
72         initImpl();
73         this.rawContent.append(seq);
74         return;
75     }
76
77     /**
78      * コンストラクタ。
79      *
80      * <p>長さ0の文字列が反映され、デコードエラー総数は0件となる。
81      *
82      * <p>文字数の初期容量を引数で指定する。
83      * 文字列長が初期容量を超えるまでの間、
84      * 文字列格納の再割り当てが起こらないことが期待される。
85      *
86      * @param capacity 文字数の初期容量
87      * @throws NegativeArraySizeException 容量が負の値
88      */
89     public DecodedContent(int capacity) throws NegativeArraySizeException{
90         super();
91         if(capacity < 0) throw new NegativeArraySizeException();
92         initImpl();
93         this.rawContent.ensureCapacity(capacity);
94         return;
95     }
96
97
98     /**
99      * ギャップ情報が加味されたデコードエラー情報を、
100      * 範囲指定込みで指定エラーリストに追加コピーする。
101      *
102      * <p>追加先エラーリストがnullだった場合、
103      * 必要に応じてエラーリストが生成され
104      * 戻り値となる場合がありうる。
105      *
106      * @param srcErrList 追加元エラーリスト
107      * @param startCharPt 範囲開始位置
108      * @param endCharPt 範囲終了位置
109      * @param dstErrList 追加先エラーリスト。nullでもよい。
110      *     追加元と異なるリストでなければならない。
111      * @param gap ギャップ量
112      * @return 引数targetErrorもしくは新規生成されたリストを返す。
113      *     なにもコピーされなければnullを返す。
114      * @throws IllegalArgumentException 追加元リストと追加先リストが
115      *     同一インスタンス
116      */
117     static List<DecodeErrorInfo> appendGappedErrorInfo(
118         List<DecodeErrorInfo> srcErrList,
119         int startCharPt, int endCharPt,
120         List<DecodeErrorInfo> dstErrList,
121         int gap
122     ){
123         if(srcErrList == dstErrList) throw new IllegalArgumentException();
124         if(startCharPt >= endCharPt) return dstErrList;
125
126         int startErrorIdx =
127                 DecodeErrorInfo.searchErrorIndex(srcErrList, startCharPt);
128         int errSize = srcErrList.size();
129         if(startErrorIdx >= errSize){
130             return null;
131         }
132
133         int endErrorIdx = calcEndIdx(srcErrList, endCharPt);
134
135         boolean hasLoop =
136                 (0 <= startErrorIdx) && (startErrorIdx <= endErrorIdx);
137         if( ! hasLoop){
138             return null;
139         }
140
141         List<DecodeErrorInfo> result;
142         if(dstErrList == null) result = createErrorList();
143         else                   result = dstErrList;
144
145         copyGappedErrorInfo(srcErrList,
146                             startErrorIdx, endErrorIdx,
147                             result, gap);
148         return result;
149     }
150
151     /**
152      * 文字列終了点からエラーリスト範囲の最終インデックスを求める。
153      *
154      * @param srcErrList エラーリスト
155      * @param endCharPt 文字列終了点
156      * @return リスト範囲の最終インデックス
157      */
158     private static int calcEndIdx(List<DecodeErrorInfo> srcErrList,
159                                   int endCharPt){
160         int lastCharPos = endCharPt - 1;
161
162         int endErrorIdx =
163                 DecodeErrorInfo.searchErrorIndex(srcErrList, lastCharPos);
164
165         int errSize = srcErrList.size();
166         if(endErrorIdx >= errSize){
167             endErrorIdx = errSize - 1;
168         }else{
169             DecodeErrorInfo lastErrorInfo = srcErrList.get(endErrorIdx);
170
171             boolean isLastErrorInfoOnLastPos =
172                     lastErrorInfo.getCharPosition() == lastCharPos;
173             if( ! isLastErrorInfoOnLastPos){
174                 endErrorIdx--;
175             }
176         }
177
178         return endErrorIdx;
179     }
180
181     /**
182      * エラーリストの一部の範囲を、gapを加味して別リストに追加コピーする。
183      *
184      * @param srcErrList コピー元リスト
185      * @param startErrorIdx コピー元の範囲開始インデックス
186      * @param endErrorIdx コピー元の範囲終了インデックス
187      * @param dstErrList コピー先リスト
188      * @param gap 代替文字出現位置ギャップ量
189      */
190     private static void copyGappedErrorInfo(
191         List<DecodeErrorInfo> srcErrList,
192         int startErrorIdx, int endErrorIdx,
193         List<DecodeErrorInfo> dstErrList,
194         int gap
195     ){
196         for(int index = startErrorIdx; index <= endErrorIdx; index++){
197             DecodeErrorInfo srcErrInfo = srcErrList.get(index);
198             DecodeErrorInfo gappedInfo = srcErrInfo.createGappedClone(gap);
199             dstErrList.add(gappedInfo);
200         }
201         return;
202     }
203
204     /**
205      * エラー格納用リストを生成する。
206      *
207      * @return リスト
208      */
209     private static List<DecodeErrorInfo> createErrorList(){
210         List<DecodeErrorInfo> result = new ArrayList<>();
211         return result;
212     }
213
214
215     /**
216      * 初期化。
217      *
218      * <p>長さ0の文字列&デコードエラー無しの状態になる。
219      * コンストラクタで新インスタンスを作るより低コスト。
220      */
221     public void init(){
222         initImpl();
223         return;
224     }
225
226     /**
227      * 初期化下請け。
228      *
229      * <p>長さ0の文字列&デコードエラー無しの状態になる。
230      */
231     private void initImpl(){
232         this.rawContent.setLength(0);
233
234         if(this.errList != null){
235             this.errList.clear();
236         }
237
238         return;
239     }
240
241     /**
242      * 事前にキャパシティを確保する。
243      *
244      * <p>指定されたキャパシティの範囲内で再割り当てが起きないことを保証する。
245      *
246      * @param minimumCapacity キャラクタ単位のキャパシティ長。
247      */
248     public void ensureCapacity(int minimumCapacity){
249         this.rawContent.ensureCapacity(minimumCapacity);
250         return;
251     }
252
253     /**
254      * デコードエラーを含むか否か判定する。
255      *
256      * @return デコードエラーを含むならtrue
257      */
258     public boolean hasDecodeError(){
259         if(this.errList == null)   return false;
260         if(this.errList.isEmpty()) return false;
261         return true;
262     }
263
264     /**
265      * デコードエラーの一覧を取得する。
266      *
267      * @return デコードエラーの一覧
268      */
269     public List<DecodeErrorInfo> getDecodeErrorList(){
270         if( ! hasDecodeError() ){
271             return EMPTY_LIST;
272         }
273         return Collections.unmodifiableList(this.errList);
274     }
275
276     /**
277      * 生の文字列を得る。
278      *
279      * <p>戻り値からエラー情報は消される。
280      *
281      * <p>高速なCharSequenceアクセス用途。
282      *
283      * @return 生の文字列。
284      */
285     public CharSequence getRawContent(){
286         return this.rawContent;
287     }
288
289     /**
290      * 指定された位置の文字を変更する。
291      *
292      * <p>デコードエラーにより追加された代替文字の変更も可能。
293      *
294      * @param index 文字位置
295      * @param ch 文字
296      * @throws IndexOutOfBoundsException 不正な位置指定
297      */
298     public void setCharAt(int index, char ch)
299             throws IndexOutOfBoundsException{
300         this.rawContent.setCharAt(index, ch);
301         return;
302     }
303
304     /**
305      * {@inheritDoc}
306      *
307      * @param index {@inheritDoc}
308      * @return {@inheritDoc}
309      */
310     @Override
311     public char charAt(int index){
312         return this.rawContent.charAt(index);
313     }
314
315     /**
316      * {@inheritDoc}
317      *
318      * @return {@inheritDoc}
319      */
320     @Override
321     public int length(){
322         return this.rawContent.length();
323     }
324
325     /**
326      * {@inheritDoc}
327      *
328      * @param startCharPt {@inheritDoc}
329      * @param endCharPt {@inheritDoc}
330      * @return {@inheritDoc}
331      */
332     @Override
333     public CharSequence subSequence(int startCharPt, int endCharPt){
334         return this.rawContent.subSequence(startCharPt, endCharPt);
335     }
336
337     /**
338      * 範囲指定されたサブコンテントを切り出す。
339      *
340      * <p>サブコンテントにはデコードエラー情報が引き継がれる。
341      *
342      * @param startCharPt 開始位置
343      * @param endCharPt 終了位置
344      * @return サブコンテント
345      * @throws IndexOutOfBoundsException start または end が負の値の場合、
346      *     end が length() より大きい場合、
347      *     あるいは start が end より大きい場合
348      */
349     public DecodedContent subContent(int startCharPt, int endCharPt)
350             throws IndexOutOfBoundsException{
351         int length = endCharPt - startCharPt;
352         if(length < 0) throw new IndexOutOfBoundsException();
353         DecodedContent result = new DecodedContent(length);
354         result.append(this, startCharPt, endCharPt);
355         return result;
356     }
357
358     /**
359      * 文字を末尾へ追加する。
360      *
361      * @param letter 追加する文字
362      * @return thisオブジェクト
363      */
364     @Override
365     public DecodedContent append(char letter){
366         this.rawContent.append(letter);
367         return this;
368     }
369
370     /**
371      * 文字列を末尾へ追加する。
372      *
373      * <p>nullが渡されると文字列"null"として解釈される。
374      *
375      * @param seq 追加する文字列
376      * @return thisオブジェクト
377      */
378     @Override
379     public DecodedContent append(CharSequence seq){
380         DecodedContent result;
381
382         if(seq == null){
383             result = append(NULLTEXT);
384         }else if(seq instanceof DecodedContent){
385             DecodedContent content = (DecodedContent) seq;
386             int seqLen = seq.length();
387             result = append(content, 0, seqLen);
388         }else{
389             this.rawContent.append(seq);
390             result = this;
391         }
392
393         return result;
394     }
395
396     /**
397      * 文字列を末尾へ追加する。
398      *
399      * <p>nullが渡されると文字列"null"として解釈される。
400      *
401      * @param seq 追加する文字列
402      * @param startCharPt 開始位置
403      * @param endCharPt 終了位置
404      * @return thisオブジェクト
405      * @throws IndexOutOfBoundsException 範囲指定が変。
406      */
407     @Override
408     public DecodedContent append(CharSequence seq,
409                                  int startCharPt, int endCharPt)
410             throws IndexOutOfBoundsException{
411         DecodedContent result;
412
413         if(seq == null){
414             result = append(NULLTEXT, startCharPt, endCharPt);
415         }else if(seq instanceof DecodedContent){
416             result = append((DecodedContent) seq, startCharPt, endCharPt);
417         }else if(   startCharPt < 0
418                  || startCharPt > endCharPt
419                  || endCharPt > seq.length()){
420             throw new IndexOutOfBoundsException();
421         }else if(startCharPt == endCharPt){
422             result = this;
423         }else{
424             this.rawContent.append(seq, startCharPt, endCharPt);
425             result = this;
426         }
427
428         return result;
429     }
430
431     /**
432      * 文字列を末尾へ追加する。
433      *
434      * @param str  追加する文字配列
435      * @param offset 追加される最初の char のインデックス
436      * @param len 追加される char の数
437      * @return thisオブジェクト
438      * @throws IndexOutOfBoundsException 範囲指定が不正。
439      * @see StringBuffer#append(char[], int, int)
440      */
441     public DecodedContent append(char[] str, int offset, int len)
442             throws IndexOutOfBoundsException{
443         this.rawContent.append(str, offset, len);
444         return this;
445     }
446
447     /**
448      * 文字列を末尾へ追加する。
449      *
450      * <p>追加元のエラー情報は追加先へ引き継がれる。
451      *
452      * <p>nullが渡されると文字列"null"として解釈される。
453      *
454      * @param source 追加する文字列
455      * @param startCharPt 開始位置
456      * @param endCharPt 終了位置
457      * @return thisオブジェクト
458      * @throws IndexOutOfBoundsException 範囲指定が変。
459      * @see Appendable#append(CharSequence, int, int)
460      */
461     public DecodedContent append(DecodedContent source,
462                                  int startCharPt, int endCharPt)
463             throws IndexOutOfBoundsException{
464         if(source == null){
465             return append(NULLTEXT, startCharPt, endCharPt);
466         }
467
468         if(   startCharPt < 0
469            || startCharPt > endCharPt
470            || endCharPt > source.length()){
471             throw new IndexOutOfBoundsException();
472         }else if(startCharPt == endCharPt){
473             return this;
474         }
475
476         int oldLength = this.rawContent.length();
477
478         this.rawContent.append(source.rawContent, startCharPt, endCharPt);
479
480         List<DecodeErrorInfo> srcErrList;
481         if(source.hasDecodeError()){
482             srcErrList = source.errList;
483         }else{
484             return this;
485         }
486
487         List<DecodeErrorInfo> dstErrList;
488         if(source == this) dstErrList = null;
489         else               dstErrList = this.errList;
490
491         int gap = startCharPt - oldLength;
492
493         dstErrList = appendGappedErrorInfo(srcErrList,
494                                            startCharPt, endCharPt,
495                                            dstErrList,
496                                            gap);
497
498         if(dstErrList == null)         return this;
499         if(dstErrList == this.errList) return this;
500
501         if(this.errList == null){
502             this.errList = dstErrList;
503         }else{
504             this.errList.addAll(dstErrList);
505         }
506
507         return this;
508     }
509
510     /**
511      * 代替文字とともにデコードエラーを末尾へ追加する。
512      *
513      * <p>※呼び出し側は、追加されるデコードエラーの位置情報が
514      * 既存のデコードエラーよりも大きいことを保証しなければならない。
515      *
516      * @param errorInfo デコードエラー
517      */
518     void addDecodeError(DecodeErrorInfo errorInfo){
519         if(this.errList == null){
520             this.errList = createErrorList();
521         }
522         this.errList.add(errorInfo);
523         this.rawContent.append(ALTCHAR);
524         return;
525     }
526
527     /**
528      * 代替文字とともにデコードエラーを末尾へ追加する。
529      *
530      * @param b1st エラー1バイト目の値
531      */
532     public void addDecodeError(byte b1st){
533         DecodeErrorInfo errInfo =
534                 new DecodeErrorInfo(this.rawContent.length(), b1st);
535         addDecodeError(errInfo);
536         return;
537     }
538
539     /**
540      * 代替文字とともに2バイトからなるデコードエラーを末尾へ追加する。
541      *
542      * <p>主にシフトJISのUnmapエラーを想定。
543      *
544      * @param b1st エラー1バイト目の値
545      * @param b2nd エラー2バイト目の値
546      */
547     public void addDecodeError(byte b1st, byte b2nd){
548         DecodeErrorInfo errInfo =
549                 new DecodeErrorInfo(this.rawContent.length(), b1st, b2nd);
550         addDecodeError(errInfo);
551         return;
552     }
553
554     /**
555      * {@inheritDoc}
556      *
557      * @return {@inheritDoc}
558      */
559     @Override
560     public String toString(){
561         return this.rawContent.toString();
562     }
563
564 }