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}で置き換えられる。
22 public class DecodedContent
23 implements CharSequence,
29 * <p>{@literal HTMLで使うなら < や > や & や " や ' は避けた方が無難}
31 public static final char ALTCHAR = '?';
33 private static final List<DecodeErrorInfo> EMPTY_LIST;
34 private static final String NULLTEXT = "null";
37 List<DecodeErrorInfo> emptyList;
38 emptyList = Collections.emptyList();
39 emptyList = Collections.unmodifiableList(emptyList);
40 EMPTY_LIST = emptyList;
42 assert createErrorList() instanceof RandomAccess;
46 private final StringBuilder rawContent = new StringBuilder();
47 private List<DecodeErrorInfo> errList;
53 * <p>長さ0の文字列が反映され、デコードエラー総数は0件となる。
55 public DecodedContent(){
64 * <p>引数の文字列が反映され、デコードエラー総数は0件となる。
66 * <p>nullが渡されると文字列"null"として解釈される。
70 public DecodedContent(CharSequence seq){
73 this.rawContent.append(seq);
80 * <p>長さ0の文字列が反映され、デコードエラー総数は0件となる。
82 * <p>文字数の初期容量を引数で指定する。
84 * 文字列格納の再割り当てが起こらないことが期待される。
86 * @param capacity 文字数の初期容量
87 * @throws NegativeArraySizeException 容量が負の値
89 public DecodedContent(int capacity) throws NegativeArraySizeException{
91 if(capacity < 0) throw new NegativeArraySizeException();
93 this.rawContent.ensureCapacity(capacity);
99 * ギャップ情報が加味されたデコードエラー情報を、
100 * 範囲指定込みで指定エラーリストに追加コピーする。
102 * <p>追加先エラーリストがnullだった場合、
106 * @param srcErrList 追加元エラーリスト
107 * @param startCharPt 範囲開始位置
108 * @param endCharPt 範囲終了位置
109 * @param dstErrList 追加先エラーリスト。nullでもよい。
110 * 追加元と異なるリストでなければならない。
112 * @return 引数targetErrorもしくは新規生成されたリストを返す。
113 * なにもコピーされなければnullを返す。
114 * @throws IllegalArgumentException 追加元リストと追加先リストが
117 static List<DecodeErrorInfo> appendGappedErrorInfo(
118 List<DecodeErrorInfo> srcErrList,
119 int startCharPt, int endCharPt,
120 List<DecodeErrorInfo> dstErrList,
123 if(srcErrList == dstErrList) throw new IllegalArgumentException();
124 if(startCharPt >= endCharPt) return dstErrList;
127 DecodeErrorInfo.searchErrorIndex(srcErrList, startCharPt);
128 int errSize = srcErrList.size();
129 if(startErrorIdx >= errSize){
133 int endErrorIdx = calcEndIdx(srcErrList, endCharPt);
136 (0 <= startErrorIdx) && (startErrorIdx <= endErrorIdx);
141 List<DecodeErrorInfo> result;
142 if(dstErrList == null) result = createErrorList();
143 else result = dstErrList;
145 copyGappedErrorInfo(srcErrList,
146 startErrorIdx, endErrorIdx,
152 * 文字列終了点からエラーリスト範囲の最終インデックスを求める。
154 * @param srcErrList エラーリスト
155 * @param endCharPt 文字列終了点
156 * @return リスト範囲の最終インデックス
158 private static int calcEndIdx(List<DecodeErrorInfo> srcErrList,
160 int lastCharPos = endCharPt - 1;
163 DecodeErrorInfo.searchErrorIndex(srcErrList, lastCharPos);
165 int errSize = srcErrList.size();
166 if(endErrorIdx >= errSize){
167 endErrorIdx = errSize - 1;
169 DecodeErrorInfo lastErrorInfo = srcErrList.get(endErrorIdx);
171 boolean isLastErrorInfoOnLastPos =
172 lastErrorInfo.getCharPosition() == lastCharPos;
173 if( ! isLastErrorInfoOnLastPos){
182 * エラーリストの一部の範囲を、gapを加味して別リストに追加コピーする。
184 * @param srcErrList コピー元リスト
185 * @param startErrorIdx コピー元の範囲開始インデックス
186 * @param endErrorIdx コピー元の範囲終了インデックス
187 * @param dstErrList コピー先リスト
188 * @param gap 代替文字出現位置ギャップ量
190 private static void copyGappedErrorInfo(
191 List<DecodeErrorInfo> srcErrList,
192 int startErrorIdx, int endErrorIdx,
193 List<DecodeErrorInfo> dstErrList,
196 for(int index = startErrorIdx; index <= endErrorIdx; index++){
197 DecodeErrorInfo srcErrInfo = srcErrList.get(index);
198 DecodeErrorInfo gappedInfo = srcErrInfo.createGappedClone(gap);
199 dstErrList.add(gappedInfo);
209 private static List<DecodeErrorInfo> createErrorList(){
210 List<DecodeErrorInfo> result = new ArrayList<>();
218 * <p>長さ0の文字列&デコードエラー無しの状態になる。
219 * コンストラクタで新インスタンスを作るより低コスト。
229 * <p>長さ0の文字列&デコードエラー無しの状態になる。
231 private void initImpl(){
232 this.rawContent.setLength(0);
234 if(this.errList != null){
235 this.errList.clear();
244 * <p>指定されたキャパシティの範囲内で再割り当てが起きないことを保証する。
246 * @param minimumCapacity キャラクタ単位のキャパシティ長。
248 public void ensureCapacity(int minimumCapacity){
249 this.rawContent.ensureCapacity(minimumCapacity);
256 * @return デコードエラーを含むならtrue
258 public boolean hasDecodeError(){
259 if(this.errList == null) return false;
260 if(this.errList.isEmpty()) return false;
269 public List<DecodeErrorInfo> getDecodeErrorList(){
270 if( ! hasDecodeError() ){
273 return Collections.unmodifiableList(this.errList);
279 * <p>戻り値からエラー情報は消される。
281 * <p>高速なCharSequenceアクセス用途。
285 public CharSequence getRawContent(){
286 return this.rawContent;
292 * <p>デコードエラーにより追加された代替文字の変更も可能。
296 * @throws IndexOutOfBoundsException 不正な位置指定
298 public void setCharAt(int index, char ch)
299 throws IndexOutOfBoundsException{
300 this.rawContent.setCharAt(index, ch);
307 * @param index {@inheritDoc}
308 * @return {@inheritDoc}
311 public char charAt(int index){
312 return this.rawContent.charAt(index);
318 * @return {@inheritDoc}
322 return this.rawContent.length();
328 * @param startCharPt {@inheritDoc}
329 * @param endCharPt {@inheritDoc}
330 * @return {@inheritDoc}
333 public CharSequence subSequence(int startCharPt, int endCharPt){
334 return this.rawContent.subSequence(startCharPt, endCharPt);
338 * 範囲指定されたサブコンテントを切り出す。
340 * <p>サブコンテントにはデコードエラー情報が引き継がれる。
342 * @param startCharPt 開始位置
343 * @param endCharPt 終了位置
345 * @throws IndexOutOfBoundsException start または end が負の値の場合、
346 * end が length() より大きい場合、
347 * あるいは start が end より大きい場合
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);
361 * @param letter 追加する文字
365 public DecodedContent append(char letter){
366 this.rawContent.append(letter);
373 * <p>nullが渡されると文字列"null"として解釈される。
379 public DecodedContent append(CharSequence seq){
380 DecodedContent result;
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);
389 this.rawContent.append(seq);
399 * <p>nullが渡されると文字列"null"として解釈される。
402 * @param startCharPt 開始位置
403 * @param endCharPt 終了位置
405 * @throws IndexOutOfBoundsException 範囲指定が変。
408 public DecodedContent append(CharSequence seq,
409 int startCharPt, int endCharPt)
410 throws IndexOutOfBoundsException{
411 DecodedContent result;
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){
424 this.rawContent.append(seq, startCharPt, endCharPt);
434 * @param str 追加する文字配列
435 * @param offset 追加される最初の char のインデックス
436 * @param len 追加される char の数
438 * @throws IndexOutOfBoundsException 範囲指定が不正。
439 * @see StringBuffer#append(char[], int, int)
441 public DecodedContent append(char[] str, int offset, int len)
442 throws IndexOutOfBoundsException{
443 this.rawContent.append(str, offset, len);
450 * <p>追加元のエラー情報は追加先へ引き継がれる。
452 * <p>nullが渡されると文字列"null"として解釈される。
454 * @param source 追加する文字列
455 * @param startCharPt 開始位置
456 * @param endCharPt 終了位置
458 * @throws IndexOutOfBoundsException 範囲指定が変。
459 * @see Appendable#append(CharSequence, int, int)
461 public DecodedContent append(DecodedContent source,
462 int startCharPt, int endCharPt)
463 throws IndexOutOfBoundsException{
465 return append(NULLTEXT, startCharPt, endCharPt);
469 || startCharPt > endCharPt
470 || endCharPt > source.length()){
471 throw new IndexOutOfBoundsException();
472 }else if(startCharPt == endCharPt){
476 int oldLength = this.rawContent.length();
478 this.rawContent.append(source.rawContent, startCharPt, endCharPt);
480 List<DecodeErrorInfo> srcErrList;
481 if(source.hasDecodeError()){
482 srcErrList = source.errList;
487 List<DecodeErrorInfo> dstErrList;
488 if(source == this) dstErrList = null;
489 else dstErrList = this.errList;
491 int gap = startCharPt - oldLength;
493 dstErrList = appendGappedErrorInfo(srcErrList,
494 startCharPt, endCharPt,
498 if(dstErrList == null) return this;
499 if(dstErrList == this.errList) return this;
501 if(this.errList == null){
502 this.errList = dstErrList;
504 this.errList.addAll(dstErrList);
511 * 代替文字とともにデコードエラーを末尾へ追加する。
513 * <p>※呼び出し側は、追加されるデコードエラーの位置情報が
514 * 既存のデコードエラーよりも大きいことを保証しなければならない。
516 * @param errorInfo デコードエラー
518 void addDecodeError(DecodeErrorInfo errorInfo){
519 if(this.errList == null){
520 this.errList = createErrorList();
522 this.errList.add(errorInfo);
523 this.rawContent.append(ALTCHAR);
528 * 代替文字とともにデコードエラーを末尾へ追加する。
530 * @param b1st エラー1バイト目の値
532 public void addDecodeError(byte b1st){
533 DecodeErrorInfo errInfo =
534 new DecodeErrorInfo(this.rawContent.length(), b1st);
535 addDecodeError(errInfo);
540 * 代替文字とともに2バイトからなるデコードエラーを末尾へ追加する。
542 * <p>主にシフトJISのUnmapエラーを想定。
544 * @param b1st エラー1バイト目の値
545 * @param b2nd エラー2バイト目の値
547 public void addDecodeError(byte b1st, byte b2nd){
548 DecodeErrorInfo errInfo =
549 new DecodeErrorInfo(this.rawContent.length(), b1st, b2nd);
550 addDecodeError(errInfo);
557 * @return {@inheritDoc}
560 public String toString(){
561 return this.rawContent.toString();