OSDN Git Service

from subversion repository
authorOlyutorskii <olyutorskii@users.osdn.me>
Thu, 26 Aug 2010 06:16:07 +0000 (15:16 +0900)
committerOlyutorskii <olyutorskii@users.osdn.me>
Thu, 26 Aug 2010 06:16:07 +0000 (15:16 +0900)
45 files changed:
.hgignore [new file with mode: 0644]
CHANGELOG.txt [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
README.txt [new file with mode: 0644]
build.xml [new file with mode: 0644]
pom.xml [new file with mode: 0644]
src/main/assembly/descriptor.xml [new file with mode: 0644]
src/main/config/checks.xml [new file with mode: 0644]
src/main/config/pmdrules.xml [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/AbstractParser.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/BasicHandler.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/ChainedParser.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/ContentBuilder.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/ContentBuilderSJ.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/ContentBuilderUCS2.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/DecodeErrorInfo.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/DecodeException.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/DecodeHandler.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/DecodedContent.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/EntityConverter.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/HtmlAdapter.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/HtmlHandler.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/HtmlParseException.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/HtmlParser.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/PageType.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/SeqRange.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/ShiftJis.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/SjisDecoder.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/StreamDecoder.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/SysEventHandler.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/SysEventParser.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/TalkHandler.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/TalkParser.java [new file with mode: 0644]
src/main/java/jp/sourceforge/jindolf/parser/package-info.java [new file with mode: 0644]
src/main/resources/jp/sourceforge/jindolf/parser/resources/version.properties [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/ContentBuilderSJTest.java [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/ContentBuilderUCS2Test.java [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/DecodeErrorInfoTest.java [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/DecodeExceptionTest.java [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/DecodedContentTest.java [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/EntityConverterTest.java [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/HtmlParseExceptionTest.java [new file with mode: 0644]
src/test/java/jp/sourceforge/jindolf/parser/ShiftJisTest.java [new file with mode: 0644]
src/test/java/sample/SampleHandler.java [new file with mode: 0644]
src/test/java/sample/SampleParser.java [new file with mode: 0644]

diff --git a/.hgignore b/.hgignore
new file mode 100644 (file)
index 0000000..0d46482
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,6 @@
+\.orig$\r
+\.orig\..*$\r
+\.chg\..*$\r
+\.rej$\r
+\.conflict\~$\r
+^nb-configuration\.xml$\r
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
new file mode 100644 (file)
index 0000000..e70ffce
--- /dev/null
@@ -0,0 +1,55 @@
+[UTF-8 Japanese]\r
+\r
+\r
+JinParser 変更履歴\r
+\r
+\r
+1.404.4 (2010-08-26)\r
+    ・MavenとMercurialによる開発体制に移行。\r
+\r
+1.404.2 (2010-05-13)\r
+    ・EXECUTEDメッセージで処刑対象が現れない場合に対処。(バグ報告#21688)\r
+\r
+1.403.2 (2010-03-25)\r
+    ・reloadインジケータより後のシステムイベントに対応。\r
+    ・ASKENTRYメッセージの変更に対処。\r
+\r
+1.402.2 (2010-03-16)\r
+    ・">>00"のようなありえないG国アンカー表記を無視する。\r
+\r
+1.401.2 (2010-03-15)\r
+    ・G国のUTF-8化に対応。\r
+    ・G国の新システムイベントに対応。\r
+\r
+1.359.2 (2010-01-17)\r
+    ・進行中1日目のASKCOMMITメッセージのパースエラーに対処。(改善要求#20321)\r
+\r
+1.358.2 (2009-10-22)\r
+    ・JinCore 1.110.2 版に対処。\r
+\r
+1.357.2 (2009-09-10)\r
+    ・JinCore 1.109.2 版に対処。\r
+\r
+1.105.2 (2009-09-09)\r
+    ・文字実体参照解決処理の改善により文字列コピー量を軽減。\r
+    ・文字列記憶容量の事前確保処理により文字列コピー量を軽減。\r
+\r
+1.104.2 (2009-08-29)\r
+    ・ビルド環境の整備。\r
+    ・ユニットテストの整備。\r
+    ・各種ソースコード診断に対処。\r
+    ・Javadocコメントの大幅な改善。\r
+    ・Shift_JISデコードエラーの揺らぎを吸収。\r
+    ・文字列処理の高速化。\r
+\r
+1.103.2 (2009-08-09)\r
+    ・JinCore 1.106.2 版に対処。\r
+\r
+1.102.2 (2009-08-08)\r
+    ・JinCore 1.105.4 版に対処。\r
+    ・カスタムビルドファイルを用意。\r
+\r
+1.101.2 (2009-08-04)\r
+    ・初回リリース。\r
+\r
+--- EOF ---\r
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..84fc943
--- /dev/null
@@ -0,0 +1,33 @@
+[UTF-8 Japanese]\r
+\r
+The MIT License\r
+\r
+\r
+Copyright(c) 2009 olyutorskii\r
+\r
+\r
+Permission is hereby granted, free of charge, to any person obtaining a copy\r
+of this software and associated documentation files (the "Software"), to deal\r
+in the Software without restriction, including without limitation the rights\r
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+copies of the Software, and to permit persons to whom the Software is\r
+furnished to do so, subject to the following conditions:\r
+\r
+The above copyright notice and this permission notice shall be included in\r
+all copies or substantial portions of the Software.\r
+\r
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
+THE SOFTWARE.\r
+\r
+\r
+JinParser作者自身からのコメント:\r
+\r
+  ※ 少なくともこのソフトウェアの実行、複製、配布、改造は自由です。\r
+  ※ 少なくともこのソフトウェアは無保証です。\r
+\r
+--- EOF ---\r
diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..926bebc
--- /dev/null
@@ -0,0 +1,98 @@
+[UTF-8 Japanese]\r
+\r
+                             JinParserライブラリ\r
+                                  README\r
+\r
+                                              Copyright(c) 2009 olyutorskii\r
+\r
+\r
+=== JinParserとは ===\r
+\r
+ JinParserライブラリは、CGIゲーム「人狼BBS」のクライアント制作者向けに作られた\r
+Javaライブラリです。\r
+ このアーカイブは、JinParserライブラリの開発資産を、ある時点で凍結したものです。\r
+\r
+ Jindolfは、CGIゲーム「人狼BBS」の専用クライアント開発プロジェクトです。\r
+ JinParserは、Jindolf以外の人狼BBSクライアント製作者向けに、\r
+JindolfのXHTML文書パース機能を提供することを目的に発足した、\r
+派生プロジェクトです。\r
+\r
+※ このアーカイブにはJindolfの実行バイナリは含まれていません。\r
+  Jindolfを動かしたい方は、jindolfで始まり拡張子が*.jarであるファイルを\r
+  別途入手してください。\r
+※ 人狼BBSのURLは [ http://homepage2.nifty.com/ninjinia/ ] まで\r
+※ 人狼BBSを主催するninjin氏は、JinParserの製作に一切関与していません。\r
+  JinParserに関する問い合わせををninjin氏へ投げかけないように!約束だよ!\r
+\r
+\r
+=== ソースコードに関して ===\r
+\r
+ - JinParserはJava言語(JLS3)で記述されたプログラムです。\r
+ - JinParserは他のプログラムに組み込まれて利用されるライブラリです。\r
+   JARファイルによるライブラリ提供や、他プロジェクトのソースツリーへの\r
+   マージの形で利用される事を想定しています。\r
+ - JinParserはJRE1.5に準拠したJava実行環境で利用できるように作られています。\r
+   原則として、JRE1.5に準拠した実行系であれば、プラットフォームを選びません。\r
+\r
+\r
+=== 依存ライブラリ ===\r
+\r
+ - JinParserはビルドに際してJinCoreライブラリを必要とします。\r
+   開発時はMaven上にJinCoreライブラリを用意してください。\r
+\r
+\r
+=== アーカイブ管理体制 ===\r
+\r
+  このアーカイブは、UTF-8による開発環境を前提として構成されています。\r
+  このアーカイブの原本となる開発資産は、\r
+      http://hg.sourceforge.jp/view/jindolf/JinParser/\r
+  を上位に持つMercurialリポジトリで管理されています。\r
+\r
+\r
+=== 開発プロジェクト運営元 ===\r
+\r
+  http://sourceforge.jp/projects/jindolf/devel/ まで。\r
+\r
+\r
+=== ディレクトリ内訳構成 ===\r
+\r
+基本的にはMaven2のmaven-archetype-quickstart構成に準じます。\r
+\r
+./README.txt\r
+    あなたが今見てるこれ。\r
+\r
+./CHANGELOG.txt\r
+    変更履歴。\r
+\r
+./LICENSE.txt\r
+    ライセンスに関して。\r
+\r
+./pom.xml\r
+    Maven2用プロジェクト構成定義ファイル。\r
+\r
+./build.xml\r
+    Ant用追加タスク。\r
+\r
+./src/main/java/\r
+    Javaのソースコード。\r
+\r
+./src/main/resources/\r
+    プロパティファイルなどの各種リソース。\r
+\r
+./src/test/java/\r
+    JUnit 4.* 用のユニットテストコード。\r
+\r
+./src/test/java/sample/\r
+    サンプルのパーサ実装。\r
+\r
+./src/main/config/checks.xml\r
+    Checkstyle用configファイル。\r
+\r
+./src/main/config/pmdrules.xml\r
+    PMD用ルール定義ファイル。\r
+\r
+./src/main/assembly/descriptor.xml\r
+    ソースアーカイブ構成定義ファイル。\r
+\r
+\r
+--- EOF ---\r
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..cbf30f2
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- ====================================================================== -->
+<!-- Ant build file (http://ant.apache.org/) for Ant 1.6.2 or above.        -->
+<!-- ====================================================================== -->
+
+<project name="jinparser" default="package" basedir=".">
+
+<!-- ====================================================================== -->
+<!-- Import maven-build.xml into the current project                        -->
+<!-- Maven2 command "mvn ant:ant" will put maven-build.xml                  -->
+<!-- ====================================================================== -->
+
+    <import optional="true" file="maven-build.xml"/>
+
+<!-- ====================================================================== -->
+<!-- Help target                                                            -->
+<!-- ====================================================================== -->
+
+    <target name="help">
+        <echo message="Please run: $ant -projecthelp"/>
+    </target>
+
+<!-- ====================================================================== -->
+<!-- sanitize files for native environment                                  -->
+<!-- ====================================================================== -->
+
+    <target description="sanitize files" name="sanitize" >
+        <echo message="sanitize files..." />
+
+        <fixcrlf
+            srcDir="./" includes="**/*.java"
+            encoding="UTF-8" outputencoding="UTF-8"
+            tablength="4" tab="remove" javafiles="true"
+            eof="remove"
+        />
+
+        <fixcrlf
+            srcDir="./" includes="**/*.xml,**/*.xsd"
+            encoding="UTF-8" outputencoding="UTF-8"
+            tablength="8" tab="remove"
+            eol="lf"
+            eof="remove"
+        />
+
+        <fixcrlf
+            srcDir="./" includes="**/*.properties"
+            encoding="ISO-8859-1" outputencoding="ISO-8859-1"
+            eol="lf"
+            eof="remove"
+        />
+
+        <fixcrlf
+            srcDir="./" includes="*.txt"
+            encoding="UTF-8" outputencoding="UTF-8"
+            tablength="8" tab="remove"
+            eof="remove"
+        />
+
+        <chmod type="file" perm="a-x">
+            <fileset dir="./" includes="**/*" excludes="**/*.sh" />
+        </chmod>
+
+    </target>
+
+</project>
+
+<!-- EOF -->
diff --git a/pom.xml b/pom.xml
new file mode 100644 (file)
index 0000000..31086fe
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Maven2 POM definition file
+-->
+
+<project
+  xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+  http://maven.apache.org/maven-v4_0_0.xsd"
+>
+    <modelVersion>4.0.0</modelVersion>
+    <!--parent/-->
+
+    <groupId>jp.sourceforge.jindolf</groupId>
+    <artifactId>jinparser</artifactId>
+
+    <version>1.404.3-SNAPSHOT</version>
+
+    <packaging>jar</packaging>
+    <name>JinParser</name>
+
+    <description><!--
+-->JinParserライブラリは、CGIゲーム「人狼BBS」のクライアント制作者向けに<!--
+-->作られたJavaライブラリです。<!--
+ --></description>
+
+    <url>http://sourceforge.jp/projects/jindolf/devel/</url>
+    <inceptionYear>2009</inceptionYear>
+
+    <organization>
+        <name>Jindolf Partners</name>
+        <url>http://sourceforge.jp/projects/jindolf/devel/</url>
+    </organization>
+
+    <licenses>
+        <license>
+            <name>The MIT License</name>
+            <url>http://www.opensource.org/licenses/mit-license.php</url>
+            <distribution>manual</distribution>
+        </license>
+    </licenses>
+
+    <developers>
+        <developer>
+            <id>olyutorskii</id>
+            <url>http://sites.google.com/site/olyutorskiipit/</url>
+            <organization>Jindolf Partners</organization>
+            <organizationUrl>http://sourceforge.jp/projects/jindolf/devel/</organizationUrl>
+            <roles>
+                <role>Project Founder</role>
+                <role>Java Developer</role>
+            </roles>
+        </developer>
+    </developers>
+
+    <contributors/>
+    <mailingLists/>
+
+    <prerequisites>
+        <maven>2.1</maven>
+    </prerequisites>
+
+    <modules/>
+
+    <scm>
+        <connection>scm:hg:http://hg.sourceforge.jp/view/jindolf/JinParser</connection>
+        <developerConnection>scm:hg:ssh://hg.sourceforge.jp//hgroot/jindolf/JinParser</developerConnection>
+        <url>http://hg.sourceforge.jp/view/jindolf/JinParser/</url>
+    </scm>
+
+    <issueManagement>
+        <system>SourceForge.JP</system>
+        <url>http://sourceforge.jp/projects/jindolf/ticket/</url>
+    </issueManagement>
+
+    <ciManagement/>
+    <distributionManagement/>
+
+    <properties>
+        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
+
+        <maven.compiler.source>1.5</maven.compiler.source>
+        <maven.compiler.target>1.5</maven.compiler.target>
+
+        <maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
+        <maven.compiler.showWarnings>true</maven.compiler.showWarnings>
+
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+
+        <project.myrepoconf>${project.basedir}/src/main/config</project.myrepoconf>
+    </properties>
+
+    <dependencyManagement/>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>[4,)</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>jp.sourceforge.jindolf</groupId>
+            <artifactId>jincore</artifactId>
+            <version>1.203.4</version>
+            <scope>compile</scope>
+        </dependency>
+
+    </dependencies>
+
+    <repositories/>
+    <pluginRepositories/>
+
+    <build>
+        <pluginManagement/>
+
+        <plugins>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-clean-plugin</artifactId>
+                <configuration>
+                    <filesets>
+                        <fileset>
+                            <directory>${project.basedir}</directory>
+                            <includes>
+                                <include>**/.DS_Store</include>
+                                <include>**/Thumbs.db</include>
+                                <include>**/core</include>
+                            </includes>
+                        </fileset>
+                    </filesets>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.5</source>  <!-- for NetBeans IDE -->
+                    <target>1.5</target>
+                    <showDeprecation>true</showDeprecation>
+                    <showWarnings>true</showWarnings>
+                    <compilerArguments>
+                        <Xlint/>
+                    </compilerArguments>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Built-By>${project.organization.name}</Built-By>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <descriptors>
+                        <descriptor>src/main/assembly/descriptor.xml</descriptor>
+                    </descriptors>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <configuration>
+                    <rules>
+                        <requireMavenVersion>
+                            <version>[2.1,3)</version>
+                        </requireMavenVersion>
+                        <requireJavaVersion>
+                            <version>[1.5,)</version>
+                        </requireJavaVersion>
+                    </rules>
+                </configuration>
+            </plugin>
+
+        </plugins>
+
+        <resources>
+
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+                <includes>
+                    <include>**/version.properties</include>
+                </includes>
+            </resource>
+
+            <resource>
+                <directory>src/main/resources</directory>
+                <includes>
+                    <include>**/*.xml</include>
+                </includes>
+            </resource>
+
+        </resources>
+
+    </build>
+
+    <reporting>
+        <plugins>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>2.7</version>
+                <configuration>
+                    <show>protected</show>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-site-plugin</artifactId>
+                <version>2.1.1</version>
+                <configuration>
+                    <locales>ja</locales>
+                    <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
+                    <outputEncoding>${project.reporting.outputEncoding}</outputEncoding>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <version>2.5</version>
+                <configuration>
+                    <configLocation>${project.myrepoconf}/checks.xml</configLocation>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-pmd-plugin</artifactId>
+                <version>2.5</version>
+                <configuration>
+                    <sourceEncoding>UTF-8</sourceEncoding>
+                    <targetJdk>${maven.compiler.target}</targetJdk>
+                    <rulesets>
+                        <ruleset>${project.myrepoconf}/pmdrules.xml</ruleset>
+                    </rulesets>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>findbugs-maven-plugin</artifactId>
+                <version>2.3.1</version>
+                <configuration>
+                    <effort>Max</effort>
+                    <threshold>Low</threshold>
+                    <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
+                    <outputEncoding>${project.reporting.outputEncoding}</outputEncoding>
+                    <!--excludeFilterFile/-->
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>cobertura-maven-plugin</artifactId>
+                <version>2.4</version>
+            </plugin>
+
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>javancss-maven-plugin</artifactId>
+                <version>2.0</version>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jxr-plugin</artifactId>
+                <version>2.2</version>
+            </plugin>
+
+        </plugins>
+    </reporting>
+
+    <profiles/>
+
+</project>
+
+<!-- EOF -->
diff --git a/src/main/assembly/descriptor.xml b/src/main/assembly/descriptor.xml
new file mode 100644 (file)
index 0000000..b744cbd
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<assembly
+  xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0
+  http://maven.apache.org/xsd/assembly-1.1.0.xsd"
+>
+
+<!--
+    SourceForge.JP用リリースファイル構成定義ファイル
+    Maven2 assembly用
+-->
+
+    <id>src</id>
+
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <fileSets>
+        <fileSet>
+            <includes>
+                <include>*.txt</include>
+                <include>pom.xml</include>
+                <include>build.xml</include>
+            </includes>
+            <useDefaultExcludes>true</useDefaultExcludes>
+        </fileSet>
+        <fileSet>
+            <directory>src/</directory>
+            <useDefaultExcludes>true</useDefaultExcludes>
+        </fileSet>
+    </fileSets>
+
+</assembly>
+
+<!-- EOF -->
diff --git a/src/main/config/checks.xml b/src/main/config/checks.xml
new file mode 100644 (file)
index 0000000..dc311b5
--- /dev/null
@@ -0,0 +1,363 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE module PUBLIC
+    "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+    "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!--
+    Checkstyle用チェック項目定義。
+
+    Checkstyle5.1以降向けに記述。
+
+    [ http://checkstyle.sourceforge.net/ ]
+
+    Copyright(c) 2010 olyutorskii
+-->
+
+
+<module name="Checker">
+
+    <property name="charset" value="UTF-8" />
+    <property name="severity" value="error" />
+
+
+    <!-- Duplicate Code -->
+    <module name="StrictDuplicateCode">
+        <property name="min" value="20" />
+    </module>
+
+
+    <!-- Headers -->
+    <module name="RegexpHeader">
+        <property name="header" value="^/\*$\n^ \*( .*)?$\n^ \*/$\n" />
+        <property name="multiLines" value="2" />
+    </module>
+
+
+    <!-- Javadoc Comments -->
+    <module name="JavadocPackage" />
+
+
+    <!-- Miscellaneous -->
+    <module name="NewlineAtEndOfFile" />
+    <module name="Translation" />
+
+
+    <!-- Regexp -->
+    <module name="RegexpSingleline">
+        <property name="format" value="\s+$" />
+        <property name="minimum" value="0" />
+        <property name="maximum" value="0" />
+    </module>
+
+
+    <!-- Size Violations -->
+    <module name="FileLength" />
+
+
+    <!-- Whitespace -->
+    <module name="FileTabCharacter" />
+
+
+<!-- Filters
+    <module name="SeverityMatchFilter" />
+    <module name="SuppressionFilter" />
+    <module name="SuppressionCommentFilter" />
+    <module name="SuppressWithNearbyCommentFilter" />
+-->
+
+
+    <module name="TreeWalker">
+
+        <property name="tabWidth" value="4" />
+
+
+    <!-- Annotations -->
+
+        <module name="AnnotationUseStyle" />
+        <module name="MissingDeprecated" />
+        <module name="MissingOverride" />
+        <module name="PackageAnnotation" />
+        <module name="SuppressWarnings" />
+
+
+    <!-- Block Checks -->
+
+        <module name="EmptyBlock" />
+        <module name="LeftCurly" />
+        <module name="NeedBraces">
+            <property name="tokens" value="LITERAL_DO" />
+        </module>
+        <module name="RightCurly" />
+        <module name="AvoidNestedBlocks" />
+
+
+    <!-- Class Design -->
+
+        <module name="VisibilityModifier" />
+        <module name="FinalClass" />
+        <module name="InterfaceIsType" />
+        <module name="HideUtilityClassConstructor" />
+<!--    <module name="DesignForExtension" />  -->
+        <module name="MutableException" />
+        <module name="ThrowsCount">
+            <property name="max" value="4" />
+        </module>
+
+
+    <!-- Coding -->
+
+        <module name="ArrayTrailingComma" />
+        <module name="AvoidInlineConditionals" />
+        <module name="CovariantEquals" />
+        <module name="DoubleCheckedLocking" />
+        <module name="EmptyStatement" />
+        <module name="EqualsAvoidNull" />
+        <module name="EqualsHashCode" />
+<!--    <module name="FinalLocalVariable" />  -->
+        <module name="HiddenField">
+            <property name="ignoreConstructorParameter" value="true" />
+            <property name="ignoreSetter" value="true" />
+            <property name="ignoreAbstractMethods" value="true" />
+        </module>
+        <module name="IllegalInstantiation" />
+        <module name="IllegalToken">
+            <property name="tokens" value="LITERAL_NATIVE, STATIC_IMPORT" />
+        </module>
+        <module name="IllegalTokenText">
+            <property name="tokens" value="NUM_INT, NUM_LONG" />
+            <property name="format" value="^0[^lx]" />
+            <property name="ignoreCase" value="true" />
+        </module>
+        <module name="InnerAssignment" />
+        <module name="MagicNumber" />
+        <module name="MissingSwitchDefault" />
+        <module name="ModifiedControlVariable" />
+        <module name="RedundantThrows">
+            <property name="allowUnchecked" value="true" />
+            <property name="allowSubclasses" value="true" />
+        </module>
+        <module name="SimplifyBooleanExpression" />
+        <module name="SimplifyBooleanReturn" />
+        <module name="StringLiteralEquality" />
+        <module name="NestedIfDepth" />
+        <module name="NestedTryDepth" />
+        <module name="NoClone" />
+        <module name="NoFinalizer" />
+        <module name="SuperClone" />
+        <module name="SuperFinalize" />
+        <module name="IllegalCatch" />
+        <module name="IllegalThrows" />
+        <module name="PackageDeclaration" />
+        <module name="JUnitTestCase" />
+        <module name="ReturnCount">
+            <property name="max" value="3" />
+        </module>
+        <module name="IllegalType" />
+        <module name="DeclarationOrder" />
+        <module name="ParameterAssignment" />
+<!--    <module name="ExplicitInitialization" />  -->
+        <module name="DefaultComesLast" />
+        <module name="MissingCtor" />
+        <module name="FallThrough" />
+        <module name="MultipleStringLiterals" />
+        <module name="MultipleVariableDeclarations" />
+        <module name="UnnecessaryParentheses" />
+
+
+    <!-- Imports -->
+
+        <module name="AvoidStarImport" />
+        <module name="AvoidStaticImport" />
+        <module name="IllegalImport" />
+        <module name="RedundantImport" />
+        <module name="UnusedImports" />
+        <module name="ImportOrder" />
+<!--    <module name="ImportControl" />  -->
+
+
+    <!-- Javadoc Comments -->
+
+        <module name="JavadocType" />
+        <module name="JavadocMethod" />
+        <module name="JavadocVariable">
+            <property name="scope" value="protected" />
+        </module>
+        <module name="JavadocStyle">
+            <property
+                name="endOfSentenceFormat"
+                value="([。.?!][ \t\n\r\f&lt;])|([。.?!]$)" />
+            <property name="checkEmptyJavadoc" value="true" />
+            <property name="checkHtml" value="true" />
+        </module>
+<!--    <module name="WriteTag" />  -->
+
+
+    <!-- Metrics -->
+
+        <module name="BooleanExpressionComplexity" />
+        <module name="ClassDataAbstractionCoupling" />
+        <module name="ClassFanOutComplexity" />
+        <module name="CyclomaticComplexity" />
+        <module name="NPathComplexity" />
+        <module name="JavaNCSS" />
+
+
+    <!-- Miscellaneous -->
+
+        <module name="TodoComment">
+            <property name="format" value="TODO" />
+        </module>
+        <module name="UncommentedMain" />
+        <module name="UpperEll" />
+        <module name="ArrayTypeStyle" />
+<!--    <module name="FinalParameters" />  -->
+        <module name="DescendantToken" />
+<!--
+        <module name="Indentation">
+            <property name="basicOffset" value="4" />
+            <property name="caseIndent" value="0" />
+        </module>
+-->
+<!--    <module name="TrailingComment" />  -->
+        <module name="Regexp">
+            <property name="format" value="@author" />
+            <property name="illegalPattern" value="true" />
+        </module>
+        <module name="Regexp">
+            <property name="format" value="^ \* Copyright\(c\)" />
+        </module>
+        <module name="Regexp">
+            <property name="format" value="^ \* License : The MIT License" />
+            <property name="duplicateLimit" value="1" />
+        </module>
+
+    <!-- Modifiers -->
+
+        <module name="ModifierOrder" />
+        <module name="RedundantModifier" />
+
+
+    <!-- Naming Conventions -->
+
+        <module name="AbstractClassName" />
+        <module name="ClassTypeParameterName" />
+        <module name="ConstantName" />
+        <module name="LocalFinalVariableName">
+            <property name="format" value="^[a-z][_a-zA-Z0-9]*$" />
+        </module>
+        <module name="LocalVariableName">
+            <property name="format" value="^[a-z][_a-zA-Z0-9]*$" />
+        </module>
+        <module name="MemberName">
+            <property name="format" value="^[a-z][_a-zA-Z0-9]*$" />
+        </module>
+        <module name="MethodName" />
+        <module name="MethodTypeParameterName" />
+        <module name="PackageName" />
+        <module name="ParameterName">
+            <property name="format" value="^[a-z][_a-zA-Z0-9]*$" />
+        </module>
+        <module name="StaticVariableName">
+            <property name="format" value="^[a-z][_a-zA-Z0-9]*$" />
+        </module>
+        <module name="TypeName" />
+
+
+    <!-- Size Violations -->
+
+        <module name="ExecutableStatementCount" />
+        <module name="LineLength">
+            <property name="max" value="78" />
+        </module>
+        <module name="MethodLength" />
+        <module name="AnonInnerLength" />
+        <module name="ParameterNumber" />
+        <module name="OuterTypeNumber" />
+
+
+    <!-- Whitespace -->
+
+        <module name="GenericWhitespace" />
+        <module name="EmptyForInitializerPad" />
+        <module name="EmptyForIteratorPad" />
+        <module name="MethodParamPad">
+            <property name="tokens" value="CTOR_DEF, LITERAL_NEW, METHOD_DEF, SUPER_CTOR_CALL" />
+        </module>
+        <module name="NoWhitespaceAfter">
+            <property name="allowLineBreaks" value="false" />
+            <property name="tokens" value="DEC, DOT, INC" />
+        </module>
+        <module name="NoWhitespaceBefore">
+            <property name="allowLineBreaks" value="false" />
+            <property name="tokens" value="POST_DEC, POST_INC" />
+        </module>
+        <module name="NoWhitespaceBefore">
+            <property name="allowLineBreaks" value="true" />
+            <property name="tokens" value="SEMI" />
+        </module>
+        <module name="OperatorWrap">
+            <property name="option" value="eol" />
+            <property
+                name="tokens"
+                value="ASSIGN,
+                       BAND_ASSIGN, BOR_ASSIGN, BXOR_ASSIGN,
+                       PLUS_ASSIGN, MINUS_ASSIGN,
+                       STAR_ASSIGN, DIV_ASSIGN, MOD_ASSIGN,
+                       SL_ASSIGN, SR_ASSIGN"
+            />
+        </module>
+        <module name="OperatorWrap">
+            <property name="option" value="nl" />
+            <property
+                name="tokens"
+                value="BAND, BOR, BXOR,
+                       MINUS, STAR, DIV, MOD,
+                       LAND, LOR,
+                       EQUAL"
+            />
+        </module>
+        <module name="ParenPad">
+            <property name="option" value="nospace" />
+            <property name="tokens" value="CTOR_CALL, METHOD_CALL, SUPER_CTOR_CALL" />
+        </module>
+        <module name="TypecastParenPad" />
+        <module name="WhitespaceAfter">
+            <property name="tokens" value="COMMA, SEMI" />
+        </module>
+        <module name="WhitespaceAround">
+            <property
+                name="tokens"
+                value="ASSIGN,
+                       LAND, LOR,
+                       BAND, BOR, BXOR, BSR,
+                       BAND_ASSIGN, BOR_ASSIGN, BXOR_ASSIGN, BSR_ASSIGN,
+                       SL, SR,
+                       SL_ASSIGN, SR_ASSIGN,
+                       MINUS, STAR, DIV, MOD,
+                       PLUS_ASSIGN, MINUS_ASSIGN, STAR_ASSIGN, DIV_ASSIGN, MOD_ASSIGN,
+                       EQUAL, NOT_EQUAL, GT, GE, LT, LE,
+                       "
+            />
+        </module>
+
+
+<!-- 代用品で解決
+        <module name="Header" />
+        <module name="RegexpSingleline" />
+        <module name="RegexpMultiline" />
+        <module name="RegexpSinglelineJava" />
+-->
+
+<!-- バグ?
+        <module name="RequireThis" />
+-->
+
+<!-- Obsolated
+        <module name="TabCharacter" />
+-->
+
+    </module>
+</module>
+
+<!-- EOF -->
diff --git a/src/main/config/pmdrules.xml b/src/main/config/pmdrules.xml
new file mode 100644 (file)
index 0000000..1b5fcf2
--- /dev/null
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    PMD用ルールセット定義
+
+    PMD [ http://pmd.sourceforge.net/ ] 4.2.5 以降用に記述されています。
+
+    Copyright(c) 2010 olyutorskii
+-->
+
+<ruleset
+  name="Custom ruleset"
+  xmlns="http://pmd.sf.net/ruleset/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
+  http://pmd.sf.net/ruleset_xml_schema.xsd"
+  xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"
+>
+
+    <rule ref="rulesets/basic.xml">
+        <exclude name="UnnecessaryReturn" />
+    </rule>
+
+    <rule ref="rulesets/braces.xml">
+        <exclude name="IfElseStmtsMustUseBraces" />
+        <exclude name="IfStmtsMustUseBraces" />
+    </rule>
+
+    <rule ref="rulesets/codesize.xml">
+        <exclude name="TooManyMethods" />
+    </rule>
+
+    <rule ref="rulesets/clone.xml" />
+
+    <rule ref="rulesets/controversial.xml">
+        <exclude name="DataflowAnomalyAnalysis" />
+        <exclude name="OnlyOneReturn" />
+        <exclude name="DefaultPackage" />
+    </rule>
+
+    <rule ref="rulesets/coupling.xml" />
+
+    <rule ref="rulesets/design.xml">
+        <exclude name="UnnecessaryLocalBeforeReturn" />
+    </rule>
+
+    <rule ref="rulesets/finalizers.xml" />
+
+    <rule ref="rulesets/imports.xml" />
+
+    <rule ref="rulesets/logging-java.xml" />
+
+    <rule ref="rulesets/migrating.xml" />
+    <rule ref="rulesets/migrating_to_15.xml" />
+
+    <rule ref="rulesets/naming.xml">
+        <exclude name="LongVariable" />
+        <exclude name="ShortVariable" />
+    </rule>
+
+    <rule ref="rulesets/optimizations.xml">
+        <exclude name="LocalVariableCouldBeFinal" />
+        <exclude name="MethodArgumentCouldBeFinal" />
+    </rule>
+
+    <rule ref="rulesets/strictexception.xml">
+        <exclude name="AvoidThrowingNullPointerException" />
+    </rule>
+
+    <rule ref="rulesets/strings.xml" />
+
+    <rule ref="rulesets/sunsecure.xml" />
+
+    <rule ref="rulesets/typeresolution.xml" />
+
+    <rule ref="rulesets/unusedcode.xml" />
+
+</ruleset>
+
+<!-- EOF -->
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/AbstractParser.java b/src/main/java/jp/sourceforge/jindolf/parser/AbstractParser.java
new file mode 100644 (file)
index 0000000..f1fbfd0
--- /dev/null
@@ -0,0 +1,390 @@
+/*\r
+ * abstract XHTML parser\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: AbstractParser.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.regex.Matcher;\r
+import java.util.regex.Pattern;\r
+import jp.sourceforge.jindolf.corelib.GameRole;\r
+\r
+/**\r
+ * 人狼BBS生成のXHTML文書を解釈するパーサの抽象基底クラス。\r
+ * {@link DecodedContent}の内容をパースし、\r
+ * 各種ハンドラへ通知する処理の基盤を構成する。\r
+ * 正規表現エンジンを実装基盤とする。\r
+ * 親パーサを指定することにより、検索対象文字列とマッチエンジンを\r
+ * 親パーサと共有することができる。\r
+ * @see Matcher\r
+ */\r
+public abstract class AbstractParser implements ChainedParser{\r
+\r
+    /** ホワイトスペース。 */\r
+    protected static final String SPCHAR = "\u0020\\t\\n\\r";\r
+    /** 0回以上連続するホワイトスペースの正規表現。 */\r
+    protected static final String SP_I = "[" +SPCHAR+ "]*";\r
+\r
+    private static final Pattern DUMMY_PATTERN = compile("\u0000");\r
+\r
+    /**\r
+     * 正規表現のコンパイルを行う。\r
+     * デフォルトで{@link java.util.regex.Pattern#DOTALL}が\r
+     * オプション指定される。\r
+     * @param regex 正規表現文字列\r
+     * @return マッチエンジン\r
+     */\r
+    protected static Pattern compile(CharSequence regex){\r
+        Pattern result = Pattern.compile(regex.toString(), Pattern.DOTALL);\r
+        return result;\r
+    }\r
+\r
+    private final ChainedParser parent;\r
+\r
+    private DecodedContent content;\r
+    private Matcher matcher;\r
+    private String contextErrorMessage;\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     */\r
+    protected AbstractParser(){\r
+        this(null);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param parent 親パーサ\r
+     */\r
+    protected AbstractParser(ChainedParser parent){\r
+        super();\r
+        this.parent = parent;\r
+        resetImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * パーサの状態をコンストラクタ直後の状態にリセットする。\r
+     * ※コンストラクタから呼ばせるためにオーバーライド不可\r
+     */\r
+    private void resetImpl(){\r
+        this.content = null;\r
+        this.matcher = null;\r
+        this.contextErrorMessage = null;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * パーサの状態をリセットする。\r
+     */\r
+    public void reset(){\r
+        if(this.parent != null){\r
+            throw new UnsupportedOperationException();\r
+        }\r
+        resetImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     */\r
+    /**\r
+     * パース対象文字列をセットする。\r
+     * パースが終わるまでこの文字列の内容を変更してはならない。\r
+     * @param content パース対象文字列\r
+     */\r
+    public void setContent(DecodedContent content){\r
+        if(this.parent != null){\r
+            throw new UnsupportedOperationException();\r
+        }\r
+\r
+        CharSequence rawContent = content.getRawContent();\r
+\r
+        this.content = content;\r
+        this.matcher = DUMMY_PATTERN.matcher(rawContent);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    public DecodedContent getContent(){\r
+        if(this.parent != null){\r
+            return this.parent.getContent();\r
+        }\r
+\r
+        return this.content;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    public Matcher getMatcher(){\r
+        if(this.parent != null){\r
+            return this.parent.getMatcher();\r
+        }\r
+\r
+        return this.matcher;\r
+    }\r
+\r
+    /**\r
+     * 文脈依存のエラーメッセージを設定する。\r
+     * {@link #buildParseException}で利用される。\r
+     * 設定内容は親へ委譲されない。\r
+     * @param errorMessage エラーメッセージ。nullも可能。\r
+     */\r
+    protected void setContextErrorMessage(String errorMessage){\r
+        this.contextErrorMessage = errorMessage;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 文脈状況に応じたパース例外を生成する。\r
+     * 例外にはリージョン開始位置が埋め込まれる。\r
+     * @return パース例外\r
+     */\r
+    protected HtmlParseException buildParseException(){\r
+        HtmlParseException result;\r
+        result = new HtmlParseException(this.contextErrorMessage,\r
+                                        regionStart() );\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * パースに使う正規表現パターンを切り替える。\r
+     * @param pattern 正規表現パターン\r
+     */\r
+    protected void switchPattern(Pattern pattern){\r
+        getMatcher().usePattern(pattern);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 最後のマッチに成功した文字領域以前をパース対象から外す。\r
+     */\r
+    protected void shrinkRegion(){\r
+        int lastMatchedEnd;\r
+        try{\r
+            lastMatchedEnd = matchEnd();\r
+        }catch(IllegalStateException e){\r
+            return;\r
+        }\r
+\r
+        int regionEnd   = regionEnd();\r
+\r
+        getMatcher().region(lastMatchedEnd, regionEnd);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 検査対象の一部が指定パターンにマッチするか判定する。\r
+     * @param pattern 指定パターン\r
+     * @return マッチすればtrue\r
+     */\r
+    protected boolean findProbe(Pattern pattern){\r
+        switchPattern(pattern);\r
+        if( getMatcher().find() ) return true;\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * 検査対象先頭が指定パターンにマッチするか判定する。\r
+     * @param pattern 指定パターン\r
+     * @return マッチすればtrue\r
+     */\r
+    protected boolean lookingAtProbe(Pattern pattern){\r
+        switchPattern(pattern);\r
+        if( getMatcher().lookingAt() ) return true;\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * 検査対象全体が指定パターンにマッチするか判定する。\r
+     * @param pattern 指定パターン\r
+     * @return マッチすればtrue\r
+     */\r
+    protected boolean matchesProbe(Pattern pattern){\r
+        switchPattern(pattern);\r
+        if( getMatcher().matches() ) return true;\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * 残りの検索対象領域からパターンがマッチする部分を探す。\r
+     * 見つからなければ例外をスローする。\r
+     * @param pattern 正規表現パターン\r
+     * @throws HtmlParseException\r
+     * マッチしなかった\r
+     */\r
+    protected void findAffirm(Pattern pattern)\r
+            throws HtmlParseException{\r
+        if( ! findProbe(pattern) ){\r
+            throw buildParseException();\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 残りの検索対象領域先頭からパターンがマッチする部分を探す。\r
+     * 見つからなければ例外をスローする。\r
+     * @param pattern 正規表現パターン\r
+     * @throws HtmlParseException\r
+     * マッチしなかった\r
+     */\r
+    protected void lookingAtAffirm(Pattern pattern)\r
+            throws HtmlParseException{\r
+        if( ! lookingAtProbe(pattern) ){\r
+            throw buildParseException();\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 残りの検索対象領域全体がパターンにマッチするか調べる。\r
+     * マッチしなければ例外をスローする。\r
+     * @param pattern 正規表現パターン\r
+     * @throws HtmlParseException\r
+     * マッチしなかった\r
+     */\r
+    protected void matchesAffirm(Pattern pattern)\r
+            throws HtmlParseException{\r
+        if( ! matchesProbe(pattern) ){\r
+            throw buildParseException();\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 最後のマッチで任意の前方参照グループがヒットしたか判定する。\r
+     * @param group グループ番号\r
+     * @return ヒットしていたらtrue\r
+     */\r
+    protected boolean isGroupMatched(int group){\r
+        if(matchStart(group) >= 0) return true;\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * 最後にマッチした前方参照グループを数値化する。\r
+     * 0以上の整数のみサポート。\r
+     * @param group グループ番号\r
+     * @return 数値\r
+     */\r
+    protected int parseGroupedInt(int group){\r
+        int result = 0;\r
+\r
+        CharSequence rawContent = getContent().getRawContent();\r
+        int start = matchStart(group);\r
+        int end   = matchEnd(group);\r
+        for(int pos = start; pos < end; pos++){\r
+            char letter = rawContent.charAt(pos);\r
+            int digit = Character.digit(letter, 10);\r
+            result = result * 10 + digit;\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * 最後にマッチした前方参照グループの開始位置を得る。\r
+     * @param group 前方参照識別番号\r
+     * @return 開始位置\r
+     */\r
+    protected int matchStart(int group){\r
+        return getMatcher().start(group);\r
+    }\r
+\r
+    /**\r
+     * 最後にマッチした前方参照グループの終了位置を得る。\r
+     * @param group 前方参照識別番号\r
+     * @return 終了位置\r
+     */\r
+    protected int matchEnd(int group){\r
+        return getMatcher().end(group);\r
+    }\r
+\r
+    /**\r
+     * 最後にマッチした全領域の開始位置を得る。\r
+     * @return 開始位置\r
+     */\r
+    protected int matchStart(){\r
+        return getMatcher().start();\r
+    }\r
+\r
+    /**\r
+     * 最後にマッチした全領域の終了位置を得る。\r
+     * @return 終了位置\r
+     */\r
+    protected int matchEnd(){\r
+        return getMatcher().end();\r
+    }\r
+\r
+    /**\r
+     * 検索領域の先頭位置を返す。\r
+     * @return 先頭位置\r
+     */\r
+    protected int regionStart(){\r
+        return getMatcher().regionStart();\r
+    }\r
+\r
+    /**\r
+     * 検索領域の末尾位置を返す。\r
+     * @return 末尾位置\r
+     */\r
+    protected int regionEnd(){\r
+        return getMatcher().regionEnd();\r
+    }\r
+\r
+    /**\r
+     * 0個以上のホワイトスペースを読み飛ばす。\r
+     * 具体的には検索対象領域の先頭が進むだけ。\r
+     */\r
+    protected void sweepSpace(){\r
+        CharSequence rawContent = getContent().getRawContent();\r
+\r
+        boolean hasSpace = false;\r
+        int regionStart = regionStart();\r
+        int regionEnd   = regionEnd();\r
+\r
+        for( ; regionStart < regionEnd; regionStart++){\r
+            char letter = rawContent.charAt(regionStart);\r
+\r
+            switch(letter){\r
+            case '\u0020':\r
+            case '\t':\r
+            case '\n':\r
+            case '\r':\r
+                hasSpace = true;\r
+                continue;\r
+            default:\r
+                break;\r
+            }\r
+\r
+            break;\r
+        }\r
+\r
+        if(hasSpace){\r
+            getMatcher().region(regionStart, regionEnd);\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 検索領域の先頭から各種役職名のマッチを試みる。\r
+     * @return 役職。何もマッチしなければnullを返す。\r
+     */\r
+    protected GameRole lookingAtRole(){\r
+        GameRole role = GameRole.lookingAtRole(getMatcher());\r
+        return role;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/BasicHandler.java b/src/main/java/jp/sourceforge/jindolf/parser/BasicHandler.java
new file mode 100644 (file)
index 0000000..7a25d0d
--- /dev/null
@@ -0,0 +1,137 @@
+/*\r
+ * basic handler for XHTML\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: BasicHandler.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import jp.sourceforge.jindolf.corelib.PeriodType;\r
+import jp.sourceforge.jindolf.corelib.VillageState;\r
+\r
+/**\r
+ * 人狼BBSの各種XHTMLの基本的な構造をパースするためのハンドラ。\r
+ * このハンドラの全メソッドはパーサ{@link HtmlParser}により呼ばれる。\r
+ *\r
+ * パーサはパース開始時に{@link #startParse(DecodedContent)}を呼び、\r
+ * パース終了直前に{@link #endParse()}を呼ぶ。\r
+ * その間に他の様々なメソッドが呼び出される。\r
+ *\r
+ * 一部のメソッドに渡される{@link DecodedContent}文字列オブジェクトは\r
+ * mutableである。\r
+ * 後々で内容が必要になるならば、ハンドラはSeqRangeで示されたこの内容の\r
+ * 必要な箇所をコピーして保存しなければならない。\r
+ *\r
+ * フラグメントや属性値中の文字参照記号列の解釈はハンドラ側の責務とする。\r
+ *\r
+ * 各メソッドは、各々の判断で{@link HtmlParseException}をスローする\r
+ * ことにより、パース作業を中断させることができる。\r
+ */\r
+public interface BasicHandler{\r
+\r
+    /**\r
+     * パース開始の通知を受け取る。\r
+     * @param content これからパースを始めるXHTML文字列\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void startParse(DecodedContent content) throws HtmlParseException;\r
+\r
+    /**\r
+     * titleタグの内容の通知を受け取る。\r
+     * 例:「人狼BBS:F F2019 新緑の村」。\r
+     * @param content パース対象文字列\r
+     * @param titleRange タイトルの範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void pageTitle(DecodedContent content, SeqRange titleRange)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * ログイン名(ID)の通知を受け取る。\r
+     * ログインせずに得られたページがパース対象であるなら、呼ばれない。\r
+     * F国のみで動作確認。\r
+     * @param content パース対象文字列\r
+     * @param loginRange ログイン名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void loginName(DecodedContent content, SeqRange loginRange)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 読み込んだページ種別を自動認識した結果を伝える。\r
+     * ページタイトルもしくはログイン名の通知の後に呼ばれうる。\r
+     * @param type ページ種別\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void pageType(PageType type)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 村の名前の通知を受け取る。\r
+     * 国名と番号と愛称に分解するのはハンドラ側の責務。\r
+     * 例:「F2019 新緑の村」。\r
+     * @param content パース対象文字列\r
+     * @param villageRange 村名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void villageName(DecodedContent content, SeqRange villageRange)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 次回更新時刻の通知を受け取る。\r
+     * 既に終了した村がパース対象の場合、あまり月日に意味はないかも。\r
+     * @param month 更新月\r
+     * @param day 更新日\r
+     * @param hour 更新時\r
+     * @param minute 更新分\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void commitTime(int month, int day, int hour, int minute)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 他の日へのリンクの通知を受け取る。\r
+     * 複数回呼ばれる場合がある。\r
+     * @param content パース対象文字列\r
+     * @param anchorRange aタグhref属性値の範囲\r
+     * @param periodType 日のタイプ。「終了」ならnull。\r
+     * @param day 日にち。「プロローグ」、「エピローグ」、「終了」では-1。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void periodLink(DecodedContent content,\r
+                     SeqRange anchorRange,\r
+                     PeriodType periodType, int day)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 村一覧リスト内の個別の村情報の通知を受け取る。\r
+     * @param content パース対象文字列\r
+     * @param anchorRange URLの範囲\r
+     * @param villageRange 村名の範囲\r
+     * @param hour 更新時。不明なら負の数。\r
+     * @param minute 更新分。不明なら負の数。\r
+     * @param state 村の状態\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void villageRecord(DecodedContent content,\r
+                         SeqRange anchorRange,\r
+                         SeqRange villageRange,\r
+                         int hour, int minute,\r
+                         VillageState state )\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * パースの終了の通知を受け取る。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void endParse() throws HtmlParseException;\r
+\r
+    // TODO 「全て表示」リンクの検出メソッドは必要?\r
+    // TODO 表示モード切り替え(人狼墓全)リンクの検出メソッドは必要?\r
+    // TODO 「次の日へ」リンクの検出メソッドは必要?\r
+    // TODO 投票先、襲撃先プルダウンリストの検出メソッドは必要?\r
+    // TODO 霊能結果の検出メソッドは必要?\r
+    // TODO 発言フォームの検出メソッドは必要?\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/ChainedParser.java b/src/main/java/jp/sourceforge/jindolf/parser/ChainedParser.java
new file mode 100644 (file)
index 0000000..102e799
--- /dev/null
@@ -0,0 +1,33 @@
+/*\r
+ * chained parser interface\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: ChainedParser.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.regex.Matcher;\r
+\r
+/**\r
+ * 連結パーサの基本インタフェース。\r
+ */\r
+public interface ChainedParser{\r
+\r
+    /**\r
+     * パース対象文字列を取得する。\r
+     * このクラスおよびこのクラスを継承するものは、\r
+     * 全てこのメソッドを介してパース対象文字列にアクセスしなければならない。\r
+     * @return パース対象文字列\r
+     */\r
+    DecodedContent getContent();\r
+\r
+    /**\r
+     * 現時点での正規表現マッチャを得る。\r
+     * このクラスおよびこのクラスを継承するものは、\r
+     * 全てこのメソッドを介してマッチャにアクセスしなければならない。\r
+     * @return 正規表現マッチャ\r
+     */\r
+    Matcher getMatcher();\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/ContentBuilder.java b/src/main/java/jp/sourceforge/jindolf/parser/ContentBuilder.java
new file mode 100644 (file)
index 0000000..84b70bd
--- /dev/null
@@ -0,0 +1,75 @@
+/*\r
+ * abstract content builder\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: ContentBuilder.java 1001 2010-03-15 12:09:35Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.nio.charset.CharsetDecoder;\r
+\r
+/**\r
+ * {@link DecodedContent}取得用抽象デコードハンドラ。\r
+ */\r
+public abstract class ContentBuilder implements DecodeHandler{\r
+\r
+    /** 文字列内容。 */\r
+    protected DecodedContent content;\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 長さ0で空の{@link DecodedContent}がセットされる。\r
+     * @param capacity 初期容量\r
+     * @throws NegativeArraySizeException 容量指定が負。\r
+     */\r
+    protected ContentBuilder(int capacity) throws NegativeArraySizeException{\r
+        super();\r
+        this.content = new DecodedContent(capacity);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコード処理の初期化。\r
+     */\r
+    protected void init(){\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * エラー情報をフラッシュする。\r
+     */\r
+    protected void flushError(){\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param decoder {@inheritDoc}\r
+     * @throws DecodeException {@inheritDoc}\r
+     */\r
+    public void startDecoding(CharsetDecoder decoder)\r
+            throws DecodeException{\r
+        init();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @throws DecodeException {@inheritDoc}\r
+     */\r
+    public void endDecoding()\r
+            throws DecodeException{\r
+        flushError();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコード結果の{@link DecodedContent}を取得する。\r
+     * @return デコード結果文字列\r
+     */\r
+    public DecodedContent getContent(){\r
+        return this.content;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/ContentBuilderSJ.java b/src/main/java/jp/sourceforge/jindolf/parser/ContentBuilderSJ.java
new file mode 100644 (file)
index 0000000..aa2b514
--- /dev/null
@@ -0,0 +1,122 @@
+/*\r
+ * content builder for Shift_JIS\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: ContentBuilderSJ.java 991 2010-03-14 10:22:35Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+/**\r
+ * "Shift_JIS"エンコーディング用デコードハンドラ。\r
+ * {@link SjisDecoder}からの通知に従い、\r
+ * {@link DecodedContent}へとデコードする。\r
+ */\r
+public class ContentBuilderSJ extends ContentBuilder{\r
+\r
+    private boolean hasByte1st;\r
+    private byte byte1st;\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 長さ0で空の{@link DecodedContent}がセットされる。\r
+     */\r
+    public ContentBuilderSJ(){\r
+        this(128);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 長さ0で空の{@link DecodedContent}がセットされる。\r
+     * @param capacity 初期容量\r
+     * @throws NegativeArraySizeException 容量指定が負。\r
+     */\r
+    public ContentBuilderSJ(int capacity) throws NegativeArraySizeException{\r
+        super(capacity);\r
+        initImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコード処理の初期化下請。\r
+     */\r
+    private void initImpl(){\r
+        this.content.init();\r
+        this.hasByte1st = false;\r
+        this.byte1st = 0x00;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコード処理の初期化。\r
+     */\r
+    @Override\r
+    protected void init(){\r
+        initImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * エラー情報をフラッシュする。\r
+     */\r
+    @Override\r
+    protected void flushError(){\r
+        if(this.hasByte1st){\r
+            this.content.addDecodeError(this.byte1st);\r
+            this.hasByte1st = false;\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param seq {@inheritDoc}\r
+     * @throws DecodeException {@inheritDoc}\r
+     */\r
+    public void charContent(CharSequence seq)\r
+            throws DecodeException{\r
+        flushError();\r
+        this.content.append(seq);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param errorArray {@inheritDoc}\r
+     * @param offset {@inheritDoc}\r
+     * @param length {@inheritDoc}\r
+     * @throws DecodeException {@inheritDoc}\r
+     */\r
+    public void decodingError(byte[] errorArray, int offset, int length)\r
+            throws DecodeException{\r
+        int limit = offset + length;\r
+        for(int bpos = offset; bpos < limit; bpos++){\r
+            byte bval = errorArray[bpos];\r
+            if( ! this.hasByte1st){\r
+                if(ShiftJis.isShiftJIS1stByte(bval)){\r
+                    this.byte1st = bval;\r
+                    this.hasByte1st = true;\r
+                }else{\r
+                    this.content.addDecodeError(bval);\r
+                }\r
+            }else{\r
+                if(ShiftJis.isShiftJIS2ndByte(bval)){   // 文字集合エラー\r
+                    this.content.addDecodeError(this.byte1st, bval);\r
+                    this.hasByte1st = false;\r
+                }else if(ShiftJis.isShiftJIS1stByte(bval)){\r
+                    this.content.addDecodeError(this.byte1st);\r
+                    this.byte1st = bval;\r
+                    this.hasByte1st = true;\r
+                }else{\r
+                    this.content.addDecodeError(this.byte1st);\r
+                    this.content.addDecodeError(bval);\r
+                    this.hasByte1st = false;\r
+                }\r
+            }\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/ContentBuilderUCS2.java b/src/main/java/jp/sourceforge/jindolf/parser/ContentBuilderUCS2.java
new file mode 100644 (file)
index 0000000..4f1e7d5
--- /dev/null
@@ -0,0 +1,129 @@
+/*\r
+ * content builder for UTF-8 (UCS2 only)\r
+ *\r
+ * Copyright(c) 2010 olyutorskii\r
+ * $Id: ContentBuilderUCS2.java 1001 2010-03-15 12:09:35Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+/**\r
+ * "UTF-8"エンコーディング用デコードハンドラ。\r
+ * {@link StreamDecoder}からの通知に従い、\r
+ * {@link DecodedContent}へとデコードする。\r
+ * UCS-4はUTF-16エラー扱い。\r
+ */\r
+public class ContentBuilderUCS2 extends ContentBuilder{\r
+\r
+    /**\r
+     * サロゲートペア文字(上位,下位)をUTF-16BEバイト列に変換する。\r
+     * @param ch 文字\r
+     * @return UTF-8バイト列\r
+     */\r
+    public static byte[] charToUTF16(char ch){\r
+        byte[] result = new byte[2];\r
+        result[0] = (byte)(ch >> 8);\r
+        result[1] = (byte)(ch & 0xff);\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 長さ0で空の{@link DecodedContent}がセットされる。\r
+     */\r
+    public ContentBuilderUCS2(){\r
+        this(128);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 長さ0で空の{@link DecodedContent}がセットされる。\r
+     * @param capacity 初期容量\r
+     * @throws NegativeArraySizeException 容量指定が負。\r
+     */\r
+    public ContentBuilderUCS2(int capacity)\r
+            throws NegativeArraySizeException{\r
+        super(capacity);\r
+        initImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコード処理の初期化下請。\r
+     */\r
+    private void initImpl(){\r
+        this.content.init();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコード処理の初期化。\r
+     */\r
+    @Override\r
+    protected void init(){\r
+        initImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param seq {@inheritDoc}\r
+     * @throws DecodeException {@inheritDoc}\r
+     */\r
+    public void charContent(CharSequence seq)\r
+            throws DecodeException{\r
+        flushError();\r
+\r
+        int length = seq.length();\r
+        int startPos = 0;\r
+\r
+        for(int pos = 0; pos < length; pos++){\r
+            char ch = seq.charAt(pos);\r
+\r
+            if(   ! Character.isHighSurrogate(ch)\r
+               && ! Character.isLowSurrogate (ch) ){\r
+                continue;\r
+            }\r
+\r
+            if(startPos < pos){\r
+                CharSequence chopped = seq.subSequence(startPos, pos);\r
+                this.content.append(chopped);\r
+                startPos = pos + 1;\r
+            }\r
+\r
+            byte[] barr = charToUTF16(ch);\r
+            for(byte bval : barr){\r
+                this.content.addDecodeError(bval);\r
+            }\r
+        }\r
+\r
+        if(startPos < length){\r
+            CharSequence chopped = seq.subSequence(startPos, length);\r
+            this.content.append(chopped);\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param errorArray {@inheritDoc}\r
+     * @param offset {@inheritDoc}\r
+     * @param length {@inheritDoc}\r
+     * @throws DecodeException {@inheritDoc}\r
+     */\r
+    public void decodingError(byte[] errorArray, int offset, int length)\r
+            throws DecodeException{\r
+        int limit = offset + length;\r
+\r
+        for(int bpos = offset; bpos < limit; bpos++){\r
+            byte bval = errorArray[bpos];\r
+            this.content.addDecodeError(bval);\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/DecodeErrorInfo.java b/src/main/java/jp/sourceforge/jindolf/parser/DecodeErrorInfo.java
new file mode 100644 (file)
index 0000000..8172c17
--- /dev/null
@@ -0,0 +1,195 @@
+/*\r
+ * invalid Shift_JIS decoding information\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: DecodeErrorInfo.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.Comparator;\r
+\r
+/**\r
+ * 不正な Shift_JIS デコードの情報。\r
+ * 1バイトもしくは2バイトで構成される。\r
+ * 1バイトの場合はおそらくエンコーディングに関するエラー。\r
+ * 2バイトの場合はおそらく文字集合に関するエラー。\r
+ */\r
+public class DecodeErrorInfo{\r
+\r
+    /** 出現位置順Comparator。 */\r
+    public static final Comparator<DecodeErrorInfo> POS_COMPARATOR =\r
+            new PosComparator();\r
+\r
+    private final int charPos;\r
+    private final boolean has2ndFlag;\r
+    private final byte rawByte1st;\r
+    private final byte rawByte2nd;\r
+\r
+    /**\r
+     * 下請けコンストラクタ。\r
+     * @param charPos デコードエラーで置き換えられた文字列の開始位置\r
+     * @param has2ndFlag 2バイト目が有効ならtrueを渡す。\r
+     * @param rawByte1st デコードエラーを引き起こした最初のバイト値\r
+     * @param rawByte2nd デコードエラーを引き起こした2番目のバイト値\r
+     * @throws IndexOutOfBoundsException charPosが負\r
+     */\r
+    private DecodeErrorInfo(int charPos,\r
+                              boolean has2ndFlag,\r
+                              byte rawByte1st,\r
+                              byte rawByte2nd)\r
+            throws IndexOutOfBoundsException{\r
+        if(charPos < 0) throw new IndexOutOfBoundsException();\r
+\r
+        this.charPos = charPos;\r
+        this.has2ndFlag = has2ndFlag;\r
+        this.rawByte1st = rawByte1st;\r
+        this.rawByte2nd = rawByte2nd;\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param charPos デコードエラーで置き換えられた文字列の開始位置\r
+     * @param rawByte1st デコードエラーを引き起こした最初のバイト値\r
+     * @param rawByte2nd デコードエラーを引き起こした2番目のバイト値\r
+     * @throws IndexOutOfBoundsException charPosが負\r
+     */\r
+    public DecodeErrorInfo(int charPos,\r
+                             byte rawByte1st,\r
+                             byte rawByte2nd)\r
+            throws IndexOutOfBoundsException{\r
+        this(charPos, true, rawByte1st, rawByte2nd);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param charPos デコードエラーで置き換えられた文字列の開始位置\r
+     * @param rawByte1st デコードエラーを引き起こしたバイト値\r
+     * @throws IndexOutOfBoundsException charPosが負\r
+     */\r
+    public DecodeErrorInfo(int charPos,\r
+                             byte rawByte1st)\r
+            throws IndexOutOfBoundsException{\r
+        this(charPos, false, rawByte1st, (byte)0x00);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコードエラーで置き換えられた文字列の開始位置を返す。\r
+     * @return デコードエラーで置き換えられた文字列の開始位置\r
+     */\r
+    public int getCharPosition(){\r
+        return this.charPos;\r
+    }\r
+\r
+    /**\r
+     * 2バイト目の情報を持つか判定する。\r
+     * @return 2バイト目の情報を持つならtrue\r
+     */\r
+    public boolean has2nd(){\r
+        return this.has2ndFlag;\r
+    }\r
+\r
+    /**\r
+     * 1バイト目の値を返す。\r
+     * @return 1バイト目の値\r
+     */\r
+    public byte getRawByte1st(){\r
+        return this.rawByte1st;\r
+    }\r
+\r
+    /**\r
+     * 2バイト目の値を返す。\r
+     * @return 2バイト目の値\r
+     * @throws IllegalStateException 2バイト目の情報を把持していないとき\r
+     */\r
+    public byte getRawByte2nd() throws IllegalStateException{\r
+        if( ! this.has2ndFlag ) throw new IllegalStateException();\r
+        return this.rawByte2nd;\r
+    }\r
+\r
+    /**\r
+     * 出現位置のみが違う複製オブジェクトを生成する。\r
+     * @param gap 出現位置から引きたい値。正の値なら文字開始位置に向かう。\r
+     * @return 複製オブジェクト\r
+     * @throws IndexOutOfBoundsException 再計算された出現位置が負\r
+     */\r
+    public DecodeErrorInfo createGappedClone(int gap)\r
+            throws IndexOutOfBoundsException{\r
+        DecodeErrorInfo result;\r
+\r
+        int newPos = this.charPos - gap;\r
+        if(this.has2ndFlag){\r
+            result = new DecodeErrorInfo(newPos,\r
+                                         this.rawByte1st, this.rawByte2nd);\r
+        }else{\r
+            result = new DecodeErrorInfo(newPos, this.rawByte1st);\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String toString(){\r
+        StringBuilder result = new StringBuilder();\r
+\r
+        result.append("start:").append(this.charPos).append(' ');\r
+\r
+        String hex;\r
+        hex = Integer.toHexString(this.rawByte1st & 0xff);\r
+        if(hex.length() <= 1) result.append('0');\r
+        result.append(hex);\r
+\r
+        if(this.has2ndFlag){\r
+            hex = Integer.toHexString(this.rawByte2nd & 0xff);\r
+            result.append(':');\r
+            if(hex.length() <= 1) result.append('0');\r
+            result.append(hex);\r
+        }\r
+\r
+        return result.toString();\r
+    }\r
+\r
+    /**\r
+     * 出現位置で順序づける比較子。\r
+     */\r
+    private static class PosComparator\r
+            implements Comparator<DecodeErrorInfo> {\r
+\r
+        /**\r
+         * コンストラクタ。\r
+         */\r
+        public PosComparator(){\r
+            super();\r
+            return;\r
+        }\r
+\r
+        /**\r
+         * {@inheritDoc}\r
+         * @param info1 {@inheritDoc}\r
+         * @param info2 {@inheritDoc}\r
+         * @return {@inheritDoc}\r
+         */\r
+        public int compare(DecodeErrorInfo info1, DecodeErrorInfo info2){\r
+            int pos1;\r
+            int pos2;\r
+\r
+            if(info1 == null) pos1 = -1;\r
+            else              pos1 = info1.charPos;\r
+\r
+            if(info2 == null) pos2 = -1;\r
+            else              pos2 = info2.charPos;\r
+\r
+            return pos1 - pos2;\r
+        }\r
+\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/DecodeException.java b/src/main/java/jp/sourceforge/jindolf/parser/DecodeException.java
new file mode 100644 (file)
index 0000000..9b0bce2
--- /dev/null
@@ -0,0 +1,104 @@
+/*\r
+ * decode exception\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: DecodeException.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+/**\r
+ * デコード異常系情報。\r
+ * {@link DecodeHandler}の各メソッドは、この例外をスローすることで\r
+ * デコード処理の即時停止を{@link StreamDecoder}に指示することができる。\r
+ * デコード元(バイトストリーム)の中のエラー発生位置と\r
+ * デコード先(CharSequence)の中のエラー発生位置を保持することができる。\r
+ * いずれの値も、エラー発生位置が不明な場合は負の値が設定される。\r
+ */\r
+@SuppressWarnings("serial")\r
+public class DecodeException extends Exception{\r
+\r
+    private final int bytePos;\r
+    private final int charPos;\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     */\r
+    public DecodeException(){\r
+        this(null);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param message メッセージ\r
+     */\r
+    public DecodeException(String message){\r
+        this(message, -1, -1);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 位置情報が不明な場合は負の値を渡す。\r
+     * @param bytePos デコード元エラー発生バイト位置\r
+     * @param charPos デコード先エラー発生文字位置\r
+     */\r
+    public DecodeException(int bytePos, int charPos){\r
+        this(null, bytePos, charPos);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 位置情報が不明な場合は負の値を渡す。\r
+     * @param message メッセージ\r
+     * @param bytePos デコード元エラー発生バイト位置\r
+     * @param charPos デコード先エラー発生文字位置\r
+     */\r
+    public DecodeException(String message, int bytePos, int charPos){\r
+        super(message);\r
+        this.bytePos = bytePos;\r
+        this.charPos = charPos;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコード元エラー発生位置を返す。\r
+     * 単位はbyte単位。\r
+     * @return エラー発生位置。不明な場合は負の値。\r
+     */\r
+    public int getBytePos(){\r
+        return this.bytePos;\r
+    }\r
+\r
+    /**\r
+     * デコード先エラー発生位置を返す。\r
+     * 単位はchar単位。\r
+     * @return エラー発生位置。不明な場合は負の値。\r
+     */\r
+    public int getCharPos(){\r
+        return this.charPos;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getMessage(){\r
+        StringBuilder result = new StringBuilder();\r
+\r
+        String message = super.getMessage();\r
+        if(message != null && message.length() > 0){\r
+            result.append(message).append(' ');\r
+        }\r
+\r
+        result.append("bytePos=").append(this.bytePos);\r
+        result.append(' ');\r
+        result.append("charPos=").append(this.charPos);\r
+\r
+        return result.toString();\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/DecodeHandler.java b/src/main/java/jp/sourceforge/jindolf/parser/DecodeHandler.java
new file mode 100644 (file)
index 0000000..2496eb7
--- /dev/null
@@ -0,0 +1,64 @@
+/*\r
+ * decode handler\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: DecodeHandler.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.nio.charset.CharsetDecoder;\r
+\r
+/**\r
+ * 文字デコードハンドラ。\r
+ * {@link StreamDecoder}により呼ばれる。\r
+ * メソッドが呼ばれる順番は\r
+ * {@link #startDecoding}が最初で\r
+ * {@link #endDecoding}が最後。\r
+ * その間、{@link #charContent}\r
+ * または{@link #decodingError}が複数回呼ばれる。\r
+ * 各メソッドは、{@link DecodeException}をスローすることで\r
+ * デコード処理を中止させることができる。\r
+ */\r
+public interface DecodeHandler{\r
+\r
+    /**\r
+     * デコード開始の通知を受け取る。\r
+     * @param decoder デコーダ\r
+     * @throws DecodeException デコードエラー\r
+     */\r
+    void startDecoding(CharsetDecoder decoder)\r
+            throws DecodeException;\r
+\r
+    /**\r
+     * 正常にデコードした文字列の通知を受け取る。\r
+     * seqの内容は、ハンドラ呼び出し元で随時変更されうる。\r
+     * seqの内容を後々再利用するつもりなら、\r
+     * 制御を呼び出し元に戻すまでの間に必要な箇所をコピーする必要がある。\r
+     * @param seq 文字列\r
+     * @throws DecodeException デコードエラー\r
+     */\r
+    void charContent(CharSequence seq)\r
+            throws DecodeException;\r
+\r
+    /**\r
+     * デコードエラーの通知を受け取る。\r
+     * errorArrayの内容は、ハンドラ呼び出し元で随時変更されうる。\r
+     * errorArrayの内容を後々再利用するつもりなら、\r
+     * 制御を呼び出し元に戻すまでの間に必要な箇所をコピーする必要がある。\r
+     * @param errorArray エラーを引き起こした入力バイトシーケンス。\r
+     * @param offset errorArrayに含まれるエラーの開始位置。\r
+     * @param length errorArrayに含まれるエラーのバイト長。\r
+     * @throws DecodeException デコードエラー\r
+     */\r
+    void decodingError(byte[] errorArray, int offset, int length)\r
+            throws DecodeException;\r
+\r
+    /**\r
+     * デコード終了の通知を受け取る。\r
+     * @throws DecodeException デコードエラー\r
+     */\r
+    void endDecoding()\r
+            throws DecodeException;\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/DecodedContent.java b/src/main/java/jp/sourceforge/jindolf/parser/DecodedContent.java
new file mode 100644 (file)
index 0000000..763cbe7
--- /dev/null
@@ -0,0 +1,472 @@
+/*\r
+ * decoded source\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: DecodedContent.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.List;\r
+import java.util.RandomAccess;\r
+\r
+/**\r
+ * ShiftJISデコードエラー情報を含む再利用可能な文字列。\r
+ * デコードエラーを起こした箇所は代替文字{@link #ALTCHAR}で置き換えられる。\r
+ * マルチスレッドには非対応。\r
+ * UCS-4コードポイントには未対応。\r
+ */\r
+public class DecodedContent\r
+        implements CharSequence,\r
+                   Appendable {\r
+\r
+    /**\r
+     * 代替文字。\r
+     * {@literal HTMLで使うなら < や > や & や " や ' はやめて!}\r
+     */\r
+    public static final char ALTCHAR = '?';\r
+\r
+    private static final List<DecodeErrorInfo> EMPTY_LIST =\r
+            Collections.emptyList();\r
+\r
+    private static final int BSEARCH_THRESHOLD = 16;\r
+\r
+    static{\r
+        assert ALTCHAR != '<';\r
+        assert ALTCHAR != '>';\r
+        assert ALTCHAR != '&';\r
+        assert ALTCHAR != '"';\r
+        assert ALTCHAR != '\'';\r
+        assert ALTCHAR != '\\';\r
+    }\r
+\r
+    /**\r
+     * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ\r
+     * デコードエラーのインデックス位置を返す。※リニアサーチ版。\r
+     * @param errList デコードエラーのリスト\r
+     * @param startPos 文字位置\r
+     * @return 0から始まるリスト内の位置。\r
+     * 一致する文字位置がなければ挿入ポイント。\r
+     */\r
+    protected static int lsearchErrorIndex(List<DecodeErrorInfo> errList,\r
+                                             int startPos){\r
+        // assert errList instanceof RandomAccess;\r
+\r
+        int errSize = errList.size();\r
+\r
+        int idx;\r
+        for(idx = 0; idx < errSize; idx++){\r
+            DecodeErrorInfo einfo = errList.get(idx);\r
+            int errPos = einfo.getCharPosition();\r
+            if(startPos <= errPos) break;\r
+        }\r
+\r
+        return idx;\r
+    }\r
+\r
+    /**\r
+     * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ\r
+     * デコードエラーのインデックス位置を返す。※バイナリサーチ版。\r
+     * @param errList デコードエラーのリスト\r
+     * @param startPos 文字位置\r
+     * @return 0から始まるリスト内の位置。\r
+     * 一致する文字位置がなければ挿入ポイント。\r
+     */\r
+    protected static int bsearchErrorIndex(List<DecodeErrorInfo> errList,\r
+                                             int startPos){\r
+        // assert errList instanceof RandomAccess;\r
+\r
+        int floor = 0;\r
+        int roof  = errList.size() - 1;\r
+\r
+        while(floor <= roof){\r
+            int midpoint = (floor + roof) / 2;  // 切り捨て\r
+            DecodeErrorInfo einfo = errList.get(midpoint);\r
+            int cmp = einfo.getCharPosition() - startPos;\r
+\r
+            if(cmp == 0) return midpoint;\r
+\r
+            if     (cmp < 0) floor = midpoint + 1;\r
+            else if(cmp > 0) roof  = midpoint - 1;\r
+        }\r
+\r
+        return floor;\r
+    }\r
+\r
+    /**\r
+     * 与えられた文字位置を含むか、またはそれ以降で最も小さな位置情報を持つ\r
+     * デコードエラーのインデックス位置を返す。\r
+     * 要素数の増減に応じてリニアサーチとバイナリサーチを使い分ける。\r
+     * @param errList デコードエラーのリスト\r
+     * @param startPos 文字位置\r
+     * @return 0から始まるリスト内の位置。\r
+     * 一致する文字位置がなければ挿入ポイント。\r
+     */\r
+    protected static int searchErrorIndex(List<DecodeErrorInfo> errList,\r
+                                            int startPos){\r
+        int result;\r
+\r
+        int errSize = errList.size();\r
+        if(errSize < BSEARCH_THRESHOLD){\r
+            // linear-search\r
+            result = lsearchErrorIndex(errList, startPos);\r
+        }else{\r
+            // binary-search\r
+            result = bsearchErrorIndex(errList, startPos);\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * ギャップ情報が加味されたデコードエラー情報を、\r
+     * 範囲指定込みで指定エラーリストに追加転記する。\r
+     * 追加先エラーリストがnullだった場合、必要に応じてエラーリストが生成され\r
+     * 戻り値となる場合がありうる。\r
+     * @param sourceContent 元の文字列\r
+     * @param startPos 範囲開始位置\r
+     * @param endPos 範囲終了位置\r
+     * @param targetError 追加先エラーリスト。nullでもよい。\r
+     * @param gap ギャップ量\r
+     * @return 引数targetErrorもしくは新規生成されたリストを返す。\r
+     */\r
+    protected static List<DecodeErrorInfo>\r
+            appendGappedErrorInfo(DecodedContent sourceContent,\r
+                                     int startPos, int endPos,\r
+                                     List<DecodeErrorInfo> targetError,\r
+                                     int gap){\r
+        List<DecodeErrorInfo> sourceError = sourceContent.decodeError;\r
+        List<DecodeErrorInfo> result = targetError;\r
+\r
+        int startErrorIdx = searchErrorIndex(sourceError, startPos);\r
+        int endErrorIdx = sourceError.size() - 1;\r
+        assert endErrorIdx >= 0;\r
+\r
+        for(int index = startErrorIdx; index <= endErrorIdx; index++){\r
+            DecodeErrorInfo einfo = sourceError.get(index);\r
+            int pos = einfo.getCharPosition();\r
+            if(pos < startPos) continue;\r
+            if(pos >= endPos) break;\r
+            DecodeErrorInfo newInfo = einfo.createGappedClone(gap);\r
+            if(result == null){\r
+                result = createErrorList();\r
+            }\r
+            result.add(newInfo);\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * エラー格納用リストを生成する。\r
+     * @return リスト\r
+     */\r
+    private static List<DecodeErrorInfo> createErrorList(){\r
+        List<DecodeErrorInfo> result = new ArrayList<DecodeErrorInfo>();\r
+        return result;\r
+    }\r
+\r
+    static{\r
+        assert createErrorList() instanceof RandomAccess;\r
+    }\r
+\r
+    private final StringBuilder rawContent = new StringBuilder();\r
+\r
+    private List<DecodeErrorInfo> decodeError;\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     */\r
+    public DecodedContent(){\r
+        this("");\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param seq 初期化文字列\r
+     * @throws NullPointerException 引数がnull\r
+     */\r
+    public DecodedContent(CharSequence seq) throws NullPointerException{\r
+        super();\r
+        if(seq == null) throw new NullPointerException();\r
+        initImpl();\r
+        this.rawContent.append(seq);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param capacity 文字数の初期容量\r
+     * @throws NegativeArraySizeException 容量が負の値\r
+     */\r
+    public DecodedContent(int capacity) throws NegativeArraySizeException{\r
+        super();\r
+        if(capacity < 0) throw new NegativeArraySizeException();\r
+        initImpl();\r
+        this.rawContent.ensureCapacity(capacity);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 初期化下請け。\r
+     * 長さ0の文字列&デコードエラー無しの状態になる。\r
+     */\r
+    private void initImpl(){\r
+        this.rawContent.setLength(0);\r
+\r
+        if(this.decodeError != null){\r
+            this.decodeError.clear();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 事前にキャパシティを確保する。\r
+     * 指定されたキャパシティの範囲内で再割り当てが起きないことを保証する。\r
+     * @param minimumCapacity キャラクタ単位のキャパシティ長。\r
+     */\r
+    public void ensureCapacity(int minimumCapacity){\r
+        this.rawContent.ensureCapacity(minimumCapacity);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 初期化。\r
+     * 長さ0の文字列&デコードエラー無しの状態になる。\r
+     * コンストラクタで新インスタンスを作るより低コスト。\r
+     */\r
+    public void init(){\r
+        initImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコードエラーを含むか判定する。\r
+     * @return デコードエラーを含むならtrue\r
+     */\r
+    public boolean hasDecodeError(){\r
+        if(this.decodeError == null) return false;\r
+        if(this.decodeError.isEmpty()) return false;\r
+        return true;\r
+    }\r
+\r
+    /**\r
+     * デコードエラーの一覧を取得する。\r
+     * @return デコードエラーの一覧\r
+     */\r
+    public List<DecodeErrorInfo> getDecodeErrorList(){\r
+        if( ! hasDecodeError() ){\r
+            return EMPTY_LIST;\r
+        }\r
+        return Collections.unmodifiableList(this.decodeError);\r
+    }\r
+\r
+    /**\r
+     * 生の文字列を得る。\r
+     * 高速なCharSequenceアクセス用途。\r
+     * @return 生の文字列。\r
+     */\r
+    public CharSequence getRawContent(){\r
+        return this.rawContent;\r
+    }\r
+\r
+    /**\r
+     * 指定された位置の文字を変更する。\r
+     * @param index 文字位置\r
+     * @param ch 文字\r
+     * @throws IndexOutOfBoundsException 不正な位置指定\r
+     */\r
+    public void setCharAt(int index, char ch)\r
+            throws IndexOutOfBoundsException{\r
+        this.rawContent.setCharAt(index, ch);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param index {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    public char charAt(int index){\r
+        return this.rawContent.charAt(index);\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    public int length(){\r
+        return this.rawContent.length();\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param start {@inheritDoc}\r
+     * @param end {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    public CharSequence subSequence(int start, int end){\r
+        return this.rawContent.subSequence(start, end);\r
+    }\r
+\r
+    /**\r
+     * 範囲指定されたサブコンテントを切り出す。\r
+     * サブコンテントにはデコードエラー情報が引き継がれる。\r
+     * @param start 開始位置\r
+     * @param end 終了位置\r
+     * @return サブコンテント\r
+     * @throws IndexOutOfBoundsException start または end が負の値の場合、\r
+     * end が length() より大きい場合、あるいは start が end より大きい場合\r
+     */\r
+    public DecodedContent subContent(int start, int end)\r
+            throws IndexOutOfBoundsException{\r
+        int length = end - start;\r
+        if(length < 0) throw new IndexOutOfBoundsException();\r
+        DecodedContent result = new DecodedContent(length);\r
+        result.append(this, start, end);\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * 文字を追加する。\r
+     * @param letter 追加する文字\r
+     * @return thisオブジェクト\r
+     */\r
+    public DecodedContent append(char letter){\r
+        this.rawContent.append(letter);\r
+        return this;\r
+    }\r
+\r
+    /**\r
+     * 文字列を追加する。\r
+     * @param seq 追加する文字列\r
+     * @return thisオブジェクト\r
+     */\r
+    public DecodedContent append(CharSequence seq){\r
+        if(seq == null){\r
+            this.rawContent.append("null");\r
+        }else if(seq instanceof DecodedContent){\r
+            append((DecodedContent)seq, 0, seq.length());\r
+        }else{\r
+            this.rawContent.append(seq);\r
+        }\r
+        return this;\r
+    }\r
+\r
+    /**\r
+     * 文字列を追加する。\r
+     * @param seq 追加する文字列\r
+     * @param startPos 開始位置\r
+     * @param endPos 終了位置\r
+     * @return thisオブジェクト\r
+     * @throws IndexOutOfBoundsException 範囲指定が変。\r
+     */\r
+    public DecodedContent append(CharSequence seq,\r
+                                  int startPos, int endPos)\r
+            throws IndexOutOfBoundsException{\r
+        if(seq == null){\r
+            this.rawContent.append("null", startPos, endPos);\r
+        }else if(seq instanceof DecodedContent){\r
+            append((DecodedContent)seq, startPos, endPos);\r
+        }else{\r
+            this.rawContent.append(seq, startPos, endPos);\r
+        }\r
+\r
+        return this;\r
+    }\r
+\r
+    /**\r
+     * 文字列を追加する。\r
+     * @param source 追加する文字列\r
+     * @param startPos 開始位置\r
+     * @param endPos 終了位置\r
+     * @return thisオブジェクト\r
+     * @throws IndexOutOfBoundsException 範囲指定が変。\r
+     */\r
+    public DecodedContent append(DecodedContent source,\r
+                                  int startPos, int endPos)\r
+            throws IndexOutOfBoundsException{\r
+        if(source == null){\r
+            return append("null", startPos, endPos);\r
+        }\r
+\r
+        int gap = startPos - this.rawContent.length();\r
+\r
+        this.rawContent.append(source.rawContent, startPos, endPos);\r
+\r
+        if( ! source.hasDecodeError() ) return this;\r
+\r
+        List<DecodeErrorInfo> targetErrorList;\r
+        if(source != this) targetErrorList = this.decodeError;\r
+        else               targetErrorList = null;\r
+\r
+        targetErrorList = appendGappedErrorInfo(source,\r
+                                                startPos, endPos,\r
+                                                targetErrorList,\r
+                                                gap);\r
+\r
+        if(targetErrorList == null)             return this;\r
+        if(targetErrorList == this.decodeError) return this;\r
+\r
+        if(this.decodeError == null){\r
+            this.decodeError = targetErrorList;\r
+        }else{\r
+            this.decodeError.addAll(targetErrorList);\r
+        }\r
+\r
+        return this;\r
+    }\r
+\r
+    /**\r
+     * 代替文字とともにデコードエラーを追加する。\r
+     * ※呼び出し側は、追加されるデコードエラーの位置情報が\r
+     * 既存のデコードエラーよりも大きいことを保証しなければならない。\r
+     * @param errorInfo デコードエラー\r
+     */\r
+    private void addDecodeError(DecodeErrorInfo errorInfo){\r
+        if(this.decodeError == null){\r
+            this.decodeError = createErrorList();\r
+        }\r
+        this.decodeError.add(errorInfo);\r
+        this.rawContent.append(ALTCHAR);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 代替文字とともにデコードエラーを追加する。\r
+     * @param b1st 1バイト目の値\r
+     */\r
+    public void addDecodeError(byte b1st){\r
+        DecodeErrorInfo errInfo =\r
+                new DecodeErrorInfo(this.rawContent.length(), b1st);\r
+        addDecodeError(errInfo);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 代替文字とともにデコードエラーを追加する。\r
+     * @param b1st 1バイト目の値\r
+     * @param b2nd 2バイト目の値\r
+     */\r
+    public void addDecodeError(byte b1st, byte b2nd){\r
+        DecodeErrorInfo errInfo =\r
+                new DecodeErrorInfo(this.rawContent.length(), b1st, b2nd);\r
+        addDecodeError(errInfo);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String toString(){\r
+        return this.rawContent.toString();\r
+    }\r
+\r
+    // TODO Windows-31Jへの再デコード処理など\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/EntityConverter.java b/src/main/java/jp/sourceforge/jindolf/parser/EntityConverter.java
new file mode 100644 (file)
index 0000000..3ba2d74
--- /dev/null
@@ -0,0 +1,175 @@
+/*\r
+ * entity converter\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: EntityConverter.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.regex.Matcher;\r
+import java.util.regex.Pattern;\r
+\r
+/**\r
+ * 人狼BBSで用いられる4種類のXHTML文字実体参照の\r
+ * 解決を伴う{@link DecodedContent}の切り出しを行う。\r
+ * 文字実体参照は{@code &gt; &lt; &quot; &amp;}が対象。\r
+ * U+005C(バックスラッシュ)をU+00A5(円通貨)に直す処理も行われる。\r
+ * ※ 人狼BBSはShift_JIS(⊃JISX0201)で運営されているので、\r
+ * バックスラッシュは登場しないはず。\r
+ * ※ が、バックスラッシュを生成するShift_JISデコーダは存在する。\r
+ * マルチスレッドには非対応。\r
+ */\r
+public class EntityConverter{\r
+\r
+    private static final String[][] XCHG_TABLE = {\r
+        {"&gt;",   ">"},\r
+        {"&lt;",   "<"},\r
+        {"&quot;", "\""},\r
+        {"&amp;",  "&"},\r
+        {"\u005c\u005c", "\u00a5"},\r
+    };\r
+\r
+    private static final Pattern XCHG_PATTERN;\r
+\r
+    static{\r
+        StringBuilder regex = new StringBuilder();\r
+        for(String[] xchg : XCHG_TABLE){\r
+            String xchgFrom = xchg[0];\r
+            if(regex.length() > 0) regex.append('|');\r
+            regex.append('(')\r
+                 .append(Pattern.quote(xchgFrom))\r
+                 .append(')');\r
+            assert xchgFrom.indexOf(DecodedContent.ALTCHAR) < 0;\r
+        }\r
+        XCHG_PATTERN = Pattern.compile(regex.toString());\r
+    }\r
+\r
+    private final Matcher matcher = XCHG_PATTERN.matcher("");\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     */\r
+    public EntityConverter(){\r
+        super();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 実体参照の変換を行う。\r
+     * @param content 変換元文書\r
+     * @return 切り出された変換済み文書\r
+     */\r
+    public DecodedContent convert(DecodedContent content){\r
+        return append(null, content, 0, content.length());\r
+    }\r
+\r
+    /**\r
+     * 実体参照の変換を行う。\r
+     * @param content 変換元文書\r
+     * @param range 範囲指定\r
+     * @return 切り出された変換済み文書\r
+     * @throws IndexOutOfBoundsException 位置指定に不正があった\r
+     */\r
+    public DecodedContent convert(DecodedContent content, SeqRange range)\r
+            throws IndexOutOfBoundsException{\r
+        return append(null, content, range.getStartPos(), range.getEndPos());\r
+    }\r
+\r
+    /**\r
+     * 実体参照の変換を行う。\r
+     * @param content 変換元文書\r
+     * @param startPos 開始位置\r
+     * @param endPos 終了位置\r
+     * @return 切り出された変換済み文書\r
+     * @throws IndexOutOfBoundsException 位置指定に不正があった\r
+     */\r
+    public DecodedContent convert(DecodedContent content,\r
+                                   int startPos, int endPos)\r
+            throws IndexOutOfBoundsException{\r
+        return append(null, content, startPos, endPos);\r
+    }\r
+\r
+    /**\r
+     * 実体参照の変換を行い既存のDecodedContentに追加を行う。\r
+     * @param target 追加先文書。nullなら新たな文書が用意される。\r
+     * @param content 変換元文書\r
+     * @return targetもしくは新規に用意された文書\r
+     * @throws IndexOutOfBoundsException 位置指定に不正があった\r
+     */\r
+    public DecodedContent  append(DecodedContent target,\r
+                                   DecodedContent content)\r
+            throws IndexOutOfBoundsException{\r
+        return append(target, content, 0, content.length());\r
+    }\r
+\r
+    /**\r
+     * 実体参照の変換を行い既存のDecodedContentに追加を行う。\r
+     * @param target 追加先文書。nullなら新たな文書が用意される。\r
+     * @param content 変換元文書\r
+     * @param range 範囲指定\r
+     * @return targetもしくは新規に用意された文書\r
+     * @throws IndexOutOfBoundsException 位置指定に不正があった\r
+     */\r
+    public DecodedContent  append(DecodedContent target,\r
+                                   DecodedContent content,\r
+                                   SeqRange range )\r
+            throws IndexOutOfBoundsException{\r
+        return append(target, content,\r
+                      range.getStartPos(), range.getEndPos());\r
+    }\r
+\r
+    /**\r
+     * 実体参照の変換を行い既存のDecodedContentに追加を行う。\r
+     * @param target 追加先文書。nullなら新たな文書が用意される。\r
+     * @param content 変換元文書\r
+     * @param startPos 開始位置\r
+     * @param endPos 終了位置\r
+     * @return targetもしくは新規に用意された文書\r
+     * @throws IndexOutOfBoundsException 位置指定に不正があった\r
+     */\r
+    public DecodedContent append(DecodedContent target,\r
+                                  DecodedContent content,\r
+                                  int startPos, int endPos)\r
+            throws IndexOutOfBoundsException{\r
+        if(   startPos > endPos\r
+           || startPos < 0\r
+           || content.length() < endPos){\r
+            throw new IndexOutOfBoundsException();\r
+        }\r
+\r
+        DecodedContent result;\r
+        if(target == null){\r
+            result = new DecodedContent(endPos - startPos);\r
+        }else{\r
+            result = target;\r
+        }\r
+\r
+        this.matcher.reset(content.getRawContent());\r
+        this.matcher.region(startPos, endPos);\r
+\r
+        int lastPos = startPos;\r
+        while(this.matcher.find()){\r
+            int group;\r
+            int matchStart = -1;\r
+            for(group = 1; group <= XCHG_TABLE.length; group++){\r
+                matchStart = this.matcher.start(group);\r
+                if(matchStart >= 0) break;\r
+            }\r
+            int matchEnd = this.matcher.end(group);\r
+\r
+            result.append(content, lastPos, matchStart);\r
+\r
+            String toStr = XCHG_TABLE[group - 1][1];\r
+            result.append(toStr);\r
+\r
+            lastPos = matchEnd;\r
+        }\r
+        result.append(content, lastPos, endPos);\r
+\r
+        this.matcher.reset("");\r
+\r
+        return result;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/HtmlAdapter.java b/src/main/java/jp/sourceforge/jindolf/parser/HtmlAdapter.java
new file mode 100644 (file)
index 0000000..3a146f5
--- /dev/null
@@ -0,0 +1,501 @@
+/*\r
+ * html handler adapter\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: HtmlAdapter.java 1014 2010-03-16 10:43:28Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import jp.sourceforge.jindolf.corelib.EventFamily;\r
+import jp.sourceforge.jindolf.corelib.GameRole;\r
+import jp.sourceforge.jindolf.corelib.PeriodType;\r
+import jp.sourceforge.jindolf.corelib.SysEventType;\r
+import jp.sourceforge.jindolf.corelib.TalkType;\r
+import jp.sourceforge.jindolf.corelib.Team;\r
+import jp.sourceforge.jindolf.corelib.VillageState;\r
+\r
+/**\r
+ * インタフェース{@link HtmlHandler}の抽象アダプタクラス。\r
+ * このクラスのメソッド自身は何もしない。\r
+ */\r
+public abstract class HtmlAdapter implements HtmlHandler{\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void startParse(DecodedContent content)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param titleRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void pageTitle(DecodedContent content, SeqRange titleRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param loginRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void loginName(DecodedContent content, SeqRange loginRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param type {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void pageType(PageType type)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param villageRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void villageName(DecodedContent content, SeqRange villageRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param month {@inheritDoc}\r
+     * @param day {@inheritDoc}\r
+     * @param hour {@inheritDoc}\r
+     * @param minute {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void commitTime(int month, int day, int hour, int minute)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param anchorRange {@inheritDoc}\r
+     * @param periodType {@inheritDoc}\r
+     * @param day {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void periodLink(DecodedContent content,\r
+                            SeqRange anchorRange,\r
+                            PeriodType periodType, int day)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param anchorRange {@inheritDoc}\r
+     * @param villageRange {@inheritDoc}\r
+     * @param hour {@inheritDoc}\r
+     * @param minute {@inheritDoc}\r
+     * @param state {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void villageRecord(DecodedContent content,\r
+                                SeqRange anchorRange,\r
+                                SeqRange villageRange,\r
+                                int hour, int minute,\r
+                                VillageState state )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void endParse() throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void startTalk() throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void endTalk() throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param talkNo {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkNo(int talkNo) throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param idRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkId(DecodedContent content, SeqRange idRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkAvatar(DecodedContent content, SeqRange avatarRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param hour {@inheritDoc}\r
+     * @param minute {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkTime(int hour, int minute) throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param urlRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkIconUrl(DecodedContent content, SeqRange urlRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param type {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkType(TalkType type) throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param textRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkText(DecodedContent content, SeqRange textRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void talkBreak() throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param eventFamily {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void startSysEvent(EventFamily eventFamily)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param type {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventType(SysEventType type) throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void endSysEvent() throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param entryNo {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventOnStage(DecodedContent content,\r
+                                  int entryNo,\r
+                                  SeqRange avatarRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param role {@inheritDoc}\r
+     * @param num {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventOpenRole(GameRole role, int num)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventSurvivor(DecodedContent content,\r
+                                   SeqRange avatarRange)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param voteByRange {@inheritDoc}\r
+     * @param voteToRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventCounting(DecodedContent content,\r
+                                   SeqRange voteByRange,\r
+                                   SeqRange voteToRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param voteByRange {@inheritDoc}\r
+     * @param voteToRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventCounting2(DecodedContent content,\r
+                                    SeqRange voteByRange,\r
+                                    SeqRange voteToRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventSuddenDeath(DecodedContent content,\r
+                                       SeqRange avatarRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventMurdered(DecodedContent content,\r
+                                   SeqRange avatarRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @param anchorRange {@inheritDoc}\r
+     * @param loginRange {@inheritDoc}\r
+     * @param isLiving {@inheritDoc}\r
+     * @param role {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventPlayerList(DecodedContent content,\r
+                                     SeqRange avatarRange,\r
+                                     SeqRange anchorRange,\r
+                                     SeqRange loginRange,\r
+                                     boolean isLiving,\r
+                                     GameRole role)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @param votes {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventExecution(DecodedContent content,\r
+                                    SeqRange avatarRange,\r
+                                    int votes )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventVanish(DecodedContent content,\r
+                                 SeqRange avatarRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param judgeByRange {@inheritDoc}\r
+     * @param judgeToRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventJudge(DecodedContent content,\r
+                                SeqRange judgeByRange,\r
+                                SeqRange judgeToRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param guardByRange {@inheritDoc}\r
+     * @param guardToRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventGuard(DecodedContent content,\r
+                                SeqRange guardByRange,\r
+                                SeqRange guardToRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param hour {@inheritDoc}\r
+     * @param minute {@inheritDoc}\r
+     * @param minLimit {@inheritDoc}\r
+     * @param maxLimit {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventAskEntry(int hour, int minute,\r
+                                   int minLimit, int maxLimit)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param hour {@inheritDoc}\r
+     * @param minute {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventAskCommit(int hour, int minute)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param avatarRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventNoComment(DecodedContent content,\r
+                                    SeqRange avatarRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param winner {@inheritDoc}\r
+     * @param hour {@inheritDoc}\r
+     * @param minute {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventStayEpilogue(Team winner, int hour, int minute)\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param contentRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventContent(DecodedContent content,\r
+                                  SeqRange contentRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventContentBreak() throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param content {@inheritDoc}\r
+     * @param anchorRange {@inheritDoc}\r
+     * @param contentRange {@inheritDoc}\r
+     * @throws HtmlParseException {@inheritDoc}\r
+     */\r
+    public void sysEventContentAnchor(DecodedContent content,\r
+                                         SeqRange anchorRange,\r
+                                         SeqRange contentRange )\r
+            throws HtmlParseException{\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/HtmlHandler.java b/src/main/java/jp/sourceforge/jindolf/parser/HtmlHandler.java
new file mode 100644 (file)
index 0000000..0d15611
--- /dev/null
@@ -0,0 +1,18 @@
+/*\r
+ * marshal handler\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: HtmlHandler.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+/**\r
+ * 各種XHTMLパース用ハンドラをまとめたインタフェース。\r
+ */\r
+public interface HtmlHandler\r
+        extends BasicHandler,\r
+                TalkHandler,\r
+                SysEventHandler {\r
+    // NOTHING\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/HtmlParseException.java b/src/main/java/jp/sourceforge/jindolf/parser/HtmlParseException.java
new file mode 100644 (file)
index 0000000..1a51b02
--- /dev/null
@@ -0,0 +1,86 @@
+/*\r
+ * html parse exception\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: HtmlParseException.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+/**\r
+ * XHTMLパースの異常系情報。\r
+ * {@link HtmlParser}の各ハンドラは、この例外をスローすることで\r
+ * パース処理の即時停止を{@link HtmlParser}に指示することができる。\r
+ * パース対象({@link DecodedContent})内のパース中断位置を\r
+ * 保持することができる。\r
+ * 中断位置が不明な場合は負の値が設定される。\r
+ */\r
+@SuppressWarnings("serial")\r
+public class HtmlParseException extends Exception{\r
+\r
+    private final int charPos;\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     */\r
+    public HtmlParseException(){\r
+        this(null, -1);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param message メッセージ\r
+     */\r
+    public HtmlParseException(String message){\r
+        this(message, -1);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param charPos パース中断位置\r
+     */\r
+    public HtmlParseException(int charPos){\r
+        this(null, charPos);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param message メッセージ\r
+     * @param charPos パース中断位置\r
+     */\r
+    public HtmlParseException(String message, int charPos){\r
+        super(message);\r
+        this.charPos = charPos;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * パース中断位置を返す。\r
+     * @return パース中断位置\r
+     */\r
+    public int getCharPos(){\r
+        return this.charPos;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getMessage(){\r
+        StringBuilder result = new StringBuilder();\r
+\r
+        String message = super.getMessage();\r
+        if(message != null && message.length() > 0){\r
+            result.append(message).append(' ');\r
+        }\r
+\r
+        result.append("charPos=").append(this.charPos);\r
+\r
+        return result.toString();\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/HtmlParser.java b/src/main/java/jp/sourceforge/jindolf/parser/HtmlParser.java
new file mode 100644 (file)
index 0000000..ea80697
--- /dev/null
@@ -0,0 +1,583 @@
+/*\r
+ * XHTML parser\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: HtmlParser.java 1021 2010-03-24 16:03:21Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.regex.Pattern;\r
+import jp.sourceforge.jindolf.corelib.PeriodType;\r
+import jp.sourceforge.jindolf.corelib.VillageState;\r
+\r
+/**\r
+ * 人狼BBS各種XHTML文字列のパースを行いハンドラに通知する。\r
+ */\r
+public class HtmlParser extends AbstractParser{\r
+\r
+    private BasicHandler basicHandler;\r
+    private final TalkParser     talkParser     = new TalkParser(this);\r
+    private final SysEventParser sysEventParser = new SysEventParser(this);\r
+\r
+    private final SeqRange rangepool_1 = new SeqRange();\r
+    private final SeqRange rangepool_2 = new SeqRange();\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     */\r
+    public HtmlParser(){\r
+        super();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@link BasicHandler}ハンドラを登録する。\r
+     * @param basicHandler ハンドラ\r
+     */\r
+    public void setBasicHandler(BasicHandler basicHandler){\r
+        this.basicHandler = basicHandler;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@link TalkHandler}ハンドラを登録する。\r
+     * @param talkHandler ハンドラ\r
+     */\r
+    public void setTalkHandler(TalkHandler talkHandler){\r
+        this.talkParser.setTalkHandler(talkHandler);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@link SysEventHandler}ハンドラを登録する。\r
+     * @param handler ハンドラ\r
+     */\r
+    public void setSysEventHandler(SysEventHandler handler){\r
+        this.sysEventParser.setSysEventHandler(handler);\r
+        return;\r
+    }\r
+\r
+    private static final Pattern XMLDECL_PATTERN =\r
+            compile("<\\?xml\u0020");\r
+    private static final Pattern O_HTML_PATTERN =\r
+            compile("<html\u0020");\r
+    private static final Pattern TITLE_PATTERN =\r
+            compile("<title>([^<]*)</title>");\r
+    private static final Pattern O_BODY_PATTERN =\r
+            compile("<body>");\r
+    private static final Pattern O_DIVMAIN_PATTERN =\r
+            compile("<div\u0020class=\"main\">");\r
+\r
+    /**\r
+     * XHTML先頭部分のパース。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseHead() throws HtmlParseException{\r
+        setContextErrorMessage("lost head part");\r
+\r
+        SeqRange titleRange = this.rangepool_1;\r
+\r
+        lookingAtAffirm(XMLDECL_PATTERN);\r
+        shrinkRegion();\r
+\r
+        findAffirm(O_HTML_PATTERN);\r
+        shrinkRegion();\r
+\r
+        findAffirm(TITLE_PATTERN);\r
+        titleRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+        shrinkRegion();\r
+\r
+        this.basicHandler.pageTitle(getContent(), titleRange);\r
+\r
+        findAffirm(O_BODY_PATTERN);\r
+        shrinkRegion();\r
+\r
+        findAffirm(O_DIVMAIN_PATTERN);\r
+        shrinkRegion();\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern LOGINFORM_PATTERN =\r
+            compile(\r
+                  "("\r
+                    +"<form"\r
+                    +"\u0020" + "action=\"index\\.rb\""\r
+                    +"\u0020" + "method=\"post\""\r
+                    +"\u0020" + "class=\"login_form\""\r
+                    +">"\r
+                + ")|("\r
+                    +"<div"\r
+                    +"\u0020" + "class=\"login_form\""\r
+                    +">"\r
+                + ")"\r
+            );\r
+    private static final Pattern C_EDIV_PATTERN =\r
+            compile(\r
+                  SP_I\r
+                + "<a\u0020href=\"[^\"]*\">[^<]*</a>"\r
+                + SP_I\r
+                + "</div>"\r
+            );\r
+    private static final Pattern USERID_PATTERN =\r
+            compile(\r
+                  "name=\"user_id\""\r
+                + "\u0020"\r
+                + "value=\"([^\"]*)\""\r
+            );\r
+    private static final Pattern C_FORM_PATTERN =\r
+            compile("</form>");\r
+\r
+    /**\r
+     * ログインフォームのパース。\r
+     * ログイン名までの認識を確認したのはF国のみ。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseLoginForm() throws HtmlParseException{\r
+        setContextErrorMessage("lost login form");\r
+\r
+        SeqRange accountRange = this.rangepool_1;\r
+\r
+        boolean isLand_E_Form;\r
+        findAffirm(LOGINFORM_PATTERN);\r
+        if(isGroupMatched(1)){\r
+            isLand_E_Form = false;\r
+        }else{                         // E国ログインフォーム検出\r
+            isLand_E_Form = true;\r
+        }\r
+        shrinkRegion();\r
+\r
+        if(isLand_E_Form){\r
+            lookingAtAffirm(C_EDIV_PATTERN);\r
+            shrinkRegion();\r
+            return;\r
+        }else{\r
+            findAffirm(USERID_PATTERN);\r
+            accountRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+            shrinkRegion();\r
+\r
+            if(accountRange.length() > 0){\r
+                this.basicHandler\r
+                    .loginName(getContent(), accountRange);\r
+            }\r
+\r
+            findAffirm(C_FORM_PATTERN);\r
+            shrinkRegion();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern VILLAGEINFO_PATTERN =\r
+            compile(\r
+                 "([^<]+?)" +SP_I          // 最短一致数量子\r
+                +"<strong>"\r
+                    +"\uff08"\r
+                    +"([0-9]+)"                       // 月\r
+                    +"/"\r
+                    +"([0-9]+)"                       // 日\r
+                    +"\u0020"\r
+                    +"(?:(?:(午前)|(午後))\u0020)?"  // AMPM\r
+                    +"([0-9]+)"                       // 時\r
+                    +"(?:時\u0020|\\:)"\r
+                    +"([0-9]+)"                       // 分\r
+                    +"分?\u0020に更新"\r
+                    +"\uff09"\r
+                +"</strong>"\r
+            );\r
+\r
+    /**\r
+     * 村に関する各種情報をパース。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseVillageInfo() throws HtmlParseException{\r
+        setContextErrorMessage("lose village information");\r
+\r
+        SeqRange villageRange = this.rangepool_1;\r
+\r
+        sweepSpace();\r
+\r
+        lookingAtAffirm(VILLAGEINFO_PATTERN);\r
+        villageRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+\r
+        int month  = parseGroupedInt(2);\r
+        int day    = parseGroupedInt(3);\r
+        int hour   = parseGroupedInt(6);\r
+        int minute = parseGroupedInt(7);\r
+        if(isGroupMatched(5)){  // 午後指定\r
+            hour = (hour + 12) % 24;\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.basicHandler.villageName(getContent(), villageRange);\r
+        this.basicHandler.commitTime(month, day, hour, minute);\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern O_PARAG_PATTERN = compile("<p>");\r
+    private static final Pattern PERIODLINK_PATTERN =\r
+            compile(\r
+            "("\r
+                + "<span\u0020class=\"time\">"\r
+            +")|(?:"\r
+                + "<a\u0020href=\"([^\"]*)\">"\r
+            +")|("\r
+                + "</p>"\r
+            +")"\r
+            );\r
+    private static final Pattern PERIOD_PATTERN =\r
+            compile(\r
+                "(プロローグ)" +\r
+            "|"+\r
+                "(エピローグ)" +\r
+            "|"+\r
+                "(終了)" +\r
+            "|"+\r
+                "([0-9]+)日目"\r
+            );\r
+    private static final Pattern C_SPAN_PATTERN   = compile("</span>");\r
+    private static final Pattern C_ANCHOR_PATTERN = compile("</a>");\r
+\r
+    /**\r
+     * Period間リンクをパース。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parsePeriodLink() throws HtmlParseException{\r
+        setContextErrorMessage("lost period link");\r
+\r
+        SeqRange anchorRange = this.rangepool_1;\r
+\r
+        findAffirm(O_PARAG_PATTERN);\r
+        shrinkRegion();\r
+\r
+        for(;;){\r
+            Pattern closePattern;\r
+            anchorRange.setInvalid();\r
+\r
+            sweepSpace();\r
+            lookingAtAffirm(PERIODLINK_PATTERN);\r
+            if(isGroupMatched(1)){\r
+                closePattern = C_SPAN_PATTERN;\r
+            }else if(isGroupMatched(2)){\r
+                closePattern = C_ANCHOR_PATTERN;\r
+                anchorRange.setLastMatchedGroupRange(getMatcher(), 2);\r
+            }else if(isGroupMatched(3)){\r
+                shrinkRegion();\r
+                break;\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+            shrinkRegion();\r
+\r
+            int day = -1;\r
+            PeriodType periodType = null;\r
+            lookingAtAffirm(PERIOD_PATTERN);\r
+            if(isGroupMatched(1)){\r
+                periodType = PeriodType.PROLOGUE;\r
+            }else if(isGroupMatched(2)){\r
+                periodType = PeriodType.EPILOGUE;\r
+            }else if(isGroupMatched(3)){\r
+                periodType = null;\r
+            }else if(isGroupMatched(4)){\r
+                periodType = PeriodType.PROGRESS;\r
+                day = parseGroupedInt(4);\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+            shrinkRegion();\r
+\r
+            lookingAtAffirm(closePattern);\r
+            shrinkRegion();\r
+\r
+            this.basicHandler.periodLink(getContent(),\r
+                                         anchorRange,\r
+                                         periodType, day );\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern O_MESSAGE_PATTERN =\r
+            compile("<div\u0020class=\"message(?:\u0020ch[0-9]+)?\">");\r
+    private static final Pattern O_RELOAD_PATTERN =\r
+            compile("<div\u0020id=\"reload\">");\r
+    private static final Pattern O_MSGKIND_PATTERN =\r
+            compile(\r
+             "(?:"\r
+                +"<div\u0020class=\"(?:(announce)|(order)|(extra))\">"\r
+            +")|(?:"\r
+                +"(?:"\r
+                +"(?:<a name=\"[^\"]*\">)?"\r
+                +SP_I\r
+                +"<span\u0020class=\"mes_no\">"\r
+                    +"([0-9]+)\\."\r
+                +"</span>)?"\r
+                +SP_I\r
+                +"(?:</a>)?"\r
+                +SP_I\r
+                +"<a\u0020name=\"([^\"]*)\"(?:\u0020class=\"ch_name\")?>"\r
+            +")"\r
+            );\r
+    private static final Pattern C_DIV_PATTERN = compile("</div>");\r
+\r
+    /**\r
+     * 各種メッセージをパース。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseMessage() throws HtmlParseException{\r
+        setContextErrorMessage("lost message");\r
+\r
+        SeqRange nameRange = this.rangepool_1;\r
+\r
+        boolean skipGarbage = true;\r
+\r
+        for(;;){\r
+            sweepSpace();\r
+\r
+            boolean matched;\r
+            if(skipGarbage){\r
+                skipGarbage = false;\r
+                matched = findProbe(O_MESSAGE_PATTERN); // 最初の1回のみ\r
+            }else{\r
+                matched = lookingAtProbe(O_MESSAGE_PATTERN);\r
+            }\r
+            if( ! matched ){\r
+                matched = lookingAtProbe(O_RELOAD_PATTERN);\r
+                if(matched){\r
+                    shrinkRegion();\r
+                    findAffirm(C_DIV_PATTERN);\r
+                    shrinkRegion();\r
+                    continue;\r
+                }\r
+                break;\r
+            }\r
+            shrinkRegion();\r
+\r
+            sweepSpace();\r
+\r
+            lookingAtAffirm(O_MSGKIND_PATTERN);\r
+            if(isGroupMatched(1)){\r
+                shrinkRegion();\r
+                this.sysEventParser.parseAnnounce();\r
+            }else if(isGroupMatched(2)){\r
+                shrinkRegion();\r
+                this.sysEventParser.parseOrder();\r
+            }else if(isGroupMatched(3)){\r
+                shrinkRegion();\r
+                this.sysEventParser.parseExtra();\r
+            }else if(isGroupMatched(5)){\r
+                nameRange.setLastMatchedGroupRange(getMatcher(), 5);\r
+                int talkNo = -1;\r
+                if(isGroupMatched(4)){\r
+                    talkNo = parseGroupedInt(4);\r
+                }\r
+                shrinkRegion();\r
+                this.talkParser.parseTalk(talkNo, nameRange);\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+\r
+            lookingAtAffirm(C_DIV_PATTERN);\r
+            shrinkRegion();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern O_LISTTABLE_PATTERN =\r
+            compile("<table\u0020class=\"list\">");\r
+    private static final Pattern ACTIVEVILLAGE =\r
+            compile(\r
+             "("\r
+                +"</table>"\r
+            +")|(?:"\r
+                +"<tr><td>"\r
+                +"<a\u0020href=\"([^\"]*)\">([^<]*)</a>"\r
+                +"\u0020<strong>\uff08"\r
+                    +"(?:(?:(午前)|(午後))\u0020)?"  // AMPM\r
+                    +"([0-9]+)"                       // 時\r
+                    +"(?:時\u0020|\\:)"\r
+                    +"([0-9]+)"                       // 分\r
+                    +"分?\u0020更新"\r
+                +"\uff09</strong>"\r
+                +"</td><td>(?:"\r
+                +"[^<]*"\r
+                    + "(参加者募集中です。)"\r
+                    +"|(開始待ちです。)"\r
+                    +"|(進行中です。)"\r
+                    +"|(勝敗が決定しました。)"\r
+                    +"|(終了・ログ公開中。)"\r
+                +")</td></tr>"\r
+            +")"\r
+            );\r
+\r
+    /**\r
+     * トップページの村一覧表のパース。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseTopList() throws HtmlParseException{\r
+        setContextErrorMessage("lost village list");\r
+\r
+        SeqRange anchorRange  = this.rangepool_1;\r
+        SeqRange villageRange = this.rangepool_2;\r
+\r
+        if( ! findProbe(O_LISTTABLE_PATTERN) ) return;\r
+        shrinkRegion();\r
+\r
+        for(;;){\r
+            lookingAtAffirm(ACTIVEVILLAGE);\r
+            if(isGroupMatched(1)) break;\r
+            anchorRange .setLastMatchedGroupRange(getMatcher(), 2);\r
+            villageRange.setLastMatchedGroupRange(getMatcher(), 3);\r
+            int hour = parseGroupedInt(6);\r
+            if(isGroupMatched(5)){\r
+                hour = (hour + 12) % 24;\r
+            }\r
+            int minute = parseGroupedInt(7);\r
+\r
+            VillageState state;\r
+            if(isGroupMatched(8)){\r
+                state = VillageState.PROLOGUE;\r
+            }else if(isGroupMatched(9)){\r
+                state = VillageState.PROLOGUE;\r
+            }else if(isGroupMatched(10)){\r
+                state = VillageState.PROGRESS;\r
+            }else if(isGroupMatched(11)){\r
+                state = VillageState.EPILOGUE;\r
+            }else if(isGroupMatched(12)){\r
+                state = VillageState.GAMEOVER;\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+\r
+            shrinkRegion();\r
+\r
+            sweepSpace();\r
+\r
+            this.basicHandler.villageRecord(getContent(),\r
+                                            anchorRange,\r
+                                            villageRange,\r
+                                            hour, minute,\r
+                                            state );\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern O_LISTLOG_PATTERN =\r
+            compile(\r
+            "<a\u0020href=\"(index[^\"]*(?:ready_0|000_ready))\">"\r
+            +"([^<]*)"\r
+            +"</a><br\u0020/>"\r
+            );\r
+\r
+    /**\r
+     * 村一覧ページのパース。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseLogList() throws HtmlParseException{\r
+        setContextErrorMessage("lost village list");\r
+\r
+        SeqRange anchorRange  = this.rangepool_1;\r
+        SeqRange villageRange = this.rangepool_2;\r
+\r
+        boolean is1st = true;\r
+        for(;;){\r
+            boolean matched;\r
+            if(is1st){\r
+                matched = findProbe(O_LISTLOG_PATTERN);\r
+                is1st = false;\r
+            }else{\r
+                matched = lookingAtProbe(O_LISTLOG_PATTERN);\r
+            }\r
+            if( ! matched ) break;\r
+\r
+            anchorRange .setLastMatchedGroupRange(getMatcher(), 1);\r
+            villageRange.setLastMatchedGroupRange(getMatcher(), 2);\r
+\r
+            shrinkRegion();\r
+\r
+            this.basicHandler.villageRecord(getContent(),\r
+                                            anchorRange,\r
+                                            villageRange,\r
+                                            -1, -1,\r
+                                            VillageState.GAMEOVER );\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern C_BODY_PATTERN =\r
+            compile("</body>");\r
+    private static final Pattern C_HTML_PATTERN =\r
+            compile(SP_I+ "</html>" +SP_I);\r
+\r
+    /**\r
+     * XHTML末尾のパース。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseTail() throws HtmlParseException{\r
+        setContextErrorMessage("lost last part");\r
+\r
+        findAffirm(C_BODY_PATTERN);\r
+        shrinkRegion();\r
+\r
+        matchesAffirm(C_HTML_PATTERN);\r
+        shrinkRegion();\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern LISTTITLE_PATTERN =\r
+            compile("終了した村の記録");\r
+\r
+    /**\r
+     * 人狼BBSのページ種別を自動認識しつつパースする。\r
+     * @param content パース対象の文字列\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    public void parseAutomatic(DecodedContent content)\r
+            throws HtmlParseException{\r
+        setContent(content);\r
+\r
+        this.basicHandler.startParse(getContent());\r
+\r
+        parseHead();\r
+\r
+        sweepSpace();\r
+\r
+        if(lookingAtProbe(LISTTITLE_PATTERN)){\r
+            shrinkRegion();\r
+            this.basicHandler.pageType(PageType.VILLAGELIST_PAGE);\r
+            parseLogList();\r
+        }else{\r
+            parseLoginForm();\r
+            sweepSpace();\r
+            if(lookingAtProbe(O_PARAG_PATTERN)){\r
+                shrinkRegion();\r
+                this.basicHandler.pageType(PageType.TOP_PAGE);\r
+                parseTopList();\r
+            }else{\r
+                this.basicHandler.pageType(PageType.PERIOD_PAGE);\r
+                parseVillageInfo();\r
+                parsePeriodLink();\r
+                parseMessage();\r
+            }\r
+        }\r
+\r
+        parseTail();\r
+\r
+        this.basicHandler.endParse();\r
+\r
+        reset();\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/PageType.java b/src/main/java/jp/sourceforge/jindolf/parser/PageType.java
new file mode 100644 (file)
index 0000000..6e95b42
--- /dev/null
@@ -0,0 +1,23 @@
+/*\r
+ * page type\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: PageType.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+/**\r
+ * 人狼BBSサーバが生成するXHTMLページの種別。\r
+ */\r
+public enum PageType{\r
+\r
+    /** トップページ。 */\r
+    TOP_PAGE,\r
+    /** 終了した村一覧。古国には存在しない。 */\r
+    VILLAGELIST_PAGE,\r
+    /** 各村の各日々。 */\r
+    PERIOD_PAGE,\r
+    ;\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/SeqRange.java b/src/main/java/jp/sourceforge/jindolf/parser/SeqRange.java
new file mode 100644 (file)
index 0000000..43cf6b1
--- /dev/null
@@ -0,0 +1,159 @@
+/*\r
+ * range of string\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: SeqRange.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.regex.MatchResult;\r
+\r
+/**\r
+ * 文字列の範囲を表す。\r
+ * 範囲は開始位置と終了位置で表される。\r
+ * 開始位置と終了位置が同じ場合、長さ0の範囲とみなされる。\r
+ * 開始位置0は文字列の左端を表す。\r
+ * 開始位置が負の場合、もしくは開始位置より終了位置が小さい場合、\r
+ * このオブジェクトは無効とみなされる。\r
+ */\r
+public class SeqRange{\r
+\r
+    private int startPos;\r
+    private int endPos;\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * 開始位置、終了位置ともに無効状態となる。\r
+     */\r
+    public SeqRange(){\r
+        this(-1, -1);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param startPos 開始位置\r
+     * @param endPos 終了位置\r
+     */\r
+    public SeqRange(int startPos, int endPos){\r
+        super();\r
+        this.startPos = startPos;\r
+        this.endPos   = endPos;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 開始位置を設定する。\r
+     * @param startPos 開始位置\r
+     */\r
+    public void setStartPos(int startPos){\r
+        this.startPos = startPos;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 終了位置を設定する。\r
+     * @param endPos 終了位置\r
+     */\r
+    public void setEndPos(int endPos){\r
+        this.endPos = endPos;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 開始位置と終了位置を設定する。\r
+     * @param startPosition 開始位置\r
+     * @param endPosition 終了位置\r
+     */\r
+    public void setRange(int startPosition, int endPosition){\r
+        this.startPos = startPosition;\r
+        this.endPos   = endPosition;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 最後にマッチした前方参照グループの範囲で設定する。\r
+     * @param result 正規表現マッチ結果\r
+     * @param groupId グループ番号\r
+     * @throws IllegalStateException マッチしていない\r
+     * @throws IndexOutOfBoundsException グループ番号が不正\r
+     */\r
+    public void setLastMatchedGroupRange(MatchResult result, int groupId)\r
+            throws IllegalStateException,\r
+                   IndexOutOfBoundsException {\r
+        this.startPos = result.start(groupId);\r
+        this.endPos   = result.end(groupId);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 最後にマッチした範囲全体で設定する。\r
+     * @param result 正規表現マッチ結果\r
+     * @throws IllegalStateException マッチしていない\r
+     */\r
+    public void setLastMatchedRange(MatchResult result)\r
+            throws IllegalStateException {\r
+        this.startPos = result.start();\r
+        this.endPos   = result.end();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 開始位置を取得する。\r
+     * @return 開始位置\r
+     */\r
+    public int getStartPos(){\r
+        return this.startPos;\r
+    }\r
+\r
+    /**\r
+     * 終了位置を取得する。\r
+     * @return 終了位置\r
+     */\r
+    public int getEndPos(){\r
+        return this.endPos;\r
+    }\r
+\r
+    /**\r
+     * 範囲の長さを得る。\r
+     * 内容が無効な場合、負の値もありえる。\r
+     * @return 長さ\r
+     */\r
+    public int length(){\r
+        int length = this.endPos - this.startPos;\r
+        return length;\r
+    }\r
+\r
+    /**\r
+     * 現在の範囲で与えられた文字列を切り出す。\r
+     * @param seq 切り出し元文字列\r
+     * @return 切り出された文字列\r
+     * @throws IndexOutOfBoundsException 範囲が無効\r
+     */\r
+    public CharSequence sliceSequence(CharSequence seq)\r
+            throws IndexOutOfBoundsException{\r
+        CharSequence result = seq.subSequence(this.startPos, this.endPos);\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * 範囲指定を無効にする。\r
+     */\r
+    public void setInvalid(){\r
+        this.startPos = -1;\r
+        this.endPos   = -1;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 範囲指定が有効か判定する。\r
+     * @return 有効であればtrue\r
+     */\r
+    public boolean isValid(){\r
+        if     (this.startPos < 0)           return false;\r
+        else if(this.startPos > this.endPos) return false;\r
+        return true;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/ShiftJis.java b/src/main/java/jp/sourceforge/jindolf/parser/ShiftJis.java
new file mode 100644 (file)
index 0000000..f69383d
--- /dev/null
@@ -0,0 +1,80 @@
+/*\r
+ * Shift_JIS encoding utilities\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: ShiftJis.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.nio.charset.Charset;\r
+\r
+/**\r
+ * シフトJIS符号化ユーティリティ。\r
+ * JIS X0208:1997 準拠。(Windows-31Jではない!)\r
+ * @see <a href="http://www.iana.org/assignments/character-sets">\r
+ * CHARACTER SETS</a>\r
+ * @see <a href="http://ja.wikipedia.org/wiki/Shift_JIS">\r
+ * Wikipedia: Shift_JIS</a>\r
+ */\r
+public final class ShiftJis{\r
+\r
+    /** エンコード名。 */\r
+    public static final String ENCODE_NAME = "Shift_JIS";\r
+    /** SHift_JIS用Charsetインスタンス。 */\r
+    public static final Charset CHARSET = Charset.forName(ENCODE_NAME);\r
+    /** char1文字をエンコードした時の最大バイト数。 */\r
+    public static final int MAX_BYTES_PER_CHAR = 2;\r
+\r
+    /**\r
+     * 任意のバイト値がシフトJISの1バイト目でありうるか否か判定する。\r
+     * 文字集合の判定は行わない。\r
+     * @param bval バイト値\r
+     * @return シフトJISの1バイト目でありうるならtrue\r
+     */\r
+    public static boolean isShiftJIS1stByte(byte bval){\r
+        if(   (byte)0x81 <= bval && bval <= (byte)0x9f\r
+           || (byte)0xe0 <= bval && bval <= (byte)0xfc){\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * 任意のバイト値がシフトJISの2バイト目でありうるか否か判定する。\r
+     * 文字集合の判定は行わない。\r
+     * @param bval バイト値\r
+     * @return シフトJISの2バイト目でありうるならtrue\r
+     */\r
+    public static boolean isShiftJIS2ndByte(byte bval){\r
+        if(   (byte)0x40 <= bval && bval <= (byte)0x7e\r
+           || (byte)0x80 <= bval && bval <= (byte)0xfc){\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * 任意のバイト値ペアがシフトJISでありうるか否か判定する。\r
+     * 文字集合の判定は行わない。\r
+     * @param b1st 第一バイト値\r
+     * @param b2nd 第二バイト値\r
+     * @return シフトJISならtrue\r
+     */\r
+    public static boolean isShiftJIS(byte b1st, byte b2nd){\r
+        if(   ShiftJis.isShiftJIS1stByte(b1st)\r
+           && ShiftJis.isShiftJIS2ndByte(b2nd)){\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * 隠しコンストラクタ。\r
+     */\r
+    private ShiftJis(){\r
+        super();\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/SjisDecoder.java b/src/main/java/jp/sourceforge/jindolf/parser/SjisDecoder.java
new file mode 100644 (file)
index 0000000..85b168a
--- /dev/null
@@ -0,0 +1,139 @@
+/*\r
+ * stream decoder for Shift_JIS\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: SjisDecoder.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.io.IOException;\r
+import java.nio.ByteBuffer;\r
+import java.nio.charset.CoderResult;\r
+\r
+/**\r
+ * Shift_JISバイト列のデコードに特化した、{@link StreamDecoder}の派生クラス。\r
+ * Java実行系の細かな仕様差異による\r
+ * デコードエラー出現パターンゆらぎの正規化も行う。\r
+ * 0x5Cが{@literal U+005C}にデコードされるか\r
+ * {@literal U+00A5}にデコードされるかはJava実行系の実装依存。\r
+ * @see <a href="http://www.iana.org/assignments/character-sets">\r
+ * CHARACTER SETS</a>\r
+ */\r
+public class SjisDecoder extends StreamDecoder{\r
+\r
+    /** 入力バッファに必要な最小サイズ(={@value})。 */\r
+    public static final int MIN_INBUFSZ = ShiftJis.MAX_BYTES_PER_CHAR * 2 + 1;\r
+\r
+    static{\r
+        assert MIN_INBUFSZ <= BYTEBUF_DEFSZ;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     */\r
+    public SjisDecoder(){\r
+        this(BYTEBUF_DEFSZ, CHARBUF_DEFSZ);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param inbuf_sz 入力バッファサイズ\r
+     * @param outbuf_sz 出力バッファサイズ\r
+     * @throws IllegalArgumentException バッファサイズが小さすぎる。\r
+     */\r
+    public SjisDecoder(int inbuf_sz, int outbuf_sz)\r
+            throws IllegalArgumentException{\r
+        super(ShiftJis.CHARSET.newDecoder(), inbuf_sz, outbuf_sz);\r
+        if(inbuf_sz < MIN_INBUFSZ){\r
+            throw new IllegalArgumentException();\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 1バイトのエラーを2バイトに統合できないか試す。\r
+     * @param result デコード異常系\r
+     * @return 修正されたデコード異常系。修正がなければ引数と同じものを返す。\r
+     * @throws IOException 入力エラー\r
+     */\r
+    private CoderResult modify1ByteError(CoderResult result)\r
+            throws IOException {\r
+        ByteBuffer inbuffer = getByteBuffer();\r
+\r
+        int currPos;\r
+        int nextPos;\r
+\r
+        currPos = inbuffer.position();\r
+        nextPos = currPos + 1;\r
+        if(nextPos >= inbuffer.limit()){\r
+            readByteBuffer();\r
+            currPos = inbuffer.position();\r
+            nextPos = currPos + 1;\r
+        }\r
+\r
+        // 入力バイト列の最後がこのデコードエラーだった場合。\r
+        if(nextPos >= inbuffer.limit()) return result;\r
+\r
+        byte curr = inbuffer.get(currPos);    // 絶対的get\r
+        byte next = inbuffer.get(nextPos);\r
+        if( ShiftJis.isShiftJIS(curr, next) ){\r
+            return CoderResult.unmappableForLength(2);\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * 2バイトのエラーを1バイトに分割できないか試す。\r
+     * ※ バイト列"FF:32" のShift_JISデコードに際して、\r
+     * 2バイト長のデコードエラーを返す1.6系実行系が存在する。\r
+     * @param result デコード異常系\r
+     * @return 修正されたデコード異常系。修正がなければ引数と同じものを返す。\r
+     */\r
+    private CoderResult modify2ByteError(CoderResult result){\r
+        ByteBuffer inbuffer = getByteBuffer();\r
+\r
+        int currPos;\r
+        int nextPos;\r
+\r
+        currPos = inbuffer.position();\r
+        nextPos = currPos + 1;\r
+\r
+        byte curr = inbuffer.get(currPos);    // 絶対的get\r
+        byte next = inbuffer.get(nextPos);\r
+        if( ! ShiftJis.isShiftJIS(curr, next) ){\r
+            return CoderResult.malformedForLength(1);\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * ライブラリ実装によるシフトJISデコードエラー出現パターンの\r
+     * 差異を吸収する。\r
+     * @param result {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     * @throws IOException {@inheritDoc}\r
+     */\r
+    @Override\r
+    protected int chopErrorSequence(CoderResult result)\r
+            throws IOException{\r
+        int errorLength = result.length();\r
+\r
+        CoderResult newResult;\r
+        if(errorLength == 1){\r
+            newResult = modify1ByteError(result);\r
+        }else if(errorLength == 2){\r
+            newResult = modify2ByteError(result);\r
+        }else{\r
+            assert false;\r
+            return -1;\r
+        }\r
+\r
+        return super.chopErrorSequence(newResult);\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/StreamDecoder.java b/src/main/java/jp/sourceforge/jindolf/parser/StreamDecoder.java
new file mode 100644 (file)
index 0000000..796478d
--- /dev/null
@@ -0,0 +1,298 @@
+/*\r
+ * stream decoder\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: StreamDecoder.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.nio.ByteBuffer;\r
+import java.nio.CharBuffer;\r
+import java.nio.channels.Channels;\r
+import java.nio.channels.ReadableByteChannel;\r
+import java.nio.charset.CharsetDecoder;\r
+import java.nio.charset.CoderResult;\r
+import java.nio.charset.CodingErrorAction;\r
+import java.util.Arrays;\r
+\r
+/**\r
+ * バイトストリームからの文字デコーダ。\r
+ * 入力バイトストリームをデコードし、デコード結果およびデコードエラーを\r
+ * 文字デコードハンドラ{@link DecodeHandler}に通知する。\r
+ * このクラスは、\r
+ * デコードエラー詳細を察知できない{@link java.io.InputStreamReader}の\r
+ * 代替品として設計された。\r
+ * マルチスレッド対応はしていない。\r
+ */\r
+public class StreamDecoder{\r
+\r
+    /** デフォルト入力バッファサイズ(={@value}bytes)。 */\r
+    public static final int BYTEBUF_DEFSZ = 4 * 1024;\r
+    /** デフォルト出力バッファサイズ(={@value}chars)。 */\r
+    public static final int CHARBUF_DEFSZ = 4 * 1024;\r
+\r
+    private final CharsetDecoder decoder;\r
+\r
+    private ReadableByteChannel channel;\r
+    private final ByteBuffer byteBuffer;\r
+    private final CharBuffer charBuffer;\r
+\r
+    private boolean isEndOfInput = false;\r
+    private boolean isFlushing = false;\r
+\r
+    private DecodeHandler decodeHandler;\r
+    private byte[] errorData = new byte[4];\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param decoder デコーダ\r
+     */\r
+    public StreamDecoder(CharsetDecoder decoder){\r
+        this(decoder, BYTEBUF_DEFSZ, CHARBUF_DEFSZ);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param decoder デコーダ\r
+     * @param inbuf_sz 入力バッファサイズ\r
+     * @param outbuf_sz 出力バッファサイズ\r
+     * @throws NullPointerException デコーダにnullを渡した。\r
+     * @throws IllegalArgumentException バッファサイズが負。\r
+     */\r
+    public StreamDecoder(CharsetDecoder decoder,\r
+                           int inbuf_sz,\r
+                           int outbuf_sz )\r
+            throws NullPointerException,\r
+                   IllegalArgumentException {\r
+        super();\r
+\r
+        if(decoder == null) throw new NullPointerException();\r
+\r
+        if(inbuf_sz <= 0 || outbuf_sz <= 0){\r
+            throw new IllegalArgumentException();\r
+        }\r
+\r
+        this.decoder = decoder;\r
+        this.byteBuffer = ByteBuffer.allocate(inbuf_sz);\r
+        this.charBuffer = CharBuffer.allocate(outbuf_sz);\r
+        this.channel = null;\r
+\r
+        initDecoderImpl();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコーダの初期化下請。\r
+     */\r
+    private void initDecoderImpl(){\r
+        this.byteBuffer.clear().flip();\r
+        this.charBuffer.clear();\r
+\r
+        this.decoder.onMalformedInput     (CodingErrorAction.REPORT);\r
+        this.decoder.onUnmappableCharacter(CodingErrorAction.REPORT);\r
+        this.decoder.reset();\r
+\r
+        this.isEndOfInput = false;\r
+        this.isFlushing = false;\r
+\r
+        Arrays.fill(this.errorData, (byte)0x00);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコーダの初期化。\r
+     */\r
+    protected void initDecoder(){\r
+        initDecoderImpl();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 入力バッファを返す。\r
+     * @return 入力バッファ\r
+     */\r
+    protected ByteBuffer getByteBuffer(){\r
+        return this.byteBuffer;\r
+    }\r
+\r
+    /**\r
+     * 出力バッファを返す。\r
+     * @return 出力バッファ\r
+     */\r
+    protected CharBuffer getCharBuffer(){\r
+        return this.charBuffer;\r
+    }\r
+\r
+    /**\r
+     * デコードハンドラの設定。\r
+     * nullオブジェクトを指定しても構わないが、\r
+     * その場合パース時に例外を起こす。\r
+     * @param decodeHandler デコードハンドラ\r
+     */\r
+    public void setDecodeHandler(DecodeHandler decodeHandler){\r
+        this.decodeHandler = decodeHandler;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコードエラー格納配列の再アサイン。\r
+     * 配列の内容は保持される。\r
+     * 決して縮小することは無い。\r
+     * メモ:java.util.Arrays#copyOf()はJRE1.5にない。\r
+     * @param size 再アサイン量。バイト長。\r
+     */\r
+    protected void reassignErrorData(int size){\r
+        int oldLength = this.errorData.length;\r
+        if(oldLength >= size) return;\r
+        int newSize = size;\r
+        if(oldLength * 2 > newSize) newSize = oldLength * 2;\r
+        byte[] newData = new byte[newSize];\r
+        System.arraycopy(this.errorData, 0, newData, 0, oldLength);\r
+        this.errorData = newData;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコードハンドラに文字列を渡す。\r
+     * @throws DecodeException デコードエラー\r
+     */\r
+    protected void flushContent() throws DecodeException{\r
+        if(this.charBuffer.position() <= 0){\r
+            return;\r
+        }\r
+\r
+        this.charBuffer.flip();\r
+        this.decodeHandler.charContent(this.charBuffer);\r
+        this.charBuffer.clear();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコードハンドラにデコードエラーを渡す。\r
+     * @param result デコード結果\r
+     * @throws DecodeException デコードエラー\r
+     * @throws IOException 入力エラー\r
+     */\r
+    protected void putDecodeError(CoderResult result)\r
+            throws IOException,\r
+                   DecodeException{\r
+        int length = chopErrorSequence(result);\r
+        this.decodeHandler.decodingError(this.errorData, 0, length);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * デコードエラーの原因バイト列を抽出する。\r
+     * {@link #errorData}の先頭にバイト列が格納され、バイト長が返される。\r
+     * @param result デコード結果\r
+     * @return 原因バイト列の長さ\r
+     * @throws IOException 入力エラー。\r
+     * ※このメソッドを継承する場合、必要に応じて先読みをしてもよいし、\r
+     * その結果生じたIO例外を投げてもよい。\r
+     */\r
+    protected int chopErrorSequence(CoderResult result) throws IOException{\r
+        int errorLength = result.length();\r
+        reassignErrorData(errorLength);\r
+        this.byteBuffer.get(this.errorData, 0, errorLength);  // 相対get\r
+        return errorLength;\r
+    }\r
+\r
+    /**\r
+     * チャンネルからの入力を読み進める。\r
+     * 前回の読み残しはバッファ前方に詰め直される。\r
+     * @return 入力バイト数。\r
+     * @throws java.io.IOException 入出力エラー\r
+     */\r
+    protected int readByteBuffer() throws IOException{\r
+        this.byteBuffer.compact();\r
+\r
+        int length = this.channel.read(this.byteBuffer);\r
+        if(length <= 0){\r
+            this.isEndOfInput = true;\r
+        }\r
+\r
+        this.byteBuffer.flip();\r
+\r
+        return length;\r
+    }\r
+\r
+    /**\r
+     * バイトストリームのデコードを開始する。\r
+     * @param istream 入力ストリーム\r
+     * @throws IOException 入出力エラー\r
+     * @throws DecodeException デコードエラー\r
+     */\r
+    public void decode(InputStream istream)\r
+            throws IOException,\r
+                   DecodeException {\r
+        this.channel = Channels.newChannel(istream);\r
+\r
+        try{\r
+            decodeChannel();\r
+        }finally{\r
+            this.channel.close();\r
+            this.channel = null;\r
+            istream.close();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 内部チャネルのデコードを開始する。\r
+     * @throws IOException 入出力エラー\r
+     * @throws DecodeException デコードエラー\r
+     */\r
+    protected void decodeChannel()\r
+            throws IOException,\r
+                   DecodeException {\r
+        initDecoder();\r
+\r
+        this.decodeHandler.startDecoding(this.decoder);\r
+\r
+        for(;;){\r
+            CoderResult result;\r
+            if(this.isFlushing){\r
+                result = this.decoder.flush(this.charBuffer);\r
+            }else{\r
+                result = this.decoder.decode(this.byteBuffer,\r
+                                             this.charBuffer,\r
+                                             this.isEndOfInput);\r
+            }\r
+\r
+            if(result.isError()){\r
+                flushContent();\r
+                putDecodeError(result);\r
+            }else if(result.isOverflow()){      // 出力バッファが一杯\r
+                flushContent();\r
+            }else if(result.isUnderflow()){     // 入力バッファが空\r
+                if( ! this.isEndOfInput ){\r
+                    readByteBuffer();\r
+                    continue;\r
+                }\r
+\r
+                if( ! this.isFlushing ){\r
+                    this.isFlushing = true;\r
+                    continue;\r
+                }\r
+\r
+                flushContent();\r
+                break;\r
+            }else{\r
+                assert false;\r
+            }\r
+        }\r
+\r
+        this.decodeHandler.endDecoding();\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/SysEventHandler.java b/src/main/java/jp/sourceforge/jindolf/parser/SysEventHandler.java
new file mode 100644 (file)
index 0000000..5b46e4c
--- /dev/null
@@ -0,0 +1,303 @@
+/*\r
+ * System event handler\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: SysEventHandler.java 1014 2010-03-16 10:43:28Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import jp.sourceforge.jindolf.corelib.EventFamily;\r
+import jp.sourceforge.jindolf.corelib.GameRole;\r
+import jp.sourceforge.jindolf.corelib.SysEventType;\r
+import jp.sourceforge.jindolf.corelib.Team;\r
+\r
+/**\r
+ * システムイベントのパース通知用のハンドラ。\r
+ *\r
+ * このハンドラの全メソッドはパーサ{@link SysEventParser}から呼び出される。\r
+ *\r
+ * パーサがシステムイベントを発見すると、まず最初に\r
+ * {@link #startSysEvent(EventFamily)}がファミリ種別と共に呼び出される。\r
+ * 次にシステムイベントのイベント種別が判明すると、\r
+ * {@link #sysEventType(SysEventType)}が呼び出される。\r
+ * イベント種別に従い、このハンドラの様々なメソッドが0回以上呼び出される。\r
+ * 最後に{@link #endSysEvent()}が呼び出される。\r
+ * その後パーサは次のシステムイベントを探し始める。\r
+ *\r
+ * 一部のメソッドに渡される{@link DecodedContent}文字列オブジェクトは\r
+ * mutableである。\r
+ * 後々で内容が必要になるならば、ハンドラはSeqRangeで示されたこの内容の\r
+ * 必要な箇所をコピーして保存しなければならない。\r
+ *\r
+ * フラグメントや属性値中の文字参照記号列の解釈はハンドラ側の責務とする。\r
+ *\r
+ * 各メソッドは、各々の判断で{@link HtmlParseException}をスローする\r
+ * ことにより、パース作業を中断させることができる。\r
+ */\r
+public interface SysEventHandler{\r
+\r
+    /**\r
+     * システムイベントのパース開始の通知を受け取る。\r
+     * @param eventFamily イベントファミリ種別\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void startSysEvent(EventFamily eventFamily)\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * システムイベント種別の通知を受け取る。\r
+     * @param type イベント種別\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void sysEventType(SysEventType type)\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * システムイベントのパース処理終了の通知を受け取る。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void endSysEvent()\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * ONSTAGEイベントの詳細の通知を受け取る。\r
+     * @param content パース対象の文字列\r
+     * @param entryNo エントリ番号\r
+     * @param avatarRange Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#ONSTAGE\r
+     */\r
+    void sysEventOnStage(DecodedContent content,\r
+                           int entryNo,\r
+                           SeqRange avatarRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * OPENROLEイベントの詳細の通知を受け取る。\r
+     * 複数回連続して呼ばれる。\r
+     * @param role 役職\r
+     * @param num 役職の人数\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#OPENROLE\r
+     */\r
+    void sysEventOpenRole(GameRole role, int num)\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * SURVIVORイベントの詳細の通知を受け取る。\r
+     * 複数回連続して呼ばれる。\r
+     * @param content パース対象の文字列\r
+     * @param avatarRange Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#SURVIVOR\r
+     */\r
+    void sysEventSurvivor(DecodedContent content,\r
+                            SeqRange avatarRange)\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * COUNTINGイベントの詳細の通知を受け取る。\r
+     * 複数回連続して呼ばれる。\r
+     * 最後の呼び出しで投票元Avatar名の位置情報が負だった場合、\r
+     * 投票先Avatar名は処刑が実行されたAvatarを表す。\r
+     * @param content パース対象の文字列。\r
+     * @param voteByRange 投票元Avatar名の範囲\r
+     * @param voteToRange 投票先Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#COUNTING\r
+     */\r
+    void sysEventCounting(DecodedContent content,\r
+                            SeqRange voteByRange,\r
+                            SeqRange voteToRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * COUNTING2イベントの詳細の通知を受け取る。※G国のみ\r
+     * 複数回連続して呼ばれる。\r
+     * @param content パース対象の文字列。\r
+     * @param voteByRange 投票元Avatar名の範囲\r
+     * @param voteToRange 投票先Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#COUNTING2\r
+     */\r
+    void sysEventCounting2(DecodedContent content,\r
+                             SeqRange voteByRange,\r
+                             SeqRange voteToRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * SUDDENDEATHイベントの詳細の通知を受け取る。\r
+     * @param content パース対象の文字列\r
+     * @param avatarRange Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#SUDDENDEATH\r
+     */\r
+    void sysEventSuddenDeath(DecodedContent content,\r
+                               SeqRange avatarRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * MURDEREDイベントの詳細の通知を受け取る。\r
+     * ハム溶けの時など、連続して複数回呼ばれる事がある。\r
+     * @param content パース対象の文字列\r
+     * @param avatarRange Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#MURDERED\r
+     */\r
+    void sysEventMurdered(DecodedContent content,\r
+                            SeqRange avatarRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * PLAYERLISTイベントの詳細の通知を受け取る。\r
+     * 複数回連続して呼ばれる。\r
+     * @param content パース対象の文字列\r
+     * @param avatarRange Avatar名の範囲\r
+     * @param anchorRange URLの範囲。無ければ無効。\r
+     * @param loginRange IDの範囲\r
+     * @param isLiving 生存していればtrue\r
+     * @param role 役職\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#PLAYERLIST\r
+     */\r
+    void sysEventPlayerList(DecodedContent content,\r
+                              SeqRange avatarRange,\r
+                              SeqRange anchorRange,\r
+                              SeqRange loginRange,\r
+                              boolean isLiving,\r
+                              GameRole role )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * EXECUTIONイベントの詳細の通知を受け取る。※G国のみ\r
+     * 複数回連続して呼ばれる。\r
+     * @param content パース対象の文字列。\r
+     * @param avatarRange 投票先Avatar名の範囲\r
+     * @param votes 得票数。負の値であれば、\r
+     * 処刑されたAvatarの通知と見なされる。\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#EXECUTION\r
+     */\r
+    void sysEventExecution(DecodedContent content,\r
+                             SeqRange avatarRange,\r
+                             int votes )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * VANISHイベントの詳細の通知を受け取る。\r
+     * @param content パース対象の文字列\r
+     * @param avatarRange 失踪したAvatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#VANISH\r
+     */\r
+    void sysEventVanish(DecodedContent content,\r
+                          SeqRange avatarRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * JUDGEイベントの詳細の通知を受け取る。\r
+     * @param content パース対象の文字列。\r
+     * @param judgeByRange 占師Avatar名の範囲\r
+     * @param judgeToRange 占われたAvatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#JUDGE\r
+     */\r
+    void sysEventJudge(DecodedContent content,\r
+                         SeqRange judgeByRange,\r
+                         SeqRange judgeToRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * GUARDイベントの詳細の通知を受け取る。\r
+     * @param content パース対象の文字列。\r
+     * @param guardByRange 狩人Avatar名の範囲\r
+     * @param guardToRange 護られたAvatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#GUARD\r
+     */\r
+    void sysEventGuard(DecodedContent content,\r
+                         SeqRange guardByRange,\r
+                         SeqRange guardToRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * ASKENTRYイベントの詳細の通知を受け取る。\r
+     * @param hour 時間\r
+     * @param minute 分\r
+     * @param minLimit 最小構成人数\r
+     * @param maxLimit 最大定員\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#ASKENTRY\r
+     */\r
+    void sysEventAskEntry(int hour, int minute,\r
+                            int minLimit, int maxLimit)\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * ASKCOMMITイベントの詳細の通知を受け取る。\r
+     * @param hour 時間(24時間制)\r
+     * @param minute 分\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#ASKCOMMIT\r
+     */\r
+    void sysEventAskCommit(int hour, int minute)\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * NOCOMMENTイベントの詳細の通知を受け取る。\r
+     * 複数回連続して呼ばれる可能性がある。\r
+     * @param content パース対象文字列\r
+     * @param avatarRange Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#NOCOMMENT\r
+     */\r
+    void sysEventNoComment(DecodedContent content,\r
+                             SeqRange avatarRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * STAYEPILOGUEイベントの詳細の通知を受け取る。\r
+     * @param winner 勝利陣営\r
+     * @param hour 時間(24時間制)\r
+     * @param minute 分\r
+     * @throws HtmlParseException パースエラー\r
+     * @see jp.sourceforge.jindolf.corelib.SysEventType#STAYEPILOGUE\r
+     */\r
+    void sysEventStayEpilogue(Team winner, int hour, int minute)\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * イベントの内容(DIV要素)の一般文字列出現の通知を受け取る。\r
+     * イベント種別は問わない。\r
+     * @param content パース対象文字列\r
+     * @param contentRange 内容テキストの範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void sysEventContent(DecodedContent content,\r
+                           SeqRange contentRange )\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * イベントの内容(DIV要素)のBRタグ出現の通知を受け取る。\r
+     * イベント種別は問わない。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void sysEventContentBreak()\r
+        throws HtmlParseException;\r
+\r
+    /**\r
+     * イベントの内容(DIV要素)のAタグ出現の通知を受け取る。\r
+     * イベント種別は問わない。\r
+     * href属性によるURL記述も通知される。\r
+     * @param content パース対象文字列\r
+     * @param anchorRange URLの範囲\r
+     * @param contentRange 内容テキストの範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void sysEventContentAnchor(DecodedContent content,\r
+                                  SeqRange anchorRange,\r
+                                  SeqRange contentRange )\r
+        throws HtmlParseException;\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/SysEventParser.java b/src/main/java/jp/sourceforge/jindolf/parser/SysEventParser.java
new file mode 100644 (file)
index 0000000..7623af5
--- /dev/null
@@ -0,0 +1,1274 @@
+/*\r
+ * System event parser\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: SysEventParser.java 1027 2010-05-13 09:47:30Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.regex.Pattern;\r
+import jp.sourceforge.jindolf.corelib.EventFamily;\r
+import jp.sourceforge.jindolf.corelib.GameRole;\r
+import jp.sourceforge.jindolf.corelib.SysEventType;\r
+import jp.sourceforge.jindolf.corelib.Team;\r
+\r
+/**\r
+ * 人狼BBSシステムが出力する各種イベント表記のパースを行うパーサ。\r
+ * パース進行に従い{@link SysEventHandler}の各種メソッドが呼び出される。\r
+ */\r
+public class SysEventParser extends AbstractParser{\r
+\r
+    private static final String AVATAR_REGEX =\r
+            "[^<、" + SPCHAR + "]+\u0020[^<、。" + SPCHAR + "]+";\r
+\r
+    private static final Pattern C_DIV_PATTERN =\r
+            compile(SP_I+ "</div>" +SP_I);\r
+    private static final Pattern AVATAR_PATTERN =\r
+            compile(AVATAR_REGEX);\r
+\r
+\r
+    private SysEventHandler sysEventHandler;\r
+\r
+    private int pushedRegionStart = -1;\r
+    private int pushedRegionEnd   = -1;\r
+\r
+    private final SeqRange rangepool_1 = new SeqRange();\r
+    private final SeqRange rangepool_2 = new SeqRange();\r
+    private final SeqRange rangepool_3 = new SeqRange();\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param parent 親パーサ\r
+     */\r
+    public SysEventParser(ChainedParser parent){\r
+        super(parent);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@link SysEventHandler}ハンドラを登録する。\r
+     * @param sysEventHandler ハンドラ\r
+     */\r
+    public void setSysEventHandler(SysEventHandler sysEventHandler){\r
+        this.sysEventHandler = sysEventHandler;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Announceメッセージをパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    public void parseAnnounce() throws HtmlParseException{\r
+        setContextErrorMessage("Unknown Announce message");\r
+\r
+        this.sysEventHandler.startSysEvent(EventFamily.ANNOUNCE);\r
+\r
+        int regionStart = regionStart();\r
+        int regionEnd   = regionEnd();\r
+\r
+        boolean result =\r
+                   probeSimpleAnnounce()\r
+                || probeOpenRole()\r
+                || probeSurvivor()\r
+                || probeMurdered()\r
+                || probeOnStage()\r
+                || probeSuddenDeath()\r
+                || probeCounting()\r
+                || probePlayerList()\r
+                || probeExecution()\r
+                || probeVanish()\r
+                ;\r
+        if( ! result ){\r
+            throw buildParseException();\r
+        }\r
+\r
+        getMatcher().region(regionStart, regionEnd);\r
+        parseContent();\r
+\r
+        lookingAtAffirm(C_DIV_PATTERN);\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.endSysEvent();\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern STARTENTRY_PATTERN =\r
+             compile(\r
+             "昼間は人間のふりをして、夜に正体を現すという人狼。<br />"\r
+            +"その人狼が、"\r
+            +"この村に紛れ込んでいるという噂が広がった。<br /><br />"\r
+            +"村人達は半信半疑ながらも、"\r
+            +"村はずれの宿に集められることになった。"\r
+            +"<br />");\r
+    private static final Pattern STARTMIRROR_PATTERN =\r
+             compile(\r
+             "さあ、自らの姿を鏡に映してみよう。<br />"\r
+            +"そこに映るのはただの村人か、"\r
+            +"それとも血に飢えた人狼か。<br /><br />"\r
+            +"例え人狼でも、多人数で立ち向かえば怖くはない。<br />"\r
+            +"問題は、だれが人狼なのかという事だ。<br />"\r
+            +"占い師の能力を持つ人間ならば、それを見破れるだろう。"\r
+            +"(?:<br />)?");\r
+    private static final Pattern STARTASSAULT_PATTERN =\r
+             compile(\r
+             "ついに犠牲者が出た。人狼はこの村人達のなかにいる。<br />"\r
+            +"しかし、それを見分ける手段はない。<br /><br />"\r
+            +"村人達は、疑わしい者を排除するため、"\r
+            +"投票を行う事にした。<br />"\r
+            +"無実の犠牲者が出るのもやむをえない。"\r
+            +"村が全滅するよりは……。<br /><br />"\r
+            +"最後まで残るのは村人か、それとも人狼か。"\r
+            +"(?:<br />)?");\r
+    private static final Pattern NOMURDER_PATTERN =\r
+             compile(\r
+             "今日は犠牲者がいないようだ。人狼は襲撃に失敗したのだろうか。");\r
+    private static final Pattern WINVILLAGE_PATTERN =\r
+             compile(\r
+             "全ての人狼を退治した……。人狼に怯える日々は去ったのだ!"\r
+            +"(?:<br />)?");\r
+    private static final Pattern WINWOLF_PATTERN =\r
+             compile(\r
+             "もう人狼に抵抗できるほど村人は残っていない……。<br />"\r
+            +"人狼は残った村人を全て食らい、"\r
+            +"別の獲物を求めてこの村を去っていった。"\r
+            +"(?:<br />)?");\r
+    private static final Pattern WINHAMSTER_PATTERN =\r
+             compile(\r
+              "全ては終わったかのように見えた。<br />"\r
+             +"だが、奴が生き残っていた……。");\r
+    private static final Pattern PANIC_PATTERN =\r
+            compile("……。");\r
+\r
+    private static Object[][] simpleRegexToType = {\r
+        { STARTENTRY_PATTERN,   SysEventType.STARTENTRY   },\r
+        { STARTMIRROR_PATTERN,  SysEventType.STARTMIRROR  },\r
+        { STARTASSAULT_PATTERN, SysEventType.STARTASSAULT },\r
+        { NOMURDER_PATTERN,     SysEventType.NOMURDER     },\r
+        { WINVILLAGE_PATTERN,   SysEventType.WINVILLAGE   },\r
+        { WINWOLF_PATTERN,      SysEventType.WINWOLF      },\r
+        { WINHAMSTER_PATTERN,   SysEventType.WINHAMSTER   },\r
+        { PANIC_PATTERN,        SysEventType.PANIC        },\r
+    };\r
+\r
+    /**\r
+     * 文字列が固定されたシンプルなAnnounceメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeSimpleAnnounce() throws HtmlParseException{\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        SysEventType matchedType = null;\r
+\r
+        for(Object[] pair : simpleRegexToType){\r
+            Pattern pattern = (Pattern)( pair[0] );\r
+\r
+            if(lookingAtProbe(pattern)){\r
+                shrinkRegion();\r
+                matchedType = (SysEventType)( pair[1] );\r
+                break;\r
+            }\r
+        }\r
+\r
+        if(matchedType == null){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        this.sysEventHandler.sysEventType(matchedType);\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern OPENROLE_HEAD_PATTERN =\r
+            compile("どうやらこの中には、");\r
+    private static final Pattern OPENROLE_NUM_PATTERN =\r
+            compile("が([0-9]+)名(?:、)?");\r
+    private static final Pattern OPENROLE_TAIL_PATTERN =\r
+            compile("いるようだ。");\r
+\r
+    /**\r
+     * OPENROLEメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeOpenRole() throws HtmlParseException{\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(OPENROLE_HEAD_PATTERN) ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.OPENROLE);\r
+\r
+        for(;;){\r
+            GameRole role = lookingAtRole();\r
+            if(role == null){\r
+                if( lookingAtProbe(OPENROLE_TAIL_PATTERN) ){\r
+                    shrinkRegion();\r
+                    break;\r
+                }\r
+                popRegion();\r
+                return false;\r
+            }\r
+            shrinkRegion();\r
+\r
+            if( ! lookingAtProbe(OPENROLE_NUM_PATTERN) ){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            int num = parseGroupedInt(1);\r
+            shrinkRegion();\r
+\r
+            this.sysEventHandler.sysEventOpenRole(role, num);\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern SURVIVOR_HEAD_PATTERN =\r
+            compile("現在の生存者は、");\r
+    private static final Pattern SURVIVOR_PATTERN =\r
+            Pattern.compile(\r
+            "(" + AVATAR_REGEX + ")"\r
+            +"(?:"\r
+                +"(?:"\r
+                    +"、"\r
+                +")|(?:"\r
+                    +"\u0020の\u0020([0-9]+)\u0020名。"\r
+                +")"\r
+            +")");\r
+\r
+    /**\r
+     * SURVIVORメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeSurvivor() throws HtmlParseException{\r
+        SeqRange avatarRange = this.rangepool_1;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(SURVIVOR_HEAD_PATTERN) ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.SURVIVOR);\r
+\r
+        int avatarNum = 0;\r
+        for(;;){\r
+            if( ! lookingAtProbe(SURVIVOR_PATTERN) ){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            avatarRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+            this.sysEventHandler\r
+                .sysEventSurvivor(getContent(), avatarRange);\r
+            avatarNum++;\r
+            if(isGroupMatched(2)){\r
+                int num = parseGroupedInt(2);\r
+                shrinkRegion();\r
+                if(num != avatarNum){\r
+                    throw new HtmlParseException(regionStart());\r
+                }\r
+                break;\r
+            }\r
+            shrinkRegion();\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern MURDERED_HEAD_PATTERN =\r
+            compile("次の日の朝、");\r
+    private static final Pattern MURDERED_SW_PATTERN =\r
+            compile(\r
+                "("\r
+                    +"\u0020と\u0020"\r
+                +")|("\r
+                    +"\u0020が無残な姿で発見された。"\r
+                    +"(?:<br />)?"  // E国対策\r
+                +")"\r
+            );\r
+\r
+    /**\r
+     * MURDEREDメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeMurdered() throws HtmlParseException{\r
+        SeqRange avatarRange  = this.rangepool_1;\r
+        SeqRange avatarRange2 = this.rangepool_2;\r
+        avatarRange .setInvalid();\r
+        avatarRange2.setInvalid();\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(MURDERED_HEAD_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.MURDERED);\r
+\r
+        for(;;){\r
+            if( ! lookingAtProbe(AVATAR_PATTERN)){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            if( ! avatarRange.isValid() ){\r
+                avatarRange.setLastMatchedRange(getMatcher());\r
+            }else if( ! avatarRange2.isValid() ){\r
+                avatarRange2.setLastMatchedRange(getMatcher());\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+            shrinkRegion();\r
+\r
+            if( ! lookingAtProbe(MURDERED_SW_PATTERN)){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            if(isGroupMatched(1)){\r
+                shrinkRegion();\r
+                continue;\r
+            }else if(isGroupMatched(2)){\r
+                shrinkRegion();\r
+                break;\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+        }\r
+\r
+        this.sysEventHandler\r
+            .sysEventMurdered(getContent(), avatarRange);\r
+        if(avatarRange2.isValid()){\r
+            this.sysEventHandler\r
+                .sysEventMurdered(getContent(), avatarRange2);\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern ONSTAGE_NO_PATTERN =\r
+            compile("([0-9]+)人目、");\r
+    private static final Pattern ONSTAGE_DOT_PATTERN =\r
+            compile(\r
+             "("\r
+            +"(?:" + AVATAR_REGEX + ")"\r
+            +"|)"    // F1556プロローグ対策\r
+            +"。");\r
+\r
+    /**\r
+     * ONSTAGEメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeOnStage() throws HtmlParseException{\r
+        SeqRange avatarRange = this.rangepool_1;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(ONSTAGE_NO_PATTERN) ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        int entryNo = parseGroupedInt(1);\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.ONSTAGE);\r
+\r
+        if( ! lookingAtProbe(ONSTAGE_DOT_PATTERN) ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        avatarRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler\r
+            .sysEventOnStage(getContent(), entryNo, avatarRange);\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern SUDDENDEATH_PATTERN =\r
+            compile(\r
+                 "("\r
+                +"(?:" + AVATAR_REGEX + ")"\r
+                +"|)"                            // F681 2d 対策\r
+                +"\u0020?は、突然死した。"\r
+            );\r
+\r
+    /**\r
+     * SUDDENDEATHメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeSuddenDeath() throws HtmlParseException{\r
+        SeqRange avatarRange = this.rangepool_1;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(SUDDENDEATH_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        avatarRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.SUDDENDEATH);\r
+        this.sysEventHandler\r
+            .sysEventSuddenDeath(getContent(), avatarRange);\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern COUNTING_PATTERN =\r
+            compile(\r
+            "(?:"\r
+                +"<br />"\r
+                +"(" + AVATAR_REGEX + ")"\r
+                +"\u0020は村人達の手により処刑された。"\r
+            +")|(?:"\r
+                +"(" + AVATAR_REGEX + ")"\r
+                +"\u0020は\u0020"\r
+                +"(" + AVATAR_REGEX + ")"\r
+                +"\u0020に投票した。"\r
+                +"(?:<br />)?"\r
+            +")"\r
+            );\r
+\r
+    /**\r
+     * COUNTINGメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeCounting() throws HtmlParseException{\r
+        SeqRange voteByRange = this.rangepool_1;\r
+        SeqRange voteToRange = this.rangepool_2;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        boolean hasVote = false;\r
+        for(;;){\r
+            if( ! lookingAtProbe(COUNTING_PATTERN) ){\r
+                break; // 処刑なし\r
+            }\r
+            if(isGroupMatched(1)){\r
+                voteByRange.setInvalid();\r
+                voteToRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+                shrinkRegion();\r
+                this.sysEventHandler\r
+                    .sysEventCounting(getContent(),\r
+                                      voteByRange,\r
+                                      voteToRange );\r
+                break;\r
+            }else if(isGroupMatched(2)){\r
+                if( ! hasVote ){\r
+                    hasVote = true;\r
+                    this.sysEventHandler.sysEventType(SysEventType.COUNTING);\r
+                }\r
+                voteByRange.setLastMatchedGroupRange(getMatcher(), 2);\r
+                voteToRange.setLastMatchedGroupRange(getMatcher(), 3);\r
+                shrinkRegion();\r
+                this.sysEventHandler\r
+                    .sysEventCounting(getContent(),\r
+                                      voteByRange,\r
+                                      voteToRange );\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+        }\r
+\r
+        if( ! hasVote ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern COUNTING2_PATTERN =\r
+            compile(\r
+                 "(" + AVATAR_REGEX + ")"\r
+                +"\u0020は\u0020"\r
+                +"(" + AVATAR_REGEX + ")"\r
+                +"\u0020に投票した。"\r
+                +"(?:<br />)?"\r
+            );\r
+\r
+    /**\r
+     * COUNTING2メッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeCounting2() throws HtmlParseException{\r
+        SeqRange voteByRange = this.rangepool_1;\r
+        SeqRange voteToRange = this.rangepool_2;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        boolean hasVote = false;\r
+        for(;;){\r
+            if( ! lookingAtProbe(COUNTING2_PATTERN) ){\r
+                break;\r
+            }\r
+            if( ! hasVote ){\r
+                hasVote = true;\r
+                this.sysEventHandler.sysEventType(SysEventType.COUNTING2);\r
+            }\r
+            voteByRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+            voteToRange.setLastMatchedGroupRange(getMatcher(), 2);\r
+            shrinkRegion();\r
+            this.sysEventHandler\r
+                .sysEventCounting2(getContent(),\r
+                                   voteByRange,\r
+                                   voteToRange );\r
+        }\r
+\r
+        if( ! hasVote ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern PLAYERID_PATTERN =\r
+            compile(\r
+                "\u0020\uff08" // 全角開き括弧\r
+                +"(?:<a\u0020href=\"([^\"]*)\">)?"\r
+                +"([^<]*)"\r
+                +"(?:</a>)?"\r
+                +"\uff09、"     // 全角閉じ括弧\r
+            );\r
+    private static final Pattern LIVEORDIE_PATTERN =\r
+            compile(\r
+                "(生存。)|(死亡。)"\r
+            );\r
+    private static final Pattern PLAYER_DELIM_PATTERN =\r
+            compile(\r
+                 "だった。"\r
+                +"(?:<br />)?"\r
+            );\r
+\r
+    /**\r
+     * PLAYERLISTメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probePlayerList() throws HtmlParseException{\r
+        SeqRange avatarRange  = this.rangepool_1;\r
+        SeqRange anchorRange  = this.rangepool_2;\r
+        SeqRange accountRange = this.rangepool_3;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        boolean hasPlayerList = false;\r
+\r
+        for(;;){\r
+            if( ! lookingAtProbe(AVATAR_PATTERN)){\r
+                break;\r
+            }\r
+            avatarRange.setLastMatchedRange(getMatcher());\r
+            shrinkRegion();\r
+\r
+            if( ! lookingAtProbe(PLAYERID_PATTERN)){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            if(isGroupMatched(1)){\r
+                anchorRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+            }else{\r
+                anchorRange.setInvalid();\r
+            }\r
+            accountRange.setLastMatchedGroupRange(getMatcher(), 2);\r
+            shrinkRegion();\r
+\r
+            boolean isLiving = false;\r
+            if( ! lookingAtProbe(LIVEORDIE_PATTERN)){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            if(isGroupMatched(1)){\r
+                isLiving = true;\r
+            }else if(isGroupMatched(2)){\r
+                isLiving = false;\r
+            }\r
+            shrinkRegion();\r
+\r
+            GameRole role = lookingAtRole();\r
+            if(role == null){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            shrinkRegion();\r
+\r
+            if( ! lookingAtProbe(PLAYER_DELIM_PATTERN)){\r
+                popRegion();\r
+                return false;\r
+            }\r
+            shrinkRegion();\r
+\r
+            if( ! hasPlayerList ){\r
+                hasPlayerList = true;\r
+                this.sysEventHandler.sysEventType(SysEventType.PLAYERLIST);\r
+            }\r
+\r
+            this.sysEventHandler\r
+                .sysEventPlayerList(getContent(),\r
+                                    avatarRange,\r
+                                    anchorRange,\r
+                                    accountRange,\r
+                                    isLiving,\r
+                                    role );\r
+        }\r
+\r
+        if( ! hasPlayerList ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern EXECUTION_PATTERN =\r
+            compile(\r
+                "(?:"\r
+                + "(" + AVATAR_REGEX + ")、([0-9]+)票。(?:<br />)?"\r
+                +")|(?:"\r
+                +"<br />(" + AVATAR_REGEX + ")\u0020は"\r
+                +"村人達の手により処刑された。"\r
+                +")"\r
+            );\r
+\r
+    /**\r
+     * EXECUTIONメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeExecution() throws HtmlParseException{\r
+        SeqRange avatarRange  = this.rangepool_1;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        boolean hasExecution = false;\r
+\r
+        for(;;){\r
+            if( ! lookingAtProbe(EXECUTION_PATTERN)){\r
+                break;\r
+            }\r
+\r
+            if( ! hasExecution ){\r
+                hasExecution = true;\r
+                this.sysEventHandler.sysEventType(SysEventType.EXECUTION);\r
+            }\r
+\r
+            if(isGroupMatched(1)){\r
+                avatarRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+                int votes = parseGroupedInt(2);\r
+                shrinkRegion();\r
+                this.sysEventHandler\r
+                    .sysEventExecution(getContent(),\r
+                                       avatarRange,\r
+                                       votes );\r
+            }else if(isGroupMatched(3)){\r
+                avatarRange.setLastMatchedGroupRange(getMatcher(), 3);\r
+                shrinkRegion();\r
+                this.sysEventHandler\r
+                    .sysEventExecution(getContent(),\r
+                                       avatarRange,\r
+                                       -1 );\r
+            }\r
+        }\r
+\r
+        if( ! hasExecution ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern VANISH_PATTERN =\r
+            compile(\r
+                 "(?:<br />)*"\r
+                +"(" + AVATAR_REGEX + ")"\r
+                +"\u0020は、失踪した。"\r
+                +"(?:<br />)*"\r
+            );\r
+\r
+    /**\r
+     * VANISHメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeVanish() throws HtmlParseException{\r
+        SeqRange avatarRange  = this.rangepool_1;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        boolean hasVanish = false;\r
+\r
+        for(;;){\r
+            if( ! lookingAtProbe(VANISH_PATTERN)){\r
+                break;\r
+            }\r
+\r
+            if( ! hasVanish ){\r
+                hasVanish = true;\r
+                this.sysEventHandler.sysEventType(SysEventType.VANISH);\r
+            }\r
+            avatarRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+\r
+            shrinkRegion();\r
+\r
+            this.sysEventHandler\r
+                .sysEventVanish(getContent(), avatarRange);\r
+        }\r
+\r
+        if( ! hasVanish ){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    /**\r
+     * Orderメッセージをパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    public void parseOrder() throws HtmlParseException{\r
+        setContextErrorMessage("Unknown Order message");\r
+\r
+        this.sysEventHandler.startSysEvent(EventFamily.ORDER);\r
+\r
+        int regionStart = regionStart();\r
+        int regionEnd   = regionEnd();\r
+\r
+        boolean result =\r
+                   probeAskEntry()\r
+                || probeAskCommit()\r
+                || probeNoComment()\r
+                || probeStayEpilogue()\r
+                || probeGameOver()\r
+                ;\r
+        if( ! result ){\r
+            throw buildParseException();\r
+        }\r
+\r
+        getMatcher().region(regionStart, regionEnd);\r
+        parseContent();\r
+\r
+        lookingAtAffirm(C_DIV_PATTERN);\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.endSysEvent();\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern ASKENTRY_PATTERN =\r
+            compile(\r
+             "演じたいキャラクターを選び、発言してください。<br />"\r
+            +"([0-2][0-9]):([0-5][0-9])\u0020に"\r
+            +"([0-9]+)名以上がエントリーしていれば進行します。<br />"\r
+            +"最大([0-9]+)名まで参加可能です。<br /><br />"\r
+            +"※[\u0020]?エントリーは取り消せません。"\r
+            +"ルールをよく理解した上でご参加下さい。<br />"\r
+            +"(?:※始めての方は、村人希望での参加となります。<br />)?"\r
+            +"(?:※希望能力についての発言は控えてください。<br />)?"\r
+            );\r
+\r
+    /**\r
+     * ASKENTRYメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeAskEntry() throws HtmlParseException{\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(ASKENTRY_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        int hour     = parseGroupedInt(1);\r
+        int minute   = parseGroupedInt(2);\r
+        int minLimit = parseGroupedInt(3);\r
+        int maxLimit = parseGroupedInt(4);\r
+\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.ASKENTRY);\r
+        this.sysEventHandler\r
+            .sysEventAskEntry(hour, minute, minLimit, maxLimit);\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern ASKCOMMIT_PATTERN =\r
+            compile(\r
+             "(?:"\r
+            +"([0-2][0-9]):([0-5][0-9])\u0020までに、"\r
+            +"誰を処刑するべきかの投票先を決定して下さい。<br />"\r
+            +"一番票を集めた人物が処刑されます。"\r
+            +"同数だった場合はランダムで決定されます。<br /><br />"\r
+            +")?"\r
+            +"特殊な能力を持つ人は、"\r
+            +"([0-2][0-9]):([0-5][0-9])\u0020までに"\r
+            +"行動を確定して下さい。<br />"\r
+            );\r
+\r
+    /**\r
+     * ASKCOMMITメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeAskCommit() throws HtmlParseException{\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(ASKCOMMIT_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        boolean is1stDay;\r
+        if(isGroupMatched(1)){\r
+            is1stDay = false;\r
+        }else{\r
+            is1stDay = true;\r
+        }\r
+\r
+        int hh1 = parseGroupedInt(1);\r
+        int mm1 = parseGroupedInt(2);\r
+        int hh2 = parseGroupedInt(3);\r
+        int mm2 = parseGroupedInt(4);\r
+\r
+        shrinkRegion();\r
+\r
+        if( ! is1stDay && (hh1 != hh2 || mm1 != mm2) ){\r
+            throw new HtmlParseException(regionStart());\r
+        }\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.ASKCOMMIT);\r
+        this.sysEventHandler.sysEventAskCommit(hh2, mm2);\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern NOCOMMENT_HEAD_PATTERN =\r
+            compile("本日まだ発言していない者は、");\r
+    private static final Pattern NOCOMMENT_AVATAR_PATTERN =\r
+            compile(\r
+             "(?:"\r
+                +"(" + AVATAR_REGEX + ")、"\r
+            +")|(?:"\r
+                +"以上\u0020([0-9]+)\u0020名。"\r
+            +")"\r
+            );\r
+\r
+    /**\r
+     * NOCOMMENTメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeNoComment() throws HtmlParseException{\r
+        SeqRange avatarRange = this.rangepool_1;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(NOCOMMENT_HEAD_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.NOCOMMENT);\r
+\r
+        int avatarNum = 0;\r
+        for(;;){\r
+            if( ! lookingAtProbe(NOCOMMENT_AVATAR_PATTERN)){\r
+                popRegion();\r
+                return false;\r
+            }\r
+\r
+            if(isGroupMatched(1)){\r
+                avatarRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+                this.sysEventHandler\r
+                    .sysEventNoComment(getContent(), avatarRange);\r
+                shrinkRegion();\r
+                avatarNum++;\r
+            }else if(isGroupMatched(2)){\r
+                int num = parseGroupedInt(2);\r
+                shrinkRegion();\r
+                if(num != avatarNum){\r
+                    throw new HtmlParseException(regionStart());\r
+                }\r
+                break;\r
+            }\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern STAYEPILOGUE_PATTERN =\r
+            compile(\r
+            "(?:(村人)|(人狼)|(ハムスター))側の勝利です!<br />"\r
+            +"全てのログとユーザー名を公開します。"\r
+            +"([0-2][0-9]):([0-5][0-9])\u0020まで"\r
+            +"自由に書き込めますので、"\r
+            +"今回の感想などをどうぞ。<br />"\r
+            );\r
+\r
+    /**\r
+     * STAYEPILOGUEメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeStayEpilogue() throws HtmlParseException{\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(STAYEPILOGUE_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        Team winner = null;\r
+        if(isGroupMatched(1)){\r
+            winner = Team.VILLAGE;\r
+        }else if(isGroupMatched(2)){\r
+            winner = Team.WOLF;\r
+        }else if(isGroupMatched(3)){\r
+            winner = Team.HAMSTER;\r
+        }\r
+\r
+        int hour = parseGroupedInt(4);\r
+        int minute = parseGroupedInt(5);\r
+\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.STAYEPILOGUE);\r
+        this.sysEventHandler.sysEventStayEpilogue(winner, hour, minute);\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern GAMEOVER_PATTERN =\r
+            compile("終了しました。" + "<br />");\r
+\r
+    /**\r
+     * GAMEOVERメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeGameOver() throws HtmlParseException{\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(GAMEOVER_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.GAMEOVER);\r
+\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    /**\r
+     * Extraメッセージをパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    public void parseExtra() throws HtmlParseException{\r
+        setContextErrorMessage("Unknown Extra message");\r
+\r
+        this.sysEventHandler.startSysEvent(EventFamily.EXTRA);\r
+\r
+        int regionStart = regionStart();\r
+        int regionEnd   = regionEnd();\r
+\r
+        boolean result =\r
+                   probeJudge()\r
+                || probeGuard()\r
+                || probeCounting2();\r
+        if( ! result ){\r
+            throw buildParseException();\r
+        }\r
+\r
+        getMatcher().region(regionStart, regionEnd);\r
+        parseContent();\r
+\r
+        lookingAtAffirm(C_DIV_PATTERN);\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.endSysEvent();\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern JUDGE_DELIM_PATTERN =\r
+            compile("\u0020は、");\r
+    private static final Pattern JUDGE_TAIL_PATTERN =\r
+            compile("\u0020を占った。");\r
+\r
+    /**\r
+     * JUDGEメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeJudge() throws HtmlParseException{\r
+        SeqRange judgeByRange = this.rangepool_1;\r
+        SeqRange judgeToRange = this.rangepool_2;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(AVATAR_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        judgeByRange.setLastMatchedRange(getMatcher());\r
+        shrinkRegion();\r
+\r
+        if( ! lookingAtProbe(JUDGE_DELIM_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        if( ! lookingAtProbe(AVATAR_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        judgeToRange.setLastMatchedRange(getMatcher());\r
+        shrinkRegion();\r
+\r
+        if( ! lookingAtProbe(JUDGE_TAIL_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.JUDGE);\r
+        this.sysEventHandler\r
+            .sysEventJudge(getContent(),\r
+                           judgeByRange,\r
+                           judgeToRange );\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern GUARD_DELIM_PATTERN =\r
+            compile("\u0020は、");\r
+    private static final Pattern GUARD_TAIL_PATTERN =\r
+            compile("\u0020を守っている。");\r
+\r
+    /**\r
+     * GUARDメッセージのパースを試みる。\r
+     * @return マッチしたらtrue\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private boolean probeGuard() throws HtmlParseException{\r
+        SeqRange guardByRange = this.rangepool_1;\r
+        SeqRange guardToRange = this.rangepool_2;\r
+\r
+        pushRegion();\r
+\r
+        sweepSpace();\r
+\r
+        if( ! lookingAtProbe(AVATAR_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        guardByRange.setLastMatchedRange(getMatcher());\r
+        shrinkRegion();\r
+\r
+        if( ! lookingAtProbe(GUARD_DELIM_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        if( ! lookingAtProbe(AVATAR_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        guardToRange.setLastMatchedRange(getMatcher());\r
+        shrinkRegion();\r
+\r
+        if( ! lookingAtProbe(GUARD_TAIL_PATTERN)){\r
+            popRegion();\r
+            return false;\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.sysEventHandler.sysEventType(SysEventType.GUARD);\r
+        this.sysEventHandler.sysEventGuard(getContent(),\r
+                                           guardByRange,\r
+                                           guardToRange );\r
+        sweepSpace();\r
+\r
+        return true;\r
+    }\r
+\r
+    private static final Pattern CONTENT_PATTERN =\r
+            compile(\r
+             "("\r
+                +"[^<>\\n\\r]+"\r
+            +")|("\r
+                +"<br />"\r
+            +")|(?:"\r
+                +"<a\u0020href=\"([^\"]*)\">([^<>]*)</a>"\r
+            +")"\r
+            );\r
+\r
+    /**\r
+     * システムイベントの内容文字列をパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseContent() throws HtmlParseException{\r
+        SeqRange anchorRange  = this.rangepool_1;\r
+        SeqRange contentRange = this.rangepool_2;\r
+\r
+        sweepSpace();\r
+\r
+        for(;;){\r
+            if( ! lookingAtProbe(CONTENT_PATTERN) ){\r
+                break;\r
+            }\r
+\r
+            if(isGroupMatched(1)){\r
+                contentRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+                this.sysEventHandler\r
+                    .sysEventContent(getContent(), contentRange);\r
+            }else if(isGroupMatched(2)){\r
+                this.sysEventHandler.sysEventContentBreak();\r
+            }else if(isGroupMatched(3)){\r
+                anchorRange.setLastMatchedGroupRange(getMatcher(), 3);\r
+                contentRange.setLastMatchedGroupRange(getMatcher(), 4);\r
+                this.sysEventHandler\r
+                    .sysEventContentAnchor(getContent(),\r
+                                           anchorRange,\r
+                                           contentRange );\r
+            }\r
+\r
+            shrinkRegion();\r
+        }\r
+\r
+        sweepSpace();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 一時的に現在の検索領域を待避する。\r
+     * 待避できるのは1回のみ。複数回スタックはできない。\r
+     * @see #popRegion()\r
+     */\r
+    private void pushRegion(){\r
+        this.pushedRegionStart = regionStart();\r
+        this.pushedRegionEnd   = regionEnd();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 一時的に待避した検索領域を復活させる。\r
+     * @throws IllegalStateException まだ何も待避していない。\r
+     * @see #pushRegion()\r
+     */\r
+    private void popRegion() throws IllegalStateException{\r
+        if(this.pushedRegionStart < 0 || this.pushedRegionEnd < 0){\r
+            throw new IllegalStateException();\r
+        }\r
+\r
+        if(   this.pushedRegionStart != regionStart()\r
+           || this.pushedRegionEnd   != regionEnd()  ){\r
+            getMatcher().region(this.pushedRegionStart, this.pushedRegionEnd);\r
+        }\r
+\r
+        this.pushedRegionStart = -1;\r
+        this.pushedRegionEnd   = -1;\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/TalkHandler.java b/src/main/java/jp/sourceforge/jindolf/parser/TalkHandler.java
new file mode 100644 (file)
index 0000000..19913f3
--- /dev/null
@@ -0,0 +1,122 @@
+/*\r
+ * handler for parse talk-part\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: TalkHandler.java 989 2010-03-13 17:17:20Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import jp.sourceforge.jindolf.corelib.TalkType;\r
+\r
+/**\r
+ * 人狼BBSの発言部XHTML断片をパースするためのハンドラ。\r
+ *\r
+ * このハンドラの全メソッドはパーサ{@link HtmlParser}から呼び出される。\r
+ *\r
+ * パーサが発言箇所を発見すると、まず最初に\r
+ * {@link #startTalk()}が呼び出される。\r
+ * 発言内容に従い、このハンドラの様々なメソッドが0回以上呼び出される。\r
+ * 最後に{@link #endTalk()}が呼び出される。\r
+ * その後パーサは次の発言を探し始める。\r
+ *\r
+ * 一部のメソッドに渡される{@link DecodedContent}文字列オブジェクトは\r
+ * mutableである。\r
+ * 後々で内容が必要になるならば、ハンドラはSeqRangeで示されたこの内容の\r
+ * 必要な箇所をコピーして保存しなければならない。\r
+ *\r
+ * フラグメントや属性値中の文字参照記号列の解釈はハンドラ側の責務とする。\r
+ *\r
+ * 各メソッドは、各々の判断で{@link HtmlParseException}をスローする\r
+ * ことにより、パース作業を中断させることができる。\r
+ */\r
+public interface TalkHandler{\r
+\r
+    /**\r
+     * 発言部パース開始の通知を受け取る。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void startTalk()\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言部パース終了の通知を受け取る。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void endTalk()\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 白発言番号を受け取る。※G国only。\r
+     * 負の値が渡ってきた場合は白発言でないので無視してよい。\r
+     * @param talkNo 白発言番号\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkNo(int talkNo)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言部ID(Aタグのname属性)の通知を受け取る。\r
+     * @param content パース対象文字列\r
+     * @param idRange IDの範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkId(DecodedContent content, SeqRange idRange)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言したAvatar名の通知を受け取る。\r
+     * @param content パース対象文字列\r
+     * @param avatarRange Avatar名の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkAvatar(DecodedContent content, SeqRange avatarRange)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言時刻の通知を受け取る。\r
+     * @param hour 時間(24時間制)\r
+     * @param minute 分\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkTime(int hour, int minute)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言者の顔アイコンURLの通知を受け取る。\r
+     * @param content パース対象文字列\r
+     * @param urlRange URLの範囲。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkIconUrl(DecodedContent content, SeqRange urlRange)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言種別の通知を受け取る。\r
+     * @param type 発言種別\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkType(TalkType type)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言テキスト内容の通知を受け取る。\r
+     * 1発言のパース中に複数回呼ばれる事もありうる。\r
+     * @param content パース対象文字列\r
+     * @param textRange テキストの範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkText(DecodedContent content, SeqRange textRange)\r
+            throws HtmlParseException;\r
+\r
+    /**\r
+     * 発言テキスト内のBRタグ出現の通知を受け取る。\r
+     * 1発言のパース中に複数回呼ばれる事もありうる。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    void talkBreak()\r
+            throws HtmlParseException;\r
+\r
+    // TODO 「発言取り消し」リンクの検出メソッドは必要?\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/TalkParser.java b/src/main/java/jp/sourceforge/jindolf/parser/TalkParser.java
new file mode 100644 (file)
index 0000000..8f97cf0
--- /dev/null
@@ -0,0 +1,248 @@
+/*\r
+ * talk-part parser\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: TalkParser.java 1016 2010-03-16 11:31:30Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.regex.Pattern;\r
+import jp.sourceforge.jindolf.corelib.TalkType;\r
+\r
+/**\r
+ * 人狼BBSシステムが出力する各発言箇所のパーサ。\r
+ * パース進行に従い{@link TalkHandler}の各種メソッドが呼び出される。\r
+ */\r
+public class TalkParser extends AbstractParser{\r
+\r
+    private TalkHandler talkHandler;\r
+\r
+    private final SeqRange rangepool_1 = new SeqRange();\r
+\r
+    /**\r
+     * コンストラクタ。\r
+     * @param parent 親パーサ\r
+     */\r
+    public TalkParser(ChainedParser parent){\r
+        super(parent);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@link TalkHandler}ハンドラを登録する。\r
+     * @param talkHandler ハンドラ\r
+     */\r
+    public void setTalkHandler(TalkHandler talkHandler){\r
+        this.talkHandler = talkHandler;\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 各Avatarの個別の発言をパースする。\r
+     * 最初のAタグは既にパース済みとする。\r
+     * @param talkNo 白発言番号\r
+     * @param nameRange Aタグのname属性値の範囲\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    public void parseTalk(int talkNo, SeqRange nameRange)\r
+            throws HtmlParseException{\r
+        this.talkHandler.startTalk();\r
+\r
+        this.talkHandler.talkNo(talkNo);\r
+        this.talkHandler.talkId(getContent(), nameRange);\r
+\r
+        parseName();\r
+        parseTime();\r
+        parseIcon();\r
+        parseType();\r
+        parseText();\r
+        parseTail();\r
+\r
+        this.talkHandler.endTalk();\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern AVATARNAME_PATTERN =\r
+            compile("([^<]*)");\r
+\r
+    /**\r
+     * 発言者名をパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseName() throws HtmlParseException{\r
+        setContextErrorMessage("lost dialog avatar-name");\r
+\r
+        SeqRange avatarRange = this.rangepool_1;\r
+\r
+        lookingAtAffirm(AVATARNAME_PATTERN);\r
+        avatarRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+        shrinkRegion();\r
+\r
+        this.talkHandler.talkAvatar(getContent(), avatarRange);\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern TALKTIME_PATTERN =\r
+            compile(\r
+                 "</a>"\r
+                +SP_I\r
+                +"<span class=\"time\">"\r
+                +"(?:(午前\u0020)|(午後\u0020))?"\r
+                +"([0-9][0-9]?)(?:時\u0020|\\:)"\r
+                +"([0-9][0-9]?)分?\u0020"\r
+                +"</span>"\r
+                +SP_I\r
+                +"<table\u0020[^>]*>"\r
+                +SP_I\r
+                +"(?:<tbody>)?"\r
+                +SP_I\r
+                +"<tr>"\r
+            );\r
+\r
+    /**\r
+     * 発言時刻をパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseTime() throws HtmlParseException{\r
+        setContextErrorMessage("lost dialog time");\r
+\r
+        lookingAtAffirm(TALKTIME_PATTERN);\r
+        int hour = parseGroupedInt(3);\r
+        int minute = parseGroupedInt(4);\r
+        if(isGroupMatched(2)){  // 午後指定\r
+            hour = (hour + 12) % 24;\r
+        }\r
+        shrinkRegion();\r
+        sweepSpace();\r
+\r
+        this.talkHandler.talkTime(hour, minute);\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern IMGSRC_PATTERN =\r
+            compile(\r
+                  "<td\u0020[^>]*><img\u0020src=\"([^\"]*)\"></td>"\r
+                 +SP_I\r
+                 +"<td\u0020[^>]*><img\u0020[^>]*></td>"\r
+            );\r
+\r
+    /**\r
+     * アイコンのURLをパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseIcon() throws HtmlParseException{\r
+        setContextErrorMessage("lost icon url");\r
+\r
+        SeqRange urlRange = this.rangepool_1;\r
+\r
+        lookingAtAffirm(IMGSRC_PATTERN);\r
+        urlRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+        shrinkRegion();\r
+        sweepSpace();\r
+\r
+        this.talkHandler.talkIconUrl(getContent(), urlRange);\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern TALKDIC_PATTERN =\r
+            compile(\r
+                 "<td>" +SP_I+ "<div(?:\u0020[^>]*)?>"\r
+                +SP_I\r
+                +"<div class=\"mes_"\r
+                +"(?:(say)|(think)|(whisper)|(groan))"\r
+                +"_body1\">"\r
+            );\r
+\r
+    /**\r
+     * 発言種別をパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseType() throws HtmlParseException{\r
+        setContextErrorMessage("lost dialog type");\r
+\r
+        lookingAtAffirm(TALKDIC_PATTERN);\r
+        TalkType type;\r
+        if(isGroupMatched(1)){\r
+            type = TalkType.PUBLIC;\r
+        }else if(isGroupMatched(2)){\r
+            type = TalkType.PRIVATE;\r
+        }else if(isGroupMatched(3)){\r
+            type = TalkType.WOLFONLY;\r
+        }else if(isGroupMatched(4)){\r
+            type = TalkType.GRAVE;\r
+        }else{\r
+            assert false;\r
+            throw buildParseException();\r
+        }\r
+        shrinkRegion();\r
+\r
+        this.talkHandler.talkType(type);\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern TEXT_PATTERN =\r
+            compile("([^<>]+)|(<br />)|(<a href=\"[^\"]*\">)|(</a>)");\r
+\r
+    /**\r
+     * 発言テキストをパースする。\r
+     * 前後のホワイトスペースは無視しない。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseText() throws HtmlParseException{\r
+        setContextErrorMessage("lost dialog text");\r
+\r
+        SeqRange textRange = this.rangepool_1;\r
+\r
+        while(lookingAtProbe(TEXT_PATTERN)){\r
+            if(isGroupMatched(1)){\r
+                textRange.setLastMatchedGroupRange(getMatcher(), 1);\r
+                this.talkHandler.talkText(getContent(), textRange);\r
+            }else if(isGroupMatched(2)){      // <br />\r
+                this.talkHandler.talkBreak();\r
+            }else if(isGroupMatched(3)){      // <a>\r
+                // IGNORE\r
+                assert true;\r
+            }else if(isGroupMatched(4)){      // </a>\r
+                // IGNORE\r
+                assert true;\r
+            }else{\r
+                assert false;\r
+                throw buildParseException();\r
+            }\r
+            shrinkRegion();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    private static final Pattern TAIL_PATTERN =\r
+            compile(\r
+                       "</div>"  // F1603 2d21:12 ペーター発言には注意\r
+                +SP_I+ "</div>"\r
+                +SP_I+ "</td>"\r
+                +SP_I+ "</tr>"\r
+                +SP_I+ "(?:</tbody>)?"\r
+                +SP_I+ "</table>"\r
+            );\r
+\r
+    /**\r
+     * 発言末尾をパースする。\r
+     * @throws HtmlParseException パースエラー\r
+     */\r
+    private void parseTail() throws HtmlParseException{\r
+        setContextErrorMessage("lost dialog termination");\r
+\r
+        lookingAtAffirm(TAIL_PATTERN);\r
+        shrinkRegion();\r
+        sweepSpace();\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/main/java/jp/sourceforge/jindolf/parser/package-info.java b/src/main/java/jp/sourceforge/jindolf/parser/package-info.java
new file mode 100644 (file)
index 0000000..f305b0f
--- /dev/null
@@ -0,0 +1,103 @@
+/*\r
+ * JinParser パッケージコメント\r
+ *\r
+ * このファイルは、SunJDK5.0以降に含まれるJavadoc用に用意された、\r
+ * 特別な名前を持つソースファイルです。\r
+ * このファイルはソースコードを含まず、\r
+ * パッケージコメントとパッケージ宣言のみが含まれます。\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: package-info.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+/**\r
+ * これは Jindolf プロジェクトにおける、\r
+ * XHTML文書のパース部分を構成するパッケージです。\r
+ *\r
+ * JinParserライブラリは、CGIゲーム「人狼BBS」のクライアント制作者向けに\r
+ * 作られたJavaライブラリです。\r
+ * JinParserライブラリは、人狼BBSの専用クライアント開発プロジェクト\r
+ * 「Jindolf」から派生しました。\r
+ *\r
+ * <hr>\r
+ *\r
+ * 任意のバイトストリームから、\r
+ * デコードエラー情報付き文字列{@code DecodedContent}を得るには、\r
+ * 次のようにします。\r
+ * <pre>\r
+ * {@code\r
+ * InputStream is = .....\r
+ * StreamDecoder decoder = new SjisDecoder();\r
+ * ContentBuilder builder = new ContentBuilder();\r
+ * decoder.setDecodeHandler(builder);\r
+ * try{\r
+ *     decoder.decode(is);\r
+ * }catch(IOException e){\r
+ *     // ERROR!\r
+ * }catch(DecodeException e){\r
+ *     // ERROR!\r
+ * }\r
+ * DecodedContent content = builder.getContent();\r
+ * }\r
+ * </pre>\r
+ *\r
+ * このようにして得られた文字列をパースして、\r
+ * あなたの実装したハンドラ{@code YourHandler}に通知するには、\r
+ * 以下のようにします。\r
+ * <pre>\r
+ * {@code\r
+ * HtmlParser parser = new HtmlParser();\r
+ * HtmlHandler handler = new YourHandler();\r
+ * parser.setBasicHandler(handler);\r
+ * parser.setTalkHandler(handler);\r
+ * parser.setSysEventHandler(handler);\r
+ * try{\r
+ *     parser.parseAutomatic(content);\r
+ * }catch(HtmlParseException e){\r
+ *     // ERROR!\r
+ * }\r
+ * }\r
+ * </pre>\r
+ *\r
+ * ハンドラ内部で、パース元となった文字列の一部を切り出したい場合は、\r
+ * {@code EntityConverter}を使うのが便利です。\r
+ *\r
+ * <hr>\r
+ *\r
+ * <p>\r
+ * The MIT License\r
+ * </p>\r
+ * <p>\r
+ * Copyright(c) 2009 olyutorskii\r
+ * </p>\r
+ * <p>\r
+ * Permission is hereby granted, free of charge, to any person obtaining a\r
+ * copy of this software and associated documentation files (the "Software"),\r
+ * to deal in the Software without restriction, including without limitation\r
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,\r
+ * and/or sell copies of the Software, and to permit persons to whom the\r
+ * Software is furnished to do so, subject to the following conditions:\r
+ * </p>\r
+ * <p>\r
+ * The above copyright notice and this permission notice shall be included in\r
+ * all copies or substantial portions of the Software.\r
+ * </p>\r
+ * <p>\r
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\r
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\r
+ * OTHER DEALINGS IN THE SOFTWARE.\r
+ * </p>\r
+ *\r
+ * <hr>\r
+ *\r
+ * @see <a href="http://jindolf.sourceforge.jp/">\r
+ * Jindolfポータルサイト</a>\r
+ * @see <a href="http://sourceforge.jp/projects/jindolf/">\r
+ * Jindolf開発プロジェクト</a>\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
diff --git a/src/main/resources/jp/sourceforge/jindolf/parser/resources/version.properties b/src/main/resources/jp/sourceforge/jindolf/parser/resources/version.properties
new file mode 100644 (file)
index 0000000..debc29b
--- /dev/null
@@ -0,0 +1,7 @@
+# Version definition
+#   [ with Maven resource filtering ]
+
+library.name = ${pom.name}
+library.version = ${pom.version}
+
+# EOF #
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/ContentBuilderSJTest.java b/src/test/java/jp/sourceforge/jindolf/parser/ContentBuilderSJTest.java
new file mode 100644 (file)
index 0000000..a8c5173
--- /dev/null
@@ -0,0 +1,388 @@
+/*\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: ContentBuilderSJTest.java 988 2010-03-13 12:46:22Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ */\r
+public class ContentBuilderSJTest {\r
+\r
+    public ContentBuilderSJTest() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    public static byte[] byteArray(CharSequence seq){\r
+        byte[] result;\r
+\r
+        List<Byte> byteList = new ArrayList<Byte>();\r
+\r
+        int length = seq.length();\r
+        for(int pos = 0; pos < length; pos++){\r
+            int val = 0;\r
+\r
+            char ch = seq.charAt(pos);\r
+\r
+            if('0' <= ch && ch <= '9'){\r
+                val += ch - '0';\r
+            }else if('a' <= ch && ch <= 'f'){\r
+                val += ch - 'a' + 10;\r
+            }else if('A' <= ch && ch <= 'F'){\r
+                val += ch - 'A' + 10;\r
+            }else{\r
+                continue;\r
+            }\r
+\r
+            pos++;\r
+            if(pos >= length) break;\r
+\r
+            val *= 16;\r
+            ch = seq.charAt(pos);\r
+\r
+            if('0' <= ch && ch <= '9'){\r
+                val += ch - '0';\r
+            }else if('a' <= ch && ch <= 'f'){\r
+                val += ch - 'a' + 10;\r
+            }else if('A' <= ch && ch <= 'F'){\r
+                val += ch - 'A' + 10;\r
+            }else{\r
+                continue;\r
+            }\r
+\r
+            byteList.add((byte)val);\r
+        }\r
+\r
+        result = new byte[byteList.size()];\r
+\r
+        for(int pos = 0; pos < result.length; pos++){\r
+            result[pos] = byteList.get(pos);\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * Test of SjisDecoder & ContentBuilder.\r
+     */\r
+    @Test\r
+    public void testDecoding() throws IOException, DecodeException{\r
+        System.out.println("Decoding");\r
+\r
+        SjisDecoder decoder;\r
+        ContentBuilderSJ builder;\r
+        byte[] bdata;\r
+        InputStream istream;\r
+        DecodedContent content;\r
+\r
+        decoder = new SjisDecoder();\r
+        builder = new ContentBuilderSJ();\r
+        decoder.setDecodeHandler(builder);\r
+\r
+        bdata = byteArray("20:41:42:43:7e");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals(" ABC~", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        bdata = byteArray("");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        bdata = byteArray("00:0A:0D:1F");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("\u0000\n\r\u001f", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        bdata = byteArray("A1:B1:B2:B3:DF");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("。アイウ゚", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        bdata = byteArray("8140:82A0:82A2:82A4:889F:EAA4");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("\u3000あいう亜熙", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        bdata = byteArray("5c");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("\\", content.toString());\r
+        assertNotSame("\u00a5", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        bdata = byteArray("8d5c");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("構", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        return;\r
+    }\r
+\r
+    private void assertUnmapError(DecodeErrorInfo einfo,\r
+                                    int charPos,\r
+                                    int b1, int b2 ){\r
+        assertEquals(charPos, einfo.getCharPosition());\r
+        assertTrue(einfo.has2nd());\r
+        assertEquals((byte)b1, einfo.getRawByte1st());\r
+        assertEquals((byte)b2, einfo.getRawByte2nd());\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of unmappable character.\r
+     */\r
+    @Test\r
+    public void testUnmap() throws IOException, DecodeException{\r
+        System.out.println("Unmap");\r
+\r
+        SjisDecoder decoder;\r
+        ContentBuilderSJ builder;\r
+        byte[] bdata;\r
+        InputStream istream;\r
+        DecodedContent content;\r
+        DecodeErrorInfo einfo;\r
+\r
+        decoder = new SjisDecoder();\r
+        builder = new ContentBuilderSJ();\r
+        decoder.setDecodeHandler(builder);\r
+\r
+        bdata = byteArray("41:8540:42"); // 9区\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("A?B", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 1, 0x85, 0x40);\r
+\r
+        bdata = byteArray("41:8740:42"); // 13区\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("A?B", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 1, 0x87, 0x40);\r
+\r
+        bdata = byteArray("41:8840:42"); // 15区\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("A?B", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 1, 0x88, 0x40);\r
+\r
+        bdata = byteArray("41:EB40:42"); // 85区\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("A?B", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 1, 0xEB, 0x40);\r
+\r
+        bdata = byteArray("41:ED40:42"); // 89区\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("A?B", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 1, 0xED, 0x40);\r
+\r
+        bdata = byteArray("41:EEFC:42"); // 92区\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("A?B", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 1, 0xEE, 0xFC);\r
+\r
+        bdata = byteArray("41:EF9F:42"); // 94区\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("A?B", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 1, 0xEF, 0x9F);\r
+\r
+        return;\r
+    }\r
+\r
+    private void assertMalformError(DecodeErrorInfo einfo,\r
+                                      int charPos,\r
+                                      int b1 ){\r
+        assertEquals(charPos, einfo.getCharPosition());\r
+        assertFalse(einfo.has2nd());\r
+        assertEquals((byte)b1, einfo.getRawByte1st());\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of malformed character.\r
+     */\r
+    @Test\r
+    public void testMalform() throws IOException, DecodeException{\r
+        System.out.println("Malform");\r
+\r
+        SjisDecoder decoder;\r
+        ContentBuilderSJ builder;\r
+        byte[] bdata;\r
+        InputStream istream;\r
+        DecodedContent content;\r
+        DecodeErrorInfo einfo;\r
+\r
+        decoder = new SjisDecoder();\r
+        builder = new ContentBuilderSJ();\r
+        decoder.setDecodeHandler(builder);\r
+\r
+        bdata = byteArray("31:FD:FE:FF:32");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("1???2", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(3, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertMalformError(einfo, 1, 0xfd);\r
+        einfo = content.getDecodeErrorList().get(1);\r
+        assertMalformError(einfo, 2, 0xfe);\r
+        einfo = content.getDecodeErrorList().get(2);\r
+        assertMalformError(einfo, 3, 0xff);\r
+\r
+        bdata = byteArray("31:82:32:33");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("1?23", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertMalformError(einfo, 1, 0x82);\r
+\r
+        bdata = byteArray("31:32:33:82");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("123?", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertMalformError(einfo, 3, 0x82);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of Bounds buffering.\r
+     */\r
+    @Test\r
+    public void testBounds() throws IOException, DecodeException{\r
+        System.out.println("Bounds");\r
+\r
+        SjisDecoder decoder;\r
+        ContentBuilderSJ builder;\r
+        byte[] bdata;\r
+        InputStream istream;\r
+        DecodedContent content;\r
+        DecodeErrorInfo einfo;\r
+\r
+        decoder = new SjisDecoder(5, 5);\r
+        builder = new ContentBuilderSJ();\r
+        decoder.setDecodeHandler(builder);\r
+\r
+        bdata = byteArray("31:32:33:34:88" + "9F:35");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("1234亜5", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+        assertEquals(0, content.getDecodeErrorList().size());\r
+\r
+        bdata = byteArray("31:32:33:34:82" + "35:36");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("1234?56", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertMalformError(einfo, 4, 0x82);\r
+\r
+        bdata = byteArray("31:32:33:34:87" + "40:35");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("1234?5", content.toString());\r
+        assertTrue(content.hasDecodeError());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        einfo = content.getDecodeErrorList().get(0);\r
+        assertUnmapError(einfo, 4, 0x87, 0x40);\r
+\r
+        decoder = new SjisDecoder(5, 3);\r
+        builder = new ContentBuilderSJ();\r
+        decoder.setDecodeHandler(builder);\r
+\r
+        bdata = byteArray("31:32:33:34:35:36");\r
+        istream = new ByteArrayInputStream(bdata);\r
+        decoder.decode(istream);\r
+        content = builder.getContent();\r
+        assertEquals("123456", content.toString());\r
+        assertFalse(content.hasDecodeError());\r
+        assertEquals(0, content.getDecodeErrorList().size());\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/ContentBuilderUCS2Test.java b/src/test/java/jp/sourceforge/jindolf/parser/ContentBuilderUCS2Test.java
new file mode 100644 (file)
index 0000000..fa30a59
--- /dev/null
@@ -0,0 +1,57 @@
+/*\r
+ * Copyright(c) 2010 olyutorskii\r
+ * $Id: ContentBuilderUCS2Test.java 988 2010-03-13 12:46:22Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ *\r
+ */\r
+public class ContentBuilderUCS2Test {\r
+\r
+    public ContentBuilderUCS2Test() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    /**\r
+     * Test of charToUTF8 method, of class ContentBuilderUCS2.\r
+     */\r
+    @Test\r
+    public void testCharToUTF16(){\r
+        System.out.println("charToUTF16");\r
+\r
+        char ch;\r
+        byte[] result;\r
+        \r
+        ch = '\ud844';\r
+        result = ContentBuilderUCS2.charToUTF16(ch);\r
+\r
+        assertEquals(2, result.length);\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/DecodeErrorInfoTest.java b/src/test/java/jp/sourceforge/jindolf/parser/DecodeErrorInfoTest.java
new file mode 100644 (file)
index 0000000..4bdc1b0
--- /dev/null
@@ -0,0 +1,255 @@
+/*\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: DecodeErrorInfoTest.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ */\r
+public class DecodeErrorInfoTest {\r
+\r
+    public DecodeErrorInfoTest() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    /**\r
+     * Test of Constructor\r
+     */\r
+    @Test\r
+    public void testConstructor(){\r
+        System.out.println("Constructor");\r
+\r
+        new DecodeErrorInfo(99, (byte)0xfe);\r
+        new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+\r
+        new DecodeErrorInfo(0, (byte)0xfe);\r
+        new DecodeErrorInfo(0, (byte)0x87, (byte)0x40);\r
+\r
+        try{\r
+            new DecodeErrorInfo(-1, (byte)0xfe);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        try{\r
+            new DecodeErrorInfo(-1, (byte)0x87, (byte)0x40);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getCharPosition method, of class DecodeErrorInfo.\r
+     */\r
+    @Test\r
+    public void testGetCharPosition(){\r
+        System.out.println("getCharPosition");\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        assertEquals(99, info.getCharPosition());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        assertEquals(999, info.getCharPosition());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of has2nd method, of class DecodeErrorInfo.\r
+     */\r
+    @Test\r
+    public void testHas2nd(){\r
+        System.out.println("has2nd");\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        assertFalse(info.has2nd());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        assertTrue(info.has2nd());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getRawByte1st method, of class DecodeErrorInfo.\r
+     */\r
+    @Test\r
+    public void testGetRawByte1st(){\r
+        System.out.println("getRawByte1st");\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        assertEquals((byte)0xfe, info.getRawByte1st());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        assertEquals((byte)0x87, info.getRawByte1st());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getRawByte2nd method, of class DecodeErrorInfo.\r
+     */\r
+    @Test\r
+    public void testGetRawByte2nd(){\r
+        System.out.println("getRawByte2nd");\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        try{\r
+            info.getRawByte2nd();\r
+            fail();\r
+        }catch(IllegalStateException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        assertEquals((byte)0x40, info.getRawByte2nd());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of createGappedClone method, of class DecodeErrorInfo.\r
+     */\r
+    @Test\r
+    public void testCreateGappedClone(){\r
+        System.out.println("createGappedClone");\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        info = info.createGappedClone(1);\r
+        assertEquals(98, info.getCharPosition());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        info = info.createGappedClone(1);\r
+        assertEquals(998, info.getCharPosition());\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        info = info.createGappedClone(-1);\r
+        assertEquals(100, info.getCharPosition());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        info = info.createGappedClone(-1);\r
+        assertEquals(1000, info.getCharPosition());\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        info = info.createGappedClone(99);\r
+        assertEquals(0, info.getCharPosition());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        info = info.createGappedClone(999);\r
+        assertEquals(0, info.getCharPosition());\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        try{\r
+            info = info.createGappedClone(100);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        try{\r
+            info = info.createGappedClone(1000);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of toString method, of class DecodeErrorInfo.\r
+     */\r
+    @Test\r
+    public void testToString(){\r
+        System.out.println("toString");\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0x09);\r
+        assertEquals("start:99 09", info.toString());\r
+\r
+        info = new DecodeErrorInfo(99, (byte)0xfe);\r
+        assertEquals("start:99 fe", info.toString());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x08, (byte)0x09);\r
+        assertEquals("start:999 08:09", info.toString());\r
+\r
+        info = new DecodeErrorInfo(999, (byte)0x87, (byte)0x40);\r
+        assertEquals("start:999 87:40", info.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of POS_COMPARATOR.\r
+     */\r
+    @Test\r
+    public void testCompare(){\r
+        System.out.println("POS_COMPARATOR");\r
+\r
+        DecodeErrorInfo info;\r
+        DecodeErrorInfo other;\r
+\r
+        info  = new DecodeErrorInfo(99, (byte)0xfe);\r
+        other = new DecodeErrorInfo(98, (byte)0xfe);\r
+        assertTrue(DecodeErrorInfo.POS_COMPARATOR.compare(info, other) > 0);\r
+\r
+        info  = new DecodeErrorInfo(99, (byte)0xfe);\r
+        other = new DecodeErrorInfo(100, (byte)0xfe);\r
+        assertTrue(DecodeErrorInfo.POS_COMPARATOR.compare(info, other) < 0);\r
+\r
+        info  = new DecodeErrorInfo(99, (byte)0xfe);\r
+        other = new DecodeErrorInfo(99, (byte)0xfe);\r
+        assertTrue(DecodeErrorInfo.POS_COMPARATOR.compare(info, other) == 0);\r
+\r
+        assertTrue(DecodeErrorInfo.POS_COMPARATOR.compare(null, other) < 0);\r
+        assertTrue(DecodeErrorInfo.POS_COMPARATOR.compare(info, null) > 0);\r
+        assertTrue(DecodeErrorInfo.POS_COMPARATOR.compare(null, null) == 0);\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/DecodeExceptionTest.java b/src/test/java/jp/sourceforge/jindolf/parser/DecodeExceptionTest.java
new file mode 100644 (file)
index 0000000..26307ea
--- /dev/null
@@ -0,0 +1,110 @@
+/*\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: DecodeExceptionTest.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ */\r
+public class DecodeExceptionTest {\r
+\r
+    public DecodeExceptionTest() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    /**\r
+     * Test of getBytePos method, of class DecodeException.\r
+     */\r
+    @Test\r
+    public void testGetBytePos(){\r
+        System.out.println("getBytePos");\r
+\r
+        DecodeException ex;\r
+\r
+        ex = new DecodeException();\r
+        assertTrue(0 > ex.getBytePos());\r
+\r
+        ex = new DecodeException("abc");\r
+        assertTrue(0 > ex.getBytePos());\r
+\r
+        ex = new DecodeException(10, 11);\r
+        assertEquals(10, ex.getBytePos());\r
+\r
+        ex = new DecodeException("abc", 10, 11);\r
+        assertEquals(10, ex.getBytePos());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getCharPos method, of class DecodeException.\r
+     */\r
+    @Test\r
+    public void testGetCharPos(){\r
+        System.out.println("getCharPos");\r
+\r
+        DecodeException ex;\r
+\r
+        ex = new DecodeException();\r
+        assertTrue(0 > ex.getCharPos());\r
+\r
+        ex = new DecodeException("abc");\r
+        assertTrue(0 > ex.getCharPos());\r
+\r
+        ex = new DecodeException(10, 11);\r
+        assertEquals(11, ex.getCharPos());\r
+\r
+        ex = new DecodeException("abc", 10, 11);\r
+        assertEquals(11, ex.getCharPos());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getMessage method, of class DecodeException.\r
+     */\r
+    @Test\r
+    public void testGetMessage(){\r
+        System.out.println("getMessage");\r
+\r
+        DecodeException ex;\r
+\r
+        ex = new DecodeException();\r
+        assertEquals("bytePos=-1 charPos=-1", ex.getMessage());\r
+\r
+        ex = new DecodeException("abc");\r
+        assertEquals("abc bytePos=-1 charPos=-1", ex.getMessage());\r
+\r
+        ex = new DecodeException(10, 11);\r
+        assertEquals("bytePos=10 charPos=11", ex.getMessage());\r
+\r
+        ex = new DecodeException("abc", 10, 11);\r
+        assertEquals("abc bytePos=10 charPos=11", ex.getMessage());\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/DecodedContentTest.java b/src/test/java/jp/sourceforge/jindolf/parser/DecodedContentTest.java
new file mode 100644 (file)
index 0000000..69559ce
--- /dev/null
@@ -0,0 +1,755 @@
+/*\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: DecodedContentTest.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ */\r
+public class DecodedContentTest {\r
+\r
+    public DecodedContentTest() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    /**\r
+     * Test of Constructor, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testConstructor(){\r
+        System.out.println("Constructor");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        assertEquals("", content.toString());\r
+\r
+        content = new DecodedContent("abc");\r
+        assertEquals("abc", content.toString());\r
+\r
+        content = new DecodedContent(128);\r
+        assertEquals("", content.toString());\r
+\r
+        content = new DecodedContent(0);\r
+        assertEquals("", content.toString());\r
+        content.append("abc");\r
+        assertEquals("abc", content.toString());\r
+\r
+        try{\r
+            content = new DecodedContent(-1);\r
+            fail();\r
+        }catch(NegativeArraySizeException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of init method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testInit(){\r
+        System.out.println("init");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append("abc");\r
+        content.addDecodeError((byte)0xff);\r
+        content.append("def");\r
+        assertEquals("abc?def", content.toString());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+\r
+        content.init();\r
+        assertEquals("", content.toString());\r
+        assertEquals(0, content.getDecodeErrorList().size());\r
+\r
+        content.append('X');\r
+        assertEquals("X", content.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of hasDecodeError method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testHasDecodeError(){\r
+        System.out.println("hasDecodeError");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        content.append("a");\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        content.addDecodeError((byte)0xff);\r
+        assertTrue(content.hasDecodeError());\r
+\r
+        content.append("b");\r
+        assertTrue(content.hasDecodeError());\r
+\r
+        content.init();\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        content.append("c");\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        content = new DecodedContent();\r
+        List list = content.getDecodeErrorList();\r
+        assertEquals(0, list.size());\r
+        assertFalse(content.hasDecodeError());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getDecodeErrorList method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testGetDecodeErrorList(){\r
+        System.out.println("getDecodeErrorList");\r
+\r
+        DecodedContent content;\r
+        List<DecodeErrorInfo> list;\r
+\r
+        content = new DecodedContent();\r
+        list = content.getDecodeErrorList();\r
+        assertEquals(0, list.size());\r
+\r
+        content.append("abc");\r
+        list = content.getDecodeErrorList();\r
+        assertEquals(0, list.size());\r
+\r
+        content.addDecodeError((byte)0xff);\r
+        list = content.getDecodeErrorList();\r
+        assertEquals(1, list.size());\r
+\r
+        content.append("def");\r
+        list = content.getDecodeErrorList();\r
+        assertEquals(1, list.size());\r
+\r
+        content.addDecodeError((byte)0x03, (byte)0x04);\r
+        list = content.getDecodeErrorList();\r
+        assertEquals(2, list.size());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getRawContent method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testGetRawContent(){\r
+        System.out.println("getRawContent");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        assertEquals("", content.getRawContent().toString());\r
+\r
+        content.append("a");\r
+        assertEquals("a", content.getRawContent().toString());\r
+\r
+        content.addDecodeError((byte)0xff);\r
+        assertEquals("a?", content.getRawContent().toString());\r
+\r
+        content.append("b");\r
+        assertEquals("a?b", content.getRawContent().toString());\r
+\r
+        assertEquals(content.toString(), content.getRawContent().toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of charAt method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testCharAt(){\r
+        System.out.println("charAt");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append("12345");\r
+        assertEquals('1', content.charAt(0));\r
+        assertEquals('3', content.charAt(2));\r
+        assertEquals('5', content.charAt(4));\r
+\r
+        try{\r
+            content.charAt(-1);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        try{\r
+            content.charAt(5);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of length method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testLength(){\r
+        System.out.println("length");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        assertEquals(0, content.length());\r
+\r
+        content.append("12345");\r
+        assertEquals(5, content.length());\r
+\r
+        content.addDecodeError((byte)0xff);\r
+        assertEquals(6, content.length());\r
+\r
+        content.init();\r
+        assertEquals(0, content.length());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of subSequence method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testSubSequence(){\r
+        System.out.println("subSequence");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+\r
+        content.append("12345");\r
+        assertEquals("234", content.subSequence(1, 4).toString());\r
+\r
+        try{\r
+            content.subSequence(-1, 4);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        try{\r
+            content.subSequence(1, 6);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        try{\r
+            content.subSequence(4, 1);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of subContent method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testSubContent(){\r
+        System.out.println("subContent");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+\r
+        content.append("12345");\r
+        assertEquals("234", content.subContent(1, 4).toString());\r
+\r
+        try{\r
+            content.subContent(-1, 4);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        try{\r
+            content.subContent(1, 6);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        try{\r
+            content.subContent(4, 1);\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+        }catch(Throwable e){\r
+            fail();\r
+        }\r
+\r
+        content = new DecodedContent();\r
+        content.append("ab");\r
+        content.addDecodeError((byte)0x01);\r
+        content.append("de");\r
+        content = content.subContent(1,4);\r
+        assertEquals("b?d", content.toString());\r
+\r
+        List<DecodeErrorInfo> list = content.getDecodeErrorList();\r
+        assertEquals(1, list.size());\r
+        assertEquals((byte)0x01, list.get(0).getRawByte1st());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of append method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testAppend_char(){\r
+        System.out.println("append");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append('a');\r
+        assertEquals("a", content.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of append method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testAppend_CharSequence(){\r
+        System.out.println("append");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        CharSequence seq = "abc";\r
+        content.append(seq);\r
+        assertEquals("abc", content.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of append method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testAppend_3args_1(){\r
+        System.out.println("append");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append("abc");\r
+        assertEquals("abc", content.toString());\r
+\r
+        CharSequence seq = "12345";\r
+        content.append(seq, 1, 4);\r
+        assertEquals("abc234", content.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of append method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testAppend_3args_2(){\r
+        System.out.println("append");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append("abc");\r
+\r
+        DecodedContent other;\r
+        other = new DecodedContent();\r
+        other.append("12345");\r
+\r
+        content.append(other, 1, 4);\r
+        assertEquals("abc234", content.toString());\r
+\r
+        content = new DecodedContent();\r
+        content.append("abc");\r
+\r
+        other = new DecodedContent();\r
+        other.addDecodeError((byte)0x01);\r
+        other.addDecodeError((byte)0x02);\r
+        other.addDecodeError((byte)0x03);\r
+        other.addDecodeError((byte)0x04);\r
+        other.addDecodeError((byte)0x05);\r
+\r
+        content.append(other, 1, 4);\r
+        assertEquals("abc???", content.toString());\r
+\r
+        List<DecodeErrorInfo> list = content.getDecodeErrorList();\r
+        assertEquals(3, list.size());\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = list.get(0);\r
+        assertEquals(3, info.getCharPosition());\r
+        assertEquals((byte)0x02, info.getRawByte1st());\r
+        info = list.get(1);\r
+        assertEquals(4, info.getCharPosition());\r
+        assertEquals((byte)0x03, info.getRawByte1st());\r
+        info = list.get(2);\r
+        assertEquals(5, info.getCharPosition());\r
+        assertEquals((byte)0x04, info.getRawByte1st());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of addDecodeError method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testAddDecodeError_byte(){\r
+        System.out.println("addDecodeError");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append("abc");\r
+        content.addDecodeError((byte)0xfe);\r
+        content.append("def");\r
+        content.addDecodeError((byte)0xff);\r
+\r
+        assertEquals("abc?def?", content.toString());\r
+        List<DecodeErrorInfo> list = content.getDecodeErrorList();\r
+        assertEquals(2, list.size());\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = list.get(0);\r
+        assertEquals(3, list.get(0).getCharPosition());\r
+        assertFalse(info.has2nd());\r
+        assertEquals((byte)0xfe, info.getRawByte1st());\r
+\r
+        info = list.get(1);\r
+        assertEquals(7, info.getCharPosition());\r
+        assertFalse(info.has2nd());\r
+        assertEquals((byte)0xff, info.getRawByte1st());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of addDecodeError method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testAddDecodeError_byte_byte(){\r
+        System.out.println("addDecodeError");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append("abc");\r
+        content.addDecodeError((byte)0x01, (byte)0x02);\r
+        content.append("def");\r
+        content.addDecodeError((byte)0xfe, (byte)0xff);\r
+\r
+        assertEquals("abc?def?", content.toString());\r
+        List<DecodeErrorInfo> list = content.getDecodeErrorList();\r
+        assertEquals(2, list.size());\r
+\r
+        DecodeErrorInfo info;\r
+\r
+        info = list.get(0);\r
+        assertEquals(3, list.get(0).getCharPosition());\r
+        assertTrue(info.has2nd());\r
+        assertEquals((byte)0x01, info.getRawByte1st());\r
+        assertEquals((byte)0x02, info.getRawByte2nd());\r
+\r
+        info = list.get(1);\r
+        assertEquals(7, info.getCharPosition());\r
+        assertTrue(info.has2nd());\r
+        assertEquals((byte)0xfe, info.getRawByte1st());\r
+        assertEquals((byte)0xff, info.getRawByte2nd());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of toString method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testToString(){\r
+        System.out.println("toString");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent();\r
+        content.append("abc");\r
+        content.addDecodeError((byte)0x01, (byte)0x02);\r
+        content.append("def");\r
+        content.addDecodeError((byte)0xfe, (byte)0xff);\r
+\r
+        assertEquals("abc?def?", content.toString());\r
+        assertEquals(content.getRawContent().toString(), content.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of lsearchErrorIndex method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testLsearchErrorIndex(){\r
+        System.out.println("lsearchErrorIndex");\r
+\r
+        List<DecodeErrorInfo> errList;\r
+        int result;\r
+\r
+        errList = new ArrayList<DecodeErrorInfo>();\r
+        result = DecodedContent.lsearchErrorIndex(errList, 10);\r
+        assertEquals(0, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(5, (byte)0x00));\r
+        result = DecodedContent.lsearchErrorIndex(errList, 10);\r
+        assertEquals(1, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(10, (byte)0x00));\r
+        result = DecodedContent.lsearchErrorIndex(errList, 10);\r
+        assertEquals(0, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(15, (byte)0x00));\r
+        result = DecodedContent.lsearchErrorIndex(errList, 10);\r
+        assertEquals(0, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(4, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(5, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(14, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(15, (byte)0x00));\r
+        result = DecodedContent.lsearchErrorIndex(errList, 10);\r
+        assertEquals(2, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(4, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(5, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(10, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(14, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(15, (byte)0x00));\r
+        result = DecodedContent.lsearchErrorIndex(errList, 10);\r
+        assertEquals(2, result);\r
+\r
+        return;\r
+     }\r
+\r
+    /**\r
+     * Test of bsearchErrorIndex method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testBsearchErrorIndex(){\r
+        System.out.println("bsearchErrorIndex");\r
+\r
+        List<DecodeErrorInfo> errList;\r
+        int result;\r
+\r
+        errList = new ArrayList<DecodeErrorInfo>();\r
+        result = DecodedContent.bsearchErrorIndex(errList, 10);\r
+        assertEquals(0, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(5, (byte)0x00));\r
+        result = DecodedContent.bsearchErrorIndex(errList, 10);\r
+        assertEquals(1, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(10, (byte)0x00));\r
+        result = DecodedContent.bsearchErrorIndex(errList, 10);\r
+        assertEquals(0, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(15, (byte)0x00));\r
+        result = DecodedContent.bsearchErrorIndex(errList, 10);\r
+        assertEquals(0, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(4, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(5, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(14, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(15, (byte)0x00));\r
+        result = DecodedContent.bsearchErrorIndex(errList, 10);\r
+        assertEquals(2, result);\r
+\r
+        errList.clear();\r
+        errList.add(new DecodeErrorInfo(4, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(5, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(10, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(14, (byte)0x00));\r
+        errList.add(new DecodeErrorInfo(15, (byte)0x00));\r
+        result = DecodedContent.bsearchErrorIndex(errList, 10);\r
+        assertEquals(2, result);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of searchErrorIndex method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testSearchErrorIndex(){\r
+        System.out.println("searchErrorIndex");\r
+\r
+        List<DecodeErrorInfo> errList;\r
+        int result;\r
+\r
+        errList = new ArrayList<DecodeErrorInfo>();\r
+\r
+        errList.clear();\r
+        for(int pos = 0; pos <= 1000; pos += 10){\r
+            errList.add(new DecodeErrorInfo(pos, (byte)0x00));\r
+        }\r
+        result = DecodedContent.searchErrorIndex(errList, 503);\r
+        assertEquals(51, result);\r
+\r
+        errList.clear();\r
+        for(int pos = 0; pos <= 50; pos += 10){\r
+            errList.add(new DecodeErrorInfo(pos, (byte)0x00));\r
+        }\r
+        result = DecodedContent.searchErrorIndex(errList, 23);\r
+        assertEquals(3, result);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of appendGappedErrorInfo method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testAppendGappedErrorInfo(){\r
+        System.out.println("appendGappedErrorInfo");\r
+\r
+        DecodedContent sourceContent;\r
+        sourceContent = new DecodedContent();\r
+        for(int pos = 0; pos <= 50; pos += 10){\r
+            sourceContent.append("123456789");\r
+            sourceContent.addDecodeError((byte)0x00);\r
+        }\r
+\r
+        List<DecodeErrorInfo> result;\r
+        result = DecodedContent.appendGappedErrorInfo(sourceContent, 15, 35, null, -100);\r
+        assertNotNull(result);\r
+        assertEquals(2, result.size());\r
+        assertEquals(119, result.get(0).getCharPosition());\r
+        assertEquals(129, result.get(1).getCharPosition());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of ensureCapacity method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testEnsureCapacity(){\r
+        System.out.println("ensureCapacity");\r
+\r
+        DecodedContent content;\r
+        \r
+        content = new DecodedContent("abc");\r
+        content.ensureCapacity(-1);\r
+        content.ensureCapacity(0);\r
+        content.ensureCapacity(1);\r
+        content.ensureCapacity(5);\r
+        content.append("def");\r
+        assertEquals("abcdef", content.toString());\r
+\r
+        content = new DecodedContent();\r
+        content.ensureCapacity(5);\r
+        content.append("abc");\r
+        assertEquals("abc", content.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of setCharAt method, of class DecodedContent.\r
+     */\r
+    @Test\r
+    public void testSetCharAt(){\r
+        System.out.println("setCharAt");\r
+\r
+        DecodedContent content;\r
+\r
+        content = new DecodedContent("abc");\r
+        content.setCharAt(1, 'B');\r
+        assertEquals("aBc", content.toString());\r
+\r
+        content = new DecodedContent("a");\r
+        content.addDecodeError((byte)0xff);\r
+        content.append('c');\r
+        assertEquals("a?c", content.toString());\r
+        content.setCharAt(1, 'B');\r
+        assertEquals("aBc", content.toString());\r
+        assertEquals(1, content.getDecodeErrorList().size());\r
+        assertEquals(1, content.getDecodeErrorList().get(0).getCharPosition());\r
+        assertEquals((byte)0xff, content.getDecodeErrorList().get(0).getRawByte1st());\r
+\r
+        content = new DecodedContent("abc");\r
+        try{\r
+            content.setCharAt(-1, 'B');\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+            // NOTHING\r
+        }\r
+        try{\r
+            content.setCharAt(10, 'B');\r
+            fail();\r
+        }catch(IndexOutOfBoundsException e){\r
+            // NOTHING\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/EntityConverterTest.java b/src/test/java/jp/sourceforge/jindolf/parser/EntityConverterTest.java
new file mode 100644 (file)
index 0000000..457d561
--- /dev/null
@@ -0,0 +1,164 @@
+/*\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: EntityConverterTest.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.util.List;\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ */\r
+public class EntityConverterTest {\r
+\r
+    public EntityConverterTest() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    /**\r
+     * Test of convert method, of class EntityConverter.\r
+     */\r
+    @Test\r
+    public void testConvert(){\r
+        System.out.println("convert");\r
+\r
+        EntityConverter converter = new EntityConverter();\r
+\r
+        DecodedContent from;\r
+        DecodedContent result;\r
+\r
+        from = new DecodedContent();\r
+        from.append("a&gt;b&lt;c&quot;d&amp;e");\r
+        result = converter.convert(from, 0, from.length());\r
+        assertEquals("a>b<c\"d&e", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("&gt;&lt;&quot;&amp;");\r
+        result = converter.convert(from, 0, from.length());\r
+        assertEquals("><\"&", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("12345");\r
+        result = converter.convert(from, 1, 3);\r
+        assertEquals("23", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("12&gt;45");\r
+        result = converter.convert(from, 1, 7);\r
+        assertEquals("2>4", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("12&gt;45");\r
+        result = converter.convert(from, 3, 7);\r
+        assertEquals("gt;4", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("&amp;gt;");\r
+        result = converter.convert(from, 0, from.length());\r
+        assertEquals("&gt;", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("a&gt;b");\r
+        result = converter.convert(from);\r
+        assertEquals("a>b", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("a&gt;b");\r
+        from.addDecodeError((byte)0x03);\r
+        from.append("c");\r
+        result = converter.convert(from);\r
+        assertEquals("a>b?c", result.toString());\r
+        assertTrue(result.hasDecodeError());\r
+        List<DecodeErrorInfo> list = result.getDecodeErrorList();\r
+        assertEquals(1, list.size());\r
+        assertEquals((byte)0x03, list.get(0).getRawByte1st());\r
+\r
+        from = new DecodedContent();\r
+        from.append("");\r
+        result = converter.convert(from, 0, 0);\r
+        assertEquals("", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("a\\b");\r
+        result = converter.convert(from, 0, from.length());\r
+        assertEquals("a¥b", result.toString());\r
+\r
+        from = new DecodedContent();\r
+        from.append("abcde");\r
+        SeqRange range = new SeqRange(1,4);\r
+        result = converter.convert(from, range);\r
+        assertEquals("bcd", result.toString());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of append method, of class EntityConverter.\r
+     */\r
+    @Test\r
+    public void testAppend(){\r
+        System.out.println("append");\r
+\r
+        EntityConverter converter = new EntityConverter();\r
+        DecodedContent target;\r
+        DecodedContent from;\r
+        DecodedContent result;\r
+\r
+        target = new DecodedContent("abc");\r
+        from = new DecodedContent("d&gt;f");\r
+        result = converter.append(target,from);\r
+        assertEquals("abcd>f", result.toString());\r
+\r
+        target = new DecodedContent("abc");\r
+        from = new DecodedContent("d&gt;fg&lt;i");\r
+        result = converter.append(target, from, 6, 12);\r
+        assertEquals("abcg<i", result.toString());\r
+\r
+        target = new DecodedContent("abc");\r
+        from = new DecodedContent("d&gt;fg&lt;i");\r
+        result = converter.append(target, from, new SeqRange(6, 12));\r
+        assertEquals("abcg<i", result.toString());\r
+\r
+        target = new DecodedContent();\r
+        target.append('a');\r
+        target.addDecodeError((byte)0xff);\r
+        target.append('c');\r
+        from = new DecodedContent();\r
+        from.append('d');\r
+        from.addDecodeError((byte)0xfe);\r
+        from.append('f');\r
+        result = converter.append(target, from);\r
+        assertEquals("a?cd?f", result.toString());\r
+        assertTrue(result.hasDecodeError());\r
+        List<DecodeErrorInfo> list = result.getDecodeErrorList();\r
+        assertEquals(2, list.size());\r
+        assertEquals((byte)0xff, list.get(0).getRawByte1st());\r
+        assertEquals((byte)0xfe, list.get(1).getRawByte1st());\r
+        assertEquals(1, list.get(0).getCharPosition());\r
+        assertEquals(4, list.get(1).getCharPosition());\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/HtmlParseExceptionTest.java b/src/test/java/jp/sourceforge/jindolf/parser/HtmlParseExceptionTest.java
new file mode 100644 (file)
index 0000000..3d82cca
--- /dev/null
@@ -0,0 +1,86 @@
+/*\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: HtmlParseExceptionTest.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ */\r
+public class HtmlParseExceptionTest {\r
+\r
+    public HtmlParseExceptionTest() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    /**\r
+     * Test of getCharPos method, of class HtmlParseException.\r
+     */\r
+    @Test\r
+    public void testGetCharPos(){\r
+        System.out.println("getCharPos");\r
+\r
+        HtmlParseException ex;\r
+\r
+        ex = new HtmlParseException();\r
+        assertTrue(0 > ex.getCharPos());\r
+\r
+        ex = new HtmlParseException("abc");\r
+        assertTrue(0 > ex.getCharPos());\r
+\r
+        ex = new HtmlParseException(99);\r
+        assertEquals(99, ex.getCharPos());\r
+\r
+        ex = new HtmlParseException("abc", 99);\r
+        assertEquals(99, ex.getCharPos());\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of getMessage method, of class HtmlParseException.\r
+     */\r
+    @Test\r
+    public void testGetMessage(){\r
+        System.out.println("getMessage");\r
+\r
+        HtmlParseException ex;\r
+\r
+        ex = new HtmlParseException();\r
+        assertEquals("charPos=-1", ex.getMessage());\r
+\r
+        ex = new HtmlParseException("abc");\r
+        assertEquals("abc charPos=-1", ex.getMessage());\r
+\r
+        ex = new HtmlParseException(99);\r
+        assertEquals("charPos=99", ex.getMessage());\r
+\r
+        ex = new HtmlParseException("abc", 99);\r
+        assertEquals("abc charPos=99", ex.getMessage());\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/jp/sourceforge/jindolf/parser/ShiftJisTest.java b/src/test/java/jp/sourceforge/jindolf/parser/ShiftJisTest.java
new file mode 100644 (file)
index 0000000..57c2f87
--- /dev/null
@@ -0,0 +1,164 @@
+/*\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: ShiftJisTest.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf.parser;\r
+\r
+import java.io.UnsupportedEncodingException;\r
+import org.junit.After;\r
+import org.junit.AfterClass;\r
+import org.junit.Before;\r
+import org.junit.BeforeClass;\r
+import org.junit.Test;\r
+import static org.junit.Assert.*;\r
+\r
+/**\r
+ */\r
+public class ShiftJisTest {\r
+\r
+    public ShiftJisTest() {\r
+    }\r
+\r
+    @BeforeClass\r
+    public static void setUpClass() throws Exception{\r
+    }\r
+\r
+    @AfterClass\r
+    public static void tearDownClass() throws Exception{\r
+    }\r
+\r
+    @Before\r
+    public void setUp() {\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+    }\r
+\r
+    /**\r
+     * Test of isShiftJIS1stByte method, of class ShiftJis.\r
+     */\r
+    @Test\r
+    public void testIsShiftJIS1stByte(){\r
+        System.out.println("isShiftJIS1stByte");\r
+\r
+        for(int ival=0x00; ival <= 0x80; ival++){\r
+            byte bval = (byte)ival;\r
+            assertFalse(ShiftJis.isShiftJIS1stByte(bval));\r
+        }\r
+\r
+        for(int ival=0x81; ival <= 0x9f; ival++){\r
+            byte bval = (byte)ival;\r
+            assertTrue(ShiftJis.isShiftJIS1stByte(bval));\r
+        }\r
+\r
+        for(int ival=0xa0; ival <= 0xdf; ival++){\r
+            byte bval = (byte)ival;\r
+            assertFalse(ShiftJis.isShiftJIS1stByte(bval));\r
+        }\r
+\r
+        for(int ival=0xe0; ival <= 0xfc; ival++){\r
+            byte bval = (byte)ival;\r
+            assertTrue(ShiftJis.isShiftJIS1stByte(bval));\r
+        }\r
+\r
+        for(int ival=0xfd; ival <= 0xff; ival++){\r
+            byte bval = (byte)ival;\r
+            assertFalse(ShiftJis.isShiftJIS1stByte(bval));\r
+        }\r
+\r
+        byte[] array;\r
+        try{\r
+            // 全角スペース\r
+            array = "\u3000".getBytes("Shift_JIS");\r
+            assertTrue(ShiftJis.isShiftJIS1stByte(array[0]));\r
+            // 「熙」\r
+            array = "\u7199".getBytes("Shift_JIS");\r
+            assertTrue(ShiftJis.isShiftJIS1stByte(array[0]));\r
+            // 「"」\r
+            array = "\uff02".getBytes("Windows-31J");\r
+            assertTrue(ShiftJis.isShiftJIS1stByte(array[0]));\r
+        }catch(UnsupportedEncodingException e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of isShiftJIS2ndByte method, of class ShiftJis.\r
+     */\r
+    @Test\r
+    public void testIsShiftJIS2ndByte(){\r
+        System.out.println("isShiftJIS2ndByte");\r
+\r
+        for(int ival=0x00; ival <= 0x3f; ival++){\r
+            byte bval = (byte)ival;\r
+            assertFalse(ShiftJis.isShiftJIS2ndByte(bval));\r
+        }\r
+\r
+        for(int ival=0x40; ival <= 0x7e; ival++){\r
+            byte bval = (byte)ival;\r
+            assertTrue(ShiftJis.isShiftJIS2ndByte(bval));\r
+        }\r
+\r
+        for(int ival=0x7f; ival <= 0x7f; ival++){\r
+            byte bval = (byte)ival;\r
+            assertFalse(ShiftJis.isShiftJIS2ndByte(bval));\r
+        }\r
+\r
+        for(int ival=0x80; ival <= 0xfc; ival++){\r
+            byte bval = (byte)ival;\r
+            assertTrue(ShiftJis.isShiftJIS2ndByte(bval));\r
+        }\r
+\r
+        for(int ival=0xfd; ival <= 0xff; ival++){\r
+            byte bval = (byte)ival;\r
+            assertFalse(ShiftJis.isShiftJIS2ndByte(bval));\r
+        }\r
+\r
+        byte[] array;\r
+        try{\r
+            // 全角スペース\r
+            array = "\u3000".getBytes("Shift_JIS");\r
+            assertTrue(ShiftJis.isShiftJIS2ndByte(array[1]));\r
+            // 「熙」\r
+            array = "\u7199".getBytes("Shift_JIS");\r
+            assertTrue(ShiftJis.isShiftJIS2ndByte(array[1]));\r
+            // 「"」\r
+            array = "\uff02".getBytes("Windows-31J");\r
+            assertTrue(ShiftJis.isShiftJIS2ndByte(array[1]));\r
+        }catch(UnsupportedEncodingException e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Test of isShiftJIS method, of class ShiftJis.\r
+     */\r
+    @Test\r
+    public void testIsShiftJIS(){\r
+        System.out.println("isShiftJIS");\r
+\r
+        byte[] array;\r
+        try{\r
+            // 全角スペース\r
+            array = "\u3000".getBytes("Shift_JIS");\r
+            assertTrue(ShiftJis.isShiftJIS(array[0], array[1]));\r
+            // 「熙」\r
+            array = "\u7199".getBytes("Shift_JIS");\r
+            assertTrue(ShiftJis.isShiftJIS(array[0], array[1]));\r
+            // 「"」\r
+            array = "\uff02".getBytes("Windows-31J");\r
+            assertTrue(ShiftJis.isShiftJIS(array[0], array[1]));\r
+        }catch(UnsupportedEncodingException e){\r
+            fail();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/sample/SampleHandler.java b/src/test/java/sample/SampleHandler.java
new file mode 100644 (file)
index 0000000..e2b974b
--- /dev/null
@@ -0,0 +1,35 @@
+/*\r
+ * sample handler\r
+ * \r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: SampleHandler.java 894 2009-11-04 07:26:59Z olyutorskii $\r
+ */\r
+\r
+package sample;\r
+\r
+import jp.sourceforge.jindolf.parser.DecodedContent;\r
+import jp.sourceforge.jindolf.parser.EntityConverter;\r
+import jp.sourceforge.jindolf.parser.HtmlAdapter;\r
+import jp.sourceforge.jindolf.parser.HtmlParseException;\r
+import jp.sourceforge.jindolf.parser.SeqRange;\r
+\r
+/**\r
+ * サンプルのハンドラ\r
+ */\r
+public class SampleHandler extends HtmlAdapter{\r
+\r
+    private final EntityConverter converter = new EntityConverter();\r
+\r
+    public SampleHandler(){\r
+        super();\r
+        return;\r
+    }\r
+\r
+    @Override\r
+    public void talkText(DecodedContent content, SeqRange textRange)\r
+            throws HtmlParseException{\r
+        System.out.println(this.converter.convert(content, textRange));\r
+        return;\r
+    }\r
+\r
+}\r
diff --git a/src/test/java/sample/SampleParser.java b/src/test/java/sample/SampleParser.java
new file mode 100644 (file)
index 0000000..e7e2b3b
--- /dev/null
@@ -0,0 +1,144 @@
+/*\r
+ * sample parser\r
+ *\r
+ * Copyright(c) 2009 olyutorskii\r
+ * $Id: SampleParser.java 1000 2010-03-15 12:08:42Z olyutorskii $\r
+ */\r
+\r
+package sample;\r
+\r
+import java.io.FileInputStream;\r
+import jp.sourceforge.jindolf.parser.*;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.nio.charset.Charset;\r
+import java.nio.charset.CharsetDecoder;\r
+import java.util.Collections;\r
+import java.util.Enumeration;\r
+import java.util.SortedMap;\r
+import java.util.TreeMap;\r
+import java.util.zip.ZipEntry;\r
+import java.util.zip.ZipFile;\r
+\r
+/**\r
+ * サンプルのパーサ\r
+ */\r
+public class SampleParser{\r
+\r
+    private static final CharsetDecoder ud;\r
+\r
+    static{\r
+        ud = Charset.forName("UTF-8").newDecoder();\r
+    }\r
+\r
+    private SampleParser(){\r
+        super();\r
+        return;\r
+    }\r
+\r
+    public static SortedMap<String, ZipEntry> createEntryMap(ZipFile file){\r
+        TreeMap<String, ZipEntry> result = new TreeMap<String, ZipEntry>();\r
+\r
+        Enumeration<? extends ZipEntry> list = file.entries();\r
+        while(list.hasMoreElements()){\r
+            ZipEntry entry = list.nextElement();\r
+            String name = entry.getName();\r
+            result.put(name, entry);\r
+        }\r
+\r
+        return Collections.unmodifiableSortedMap(result);\r
+    }\r
+\r
+    public static DecodedContent contentFromStream(InputStream istream)\r
+            throws IOException, DecodeException{\r
+        StreamDecoder decoder = new StreamDecoder(ud);\r
+        ContentBuilderUCS2 builder = new ContentBuilderUCS2();\r
+\r
+        decoder.setDecodeHandler(builder);\r
+\r
+        decoder.decode(istream);\r
+\r
+        DecodedContent content = builder.getContent();\r
+\r
+        return content;\r
+    }\r
+\r
+    public static void parseContent(DecodedContent content)\r
+            throws HtmlParseException{\r
+        HtmlParser parser = new HtmlParser();\r
+        HtmlHandler handler = new SampleHandler();\r
+\r
+        parser.setBasicHandler   (handler);\r
+        parser.setTalkHandler    (handler);\r
+        parser.setSysEventHandler(handler);\r
+\r
+        parser.parseAutomatic(content);\r
+\r
+        return;\r
+    }\r
+\r
+    public static void parseStream(InputStream istream)\r
+            throws IOException,\r
+                   DecodeException,\r
+                   HtmlParseException{\r
+        DecodedContent content = contentFromStream(istream);\r
+\r
+        parseContent(content);\r
+\r
+        return;\r
+    }\r
+\r
+    public static void main(String[] args)\r
+            throws IOException,\r
+                   DecodeException,\r
+                   HtmlParseException {\r
+        if(args.length == 0){\r
+            System.out.println(\r
+                     "標準入力から人狼BBSのXHTML文書の読み取りを"\r
+                    +"開始します...");\r
+\r
+            parseStream(System.in);\r
+\r
+            System.exit(0);\r
+\r
+            return;\r
+        }else if(args[0].endsWith(".zip")){\r
+            System.out.println(\r
+                     "ZIPアーカイブ内の*.htmlファイルから"\r
+                    +"人狼BBSのXHTML文書の読み取りを開始します...");\r
+\r
+            ZipFile zipfile = new ZipFile(args[0]);\r
+\r
+            SortedMap<String, ZipEntry> map = createEntryMap(zipfile);\r
+\r
+            for(ZipEntry entry : map.values()){\r
+                String name = entry.getName();\r
+                if( ! name.endsWith(".html") ) continue;\r
+\r
+                System.out.println(name + "のパースを開始...");\r
+\r
+                InputStream istream = zipfile.getInputStream(entry);\r
+                parseStream(istream);\r
+\r
+                istream.close();\r
+            }\r
+\r
+            zipfile.close();\r
+\r
+            System.exit(0);\r
+\r
+            return;\r
+        }else{\r
+            System.out.println(args[0] + "のパースを開始...");\r
+\r
+            InputStream istream = new FileInputStream(args[0]);\r
+            parseStream(istream);\r
+            istream.close();\r
+\r
+            System.exit(0);\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+}\r