OSDN Git Service

rename package
[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  */
21 public class DecodedContent
22         implements CharSequence,
23                    Appendable {
24
25     /**
26      * 代替文字。
27      * {@literal HTMLで使うなら < や > や & や " や ' はやめて!}
28      */
29     public static final char ALTCHAR = '?';
30
31     private static final String NULLTEXT = "null";
32
33     private static final List<DecodeErrorInfo> EMPTY_LIST =
34             Collections.emptyList();
35
36     private static final int BSEARCH_THRESHOLD = 16;
37
38     static{
39         assert ALTCHAR != '<';
40         assert ALTCHAR != '>';
41         assert ALTCHAR != '&';
42         assert ALTCHAR != '"';
43         assert ALTCHAR != '\'';
44         assert ALTCHAR != '\\';
45     }
46
47
48     private final StringBuilder rawContent = new StringBuilder();
49
50     private List<DecodeErrorInfo> decodeError;
51
52
53     /**
54      * コンストラクタ。
55      */
56     public DecodedContent(){
57         this("");
58         return;
59     }
60
61     /**
62      * コンストラクタ。
63      * @param seq 初期化文字列
64      * @throws NullPointerException 引数がnull
65      */
66     public DecodedContent(CharSequence seq) throws NullPointerException{
67         super();
68         if(seq == null) throw new NullPointerException();
69         initImpl();
70         this.rawContent.append(seq);
71         return;
72     }
73
74     /**
75      * コンストラクタ。
76      * @param capacity 文字数の初期容量
77      * @throws NegativeArraySizeException 容量が負の値
78      */
79     public DecodedContent(int capacity) throws NegativeArraySizeException{
80         super();
81         if(capacity < 0) throw new NegativeArraySizeException();
82         initImpl();
83         this.rawContent.ensureCapacity(capacity);
84         return;
85     }
86
87
88     /**
89      * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ
90      * デコードエラーのインデックス位置を返す。※リニアサーチ版。
91      * @param errList デコードエラーのリスト
92      * @param startPos 文字位置
93      * @return 0から始まるリスト内の位置。
94      *     一致する文字位置がなければ挿入ポイント。
95      */
96     protected static int lsearchErrorIndex(List<DecodeErrorInfo> errList,
97                                              int startPos){
98         // assert errList instanceof RandomAccess;
99
100         int errSize = errList.size();
101
102         int idx;
103         for(idx = 0; idx < errSize; idx++){
104             DecodeErrorInfo einfo = errList.get(idx);
105             int errPos = einfo.getCharPosition();
106             if(startPos <= errPos) break;
107         }
108
109         return idx;
110     }
111
112     /**
113      * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ
114      * デコードエラーのインデックス位置を返す。※バイナリサーチ版。
115      * @param errList デコードエラーのリスト
116      * @param startPos 文字位置
117      * @return 0から始まるリスト内の位置。
118      *     一致する文字位置がなければ挿入ポイント。
119      */
120     protected static int bsearchErrorIndex(List<DecodeErrorInfo> errList,
121                                              int startPos){
122         // assert errList instanceof RandomAccess;
123
124         int floor = 0;
125         int roof  = errList.size() - 1;
126
127         while(floor <= roof){
128             int gapHalf = (roof - floor) / 2;  // 切り捨て
129             int midpoint = floor + gapHalf;
130             DecodeErrorInfo einfo = errList.get(midpoint);
131             int cmp = einfo.getCharPosition() - startPos;
132
133             if     (cmp < 0) floor = midpoint + 1;
134             else if(cmp > 0) roof  = midpoint - 1;
135             else return midpoint;
136         }
137
138         return floor;
139     }
140
141     /**
142      * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ
143      * デコードエラーのインデックス位置を返す。
144      * 要素数の増減に応じてリニアサーチとバイナリサーチを使い分ける。
145      * @param errList デコードエラーのリスト
146      * @param startPos 文字位置
147      * @return 0から始まるリスト内の位置。
148      *     一致する文字位置がなければ挿入ポイント。
149      */
150     protected static int searchErrorIndex(List<DecodeErrorInfo> errList,
151                                             int startPos){
152         int result;
153
154         int errSize = errList.size();
155         if(errSize < BSEARCH_THRESHOLD){
156             // linear-search
157             result = lsearchErrorIndex(errList, startPos);
158         }else{
159             // binary-search
160             result = bsearchErrorIndex(errList, startPos);
161         }
162
163         return result;
164     }
165
166     /**
167      * ギャップ情報が加味されたデコードエラー情報を、
168      * 範囲指定込みで指定エラーリストに追加転記する。
169      * 追加先エラーリストがnullだった場合、必要に応じてエラーリストが生成され
170      * 戻り値となる場合がありうる。
171      * @param sourceContent 元の文字列
172      * @param startPos 範囲開始位置
173      * @param endPos 範囲終了位置
174      * @param targetError 追加先エラーリスト。nullでもよい。
175      * @param gap ギャップ量
176      * @return 引数targetErrorもしくは新規生成されたリストを返す。
177      */
178     protected static List<DecodeErrorInfo>
179             appendGappedErrorInfo(DecodedContent sourceContent,
180                                      int startPos, int endPos,
181                                      List<DecodeErrorInfo> targetError,
182                                      int gap){
183         List<DecodeErrorInfo> sourceError = sourceContent.decodeError;
184         List<DecodeErrorInfo> result = targetError;
185
186         int startErrorIdx = searchErrorIndex(sourceError, startPos);
187         int endErrorIdx = sourceError.size() - 1;
188         assert endErrorIdx >= 0;
189
190         for(int index = startErrorIdx; index <= endErrorIdx; index++){
191             DecodeErrorInfo einfo = sourceError.get(index);
192             int pos = einfo.getCharPosition();
193             if(pos < startPos) continue;
194             if(pos >= endPos) break;
195             DecodeErrorInfo newInfo = einfo.createGappedClone(gap);
196             if(result == null){
197                 result = createErrorList();
198             }
199             result.add(newInfo);
200         }
201
202         return result;
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     static{
215         assert createErrorList() instanceof RandomAccess;
216     }
217
218     /**
219      * 初期化下請け。
220      * 長さ0の文字列&デコードエラー無しの状態になる。
221      */
222     private void initImpl(){
223         this.rawContent.setLength(0);
224
225         if(this.decodeError != null){
226             this.decodeError.clear();
227         }
228
229         return;
230     }
231
232     /**
233      * 事前にキャパシティを確保する。
234      * 指定されたキャパシティの範囲内で再割り当てが起きないことを保証する。
235      * @param minimumCapacity キャラクタ単位のキャパシティ長。
236      */
237     public void ensureCapacity(int minimumCapacity){
238         this.rawContent.ensureCapacity(minimumCapacity);
239         return;
240     }
241
242     /**
243      * 初期化。
244      * 長さ0の文字列&デコードエラー無しの状態になる。
245      * コンストラクタで新インスタンスを作るより低コスト。
246      */
247     public void init(){
248         initImpl();
249         return;
250     }
251
252     /**
253      * デコードエラーを含むか判定する。
254      * @return デコードエラーを含むならtrue
255      */
256     public boolean hasDecodeError(){
257         if(this.decodeError == null) return false;
258         if(this.decodeError.isEmpty()) return false;
259         return true;
260     }
261
262     /**
263      * デコードエラーの一覧を取得する。
264      * @return デコードエラーの一覧
265      */
266     public List<DecodeErrorInfo> getDecodeErrorList(){
267         if( ! hasDecodeError() ){
268             return EMPTY_LIST;
269         }
270         return Collections.unmodifiableList(this.decodeError);
271     }
272
273     /**
274      * 生の文字列を得る。
275      * 高速なCharSequenceアクセス用途。
276      * @return 生の文字列。
277      */
278     public CharSequence getRawContent(){
279         return this.rawContent;
280     }
281
282     /**
283      * 指定された位置の文字を変更する。
284      * @param index 文字位置
285      * @param ch 文字
286      * @throws IndexOutOfBoundsException 不正な位置指定
287      */
288     public void setCharAt(int index, char ch)
289             throws IndexOutOfBoundsException{
290         this.rawContent.setCharAt(index, ch);
291         return;
292     }
293
294     /**
295      * {@inheritDoc}
296      * @param index {@inheritDoc}
297      * @return {@inheritDoc}
298      */
299     @Override
300     public char charAt(int index){
301         return this.rawContent.charAt(index);
302     }
303
304     /**
305      * {@inheritDoc}
306      * @return {@inheritDoc}
307      */
308     @Override
309     public int length(){
310         return this.rawContent.length();
311     }
312
313     /**
314      * {@inheritDoc}
315      * @param start {@inheritDoc}
316      * @param end {@inheritDoc}
317      * @return {@inheritDoc}
318      */
319     @Override
320     public CharSequence subSequence(int start, int end){
321         return this.rawContent.subSequence(start, end);
322     }
323
324     /**
325      * 範囲指定されたサブコンテントを切り出す。
326      * サブコンテントにはデコードエラー情報が引き継がれる。
327      * @param start 開始位置
328      * @param end 終了位置
329      * @return サブコンテント
330      * @throws IndexOutOfBoundsException start または end が負の値の場合、
331      *     end が length() より大きい場合、
332      *     あるいは start が end より大きい場合
333      */
334     public DecodedContent subContent(int start, int end)
335             throws IndexOutOfBoundsException{
336         int length = end - start;
337         if(length < 0) throw new IndexOutOfBoundsException();
338         DecodedContent result = new DecodedContent(length);
339         result.append(this, start, end);
340         return result;
341     }
342
343     /**
344      * 文字を追加する。
345      * @param letter 追加する文字
346      * @return thisオブジェクト
347      */
348     @Override
349     public DecodedContent append(char letter){
350         this.rawContent.append(letter);
351         return this;
352     }
353
354     /**
355      * 文字列を追加する。
356      * @param seq 追加する文字列
357      * @return thisオブジェクト
358      */
359     @Override
360     public DecodedContent append(CharSequence seq){
361         if(seq == null){
362             this.rawContent.append(NULLTEXT);
363         }else if(seq instanceof DecodedContent){
364             append((DecodedContent) seq, 0, seq.length());
365         }else{
366             this.rawContent.append(seq);
367         }
368         return this;
369     }
370
371     /**
372      * 文字列を追加する。
373      * @param seq 追加する文字列
374      * @param startPos 開始位置
375      * @param endPos 終了位置
376      * @return thisオブジェクト
377      * @throws IndexOutOfBoundsException 範囲指定が変。
378      */
379     @Override
380     public DecodedContent append(CharSequence seq,
381                                   int startPos, int endPos)
382             throws IndexOutOfBoundsException{
383         if(seq == null){
384             this.rawContent.append(NULLTEXT);
385         }else if(seq instanceof DecodedContent){
386             append((DecodedContent) seq, startPos, endPos);
387         }else{
388             this.rawContent.append(seq, startPos, endPos);
389         }
390
391         return this;
392     }
393
394     /**
395      * 文字列を追加する。
396      *
397      * @param str  追加される文字
398      * @param offset 追加される最初の char のインデックス
399      * @param len 追加される char の数
400      * @return thisオブジェクト
401      * @throws IndexOutOfBoundsException 範囲指定が不正。
402      */
403     public DecodedContent append(char[] str, int offset, int len)
404             throws IndexOutOfBoundsException{
405         if(str == null){
406             this.rawContent.append(NULLTEXT);
407         }else{
408             this.rawContent.append(str, offset, len);
409         }
410
411         return this;
412     }
413
414     /**
415      * 文字列を追加する。
416      * @param source 追加する文字列
417      * @param startPos 開始位置
418      * @param endPos 終了位置
419      * @return thisオブジェクト
420      * @throws IndexOutOfBoundsException 範囲指定が変。
421      */
422     public DecodedContent append(DecodedContent source,
423                                   int startPos, int endPos)
424             throws IndexOutOfBoundsException{
425         if(source == null){
426             return append(NULLTEXT);
427         }
428
429         int gap = startPos - this.rawContent.length();
430
431         this.rawContent.append(source.rawContent, startPos, endPos);
432
433         if( ! source.hasDecodeError() ) return this;
434
435         List<DecodeErrorInfo> targetErrorList;
436         if(source != this) targetErrorList = this.decodeError;
437         else               targetErrorList = null;
438
439         targetErrorList = appendGappedErrorInfo(source,
440                                                 startPos, endPos,
441                                                 targetErrorList,
442                                                 gap);
443
444         if(targetErrorList == null)             return this;
445         if(targetErrorList == this.decodeError) return this;
446
447         if(this.decodeError == null){
448             this.decodeError = targetErrorList;
449         }else{
450             this.decodeError.addAll(targetErrorList);
451         }
452
453         return this;
454     }
455
456     /**
457      * 代替文字とともにデコードエラーを追加する。
458      * ※呼び出し側は、追加されるデコードエラーの位置情報が
459      * 既存のデコードエラーよりも大きいことを保証しなければならない。
460      * @param errorInfo デコードエラー
461      */
462     private void addDecodeError(DecodeErrorInfo errorInfo){
463         if(this.decodeError == null){
464             this.decodeError = createErrorList();
465         }
466         this.decodeError.add(errorInfo);
467         this.rawContent.append(ALTCHAR);
468         return;
469     }
470
471     /**
472      * 代替文字とともにデコードエラーを追加する。
473      * @param b1st 1バイト目の値
474      */
475     public void addDecodeError(byte b1st){
476         DecodeErrorInfo errInfo =
477                 new DecodeErrorInfo(this.rawContent.length(), b1st);
478         addDecodeError(errInfo);
479         return;
480     }
481
482     /**
483      * 代替文字とともに2バイトからなるデコードエラーを追加する。
484      *
485      * <p>主にシフトJISのUnmapエラーを想定。
486      *
487      * @param b1st 1バイト目の値
488      * @param b2nd 2バイト目の値
489      */
490     public void addDecodeError(byte b1st, byte b2nd){
491         DecodeErrorInfo errInfo =
492                 new DecodeErrorInfo(this.rawContent.length(), b1st, b2nd);
493         addDecodeError(errInfo);
494         return;
495     }
496
497     /**
498      * {@inheritDoc}
499      * @return {@inheritDoc}
500      */
501     @Override
502     public String toString(){
503         return this.rawContent.toString();
504     }
505
506 }