OSDN Git Service

5281bcda5b2d490d4f2a9829f0aee2845675a0ff
[jindolf/JinParser.git] / src / main / java / jp / osdn / jindolf / parser / AbstractParser.java
1 /*
2  * abstract XHTML parser
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.osdn.jindolf.parser;
9
10 import java.util.regex.Matcher;
11 import java.util.regex.Pattern;
12 import jp.osdn.jindolf.parser.content.DecodedContent;
13 import jp.sourceforge.jindolf.corelib.GameRole;
14
15 /**
16  * 人狼BBS生成のXHTML文書を解釈するパーサの抽象基底クラス。
17  * {@link DecodedContent}の内容をパースし、
18  * 各種ハンドラへ通知する処理の基盤を構成する。
19  * 正規表現エンジンを実装基盤とする。
20  * 親パーサを指定することにより、検索対象文字列とマッチエンジンを
21  * 親パーサと共有することができる。
22  * @see Matcher
23  */
24 public abstract class AbstractParser implements ChainedParser{
25
26     /** ホワイトスペース。 */
27     protected static final String SPCHAR = "\u0020\\t\\n\\r";
28     /** 0回以上連続するホワイトスペースの正規表現。 */
29     protected static final String SP_I = "[" +SPCHAR+ "]*";
30
31     private static final Pattern DUMMY_PATTERN = compile("\u0000");
32
33
34     private final ChainedParser parent;
35
36     private DecodedContent content;
37     private Matcher matcher;
38     private String contextErrorMessage;
39
40
41     /**
42      * コンストラクタ。
43      */
44     protected AbstractParser(){
45         this(null);
46         return;
47     }
48
49     /**
50      * コンストラクタ。
51      * @param parent 親パーサ
52      */
53     protected AbstractParser(ChainedParser parent){
54         super();
55         this.parent = parent;
56         resetImpl();
57         return;
58     }
59
60
61     /**
62      * 正規表現のコンパイルを行う。
63      * デフォルトで{@link java.util.regex.Pattern#DOTALL}が
64      * オプション指定される。
65      * @param regex 正規表現文字列
66      * @return マッチエンジン
67      */
68     protected static Pattern compile(CharSequence regex){
69         Pattern result = Pattern.compile(regex.toString(), Pattern.DOTALL);
70         return result;
71     }
72
73     /**
74      * パーサの状態をコンストラクタ直後の状態にリセットする。
75      * ※コンストラクタから呼ばせるためにオーバーライド不可
76      */
77     private void resetImpl(){
78         this.content = null;
79         this.matcher = null;
80         this.contextErrorMessage = null;
81         return;
82     }
83
84     /**
85      * パーサの状態をリセットする。
86      */
87     public void reset(){
88         if(this.parent != null){
89             throw new UnsupportedOperationException();
90         }
91         resetImpl();
92         return;
93     }
94
95     /**
96      * パース対象文字列をセットする。
97      * パースが終わるまでこの文字列の内容を変更してはならない。
98      * @param content パース対象文字列
99      */
100     public void setContent(DecodedContent content){
101         if(this.parent != null){
102             throw new UnsupportedOperationException();
103         }
104
105         CharSequence rawContent = content.getRawContent();
106
107         this.content = content;
108         this.matcher = DUMMY_PATTERN.matcher(rawContent);
109
110         return;
111     }
112
113     /**
114      * {@inheritDoc}
115      * @return {@inheritDoc}
116      */
117     @Override
118     public DecodedContent getContent(){
119         if(this.parent != null){
120             return this.parent.getContent();
121         }
122
123         return this.content;
124     }
125
126     /**
127      * {@inheritDoc}
128      * @return {@inheritDoc}
129      */
130     @Override
131     public Matcher getMatcher(){
132         if(this.parent != null){
133             return this.parent.getMatcher();
134         }
135
136         return this.matcher;
137     }
138
139     /**
140      * 文脈依存のエラーメッセージを設定する。
141      * {@link #buildParseException}で利用される。
142      * 設定内容は親へ委譲されない。
143      * @param errorMessage エラーメッセージ。nullも可能。
144      */
145     protected void setContextErrorMessage(String errorMessage){
146         this.contextErrorMessage = errorMessage;
147         return;
148     }
149
150     /**
151      * 文脈状況に応じたパース例外を生成する。
152      * 例外にはリージョン開始位置が埋め込まれる。
153      * @return パース例外
154      */
155     protected HtmlParseException buildParseException(){
156         HtmlParseException result;
157         result = new HtmlParseException(this.contextErrorMessage,
158                                         regionStart() );
159         return result;
160     }
161
162     /**
163      * パースに使う正規表現パターンを切り替える。
164      * @param pattern 正規表現パターン
165      */
166     protected void switchPattern(Pattern pattern){
167         getMatcher().usePattern(pattern);
168         return;
169     }
170
171     /**
172      * 最後のマッチに成功した文字領域以前をパース対象から外す。
173      */
174     protected void shrinkRegion(){
175         int lastMatchedEnd;
176         try{
177             lastMatchedEnd = matchEnd();
178         }catch(IllegalStateException e){
179             return;
180         }
181
182         int regionEnd   = regionEnd();
183
184         getMatcher().region(lastMatchedEnd, regionEnd);
185
186         return;
187     }
188
189     /**
190      * 検査対象の一部が指定パターンにマッチするか判定する。
191      * @param pattern 指定パターン
192      * @return マッチすればtrue
193      */
194     protected boolean findProbe(Pattern pattern){
195         switchPattern(pattern);
196         boolean result = getMatcher().find();
197         return result;
198     }
199
200     /**
201      * 検査対象先頭が指定パターンにマッチするか判定する。
202      * @param pattern 指定パターン
203      * @return マッチすればtrue
204      */
205     protected boolean lookingAtProbe(Pattern pattern){
206         switchPattern(pattern);
207         boolean result = getMatcher().lookingAt();
208         return result;
209     }
210
211     /**
212      * 検査対象全体が指定パターンにマッチするか判定する。
213      * @param pattern 指定パターン
214      * @return マッチすればtrue
215      */
216     protected boolean matchesProbe(Pattern pattern){
217         switchPattern(pattern);
218         boolean result = getMatcher().matches();
219         return result;
220     }
221
222     /**
223      * 残りの検索対象領域からパターンがマッチする部分を探す。
224      * 見つからなければ例外をスローする。
225      * @param pattern 正規表現パターン
226      * @throws HtmlParseException マッチしなかった
227      */
228     protected void findAffirm(Pattern pattern)
229             throws HtmlParseException{
230         if( ! findProbe(pattern) ){
231             throw buildParseException();
232         }
233         return;
234     }
235
236     /**
237      * 残りの検索対象領域先頭からパターンがマッチする部分を探す。
238      * 見つからなければ例外をスローする。
239      * @param pattern 正規表現パターン
240      * @throws HtmlParseException マッチしなかった
241      */
242     protected void lookingAtAffirm(Pattern pattern)
243             throws HtmlParseException{
244         if( ! lookingAtProbe(pattern) ){
245             throw buildParseException();
246         }
247         return;
248     }
249
250     /**
251      * 残りの検索対象領域全体がパターンにマッチするか調べる。
252      * マッチしなければ例外をスローする。
253      * @param pattern 正規表現パターン
254      * @throws HtmlParseException マッチしなかった
255      */
256     protected void matchesAffirm(Pattern pattern)
257             throws HtmlParseException{
258         if( ! matchesProbe(pattern) ){
259             throw buildParseException();
260         }
261         return;
262     }
263
264     /**
265      * 最後のマッチで任意の前方参照グループがヒットしたか判定する。
266      * @param group グループ番号
267      * @return ヒットしていたらtrue
268      */
269     protected boolean isGroupMatched(int group){
270         boolean result = matchStart(group) >= 0;
271         return result;
272     }
273
274     /**
275      * 最後にマッチした前方参照グループを数値化する。
276      * 0以上の整数のみサポート。
277      * @param group グループ番号
278      * @return 数値
279      */
280     protected int parseGroupedInt(int group){
281         int result = 0;
282
283         CharSequence rawContent = getContent().getRawContent();
284         int start = matchStart(group);
285         int end   = matchEnd(group);
286         for(int pos = start; pos < end; pos++){
287             char letter = rawContent.charAt(pos);
288             int digit = Character.digit(letter, 10);
289             result = result * 10 + digit;
290         }
291
292         return result;
293     }
294
295     /**
296      * 最後にマッチした前方参照グループの開始位置を得る。
297      * @param group 前方参照識別番号
298      * @return 開始位置
299      */
300     protected int matchStart(int group){
301         return getMatcher().start(group);
302     }
303
304     /**
305      * 最後にマッチした全領域の開始位置を得る。
306      * @return 開始位置
307      */
308     protected int matchStart(){
309         return getMatcher().start();
310     }
311
312     /**
313      * 最後にマッチした前方参照グループの終了位置を得る。
314      * @param group 前方参照識別番号
315      * @return 終了位置
316      */
317     protected int matchEnd(int group){
318         return getMatcher().end(group);
319     }
320
321     /**
322      * 最後にマッチした全領域の終了位置を得る。
323      * @return 終了位置
324      */
325     protected int matchEnd(){
326         return getMatcher().end();
327     }
328
329     /**
330      * 検索領域の先頭位置を返す。
331      * @return 先頭位置
332      */
333     protected int regionStart(){
334         return getMatcher().regionStart();
335     }
336
337     /**
338      * 検索領域の末尾位置を返す。
339      * @return 末尾位置
340      */
341     protected int regionEnd(){
342         return getMatcher().regionEnd();
343     }
344
345     /**
346      * 0個以上のホワイトスペースを読み飛ばす。
347      * 具体的には検索対象領域の先頭が進むだけ。
348      */
349     protected void sweepSpace(){
350         CharSequence rawContent = getContent().getRawContent();
351
352         boolean hasSpace = false;
353         int regionStart = regionStart();
354         int regionEnd   = regionEnd();
355
356         for( ; regionStart < regionEnd; regionStart++){
357             char letter = rawContent.charAt(regionStart);
358
359             boolean spaceCh;
360             switch(letter){
361             case '\u0020':
362             case '\t':
363             case '\n':
364             case '\r':
365                 spaceCh = true;
366                 break;
367             default:
368                 spaceCh = false;
369                 break;
370             }
371
372             if(spaceCh){
373                 hasSpace = true;
374             }else{
375                 break;
376             }
377         }
378
379         if(hasSpace){
380             getMatcher().region(regionStart, regionEnd);
381         }
382
383         return;
384     }
385
386     /**
387      * 検索領域の先頭から各種役職名のマッチを試みる。
388      * @return 役職。何もマッチしなければnullを返す。
389      */
390     protected GameRole lookingAtRole(){
391         GameRole role = GameRole.lookingAtRole(getMatcher());
392         return role;
393     }
394
395 }