4 * License : The MIT License
5 * Copyright(c) 2009 olyutorskii
8 package jp.osdn.jindolf.parser.content;
10 import java.util.ArrayList;
11 import java.util.Collections;
12 import java.util.List;
13 import java.util.RandomAccess;
16 * デコードエラー情報を含む再利用可能な文字列。
18 * <p>デコードエラーを起こした箇所は代替文字{@link #ALTCHAR}で置き換えられる。
21 public class DecodedContent
22 implements CharSequence,
27 * {@literal HTMLで使うなら < や > や & や " や ' はやめて!}
29 public static final char ALTCHAR = '?';
31 private static final String NULLTEXT = "null";
33 private static final List<DecodeErrorInfo> EMPTY_LIST =
34 Collections.emptyList();
36 private static final int BSEARCH_THRESHOLD = 16;
39 assert ALTCHAR != '<';
40 assert ALTCHAR != '>';
41 assert ALTCHAR != '&';
42 assert ALTCHAR != '"';
43 assert ALTCHAR != '\'';
44 assert ALTCHAR != '\\';
48 private final StringBuilder rawContent = new StringBuilder();
50 private List<DecodeErrorInfo> decodeError;
56 public DecodedContent(){
64 * @throws NullPointerException 引数がnull
66 public DecodedContent(CharSequence seq) throws NullPointerException{
68 if(seq == null) throw new NullPointerException();
70 this.rawContent.append(seq);
76 * @param capacity 文字数の初期容量
77 * @throws NegativeArraySizeException 容量が負の値
79 public DecodedContent(int capacity) throws NegativeArraySizeException{
81 if(capacity < 0) throw new NegativeArraySizeException();
83 this.rawContent.ensureCapacity(capacity);
89 * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ
90 * デコードエラーのインデックス位置を返す。※リニアサーチ版。
91 * @param errList デコードエラーのリスト
92 * @param startPos 文字位置
93 * @return 0から始まるリスト内の位置。
94 * 一致する文字位置がなければ挿入ポイント。
96 protected static int lsearchErrorIndex(List<DecodeErrorInfo> errList,
98 // assert errList instanceof RandomAccess;
100 int errSize = errList.size();
103 for(idx = 0; idx < errSize; idx++){
104 DecodeErrorInfo einfo = errList.get(idx);
105 int errPos = einfo.getCharPosition();
106 if(startPos <= errPos) break;
113 * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ
114 * デコードエラーのインデックス位置を返す。※バイナリサーチ版。
115 * @param errList デコードエラーのリスト
116 * @param startPos 文字位置
117 * @return 0から始まるリスト内の位置。
118 * 一致する文字位置がなければ挿入ポイント。
120 protected static int bsearchErrorIndex(List<DecodeErrorInfo> errList,
122 // assert errList instanceof RandomAccess;
125 int roof = errList.size() - 1;
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;
133 if (cmp < 0) floor = midpoint + 1;
134 else if(cmp > 0) roof = midpoint - 1;
135 else return midpoint;
142 * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ
143 * デコードエラーのインデックス位置を返す。
144 * 要素数の増減に応じてリニアサーチとバイナリサーチを使い分ける。
145 * @param errList デコードエラーのリスト
146 * @param startPos 文字位置
147 * @return 0から始まるリスト内の位置。
148 * 一致する文字位置がなければ挿入ポイント。
150 protected static int searchErrorIndex(List<DecodeErrorInfo> errList,
154 int errSize = errList.size();
155 if(errSize < BSEARCH_THRESHOLD){
157 result = lsearchErrorIndex(errList, startPos);
160 result = bsearchErrorIndex(errList, startPos);
167 * ギャップ情報が加味されたデコードエラー情報を、
168 * 範囲指定込みで指定エラーリストに追加転記する。
169 * 追加先エラーリストがnullだった場合、必要に応じてエラーリストが生成され
171 * @param sourceContent 元の文字列
172 * @param startPos 範囲開始位置
173 * @param endPos 範囲終了位置
174 * @param targetError 追加先エラーリスト。nullでもよい。
176 * @return 引数targetErrorもしくは新規生成されたリストを返す。
178 protected static List<DecodeErrorInfo>
179 appendGappedErrorInfo(DecodedContent sourceContent,
180 int startPos, int endPos,
181 List<DecodeErrorInfo> targetError,
183 List<DecodeErrorInfo> sourceError = sourceContent.decodeError;
184 List<DecodeErrorInfo> result = targetError;
186 int startErrorIdx = searchErrorIndex(sourceError, startPos);
187 int endErrorIdx = sourceError.size() - 1;
188 assert endErrorIdx >= 0;
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);
197 result = createErrorList();
209 private static List<DecodeErrorInfo> createErrorList(){
210 List<DecodeErrorInfo> result = new ArrayList<>();
215 assert createErrorList() instanceof RandomAccess;
220 * 長さ0の文字列&デコードエラー無しの状態になる。
222 private void initImpl(){
223 this.rawContent.setLength(0);
225 if(this.decodeError != null){
226 this.decodeError.clear();
234 * 指定されたキャパシティの範囲内で再割り当てが起きないことを保証する。
235 * @param minimumCapacity キャラクタ単位のキャパシティ長。
237 public void ensureCapacity(int minimumCapacity){
238 this.rawContent.ensureCapacity(minimumCapacity);
244 * 長さ0の文字列&デコードエラー無しの状態になる。
245 * コンストラクタで新インスタンスを作るより低コスト。
254 * @return デコードエラーを含むならtrue
256 public boolean hasDecodeError(){
257 if(this.decodeError == null) return false;
258 if(this.decodeError.isEmpty()) return false;
266 public List<DecodeErrorInfo> getDecodeErrorList(){
267 if( ! hasDecodeError() ){
270 return Collections.unmodifiableList(this.decodeError);
275 * 高速なCharSequenceアクセス用途。
278 public CharSequence getRawContent(){
279 return this.rawContent;
286 * @throws IndexOutOfBoundsException 不正な位置指定
288 public void setCharAt(int index, char ch)
289 throws IndexOutOfBoundsException{
290 this.rawContent.setCharAt(index, ch);
296 * @param index {@inheritDoc}
297 * @return {@inheritDoc}
300 public char charAt(int index){
301 return this.rawContent.charAt(index);
306 * @return {@inheritDoc}
310 return this.rawContent.length();
315 * @param start {@inheritDoc}
316 * @param end {@inheritDoc}
317 * @return {@inheritDoc}
320 public CharSequence subSequence(int start, int end){
321 return this.rawContent.subSequence(start, end);
325 * 範囲指定されたサブコンテントを切り出す。
326 * サブコンテントにはデコードエラー情報が引き継がれる。
330 * @throws IndexOutOfBoundsException start または end が負の値の場合、
331 * end が length() より大きい場合、
332 * あるいは start が end より大きい場合
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);
345 * @param letter 追加する文字
349 public DecodedContent append(char letter){
350 this.rawContent.append(letter);
360 public DecodedContent append(CharSequence seq){
362 this.rawContent.append(NULLTEXT);
363 }else if(seq instanceof DecodedContent){
364 append((DecodedContent) seq, 0, seq.length());
366 this.rawContent.append(seq);
374 * @param startPos 開始位置
377 * @throws IndexOutOfBoundsException 範囲指定が変。
380 public DecodedContent append(CharSequence seq,
381 int startPos, int endPos)
382 throws IndexOutOfBoundsException{
384 this.rawContent.append(NULLTEXT);
385 }else if(seq instanceof DecodedContent){
386 append((DecodedContent) seq, startPos, endPos);
388 this.rawContent.append(seq, startPos, endPos);
398 * @param offset 追加される最初の char のインデックス
399 * @param len 追加される char の数
401 * @throws IndexOutOfBoundsException 範囲指定が不正。
403 public DecodedContent append(char[] str, int offset, int len)
404 throws IndexOutOfBoundsException{
406 this.rawContent.append(NULLTEXT);
408 this.rawContent.append(str, offset, len);
416 * @param source 追加する文字列
417 * @param startPos 開始位置
420 * @throws IndexOutOfBoundsException 範囲指定が変。
422 public DecodedContent append(DecodedContent source,
423 int startPos, int endPos)
424 throws IndexOutOfBoundsException{
426 return append(NULLTEXT);
429 int gap = startPos - this.rawContent.length();
431 this.rawContent.append(source.rawContent, startPos, endPos);
433 if( ! source.hasDecodeError() ) return this;
435 List<DecodeErrorInfo> targetErrorList;
436 if(source != this) targetErrorList = this.decodeError;
437 else targetErrorList = null;
439 targetErrorList = appendGappedErrorInfo(source,
444 if(targetErrorList == null) return this;
445 if(targetErrorList == this.decodeError) return this;
447 if(this.decodeError == null){
448 this.decodeError = targetErrorList;
450 this.decodeError.addAll(targetErrorList);
457 * 代替文字とともにデコードエラーを追加する。
458 * ※呼び出し側は、追加されるデコードエラーの位置情報が
459 * 既存のデコードエラーよりも大きいことを保証しなければならない。
460 * @param errorInfo デコードエラー
462 private void addDecodeError(DecodeErrorInfo errorInfo){
463 if(this.decodeError == null){
464 this.decodeError = createErrorList();
466 this.decodeError.add(errorInfo);
467 this.rawContent.append(ALTCHAR);
472 * 代替文字とともにデコードエラーを追加する。
473 * @param b1st 1バイト目の値
475 public void addDecodeError(byte b1st){
476 DecodeErrorInfo errInfo =
477 new DecodeErrorInfo(this.rawContent.length(), b1st);
478 addDecodeError(errInfo);
483 * 代替文字とともに2バイトからなるデコードエラーを追加する。
485 * <p>主にシフトJISのUnmapエラーを想定。
487 * @param b1st 1バイト目の値
488 * @param b2nd 2バイト目の値
490 public void addDecodeError(byte b1st, byte b2nd){
491 DecodeErrorInfo errInfo =
492 new DecodeErrorInfo(this.rawContent.length(), b1st, b2nd);
493 addDecodeError(errInfo);
499 * @return {@inheritDoc}
502 public String toString(){
503 return this.rawContent.toString();