OSDN Git Service

Merge branch 'release/v4.101.2' v4.101.2
authorOlyutorskii <olyutorskii@users.osdn.me>
Mon, 20 Apr 2020 03:35:06 +0000 (12:35 +0900)
committerOlyutorskii <olyutorskii@users.osdn.me>
Mon, 20 Apr 2020 03:35:06 +0000 (12:35 +0900)
122 files changed:
CHANGELOG.txt
README.txt
config/checkstyle/checkstyle-suppressions.xml
config/checkstyle/checkstyle.xml
config/pmd/pmdrules.xml
pom.xml
src/assembly/src.xml
src/main/java/jp/sfjp/jindolf/Controller.java
src/main/java/jp/sfjp/jindolf/Jindolf.java
src/main/java/jp/sfjp/jindolf/JindolfJre18.java [moved from src/main/java/jp/sfjp/jindolf/JindolfJre17.java with 97% similarity]
src/main/java/jp/sfjp/jindolf/JindolfMain.java
src/main/java/jp/sfjp/jindolf/JreChecker.java
src/main/java/jp/sfjp/jindolf/ResourceManager.java
src/main/java/jp/sfjp/jindolf/config/AppSetting.java
src/main/java/jp/sfjp/jindolf/config/ConfigFile.java
src/main/java/jp/sfjp/jindolf/config/ConfigStore.java
src/main/java/jp/sfjp/jindolf/data/Anchor.java
src/main/java/jp/sfjp/jindolf/data/Avatar.java
src/main/java/jp/sfjp/jindolf/data/CoreData.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/InterPlay.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/Land.java
src/main/java/jp/sfjp/jindolf/data/LandsTreeModel.java [moved from src/main/java/jp/sfjp/jindolf/data/LandsModel.java with 93% similarity]
src/main/java/jp/sfjp/jindolf/data/Nominated.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/Period.java
src/main/java/jp/sfjp/jindolf/data/SysEvent.java
src/main/java/jp/sfjp/jindolf/data/Talk.java
src/main/java/jp/sfjp/jindolf/data/Village.java
src/main/java/jp/sfjp/jindolf/data/html/PeriodHandler.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/html/PeriodLoader.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/html/VillageInfoHandler.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/html/VillageInfoLoader.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/html/VillageListHandler.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/html/VillageListLoader.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/html/VillageRecord.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/html/package-info.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/xml/BotherHandler.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/xml/ElemTag.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/xml/NoopEntityResolver.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/xml/VillageHandler.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/xml/VillageLoader.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/xml/XmlDecoder.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/data/xml/package-info.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/dxchg/WebIPCDialog.java
src/main/java/jp/sfjp/jindolf/dxchg/WolfBBS.java
src/main/java/jp/sfjp/jindolf/editor/BalloonBorder.java [deleted file]
src/main/java/jp/sfjp/jindolf/editor/EditArray.java [deleted file]
src/main/java/jp/sfjp/jindolf/editor/TalkEditor.java [deleted file]
src/main/java/jp/sfjp/jindolf/editor/TalkPreview.java [deleted file]
src/main/java/jp/sfjp/jindolf/editor/TextEditor.java [deleted file]
src/main/java/jp/sfjp/jindolf/editor/package-info.java [deleted file]
src/main/java/jp/sfjp/jindolf/glyph/AnchorDraw.java
src/main/java/jp/sfjp/jindolf/glyph/Discussion.java
src/main/java/jp/sfjp/jindolf/glyph/TalkDraw.java
src/main/java/jp/sfjp/jindolf/net/AuthManager.java [deleted file]
src/main/java/jp/sfjp/jindolf/net/HttpUtils.java
src/main/java/jp/sfjp/jindolf/net/ProxyChooser.java
src/main/java/jp/sfjp/jindolf/net/ServerAccess.java
src/main/java/jp/sfjp/jindolf/summary/DaySummary.java
src/main/java/jp/sfjp/jindolf/summary/GameSummary.java
src/main/java/jp/sfjp/jindolf/summary/VillageDigest.java
src/main/java/jp/sfjp/jindolf/util/StringUtils.java
src/main/java/jp/sfjp/jindolf/view/AccountPanel.java [deleted file]
src/main/java/jp/sfjp/jindolf/view/ActionManager.java
src/main/java/jp/sfjp/jindolf/view/AvatarPics.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/view/HelpFrame.java
src/main/java/jp/sfjp/jindolf/view/LandsTree.java
src/main/java/jp/sfjp/jindolf/view/LocalAvatarImg.java [new file with mode: 0644]
src/main/java/jp/sfjp/jindolf/view/PeriodView.java
src/main/java/jp/sfjp/jindolf/view/TabBrowser.java
src/main/java/jp/sfjp/jindolf/view/TopFrame.java
src/main/java/jp/sfjp/jindolf/view/TopView.java
src/main/java/jp/sfjp/jindolf/view/WindowManager.java
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body01.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body02.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body03.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body04.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body05.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body06.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body07.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body08.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body09.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body10.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body11.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body12.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body13.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body14.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body15.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body16.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body17.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body18.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body19.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body20.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body99.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face01.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face02.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face03.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face04.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face05.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face06.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face07.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face08.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face09.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face10.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face11.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face12.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face13.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face14.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face15.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face16.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face17.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face18.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face19.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face20.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face99.png [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/image/avatarCache.json [new file with mode: 0644]
src/main/resources/jp/sfjp/jindolf/resources/wolfbbs/faceIconSet.properties
src/test/java/jp/sfjp/jindolf/JreCheckerTest.java
src/test/java/jp/sfjp/jindolf/data/AnchorTest.java [new file with mode: 0644]
src/test/java/jp/sfjp/jindolf/data/AvatarTest.java
src/test/java/jp/sfjp/jindolf/data/TalkTest.java [new file with mode: 0644]
src/test/java/jp/sfjp/jindolf/net/HttpUtilsTest.java
src/test/java/jp/sfjp/jindolf/util/StringUtilsTest.java

index a5da832..143fcd3 100644 (file)
@@ -4,6 +4,12 @@
 Jindolf 変更履歴
 
 
+4.101.2 (2020-04-20)
+    ・G国亡国に伴い JinParser 2.102.2 に対応。
+    ・ログイン管理画面の廃止。
+    ・発言エディタの廃止。
+    ・JinArchiverによるXMLアーカイブファイルのビューア機能。
+
 3.303.106 (2019-05-07)
     ・必須環境をJavaSE8に引き上げ。
     ・Mercurial(3.303.5-SNAPSHOT)からGit(3.303.105-SNAPSHOT)へSCMを移行。
index a0d237c..206473e 100644 (file)
@@ -26,7 +26,7 @@
  - JindolfはGUIを通じて操作するプログラムのため、その実行においては
    ビットマップディスプレイとポインティングデバイスとキーボードへの接続を
    必要とします。
- - Jindolfã\81¯äººç\8b¼BBSã\82µã\83¼ã\83\90ã\81¨HTTPé\80\9aä¿¡ã\82\92è¡\8cã\81\86ã\81\9fã\82\81、TCP/IPネットワーク環境を
+ - Jindolfã\81\8c人ç\8b¼BBSã\82µã\83¼ã\83\90ã\81¨HTTPé\80\9aä¿¡ã\82\92è¡\8cã\81\86å ´å\90\88、TCP/IPネットワーク環境を
    必要とします。
  - 人狼BBSは符号化された日本語で遊ばれるため、Jindolfの実行には日本語環境が
    必要です。
index 6ada9ac..bd37194 100644 (file)
@@ -6,7 +6,7 @@
 
 <!--
     Checkstyle suppressions
-    for Checkstyle 8.20 or later
+    for Checkstyle 8.29 or later
 
     [ https://checkstyle.org/ ]
 
     <suppress files="" checks="DesignForExtension" />
 
     <!-- Coding -->
+    <suppress files="" checks="AvoidNoArgumentSuperConstructorCall" />
     <suppress files="" checks="ExplicitInitialization" />
     <suppress files="" checks="FinalLocalVariable" />
     <suppress files="" checks="MagicNumber" />
-    <suppress files="" checks="OneStatementPerLine" />
+    <suppress files="" checks="NoArrayTrailingComma" />
+    <suppress files="" checks="NoEnumTrailingComma" />
 
     <!-- Imports -->
     <suppress files="" checks="ImportControl" />
@@ -40,8 +42,9 @@
     <suppress files="" checks="FinalParameters" />
     <suppress files="" checks="TrailingComment" />
 
-    <!-- Modifiers -->
+    <!-- Modifier -->
     <suppress files="" checks="InterfaceMemberImpliedModifier" />
+    <suppress files="" checks="RedundantModifier" />
 
     <!-- Whitespace -->
     <suppress files="" checks="SingleSpaceSeparator" />
index 59fd165..de1218d 100644 (file)
@@ -6,7 +6,7 @@
 
 <!--
     Checkstyle modules
-    for Checkstyle 8.20 or later
+    for Checkstyle 8.31 or later
 
     [ https://checkstyle.org/ ]
 
     <property name="localeCountry" value="JP" />
     <property name="localeLanguage" value="en" />
     <!--property name="localeLanguage" value="ja" /-->
-    <property name="fileExtensions" value="java, xml, properties" />
+    <property name="fileExtensions" value="java, properties, xml, xsd, md, txt" />
     <property name="severity" value="error" />
 
 
     <!-- Filters -->
+
     <module name="SeverityMatchFilter" />
     <!--module name="SuppressionFilter" /-->
+    <!--module name="SuppressionSingleFilter" /-->
     <module name="SuppressWarningsFilter" />
     <module name="SuppressWithPlainTextCommentFilter" />
 
 
     <!-- Headers -->
+
     <module name="Header">
         <property name="header" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;" />
         <property name="fileExtensions" value="xml" />
 
 
     <!-- Javadoc Comments -->
+
     <module name="JavadocPackage" />
 
 
     <!-- Miscellaneous -->
+
     <module name="NewlineAtEndOfFile">
-        <property name="fileExtensions" value="java" />
+        <property name="fileExtensions" value="java, properties, xml, xsd, md, txt" />
     </module>
+    <module name="OrderedProperties" />
     <module name="Translation" />
     <module name="UniqueProperties" />
 
 
     <!-- Regexp -->
+
     <module name="RegexpMultiline">
         <property name="format" value="[\u000b\f\u001a]" />
     </module>
 
 
     <!-- Size Violations -->
+
     <module name="FileLength" />
+    <module name="LineLength">
+        <property name="fileExtensions" value="java" />
+        <property name="max" value="78" />
+    </module>
 
 
     <!-- Whitespace -->
+
     <module name="FileTabCharacter" />
 
 
     <!-- Coding -->
 
         <module name="ArrayTrailingComma" />
+        <module name="AvoidDoubleBraceInitialization" />
         <module name="AvoidInlineConditionals" />
+        <module name="AvoidNoArgumentSuperConstructorCall" />
         <module name="CovariantEquals" />
         <module name="DeclarationOrder" />
         <module name="DefaultComesLast" />
         <module name="NestedForDepth" />
         <module name="NestedIfDepth" />
         <module name="NestedTryDepth" />
+        <module name="NoArrayTrailingComma" />
         <module name="NoClone" />
+        <module name="NoEnumTrailingComma" />
         <module name="NoFinalizer" />
         <module name="OneStatementPerLine" />
         <module name="OverloadMethodsDeclarationOrder" />
         <module name="SuperClone" />
         <module name="SuperFinalize" />
         <module name="UnnecessaryParentheses" />
+        <module name="UnnecessarySemicolonAfterOuterTypeDeclaration" />
+        <module name="UnnecessarySemicolonAfterTypeMemberDeclaration" />
+        <module name="UnnecessarySemicolonInEnumeration" />
+        <module name="UnnecessarySemicolonInTryWithResources" />
         <module name="VariableDeclarationUsageDistance" />
 
 
     <!-- Javadoc Comments -->
 
         <module name="AtclauseOrder" />
+        <module name="InvalidJavadocPosition" />
+        <module name="JavadocBlockTagLocation" />
+        <module name="JavadocContentLocationCheck" />
         <module name="JavadocMethod" />
         <module name="JavadocParagraph" />
         <module name="JavadocStyle">
         <module name="JavadocVariable">
             <property name="scope" value="protected" />
         </module>
+        <module name="MissingJavadocMethod" />
+        <module name="MissingJavadocPackage" />
+        <module name="MissingJavadocType" />
         <module name="NonEmptyAtclauseDescription" />
         <module name="SingleLineJavadoc" />
         <module name="SummaryJavadocCheck" />
 
         <module name="AnonInnerLength" />
         <module name="ExecutableStatementCount" />
-        <module name="LineLength">
-            <property name="max" value="78" />
-        </module>
         <module name="MethodCount" />
         <module name="MethodLength" />
         <module name="OuterTypeNumber" />
index f9c0994..ecf9fa0 100644 (file)
@@ -2,7 +2,7 @@
 
 <!--
     Custom rule set
-    for PMD [ https://pmd.github.io/ ] 6.13.0 or later
+    for PMD [ https://pmd.github.io/ ] 6.21.0 or later
 
     Copyright(c) 2019 olyutorskii
 -->
@@ -11,7 +11,7 @@
   xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0
-  http://pmd.sourceforge.net/ruleset_2_0_0.xsd"
+  https://pmd.sourceforge.io/ruleset_2_0_0.xsd"
   name="Custom ruleset"
 >
 
@@ -31,6 +31,7 @@
         <exclude name="OnlyOneReturn" />
         <exclude name="ShortVariable" />
         <exclude name="UnnecessaryLocalBeforeReturn" />
+        <exclude name="UnnecessaryModifier" />
         <exclude name="UnnecessaryReturn" />
     </rule>
     <rule ref="category/java/codestyle.xml/ControlStatementBraces" >
diff --git a/pom.xml b/pom.xml
index 6eb41cf..0b6f6a7 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
     <groupId>jp.sourceforge.jindolf</groupId>
     <artifactId>jindolf</artifactId>
 
-    <version>3.303.106</version>
+    <version>4.101.2</version>
 
     <packaging>jar</packaging>
     <name>Jindolf</name>
@@ -79,6 +79,7 @@
     <properties>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
+        <!--maven.compiler.release>8</maven.compiler.release-->
 
         <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>
 
-        <locale>en</locale>
-        <site.locales>${locale}</site.locales>
-        <javadoc.locale>${locale}</javadoc.locale>
-        <spotbugs.jvmArgs>-Duser.language=${locale}</spotbugs.jvmArgs>
+        <!-- DO NOT USE ${locale} with site-plugin -->
+        <site.locales>en</site.locales>
+        <javadoc.locale>en</javadoc.locale>
+        <spotbugs.jvmArgs>-Duser.language=en</spotbugs.jvmArgs>
 
-        <!-- Walk around: JDK 11 javadoc + Maven -->
-        <detectJavaApiLink>false</detectJavaApiLink>
-
-        <surefire-plugin.version>3.0.0-M3</surefire-plugin.version>
-        <jacoco-plugin.version>0.8.3</jacoco-plugin.version>
-
-        <checkstyle-plugin.version>3.0.0</checkstyle-plugin.version>
-        <checkstyleruntime.version>8.20</checkstyleruntime.version>
-        <checkstyle.config.location>${project.basedir}/config/checkstyle/checkstyle.xml</checkstyle.config.location>
-        <checkstyle.suppressions.location>${project.basedir}/config/checkstyle/checkstyle-suppressions.xml</checkstyle.suppressions.location>
+        <checkstyle.config.location>config/checkstyle/checkstyle.xml</checkstyle.config.location>
+        <checkstyle.suppressions.location>config/checkstyle/checkstyle-suppressions.xml</checkstyle.suppressions.location>
         <checkstyle.enable.rss>false</checkstyle.enable.rss>
 
-        <pmd-plugin.version>3.12.0</pmd-plugin.version>
-        <pmd.analysisCache>true</pmd.analysisCache>
-
-        <spotbugs-plugin.version>3.1.11</spotbugs-plugin.version>
         <spotbugs.effort>Max</spotbugs.effort>
         <spotbugs.threshold>Low</spotbugs.threshold>
         <!-- for Jenkins -->
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
-            <version>4.12</version>
+            <version>4.13</version>
             <scope>test</scope>
         </dependency>
 
         <dependency>
             <groupId>jp.osdn.jindolf</groupId>
             <artifactId>jinparser</artifactId>
-            <version>2.101.106</version>
+            <version>2.102.2</version>
             <scope>compile</scope>
         </dependency>
 
 
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-clean-plugin</artifactId>
+                    <version>3.1.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-enforcer-plugin</artifactId>
+                    <version>3.0.0-M3</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-resources-plugin</artifactId>
+                    <version>3.1.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>3.8.1</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>3.0.0-M4</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-report-plugin</artifactId>
+                    <version>3.0.0-M4</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.jacoco</groupId>
+                    <artifactId>jacoco-maven-plugin</artifactId>
+                    <version>0.8.5</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-jar-plugin</artifactId>
+                    <version>3.2.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-shade-plugin</artifactId>
+                    <version>3.2.1</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-source-plugin</artifactId>
+                    <version>3.2.1</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-install-plugin</artifactId>
+                    <version>3.0.0-M1</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-deploy-plugin</artifactId>
+                    <version>3.0.0-M1</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-site-plugin</artifactId>
+                    <version>3.9.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-assembly-plugin</artifactId>
+                    <version>3.2.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-project-info-reports-plugin</artifactId>
+                    <version>3.0.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-javadoc-plugin</artifactId>
+                    <version>3.2.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-jxr-plugin</artifactId>
+                    <version>3.0.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-checkstyle-plugin</artifactId>
-                    <version>${checkstyle-plugin.version}</version>
+                    <version>3.1.1</version>
                     <dependencies>
                         <dependency>
                             <groupId>com.puppycrawl.tools</groupId>
                             <artifactId>checkstyle</artifactId>
-                            <version>${checkstyleruntime.version}</version>
+                            <version>8.31</version>
+                        </dependency>
+                    </dependencies>
+                </plugin>
+
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-pmd-plugin</artifactId>
+                    <version>3.13.0</version>
+                </plugin>
+
+                <plugin>
+                    <groupId>com.github.spotbugs</groupId>
+                    <artifactId>spotbugs-maven-plugin</artifactId>
+                    <version>4.0.0</version>
+                    <dependencies>
+                        <dependency>
+                            <groupId>com.github.spotbugs</groupId>
+                            <artifactId>spotbugs</artifactId>
+                            <version>4.0.2</version>
                         </dependency>
                     </dependencies>
                 </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-clean-plugin</artifactId>
-                <version>3.1.0</version>
                 <configuration>
                     <filesets>
                         <fileset>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-enforcer-plugin</artifactId>
-                <version>3.0.0-M2</version>
                 <executions>
                     <execution>
                         <id>enforce-versions</id>
 
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-resources-plugin</artifactId>
-                <version>3.1.0</version>
-            </plugin>
-
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.8.0</version>
                 <configuration>
                     <source>1.8</source>  <!-- for NetBeans IDE -->
                     <target>1.8</target>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>${surefire-plugin.version}</version>
                 <configuration>
                     <enableAssertions>true</enableAssertions>
                 </configuration>
             <plugin>
                 <groupId>org.jacoco</groupId>
                 <artifactId>jacoco-maven-plugin</artifactId>
-                <version>${jacoco-plugin.version}</version>
                 <executions>
                     <execution>
                         <id>default-prepare-agent</id>
                             <goal>prepare-agent</goal>
                         </goals>
                     </execution>
-                    <execution>
-                        <id>default-report</id>
-                        <phase>prepare-package</phase>
-                        <goals>
-                            <goal>report</goal>
-                        </goals>
-                    </execution>
-                    <execution>
-                        <id>default-check</id>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                        <configuration>
-                            <rules>
-                                <rule implementation="org.jacoco.maven.RuleConfiguration">
-                                    <element>BUNDLE</element>
-                                    <limits>
-                                        <limit implementation="org.jacoco.report.check.Limit">
-                                            <counter>COMPLEXITY</counter>
-                                            <value>COVEREDRATIO</value>
-                                            <minimum>0.0</minimum>
-                                        </limit>
-                                    </limits>
-                                </rule>
-                            </rules>
-                        </configuration>
-                    </execution>
                 </executions>
             </plugin>
 
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.1.1</version>
                 <configuration>
                     <archive>
                         <manifest>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-shade-plugin</artifactId>
-                <version>3.2.1</version>
                 <executions>
                     <execution>
                         <phase>package</phase>
                         <goals>
-                          <goal>shade</goal>
+                            <goal>shade</goal>
                         </goals>
                         <configuration>
                             <createDependencyReducedPom>false</createDependencyReducedPom>
                 </executions>
             </plugin>
 
+            <!-- site lifecycle -->
+
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>3.0.1</version>
+                <artifactId>maven-site-plugin</artifactId>
                 <configuration>
-                    <includePom>true</includePom>
-                    <archive>
-                        <manifestEntries>
-                            <Built-By>${project.organization.name}</Built-By>
-                        </manifestEntries>
-                    </archive>
+                    <locales>${site.locales}</locales>
                 </configuration>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <phase>verify</phase>
-                        <goals>
-                          <goal>jar-no-fork</goal>
-                        </goals>
-                    </execution>
-                </executions>
             </plugin>
 
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-install-plugin</artifactId>
-                <version>3.0.0-M1</version>
-            </plugin>
 
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-deploy-plugin</artifactId>
-                <version>3.0.0-M1</version>
-            </plugin>
-
-
-            <!-- site lifecycle -->
+            <!-- goals without lifecycle -->
 
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-site-plugin</artifactId>
-                <version>3.7.1</version>
+                <artifactId>maven-javadoc-plugin</artifactId>
                 <configuration>
-                    <locales>${site.locales}</locales>
+                    <locale>${javadoc.locale}</locale>
+                    <!-- for JDK11 javadoc -->
+                    <additionalJOption>-J-Duser.language=${javadoc.locale}</additionalJOption>
+                    <source>${maven.compiler.source}</source>
+                    <notimestamp>true</notimestamp>
+                    <header>${project.name} ${project.version} API</header>
+                    <nohelp>true</nohelp>
+                    <author>false</author>
+                    <quiet>true</quiet>
+                    <doclint>all</doclint>
+                    <show>protected</show>
                 </configuration>
             </plugin>
 
-
-            <!-- goals without lifecycle -->
-
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.1.1</version>
                 <configuration>
                     <descriptors>
                         <descriptor>src/assembly/src.xml</descriptor>
 
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>${checkstyle-plugin.version}</version>
-            </plugin>
-
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-pmd-plugin</artifactId>
-                <version>${pmd-plugin.version}</version>
                 <configuration>
                     <rulesets>
-                        <ruleset>${project.basedir}/config/pmd/pmdrules.xml</ruleset>
+                        <ruleset>config/pmd/pmdrules.xml</ruleset>
                     </rulesets>
                 </configuration>
             </plugin>
 
-            <plugin>
-                <groupId>com.github.spotbugs</groupId>
-                <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>${spotbugs-plugin.version}</version>
-            </plugin>
-
         </plugins>
 
         <resources>
                     <include>**/*.gif</include>
                     <include>**/*.jpeg</include>
                     <include>**/*.jpg</include>
+
+                    <include>**/*.json</include>
                 </includes>
                 <excludes>
                     <exclude>**/version.properties</exclude>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-project-info-reports-plugin</artifactId>
-                <version>3.0.0</version>
                 <configuration>
                     <linkOnly>true</linkOnly>
                     <offline>true</offline>
                             <report>dependencies</report>
                             <report>dependency-convergence</report>
                             <report>plugins</report>
-                            <report>plugin-management</report>
                             <report>team</report>
                             <report>issue-management</report>
                             <report>scm</report>
                             <report>ci-management</report>
                             <report>mailing-lists</report>
                             <report>modules</report>
+                            <report>plugin-management</report>
 -->
                         </reports>
                     </reportSet>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.1.0</version>
                 <configuration>
-                    <author>false</author>
-                    <notimestamp>true</notimestamp>
-                    <quiet>true</quiet>
-                    <show>protected</show>
-                    <header>${project.name} ${project.version} API</header>
-                    <version>true</version>
                     <locale>${javadoc.locale}</locale>
                     <!-- for JDK11 javadoc -->
                     <additionalJOption>-J-Duser.language=${javadoc.locale}</additionalJOption>
+                    <source>${maven.compiler.source}</source>
+                    <notimestamp>true</notimestamp>
+                    <header>${project.name} ${project.version} API</header>
+                    <nohelp>true</nohelp>
+                    <author>false</author>
+                    <quiet>true</quiet>
+                    <doclint>all</doclint>
+                    <show>protected</show>
                 </configuration>
                 <reportSets>
                     <reportSet>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jxr-plugin</artifactId>
-                <version>3.0.0</version>
             </plugin>
 
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-report-plugin</artifactId>
-                <version>${surefire-plugin.version}</version>
             </plugin>
 
             <plugin>
                 <groupId>org.jacoco</groupId>
                 <artifactId>jacoco-maven-plugin</artifactId>
-                <version>${jacoco-plugin.version}</version>
                 <reportSets>
                     <reportSet>
                         <reports>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>${checkstyle-plugin.version}</version>
                 <reportSets>
                     <reportSet>
                         <reports>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-pmd-plugin</artifactId>
-                <version>${pmd-plugin.version}</version>
                 <configuration>
                     <rulesets>
-                        <ruleset>${project.basedir}/config/pmd/pmdrules.xml</ruleset>
+                        <ruleset>config/pmd/pmdrules.xml</ruleset>
                     </rulesets>
                 </configuration>
                 <reportSets>
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>${spotbugs-plugin.version}</version>
             </plugin>
 
         </plugins>
 
     </reporting>
 
-    <profiles/>
+    <profiles>
+
+        <profile>
+            <id>release-profile</id>
+
+            <activation>
+                <property>
+                    <name>performRelease</name>
+                    <value>true</value>
+                </property>
+            </activation>
+
+            <build>
+                <plugins>
+
+                    <plugin>
+                        <inherited>true</inherited>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-source-plugin</artifactId>
+                        <configuration>
+                            <includePom>true</includePom>
+                            <archive>
+                                <manifestEntries>
+                                    <Built-By>${project.organization.name}</Built-By>
+                                </manifestEntries>
+                            </archive>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>attach-sources</id>
+                                <goals>
+                                    <goal>jar-no-fork</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                    <plugin>
+                        <inherited>true</inherited>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-javadoc-plugin</artifactId>
+                        <configuration>
+                            <show>protected</show>
+                            <archive>
+                                <manifestEntries>
+                                    <Built-By>${project.organization.name}</Built-By>
+                                </manifestEntries>
+                            </archive>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>attach-javadocs</id>
+                                <goals>
+                                    <goal>jar</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                </plugins>
+            </build>
+        </profile>
+
+    </profiles>
 
 </project>
 
index b4f4424..c543c11 100644 (file)
@@ -8,13 +8,13 @@
 >
 
 <!--
-    OSDN.net用リリースファイル構成定義ファイル
-    Maven3 assembly用
+    for maven-assembly-plugin
 -->
 
     <id>src</id>
 
     <formats>
+        <format>tar.gz</format>
         <format>zip</format>
     </formats>
 
index 333268a..e41dcb8 100644 (file)
@@ -7,8 +7,6 @@
 
 package jp.sfjp.jindolf;
 
-import java.awt.Component;
-import java.awt.Cursor;
 import java.awt.EventQueue;
 import java.awt.Frame;
 import java.awt.Window;
@@ -22,7 +20,6 @@ import java.lang.reflect.InvocationTargetException;
 import java.net.URL;
 import java.text.MessageFormat;
 import java.util.List;
-import java.util.SortedSet;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.logging.Handler;
@@ -31,12 +28,10 @@ import java.util.logging.Logger;
 import java.util.regex.Pattern;
 import javax.swing.JButton;
 import javax.swing.JDialog;
+import javax.swing.JFileChooser;
 import javax.swing.JOptionPane;
 import javax.swing.JToolBar;
 import javax.swing.JTree;
-import javax.swing.LookAndFeel;
-import javax.swing.SwingUtilities;
-import javax.swing.UIManager;
 import javax.swing.UnsupportedLookAndFeelException;
 import javax.swing.WindowConstants;
 import javax.swing.event.ChangeEvent;
@@ -45,6 +40,8 @@ import javax.swing.event.TreeExpansionEvent;
 import javax.swing.event.TreeSelectionEvent;
 import javax.swing.event.TreeSelectionListener;
 import javax.swing.event.TreeWillExpandListener;
+import javax.swing.filechooser.FileFilter;
+import javax.swing.filechooser.FileNameExtensionFilter;
 import javax.swing.tree.TreePath;
 import jp.sfjp.jindolf.config.AppSetting;
 import jp.sfjp.jindolf.config.ConfigStore;
@@ -52,15 +49,18 @@ import jp.sfjp.jindolf.config.OptionInfo;
 import jp.sfjp.jindolf.data.Anchor;
 import jp.sfjp.jindolf.data.DialogPref;
 import jp.sfjp.jindolf.data.Land;
-import jp.sfjp.jindolf.data.LandsModel;
+import jp.sfjp.jindolf.data.LandsTreeModel;
 import jp.sfjp.jindolf.data.Period;
 import jp.sfjp.jindolf.data.RegexPattern;
 import jp.sfjp.jindolf.data.Talk;
 import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.data.html.PeriodLoader;
+import jp.sfjp.jindolf.data.html.VillageInfoLoader;
+import jp.sfjp.jindolf.data.html.VillageListLoader;
+import jp.sfjp.jindolf.data.xml.VillageLoader;
 import jp.sfjp.jindolf.dxchg.CsvExporter;
 import jp.sfjp.jindolf.dxchg.WebIPCDialog;
 import jp.sfjp.jindolf.dxchg.WolfBBS;
-import jp.sfjp.jindolf.editor.TalkPreview;
 import jp.sfjp.jindolf.glyph.AnchorHitEvent;
 import jp.sfjp.jindolf.glyph.AnchorHitListener;
 import jp.sfjp.jindolf.glyph.Discussion;
@@ -75,8 +75,8 @@ import jp.sfjp.jindolf.summary.DaySummary;
 import jp.sfjp.jindolf.summary.VillageDigest;
 import jp.sfjp.jindolf.util.GUIUtils;
 import jp.sfjp.jindolf.util.StringUtils;
-import jp.sfjp.jindolf.view.AccountPanel;
 import jp.sfjp.jindolf.view.ActionManager;
+import jp.sfjp.jindolf.view.AvatarPics;
 import jp.sfjp.jindolf.view.FilterPanel;
 import jp.sfjp.jindolf.view.FindPanel;
 import jp.sfjp.jindolf.view.HelpFrame;
@@ -89,30 +89,38 @@ import jp.sfjp.jindolf.view.TopView;
 import jp.sfjp.jindolf.view.WindowManager;
 import jp.sourceforge.jindolf.corelib.VillageState;
 import jp.sourceforge.jovsonz.JsObject;
+import org.xml.sax.SAXException;
 
 /**
  * いわゆるMVCでいうとこのコントローラ。
  */
 public class Controller
         implements ActionListener,
-                   TreeWillExpandListener,
-                   TreeSelectionListener,
-                   ChangeListener,
                    AnchorHitListener {
     private static final Logger LOGGER = Logger.getAnonymousLogger();
 
     private static final String ERRTITLE_LAF = "Look&Feel";
-    private static final String ERRFORM_LAF =
+    private static final String ERRFORM_LAFGEN =
             "このLook&Feel[{0}]を生成する事ができません。";
 
 
-    private final LandsModel model;
+    private final LandsTreeModel model;
     private final WindowManager windowManager;
     private final ActionManager actionManager;
     private final AppSetting appSetting;
 
     private final TopView topView;
 
+    private final JFileChooser xmlFileChooser = buildFileChooser();
+
+    private final VillageTreeWatcher treeVillageWatcher =
+            new VillageTreeWatcher();
+    private final ChangeListener tabPeriodWatcher =
+            new TabPeriodWatcher();
+    private final ChangeListener filterWatcher =
+            new FilterWatcher();
+
+    private final Executor executor = Executors.newCachedThreadPool();
     private volatile boolean isBusyNow;
 
 
@@ -124,7 +132,7 @@ public class Controller
      * @param setting アプリ設定
      */
     @SuppressWarnings("LeakingThisInConstructor")
-    public Controller(LandsModel model,
+    public Controller(LandsTreeModel model,
                       WindowManager windowManager,
                       ActionManager actionManager,
                       AppSetting setting){
@@ -144,12 +152,13 @@ public class Controller
 
         JTree treeView = this.topView.getTreeView();
         treeView.setModel(this.model);
-        treeView.addTreeWillExpandListener(this);
-        treeView.addTreeSelectionListener(this);
+        treeView.addTreeWillExpandListener(this.treeVillageWatcher);
+        treeView.addTreeSelectionListener(this.treeVillageWatcher);
 
-        this.topView.getTabBrowser().addChangeListener(this);
-        this.topView.getTabBrowser().addActionListener(this);
-        this.topView.getTabBrowser().addAnchorHitListener(this);
+        TabBrowser periodTab = this.topView.getTabBrowser();
+        periodTab.addChangeListener(this.tabPeriodWatcher);
+        periodTab.addActionListener(this);
+        periodTab.addAnchorHitListener(this);
 
         JButton reloadVillageListButton = this.topView
                                          .getLandsTree()
@@ -158,12 +167,10 @@ public class Controller
         reloadVillageListButton.setEnabled(false);
 
         TopFrame topFrame         = this.windowManager.getTopFrame();
-        TalkPreview talkPreview   = this.windowManager.getTalkPreview();
         OptionPanel optionPanel   = this.windowManager.getOptionPanel();
         FindPanel findPanel       = this.windowManager.getFindPanel();
         FilterPanel filterPanel   = this.windowManager.getFilterPanel();
         LogFrame logFrame         = this.windowManager.getLogFrame();
-        AccountPanel accountPanel = this.windowManager.getAccountPanel();
         HelpFrame helpFrame       = this.windowManager.getHelpFrame();
 
         topFrame.setJMenuBar(this.actionManager.getMenuBar());
@@ -178,33 +185,27 @@ public class Controller
             }
         });
 
-        filterPanel.addChangeListener(this);
+        filterPanel.addChangeListener(this.filterWatcher);
 
         Handler newHandler = logFrame.getHandler();
         LogUtils.switchHandler(newHandler);
 
         ConfigStore config = this.appSetting.getConfigStore();
 
-        JsObject draft = config.loadDraftConfig();
-        talkPreview.putJson(draft);
-
         JsObject history = config.loadHistoryConfig();
         findPanel.putJson(history);
 
         FontInfo fontInfo = this.appSetting.getFontInfo();
-        this.topView.getTabBrowser().setFontInfo(fontInfo);
-        talkPreview.setFontInfo(fontInfo);
+        periodTab.setFontInfo(fontInfo);
         optionPanel.getFontChooser().setFontInfo(fontInfo);
 
         ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
         optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
 
         DialogPref pref = this.appSetting.getDialogPref();
-        this.topView.getTabBrowser().setDialogPref(pref);
+        periodTab.setDialogPref(pref);
         optionPanel.getDialogPrefPanel().setDialogPref(pref);
 
-        accountPanel.setModel(this.model);
-
         OptionInfo optInfo = this.appSetting.getOptionInfo();
         ConfigStore configStore = this.appSetting.getConfigStore();
         helpFrame.updateVmInfo(optInfo, configStore);
@@ -212,6 +213,54 @@ public class Controller
         return;
     }
 
+
+    /**
+     * フレーム表示のトグル処理。
+     * @param window フレーム
+     */
+    private static void toggleWindow(Window window){
+        if(window == null) return;
+
+        if(window instanceof Frame){
+            Frame frame = (Frame) window;
+            int winState = frame.getExtendedState();
+            boolean isIconified = (winState & Frame.ICONIFIED) != 0;
+            if(isIconified){
+                winState &= ~(Frame.ICONIFIED);
+                frame.setExtendedState(winState);
+                frame.setVisible(true);
+                return;
+            }
+        }
+
+        if(window.isVisible()){
+            window.setVisible(false);
+            window.dispose();
+        }else{
+            window.setVisible(true);
+        }
+        return;
+    }
+
+    /**
+     * XMLファイルを選択するためのChooserを生成する。
+     *
+     * @return Chooser
+     */
+    private static JFileChooser buildFileChooser(){
+        JFileChooser chooser = new JFileChooser();
+        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+
+        FileFilter filter;
+        filter = new FileNameExtensionFilter("XML files (*.xml)", "xml", "XML");
+        chooser.setFileFilter(filter);
+
+        chooser.setDialogTitle("アーカイブXMLファイルを開く");
+
+        return chooser;
+    }
+
+
     /**
      * ウィンドウマネジャを返す。
      * @return ウィンドウマネジャ
@@ -230,6 +279,87 @@ public class Controller
     }
 
     /**
+     * トップフレームのタイトルを設定する。
+     * タイトルは指定された国or村名 + " - Jindolf"
+     * @param name 国or村名
+     */
+    private void setFrameTitle(String name){
+        String title = VerInfo.getFrameTitle(name);
+        TopFrame topFrame = this.windowManager.getTopFrame();
+        topFrame.setTitle(title);
+        return;
+    }
+
+    /**
+     * 現在選択中のPeriodを内包するPeriodViewを返す。
+     * @return PeriodView
+     */
+    private PeriodView currentPeriodView(){
+        TabBrowser tb = this.topView.getTabBrowser();
+        PeriodView result = tb.currentPeriodView();
+        return result;
+    }
+
+    /**
+     * 現在選択中のPeriodを内包するDiscussionを返す。
+     * @return Discussion
+     */
+    private Discussion currentDiscussion(){
+        PeriodView periodView = currentPeriodView();
+        if(periodView == null) return null;
+        Discussion result = periodView.getDiscussion();
+        return result;
+    }
+
+    /**
+     * 現在選択中の村を返す。
+     *
+     * @return 選択中の村。なければnull。
+     */
+    private Village getVillage(){
+        TabBrowser browser = this.topView.getTabBrowser();
+        Village village = browser.getVillage();
+        return village;
+    }
+
+    /**
+     * ビジー状態の設定を行う。
+     *
+     * <p>ヘビーなタスク実行をアピールするために、
+     * プログレスバーとカーソルの設定を行う。
+     *
+     * <p>ビジー中のActionコマンド受信は無視される。
+     *
+     * <p>ビジー中のトップフレームのマウス操作、キーボード入力は
+     * 全てグラブされるため無視される。
+     *
+     * @param isBusy trueならプログレスバーのアニメ開始&amp;WAITカーソル。
+     * falseなら停止&amp;通常カーソル。
+     * @param msg フッタメッセージ。nullなら変更なし。
+     */
+    private void setBusy(boolean isBusy, String msg){
+        this.isBusyNow = isBusy;
+
+        TopFrame topFrame = getTopFrame();
+
+        topFrame.setBusy(isBusy);
+        if(msg != null){
+            this.topView.updateSysMessage(msg);
+        }
+
+        return;
+    }
+
+    /**
+     * ステータスバーを更新する。
+     * @param message メッセージ
+     */
+    private void updateStatusBar(String message){
+        this.topView.updateSysMessage(message);
+        return;
+    }
+
+    /**
      * ビジー状態を設定する。
      *
      * <p>EDT以外から呼ばれると実際の処理が次回のEDT移行に遅延される。
@@ -237,19 +367,20 @@ public class Controller
      * @param isBusy ビジーならtrue
      * @param message ステータスバー表示。nullなら変更なし
      */
-    public void submitBusyStatus(final boolean isBusy, final String message){
-        Runnable task = new Runnable(){
-            /** {@inheritDoc} */
-            @Override
-            public void run(){
-                if(isBusy) setBusy(true);
-                if(message != null) updateStatusBar(message);
-                if( ! isBusy ) setBusy(false);
-                return;
-            }
+    public void submitBusyStatus(boolean isBusy, String message){
+        Runnable task = () -> {
+            setBusy(isBusy, message);
         };
 
-        EventQueue.invokeLater(task);
+        if(EventQueue.isDispatchThread()){
+            task.run();
+        }else{
+            try{
+                EventQueue.invokeAndWait(task);
+            }catch(InvocationTargetException | InterruptedException e){
+                LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
+            }
+        }
 
         return;
     }
@@ -277,26 +408,6 @@ public class Controller
     }
 
     /**
-     * 軽量タスクをEDTで実行する。
-     *
-     * <p>タスク実行中はビジー状態となる。
-     *
-     * <p>軽量タスク実行中はイベントループが停止するので、
-     * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
-     *
-     * <p>タスク終了時、ステータス文字列はタスク実行前の状態に戻る。
-     *
-     * @param task 軽量タスク
-     * @param beforeMsg ビジー中ステータス文字列。
-     *     ビジー復帰時は元のステータス文字列に戻る。
-     */
-    public void submitLightBusyTask(Runnable task, String beforeMsg){
-        String afterMsg = this.topView.getSysMessage();
-        submitLightBusyTask(task, beforeMsg, afterMsg);
-        return;
-    }
-
-    /**
      * 重量級タスクをEDTとは別のスレッドで実行する。
      *
      * <p>タスク実行中はビジー状態となる。
@@ -310,50 +421,26 @@ public class Controller
                                     final String afterMsg ){
         submitBusyStatus(true, beforeMsg);
 
-        final Runnable busyManager = new Runnable(){
-            /** {@inheritDoc} */
-            @Override
-            @SuppressWarnings("CallToThreadYield")
-            public void run(){
-                Thread.yield();
+        EventQueue.invokeLater(() -> {
+            fork(() -> {
                 try{
                     heavyTask.run();
                 }finally{
                     submitBusyStatus(false, afterMsg);
                 }
-                return;
-            }
-        };
-
-        Runnable forkLauncher = new Runnable(){
-            /** {@inheritDoc} */
-            @Override
-            public void run(){
-                Executor executor = Executors.newCachedThreadPool();
-                executor.execute(busyManager);
-                return;
-            }
-        };
-
-        EventQueue.invokeLater(forkLauncher);
+            });
+        });
 
         return;
     }
 
     /**
-     * 重量級タスクをEDTとは別のスレッドで実行する。
-     *
-     * <p>タスク実行中はビジー状態となる。
-     *
-     * <p>タスク終了時、ステータス文字列はタスク実行前の状態に戻る。
+     * スレッドプールを用いて非EDTなタスクを投入する。
      *
-     * @param task 重量級タスク
-     * @param beforeMsg ビジー中ステータス文字列。
-     *     ビジー復帰時は元のステータス文字列に戻る。
+     * @param task タスク
      */
-    public void submitHeavyBusyTask(Runnable task, String beforeMsg){
-        String afterMsg = this.topView.getSysMessage();
-        submitHeavyBusyTask(task, beforeMsg, afterMsg);
+    private void fork(Runnable task){
+        this.executor.execute(task);
         return;
     }
 
@@ -398,8 +485,7 @@ public class Controller
      * 村をWebブラウザで表示する。
      */
     private void actionShowWebVillage(){
-        TabBrowser browser = this.topView.getTabBrowser();
-        Village village = browser.getVillage();
+        Village village = getVillage();
         if(village == null) return;
 
         Land land = village.getParentLand();
@@ -421,8 +507,7 @@ public class Controller
      * 村に対応するまとめサイトをWebブラウザで表示する。
      */
     private void actionShowWebWiki(){
-        TabBrowser browser = this.topView.getTabBrowser();
-        Village village = browser.getVillage();
+        Village village = getVillage();
         if(village == null) return;
 
         String urlTxt = WolfBBS.getCastGeneratorUrl(village);
@@ -441,8 +526,7 @@ public class Controller
         Period period = periodView.getPeriod();
         if(period == null) return;
 
-        TabBrowser browser = this.topView.getTabBrowser();
-        Village village = browser.getVillage();
+        Village village = getVillage();
         if(village == null) return;
 
         Land land = village.getParentLand();
@@ -451,7 +535,6 @@ public class Controller
         URL url = server.getPeriodURL(period);
 
         String urlText = url.toString();
-        if(period.isHot()) urlText += "#bottom";
 
         WebIPCDialog.showDialog(getTopFrame(), urlText);
 
@@ -462,15 +545,14 @@ public class Controller
      * 個別の発言をWebブラウザで表示する。
      */
     private void actionShowWebTalk(){
-        TabBrowser browser = this.topView.getTabBrowser();
-        Village village = browser.getVillage();
+        Village village = getVillage();
         if(village == null) return;
 
         PeriodView periodView = currentPeriodView();
         if(periodView == null) return;
 
         Discussion discussion = periodView.getDiscussion();
-        Talk talk = discussion.getPopupedTalk();
+        Talk talk = discussion.getActiveTalk();
         if(talk == null) return;
 
         Period period = periodView.getPeriod();
@@ -513,70 +595,47 @@ public class Controller
     }
 
     /**
-     * L&Fの変更を行う
+     * L&amp;Fの変更指示を受信する
      */
     private void actionChangeLaF(){
         String className = this.actionManager.getSelectedLookAndFeel();
+        if(className == null) return;
 
-        Class<?> lnfClass;
-        try{
-            lnfClass = Class.forName(className);
-        }catch(ClassNotFoundException e){
-            String warnMsg = MessageFormat.format(
-                    "このLook&Feel[{0}]を読み込む事ができません。",
-                    className );
-            warnDialog(ERRTITLE_LAF, warnMsg, e);
-            return;
-        }
-
-        final LookAndFeel lnf;
-        try{
-            lnf = (LookAndFeel) ( lnfClass.newInstance() );
-        }catch(   InstantiationException
-                | IllegalAccessException
-                | ClassCastException
-                e){
-            String warnMsg = MessageFormat.format(ERRFORM_LAF, className);
-            warnDialog(ERRTITLE_LAF, warnMsg, e);
-            return;
-        }
-
-        Runnable lafTask = new Runnable(){
-            @Override
-            public void run(){
-                changeLaF(lnf);
-                return;
-            }
-        };
-
-        submitLightBusyTask(lafTask,
-                            "Look&Feelを更新中…",
-                            "Look&Feelが更新されました" );
+        submitLightBusyTask(
+            () -> {taskChangeLaF(className);},
+            "Look&Feelを更新中…",
+            "Look&Feelが更新されました"
+        );
 
         return;
     }
 
     /**
-     * LookAndFeelの実際の更新を行う。
+     * LookAndFeelの実際の更新を行う軽量タスク。
+     *
      * @param lnf LookAndFeel
      */
-    private void changeLaF(LookAndFeel lnf){
+    private void taskChangeLaF(String className){
         assert EventQueue.isDispatchThread();
 
         try{
-            UIManager.setLookAndFeel(lnf);
+            this.windowManager.changeAllWindowUI(className);
         }catch(UnsupportedLookAndFeelException e){
             String warnMsg = MessageFormat.format(
                     "このLook&Feel[{0}]はサポートされていません。",
-                    lnf.getName() );
+                    className);
+            warnDialog(ERRTITLE_LAF, warnMsg, e);
+            return;
+        }catch(ReflectiveOperationException e){
+            String warnMsg = MessageFormat.format(ERRFORM_LAFGEN, className);
             warnDialog(ERRTITLE_LAF, warnMsg, e);
             return;
         }
 
-        this.windowManager.changeAllWindowUI();
+        this.xmlFileChooser.updateUI();
 
         LOGGER.log(Level.INFO,
-                   "Look&Feelが[{0}]に変更されました。", lnf.getName() );
+                   "Look&Feelが[{0}]に変更されました。", className );
 
         return;
     }
@@ -591,15 +650,6 @@ public class Controller
     }
 
     /**
-     * アカウント管理画面を表示する。
-     */
-    private void actionShowAccount(){
-        AccountPanel accountPanel = this.windowManager.getAccountPanel();
-        toggleWindow(accountPanel);
-        return;
-    }
-
-    /**
      * ログ表示画面を表示する。
      */
     private void actionShowLog(){
@@ -609,15 +659,6 @@ public class Controller
     }
 
     /**
-     * 発言エディタを表示する。
-     */
-    private void actionTalkPreview(){
-        TalkPreview talkPreview = this.windowManager.getTalkPreview();
-        toggleWindow(talkPreview);
-        return;
-    }
-
-    /**
      * オプション設定画面を表示する。
      */
     private void actionOption(){
@@ -659,11 +700,9 @@ public class Controller
 
         this.topView.getTabBrowser().setFontInfo(newFontInfo);
 
-        TalkPreview talkPreview = this.windowManager.getTalkPreview();
         OptionPanel optionPanel = this.windowManager.getOptionPanel();
         FontChooser fontChooser = optionPanel.getFontChooser();
 
-        talkPreview.setFontInfo(newFontInfo);
         fontChooser.setFontInfo(newFontInfo);
 
         return;
@@ -706,8 +745,7 @@ public class Controller
      * 村ダイジェスト画面を表示する。
      */
     private void actionShowDigest(){
-        TabBrowser browser = this.topView.getTabBrowser();
-        final Village village = browser.getVillage();
+        Village village = getVillage();
         if(village == null) return;
 
         VillageState villageState = village.getState();
@@ -729,22 +767,20 @@ public class Controller
 
         VillageDigest villageDigest = this.windowManager.getVillageDigest();
         final VillageDigest digest = villageDigest;
-        Executor executor = Executors.newCachedThreadPool();
-        executor.execute(new Runnable(){
-            @Override
-            public void run(){
-                taskFullOpenAllPeriod();
-                EventQueue.invokeLater(new Runnable(){
-                    @Override
-                    public void run(){
-                        digest.setVillage(village);
-                        digest.setVisible(true);
-                        return;
-                    }
-                });
-                return;
-            }
-        });
+
+        Runnable task = () -> {
+            taskFullOpenAllPeriod();
+            EventQueue.invokeLater(() -> {
+                digest.setVillage(village);
+                digest.setVisible(true);
+            });
+        };
+
+        submitHeavyBusyTask(
+                task,
+                "一括読み込み開始",
+                "一括読み込み完了"
+        );
 
         return;
     }
@@ -754,32 +790,25 @@ public class Controller
      */
     // TODO taskLoadAllPeriodtと一体化したい。
     private void taskFullOpenAllPeriod(){
-        setBusy(true);
-        updateStatusBar("一括読み込み開始");
-        try{
-            TabBrowser browser = this.topView.getTabBrowser();
-            Village village = browser.getVillage();
-            if(village == null) return;
-            for(PeriodView periodView : browser.getPeriodViewList()){
-                Period period = periodView.getPeriod();
-                if(period == null) continue;
-                if(period.isFullOpen()) continue;
-                String message =
-                        period.getDay()
-                        + "日目のデータを読み込んでいます";
-                updateStatusBar(message);
-                try{
-                    Period.parsePeriod(period, true);
-                }catch(IOException e){
-                    showNetworkError(village, e);
-                    return;
-                }
-                periodView.showTopics();
+        TabBrowser browser = this.topView.getTabBrowser();
+        Village village = getVillage();
+        if(village == null) return;
+        for(PeriodView periodView : browser.getPeriodViewList()){
+            Period period = periodView.getPeriod();
+            if(period == null) continue;
+            String message =
+                    period.getDay()
+                    + "日目のデータを読み込んでいます";
+            updateStatusBar(message);
+            try{
+                PeriodLoader.parsePeriod(period, false);
+            }catch(IOException e){
+                showNetworkError(village, e);
+                return;
             }
-        }finally{
-            updateStatusBar("一括読み込み完了");
-            setBusy(false);
+            periodView.showTopics();
         }
+
         return;
     }
 
@@ -833,14 +862,11 @@ public class Controller
      * 一括検索処理。
      */
     private void bulkSearch(){
-        Executor executor = Executors.newCachedThreadPool();
-        executor.execute(new Runnable(){
-            @Override
-            public void run(){
-                taskBulkSearch();
-                return;
-            }
-        });
+        submitHeavyBusyTask(
+                () -> {taskBulkSearch();},
+                null, null
+        );
+        return;
     }
 
     /**
@@ -965,8 +991,7 @@ public class Controller
     private void actionReloadPeriod(){
         updatePeriod(true);
 
-        TabBrowser tabBrowser = this.topView.getTabBrowser();
-        Village village = tabBrowser.getVillage();
+        Village village = getVillage();
         if(village == null) return;
         if(village.getState() != VillageState.EPILOGUE) return;
 
@@ -991,14 +1016,11 @@ public class Controller
      * 全日程の一括ロード。
      */
     private void actionLoadAllPeriod(){
-        Executor executor = Executors.newCachedThreadPool();
-        executor.execute(new Runnable(){
-            @Override
-            public void run(){
-                taskLoadAllPeriod();
-                return;
-            }
-        });
+        submitHeavyBusyTask(
+                () -> {taskLoadAllPeriod();},
+                "一括読み込み開始",
+                "一括読み込み完了"
+        );
 
         return;
     }
@@ -1007,31 +1029,25 @@ public class Controller
      * 全日程の一括ロード。ヘビータスク版。
      */
     private void taskLoadAllPeriod(){
-        setBusy(true);
-        updateStatusBar("一括読み込み開始");
-        try{
-            TabBrowser browser = this.topView.getTabBrowser();
-            Village village = browser.getVillage();
-            if(village == null) return;
-            for(PeriodView periodView : browser.getPeriodViewList()){
-                Period period = periodView.getPeriod();
-                if(period == null) continue;
-                String message =
-                        period.getDay()
-                        + "日目のデータを読み込んでいます";
-                updateStatusBar(message);
-                try{
-                    Period.parsePeriod(period, false);
-                }catch(IOException e){
-                    showNetworkError(village, e);
-                    return;
-                }
-                periodView.showTopics();
+        TabBrowser browser = this.topView.getTabBrowser();
+        Village village = getVillage();
+        if(village == null) return;
+        for(PeriodView periodView : browser.getPeriodViewList()){
+            Period period = periodView.getPeriod();
+            if(period == null) continue;
+            String message =
+                    period.getDay()
+                    + "日目のデータを読み込んでいます";
+            updateStatusBar(message);
+            try{
+                PeriodLoader.parsePeriod(period, false);
+            }catch(IOException e){
+                showNetworkError(village, e);
+                return;
             }
-        }finally{
-            updateStatusBar("一括読み込み完了");
-            setBusy(false);
+            periodView.showTopics();
         }
+
         return;
     }
 
@@ -1093,6 +1109,28 @@ public class Controller
     }
 
     /**
+     * アンカー先を含むPeriodの全会話を事前にロードする。
+     *
+     * @param village 村
+     * @param anchor アンカー
+     * @return アンカー先を含むPeriod。
+     * アンカーがG国発言番号ならnull。
+     * Periodが見つからないならnull。
+     * @throws IOException 入力エラー
+     */
+    private Period loadAnchoredPeriod(Village village, Anchor anchor)
+            throws IOException{
+        if(anchor.hasTalkNo()) return null;
+
+        Period anchorPeriod = village.getPeriod(anchor);
+        if(anchorPeriod == null) return null;
+
+        PeriodLoader.parsePeriod(anchorPeriod, false);
+
+        return anchorPeriod;
+    }
+
+    /**
      * アンカーにジャンプする。
      */
     private void actionJumpAnchor(){
@@ -1100,62 +1138,96 @@ public class Controller
         if(periodView == null) return;
         Discussion discussion = periodView.getDiscussion();
 
-        final TabBrowser browser = this.topView.getTabBrowser();
-        final Village village = browser.getVillage();
-        final Anchor anchor = discussion.getPopupedAnchor();
+        TabBrowser browser = this.topView.getTabBrowser();
+        Village village = getVillage();
+        final Anchor anchor = discussion.getActiveAnchor();
         if(anchor == null) return;
 
-        Executor executor = Executors.newCachedThreadPool();
-        executor.execute(new Runnable(){
-            @Override
-            public void run(){
-                setBusy(true);
-                updateStatusBar("ジャンプ先の読み込み中…");
+        Runnable task = () -> {
+            if(anchor.hasTalkNo()){
+                // TODO もう少し賢くならない?
+                taskLoadAllPeriod();
+            }
 
-                if(anchor.hasTalkNo()){
-                    // TODO もう少し賢くならない?
-                    taskLoadAllPeriod();
+            final List<Talk> talkList;
+            try{
+                loadAnchoredPeriod(village, anchor);
+                talkList = village.getTalkListFromAnchor(anchor);
+                if(talkList == null || talkList.size() <= 0){
+                    updateStatusBar(
+                            "アンカーのジャンプ先["
+                                    + anchor.toString()
+                                    + "]が見つかりません");
+                    return;
                 }
 
-                final List<Talk> talkList;
-                try{
-                    talkList = village.getTalkListFromAnchor(anchor);
-                    if(talkList == null || talkList.size() <= 0){
-                        updateStatusBar(
-                                  "アンカーのジャンプ先["
+                Talk targetTalk = talkList.get(0);
+                Period targetPeriod = targetTalk.getPeriod();
+                int periodIndex = targetPeriod.getDay();
+                PeriodView target = browser.getPeriodView(periodIndex);
+
+                EventQueue.invokeLater(() -> {
+                    browser.showPeriodTab(periodIndex);
+                    target.setPeriod(targetPeriod);
+                    target.scrollToTalk(targetTalk);
+                });
+                updateStatusBar(
+                        "アンカー["
                                 + anchor.toString()
-                                + "]が見つかりません");
-                        return;
-                    }
+                                + "]にジャンプしました");
+            }catch(IOException e){
+                updateStatusBar(
+                        "アンカーの展開中にエラーが起きました");
+            }
 
-                    final Talk targetTalk = talkList.get(0);
-                    final Period targetPeriod = targetTalk.getPeriod();
-                    final int tabIndex = targetPeriod.getDay() + 1;
-                    final PeriodView target = browser.getPeriodView(tabIndex);
-
-                    EventQueue.invokeLater(new Runnable(){
-                        @Override
-                        public void run(){
-                            browser.setSelectedIndex(tabIndex);
-                            target.setPeriod(targetPeriod);
-                            target.scrollToTalk(targetTalk);
-                            return;
-                        }
-                    });
-                    updateStatusBar(
-                              "アンカー["
-                            + anchor.toString()
-                            + "]にジャンプしました");
-                }catch(IOException e){
-                    updateStatusBar(
-                            "アンカーの展開中にエラーが起きました");
-                }finally{
-                    setBusy(false);
-                }
+        };
 
+        submitHeavyBusyTask(
+                task,
+                "ジャンプ先の読み込み中…",
+                null
+        );
+
+        return;
+    }
+
+    /**
+     * ローカルなXMLファイルを読み込む。
+     */
+    private void actionOpenXml(){
+        int result = this.xmlFileChooser.showOpenDialog(getTopFrame());
+        if(result != JFileChooser.APPROVE_OPTION) return;
+        File selected = this.xmlFileChooser.getSelectedFile();
+
+        submitHeavyBusyTask(() -> {
+            Village village;
+
+            try{
+                village = VillageLoader.parseVillage(selected);
+            }catch(IOException e){
+                String warnMsg = MessageFormat.format(
+                        "XMLファイル[ {0} ]を読み込むことができません",
+                        selected.getPath()
+                );
+                warnDialog("XML I/O error", warnMsg, e);
+                return;
+            }catch(SAXException e){
+                String warnMsg = MessageFormat.format(
+                        "XMLファイル[ {0} ]の形式が不正なため読み込むことができません",
+                        selected.getPath()
+                );
+                warnDialog("XML form error", warnMsg, e);
                 return;
             }
-        });
+
+            village.setLocalArchive(true);
+            AvatarPics avatarPics = village.getAvatarPics();
+            this.appSetting.applyLocalImage(avatarPics);
+            avatarPics.preload();
+            EventQueue.invokeLater(() -> {
+                selectedVillage(village);
+            });
+        }, "XML読み込み中", "XML読み込み完了");
 
         return;
     }
@@ -1165,18 +1237,11 @@ public class Controller
      * @param land 国
      */
     private void submitReloadVillageList(final Land land){
-        Runnable heavyTask = new Runnable(){
-            @Override
-            public void run(){
-                taskReloadVillageList(land);
-                return;
-            }
-        };
-
-        submitHeavyBusyTask(heavyTask,
-                            "村一覧を読み込み中…",
-                            "村一覧の読み込み完了" );
-
+        submitHeavyBusyTask(
+            () -> {taskReloadVillageList(land);},
+            "村一覧を読み込み中…",
+            "村一覧の読み込み完了"
+        );
         return;
     }
 
@@ -1185,9 +1250,9 @@ public class Controller
      * @param land 国
      */
     private void taskReloadVillageList(Land land){
-        SortedSet<Village> villageList;
+        List<Village> villageList;
         try{
-            villageList = land.downloadVillageList();
+            villageList = VillageListLoader.loadVillageList(land);
         }catch(IOException e){
             showNetworkError(land, e);
             return;
@@ -1207,109 +1272,42 @@ public class Controller
      * @param force trueならPeriodデータを強制再読み込み。
      */
     private void updatePeriod(final boolean force){
-        final TabBrowser tabBrowser = this.topView.getTabBrowser();
-        final Village village = tabBrowser.getVillage();
+        Village village = getVillage();
         if(village == null) return;
-        setFrameTitle(village.getVillageFullName());
 
-        final PeriodView periodView = currentPeriodView();
+        String fullName = village.getVillageFullName();
+        setFrameTitle(fullName);
+
+        PeriodView periodView = currentPeriodView();
         Discussion discussion = currentDiscussion();
         if(discussion == null) return;
+
         FilterPanel filterPanel = this.windowManager.getFilterPanel();
         discussion.setTopicFilter(filterPanel);
-        final Period period = discussion.getPeriod();
-        if(period == null) return;
 
-        Executor executor = Executors.newCachedThreadPool();
-        executor.execute(new Runnable(){
-            @Override
-            public void run(){
-                setBusy(true);
-                try{
-                    boolean wasHot = loadPeriod();
-
-                    if(wasHot && ! period.isHot() ){
-                        if( ! updatePeriodList() ) return;
-                    }
+        Period period = discussion.getPeriod();
+        if(period == null) return;
 
-                    renderBrowser();
-                }finally{
-                    setBusy(false);
-                }
+        Runnable task = () -> {
+            try{
+                PeriodLoader.parsePeriod(period, force);
+            }catch(IOException e){
+                showNetworkError(village, e);
                 return;
             }
 
-            private boolean loadPeriod(){
-                updateStatusBar("1日分のデータを読み込んでいます…");
-                boolean wasHot;
-                try{
-                    wasHot = period.isHot();
-                    try{
-                        Period.parsePeriod(period, force);
-                    }catch(IOException e){
-                        showNetworkError(village, e);
-                    }
-                }finally{
-                    updateStatusBar("1日分のデータを読み終わりました");
-                }
-                return wasHot;
-            }
-
-            private boolean updatePeriodList(){
-                updateStatusBar("村情報を読み直しています…");
-                try{
-                    Village.updateVillage(village);
-                }catch(IOException e){
-                    showNetworkError(village, e);
-                    return false;
-                }
-                try{
-                    SwingUtilities.invokeAndWait(new Runnable(){
-                        @Override
-                        public void run(){
-                            tabBrowser.setVillage(village);
-                            return;
-                        }
-                    });
-                }catch(InvocationTargetException | InterruptedException e){
-                    LOGGER.log(Level.SEVERE,
-                            "タブ操作で致命的な障害が発生しました", e);
-                }
-                updateStatusBar("村情報を読み直しました…");
-                return true;
-            }
+            EventQueue.invokeLater(() -> {
+                int lastPos = periodView.getVerticalPosition();
+                periodView.showTopics();
+                periodView.setVerticalPosition(lastPos);
+            });
+        };
 
-            private void renderBrowser(){
-                updateStatusBar("レンダリング中…");
-                try{
-                    final int lastPos = periodView.getVerticalPosition();
-                    try{
-                        SwingUtilities.invokeAndWait(new Runnable(){
-                            @Override
-                            public void run(){
-                                periodView.showTopics();
-                                return;
-                            }
-                        });
-                    }catch(   InvocationTargetException
-                            | InterruptedException
-                            e){
-                        LOGGER.log(Level.SEVERE,
-                                "ブラウザ表示で致命的な障害が発生しました",
-                                e );
-                    }
-                    EventQueue.invokeLater(new Runnable(){
-                        @Override
-                        public void run(){
-                            periodView.setVerticalPosition(lastPos);
-                        }
-                    });
-                }finally{
-                    updateStatusBar("レンダリング完了");
-                }
-                return;
-            }
-        });
+        submitHeavyBusyTask(
+                task,
+                "会話の読み込み中",
+                "会話の表示が完了"
+        );
 
         return;
     }
@@ -1330,55 +1328,6 @@ public class Controller
     }
 
     /**
-     * 現在選択中のPeriodを内包するPeriodViewを返す。
-     * @return PeriodView
-     */
-    private PeriodView currentPeriodView(){
-        TabBrowser tb = this.topView.getTabBrowser();
-        PeriodView result = tb.currentPeriodView();
-        return result;
-    }
-
-    /**
-     * 現在選択中のPeriodを内包するDiscussionを返す。
-     * @return Discussion
-     */
-    private Discussion currentDiscussion(){
-        PeriodView periodView = currentPeriodView();
-        if(periodView == null) return null;
-        Discussion result = periodView.getDiscussion();
-        return result;
-    }
-
-    /**
-     * フレーム表示のトグル処理。
-     * @param window フレーム
-     */
-    private void toggleWindow(Window window){
-        if(window == null) return;
-
-        if(window instanceof Frame){
-            Frame frame = (Frame) window;
-            int winState = frame.getExtendedState();
-            boolean isIconified = (winState & Frame.ICONIFIED) != 0;
-            if(isIconified){
-                winState &= ~(Frame.ICONIFIED);
-                frame.setExtendedState(winState);
-                frame.setVisible(true);
-                return;
-            }
-        }
-
-        if(window.isVisible()){
-            window.setVisible(false);
-            window.dispose();
-        }else{
-            window.setVisible(true);
-        }
-        return;
-    }
-
-    /**
      * ネットワークエラーを通知するモーダルダイアログを表示する。
      * OKボタンを押すまでこのメソッドは戻ってこない。
      * @param village 村
@@ -1423,182 +1372,155 @@ public class Controller
     }
 
     /**
-     * {@inheritDoc}
-     * ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
-     * @param event イベント {@inheritDoc}
+     * 国を選択する。
+     *
+     * @param land 国
      */
-    @Override
-    public void valueChanged(TreeSelectionEvent event){
-        TreePath path = event.getNewLeadSelectionPath();
-        if(path == null) return;
+    private void selectedLand(Land land){
+        String landName = land.getLandDef().getLandName();
+        setFrameTitle(landName);
 
-        Object selObj = path.getLastPathComponent();
-
-        if( selObj instanceof Land ){
-            Land land = (Land) selObj;
-            setFrameTitle(land.getLandDef().getLandName());
-            this.topView.showLandInfo(land);
-            this.actionManager.appearVillage(false);
-            this.actionManager.appearPeriod(false);
-        }else if( selObj instanceof Village ){
-            final Village village = (Village) selObj;
-
-            Executor executor = Executors.newCachedThreadPool();
-            executor.execute(new Runnable(){
-                @Override
-                public void run(){
-                    setBusy(true);
-                    updateStatusBar("村情報を読み込み中…");
-
-                    try{
-                        Village.updateVillage(village);
-                    }catch(IOException e){
-                        showNetworkError(village, e);
-                        return;
-                    }finally{
-                        updateStatusBar("村情報の読み込み完了");
-                        setBusy(false);
-                    }
-
-                    Controller.this.actionManager.appearVillage(true);
-                    setFrameTitle(village.getVillageFullName());
+        this.actionManager.exposeVillage(false);
+        this.actionManager.exposePeriod(false);
 
-                    EventQueue.invokeLater(new Runnable(){
-                        @Override
-                        public void run(){
-                            Controller.this.topView.showVillageInfo(village);
-                            return;
-                        }
-                    });
-
-                    return;
-                }
-            });
-        }
+        this.topView.showLandInfo(land);
 
         return;
     }
 
     /**
-     * {@inheritDoc}
-     * Periodがタブ選択されたときもしくは発言フィルタが操作されたときの処理。
-     * @param event イベント {@inheritDoc}
+     * 村を選択する。
+     *
+     * @param village 村
      */
-    @Override
-    public void stateChanged(ChangeEvent event){
-        Object source = event.getSource();
-
-        if(source == this.windowManager.getFilterPanel()){
-            filterChanged();
-        }else if(source instanceof TabBrowser){
-            updateFindPanel();
-            updatePeriod(false);
-            PeriodView periodView = currentPeriodView();
-            if(periodView == null) this.actionManager.appearPeriod(false);
-            else                   this.actionManager.appearPeriod(true);
+    private void selectedVillage(Village village){
+        setFrameTitle(village.getVillageFullName());
+        if(village.isLocalArchive()){
+            this.actionManager.exposeVillageLocal(true);
+        }else{
+            this.actionManager.exposeVillage(true);
         }
+
+        Runnable task = () -> {
+            try{
+                if( ! village.hasSchedule() ){
+                    VillageInfoLoader.updateVillageInfo(village);
+                }
+            }catch(IOException e){
+                showNetworkError(village, e);
+                return;
+            }
+
+            EventQueue.invokeLater(() -> {
+                this.topView.showVillageInfo(village);
+            });
+        };
+
+        submitHeavyBusyTask(
+                task,
+                "村情報を読み込み中…",
+                "村情報の読み込み完了"
+        );
+
         return;
     }
 
     /**
      * {@inheritDoc}
-     * 主にメニュー選択やボタン押下など。
-     * @param e イベント {@inheritDoc}
+     *
+     * <p>主にメニュー選択やボタン押下などのアクションをディスパッチする。
+     *
+     * <p>ビジーな状態では何もしない。
+     *
+     * @param ev {@inheritDoc}
      */
     @Override
-    public void actionPerformed(ActionEvent e){
+    public void actionPerformed(ActionEvent ev){
         if(this.isBusyNow) return;
 
-        String cmd = e.getActionCommand();
-        if(cmd.equals(ActionManager.CMD_ACCOUNT)){
-            actionShowAccount();
-        }else if(cmd.equals(ActionManager.CMD_EXIT)){
+        String cmd = ev.getActionCommand();
+        if(cmd == null) return;
+
+        switch(cmd){
+        case ActionManager.CMD_OPENXML:
+            actionOpenXml();
+            break;
+        case ActionManager.CMD_EXIT:
             actionExit();
-        }else if(cmd.equals(ActionManager.CMD_COPY)){
+            break;
+        case ActionManager.CMD_COPY:
             actionCopySelected();
-        }else if(cmd.equals(ActionManager.CMD_SHOWFIND)){
+            break;
+        case ActionManager.CMD_SHOWFIND:
             actionShowFind();
-        }else if(cmd.equals(ActionManager.CMD_SEARCHNEXT)){
+            break;
+        case ActionManager.CMD_SEARCHNEXT:
             actionSearchNext();
-        }else if(cmd.equals(ActionManager.CMD_SEARCHPREV)){
+            break;
+        case ActionManager.CMD_SEARCHPREV:
             actionSearchPrev();
-        }else if(cmd.equals(ActionManager.CMD_ALLPERIOD)){
+            break;
+        case ActionManager.CMD_ALLPERIOD:
             actionLoadAllPeriod();
-        }else if(cmd.equals(ActionManager.CMD_SHOWDIGEST)){
+            break;
+        case ActionManager.CMD_SHOWDIGEST:
             actionShowDigest();
-        }else if(cmd.equals(ActionManager.CMD_WEBVILL)){
+            break;
+        case ActionManager.CMD_WEBVILL:
             actionShowWebVillage();
-        }else if(cmd.equals(ActionManager.CMD_WEBWIKI)){
+            break;
+        case ActionManager.CMD_WEBWIKI:
             actionShowWebWiki();
-        }else if(cmd.equals(ActionManager.CMD_RELOAD)){
+            break;
+        case ActionManager.CMD_RELOAD:
             actionReloadPeriod();
-        }else if(cmd.equals(ActionManager.CMD_DAYSUMMARY)){
+            break;
+        case ActionManager.CMD_DAYSUMMARY:
             actionDaySummary();
-        }else if(cmd.equals(ActionManager.CMD_DAYEXPCSV)){
+            break;
+        case ActionManager.CMD_DAYEXPCSV:
             actionDayExportCsv();
-        }else if(cmd.equals(ActionManager.CMD_WEBDAY)){
+            break;
+        case ActionManager.CMD_WEBDAY:
             actionShowWebDay();
-        }else if(cmd.equals(ActionManager.CMD_OPTION)){
+            break;
+        case ActionManager.CMD_OPTION:
             actionOption();
-        }else if(cmd.equals(ActionManager.CMD_LANDF)){
+            break;
+        case ActionManager.CMD_LANDF:
             actionChangeLaF();
-        }else if(cmd.equals(ActionManager.CMD_SHOWFILT)){
+            break;
+        case ActionManager.CMD_SHOWFILT:
             actionShowFilter();
-        }else if(cmd.equals(ActionManager.CMD_SHOWEDIT)){
-            actionTalkPreview();
-        }else if(cmd.equals(ActionManager.CMD_SHOWLOG)){
+            break;
+        case ActionManager.CMD_SHOWLOG:
             actionShowLog();
-        }else if(cmd.equals(ActionManager.CMD_HELPDOC)){
+            break;
+        case ActionManager.CMD_HELPDOC:
             actionHelp();
-        }else if(cmd.equals(ActionManager.CMD_SHOWPORTAL)){
+            break;
+        case ActionManager.CMD_SHOWPORTAL:
             actionShowPortal();
-        }else if(cmd.equals(ActionManager.CMD_ABOUT)){
+            break;
+        case ActionManager.CMD_ABOUT:
             actionAbout();
-        }else if(cmd.equals(ActionManager.CMD_VILLAGELIST)){
+            break;
+        case ActionManager.CMD_VILLAGELIST:
             actionReloadVillageList();
-        }else if(cmd.equals(ActionManager.CMD_COPYTALK)){
+            break;
+        case ActionManager.CMD_COPYTALK:
             actionCopyTalk();
-        }else if(cmd.equals(ActionManager.CMD_JUMPANCHOR)){
+            break;
+        case ActionManager.CMD_JUMPANCHOR:
             actionJumpAnchor();
-        }else if(cmd.equals(ActionManager.CMD_WEBTALK)){
+            break;
+        case ActionManager.CMD_WEBTALK:
             actionShowWebTalk();
-        }
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 村選択ツリーリストが畳まれるとき呼ばれる。
-     * @param event ツリーイベント {@inheritDoc}
-     */
-    @Override
-    public void treeWillCollapse(TreeExpansionEvent event){
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 村選択ツリーリストが展開されるとき呼ばれる。
-     * @param event ツリーイベント {@inheritDoc}
-     */
-    @Override
-    public void treeWillExpand(TreeExpansionEvent event){
-        if(!(event.getSource() instanceof JTree)){
-            return;
+            break;
+        default:
+            break;
         }
 
-        TreePath path = event.getPath();
-        Object lastObj = path.getLastPathComponent();
-        if(!(lastObj instanceof Land)){
-            return;
-        }
-        final Land land = (Land) lastObj;
-        if(land.getVillageCount() > 0){
-            return;
-        }
-
-        submitReloadVillageList(land);
-
         return;
     }
 
@@ -1618,140 +1540,230 @@ public class Controller
         final Anchor anchor = event.getAnchor();
         final Discussion discussion = periodView.getDiscussion();
 
-        Executor executor = Executors.newCachedThreadPool();
-        executor.execute(new Runnable(){
-            @Override
-            public void run(){
-                setBusy(true);
-                updateStatusBar("アンカーの展開中…");
-
-                if(anchor.hasTalkNo()){
-                    // TODO もう少し賢くならない?
-                    taskLoadAllPeriod();
-                }
+        Runnable task = () -> {
+            if(anchor.hasTalkNo()){
+                // TODO もう少し賢くならない?
+                taskLoadAllPeriod();
+            }
 
-                final List<Talk> talkList;
-                try{
-                    talkList = village.getTalkListFromAnchor(anchor);
-                    if(talkList == null || talkList.size() <= 0){
-                        updateStatusBar(
-                                  "アンカーの展開先["
-                                + anchor.toString()
-                                + "]が見つかりません");
-                        return;
-                    }
-                    EventQueue.invokeLater(new Runnable(){
-                        @Override
-                        public void run(){
-                            talkDraw.showAnchorTalks(anchor, talkList);
-                            discussion.layoutRows();
-                            return;
-                        }
-                    });
-                    updateStatusBar(
-                              "アンカー["
-                            + anchor.toString()
-                            + "]の展開完了");
-                }catch(IOException e){
+            final List<Talk> talkList;
+            try{
+                loadAnchoredPeriod(village, anchor);
+                talkList = village.getTalkListFromAnchor(anchor);
+                if(talkList == null || talkList.size() <= 0){
                     updateStatusBar(
-                            "アンカーの展開中にエラーが起きました");
-                }finally{
-                    setBusy(false);
+                            "アンカーの展開先["
+                                    + anchor.toString()
+                                    + "]が見つかりません");
+                    return;
                 }
-
-                return;
+                EventQueue.invokeLater(() -> {
+                    talkDraw.showAnchorTalks(anchor, talkList);
+                    discussion.layoutRows();
+                });
+                updateStatusBar(
+                        "アンカー["
+                                + anchor.toString()
+                                + "]の展開完了");
+            }catch(IOException e){
+                updateStatusBar(
+                        "アンカーの展開中にエラーが起きました");
             }
-        });
+        };
+
+        submitHeavyBusyTask(
+                task,
+                "アンカーの展開中…",
+                null
+        );
 
         return;
     }
 
     /**
-     * ヘビーなタスク実行をアピール。
-     * プログレスバーとカーソルの設定を行う。
-     * @param isBusy trueならプログレスバーのアニメ開始&WAITカーソル。
-     *                falseなら停止&通常カーソル。
+     * アプリ正常終了処理。
      */
-    private void setBusy(final boolean isBusy){
-        this.isBusyNow = isBusy;
-
-        Runnable microJob = new Runnable(){
-            @Override
-            public void run(){
-                Cursor cursor;
-                if(isBusy){
-                    cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
-                }else{
-                    cursor = Cursor.getDefaultCursor();
-                }
+    private void shutdown(){
+        ConfigStore configStore = this.appSetting.getConfigStore();
 
-                Component glass = getTopFrame().getGlassPane();
-                glass.setCursor(cursor);
-                glass.setVisible(isBusy);
-                Controller.this.topView.setBusy(isBusy);
+        FindPanel findPanel = this.windowManager.getFindPanel();
+        JsObject findConf = findPanel.getJson();
+        if( ! findPanel.hasConfChanged(findConf) ){
+            configStore.saveHistoryConfig(findConf);
+        }
 
-                return;
-            }
-        };
+        this.appSetting.saveConfig();
 
-        if(SwingUtilities.isEventDispatchThread()){
-            microJob.run();
-        }else{
-            try{
-                SwingUtilities.invokeAndWait(microJob);
-            }catch(InvocationTargetException | InterruptedException e){
-                LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
-            }
-        }
+        LOGGER.info("VMごとアプリケーションを終了します。");
+        System.exit(0);  // invoke shutdown hooks... BYE !
 
+        assert false;
         return;
     }
 
+
     /**
-     * ステータスバーを更新する。
-     * @param message メッセージ
+     * 発言フィルタ操作を監視する。
      */
-    private void updateStatusBar(String message){
-        this.topView.updateSysMessage(message);
+    private class FilterWatcher implements ChangeListener{
+
+        /**
+         * constructor.
+         */
+        FilterWatcher(){
+            super();
+            return;
+        }
+
+
+        /**
+         * {@inheritDoc}
+         *
+         * <p>発言フィルタが操作されたときの処理。
+         *
+         * @param event {@inheritDoc}
+         */
+        @Override
+        public void stateChanged(ChangeEvent event){
+            Object source = event.getSource();
+
+            if(source == Controller.this.windowManager.getFilterPanel()){
+                filterChanged();
+            }
+
+            return;
+        }
+
     }
 
     /**
-     * トップフレームのタイトルを設定する。
-     * タイトルは指定された国or村名 + " - Jindolf"
-     * @param name 国or村名
+     * Period一覧タブのタブ操作を監視する。
      */
-    private void setFrameTitle(String name){
-        String title = VerInfo.getFrameTitle(name);
-        TopFrame topFrame = this.windowManager.getTopFrame();
-        topFrame.setTitle(title);
-        return;
+    private class TabPeriodWatcher implements ChangeListener{
+
+        /**
+         * constructor.
+         */
+        TabPeriodWatcher(){
+            super();
+            return;
+        }
+
+
+        /**
+         * {@inheritDoc}
+         *
+         * <p>Periodがタブ選択されたときの処理。
+         *
+         * @param event {@inheritDoc}
+         */
+        @Override
+        public void stateChanged(ChangeEvent event){
+            Object source = event.getSource();
+
+            if(source instanceof TabBrowser){
+                updateFindPanel();
+                updatePeriod(false);
+                PeriodView periodView = currentPeriodView();
+                boolean hasCurrentPeriod;
+                if(periodView == null) hasCurrentPeriod = false;
+                else                   hasCurrentPeriod = true;
+                Controller.this.actionManager.exposePeriod(hasCurrentPeriod);
+                if(hasCurrentPeriod){
+                    Village village = getVillage();
+                    if(village.isLocalArchive()){
+                        Controller.this.actionManager.exposeVillageLocal(hasCurrentPeriod);
+                    }else{
+                        Controller.this.actionManager.exposeVillage(hasCurrentPeriod);
+                    }
+                }
+            }
+
+            return;
+        }
+
     }
 
     /**
-     * アプリ正常終了処理
+     * 国村選択リストの選択展開操作を監視する
      */
-    private void shutdown(){
-        ConfigStore configStore = this.appSetting.getConfigStore();
+    private class VillageTreeWatcher
+            implements TreeSelectionListener, TreeWillExpandListener{
 
-        FindPanel findPanel = this.windowManager.getFindPanel();
-        JsObject findConf = findPanel.getJson();
-        if( ! findPanel.hasConfChanged(findConf) ){
-            configStore.saveHistoryConfig(findConf);
+        /**
+         * Constructor.
+         */
+        VillageTreeWatcher(){
+            super();
+            return;
         }
 
-        TalkPreview talkPreview = this.windowManager.getTalkPreview();
-        JsObject draftConf = talkPreview.getJson();
-        if( ! talkPreview.hasConfChanged(draftConf) ){
-            configStore.saveDraftConfig(draftConf);
+
+        /**
+         * {@inheritDoc}
+         *
+         * <p>ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
+         *
+         * @param event {@inheritDoc}
+         */
+        @Override
+        public void valueChanged(TreeSelectionEvent event){
+            TreePath path = event.getNewLeadSelectionPath();
+            if(path == null) return;
+
+            Object selObj = path.getLastPathComponent();
+            if(selObj instanceof Land){
+                Land land = (Land) selObj;
+                selectedLand(land);
+            }else if(selObj instanceof Village){
+                Village village = (Village) selObj;
+                village.setLocalArchive(false);
+                selectedVillage(village);
+            }
+
+            return;
         }
 
-        this.appSetting.saveConfig();
+        /**
+         * {@inheritDoc}
+         *
+         * <p>村選択ツリーリストが畳まれるとき呼ばれる。
+         *
+         * @param event ツリーイベント {@inheritDoc}
+         */
+        @Override
+        public void treeWillCollapse(TreeExpansionEvent event){
+            return;
+        }
 
-        LOGGER.info("VMごとアプリケーションを終了します。");
-        System.exit(0);  // invoke shutdown hooks... BYE !
+        /**
+         * {@inheritDoc}
+         *
+         * <p>村選択ツリーリストが展開されるとき呼ばれる。
+         *
+         * @param event ツリーイベント {@inheritDoc}
+         */
+        @Override
+        public void treeWillExpand(TreeExpansionEvent event){
+            if(!(event.getSource() instanceof JTree)){
+                return;
+            }
+
+            TreePath path = event.getPath();
+            Object lastObj = path.getLastPathComponent();
+            if(!(lastObj instanceof Land)){
+                return;
+            }
+            Land land = (Land) lastObj;
+            if(land.getVillageCount() > 0){
+                return;
+            }
+
+            submitReloadVillageList(land);
+
+            return;
+        }
 
-        assert false;
-        return;
     }
 
 }
index 7bc65bc..3160699 100644 (file)
@@ -35,7 +35,7 @@ public final class Jindolf {
      * Jindolf のスタートアップエントリ。
      *
      * <p>互換性検査が行われた後、
-     * JRE1.7解除版エントリ{@link JindolfJre17}
+     * JRE1.8解除版エントリ{@link JindolfJre18}
      * に制御を渡す。
      *
      * @param args コマンドライン引数
@@ -46,7 +46,7 @@ public final class Jindolf {
         exitCode = JreChecker.checkJre();
         if(exitCode != 0) System.exit(exitCode);
 
-        exitCode = JindolfJre17.main(args);
+        exitCode = JindolfJre18.main(args);
         if(exitCode != 0) System.exit(exitCode);
 
         // デーモンスレッドがいなければ、(アプリ画面が出ていなければ)
@@ -15,7 +15,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import javax.swing.JOptionPane;
 
 /**
- * JRE1.7の利用が解禁されたJindolfエントリ。
+ * JRE1.8の利用が解禁されたJindolfエントリ。
  *
  * <p>起動クラスJindolfの下請けとしての機能が想定される。
  *
@@ -24,7 +24,7 @@ import javax.swing.JOptionPane;
  *
  * <p>各種診断を通過した後、JindolfMainに制御を渡す。
  */
-public final class JindolfJre17 {
+public final class JindolfJre18 {
 
     /** GUI環境に接続できないときの終了コード。 */
     public static final int EXIT_CODE_HEADLESS = 1;
@@ -72,7 +72,7 @@ public final class JindolfJre17 {
     /**
      * 隠しコンストラクタ。
      */
-    private JindolfJre17(){
+    private JindolfJre18(){
         assert false;
     }
 
@@ -200,7 +200,7 @@ public final class JindolfJre17 {
     /**
      * Jindolf のスタートアップエントリ。
      *
-     * <p>ここからJRE1.7の利用が解禁される。
+     * <p>ここからJRE1.8の利用が解禁される。
      *
      * <p>最終的に{@link JindolfMain}へ制御が渡される。
      *
index 972ac20..e672c2c 100644 (file)
@@ -21,7 +21,7 @@ import jp.sfjp.jindolf.config.CmdOption;
 import jp.sfjp.jindolf.config.ConfigStore;
 import jp.sfjp.jindolf.config.EnvInfo;
 import jp.sfjp.jindolf.config.OptionInfo;
-import jp.sfjp.jindolf.data.LandsModel;
+import jp.sfjp.jindolf.data.LandsTreeModel;
 import jp.sfjp.jindolf.log.LogUtils;
 import jp.sfjp.jindolf.log.LoggingDispatcher;
 import jp.sfjp.jindolf.util.GUIUtils;
@@ -31,7 +31,7 @@ import jp.sfjp.jindolf.view.WindowManager;
 /**
  * Jindolf スタートアップクラス。
  *
- * <p>{@link JindolfJre17}の下請けとして本格的なJindolf起動処理に入る。
+ * <p>{@link JindolfJre18}の下請けとして本格的なJindolf起動処理に入る。
  */
 public final class JindolfMain {
 
@@ -137,7 +137,7 @@ public final class JindolfMain {
 
         ConfigStore configStore = appSetting.getConfigStore();
         if(configStore.useStoreFile()){
-            LOGGER.log(Level.INFO, LOG_CONF, configStore.getConfigPath());
+            LOGGER.log(Level.INFO, LOG_CONF, configStore.getConfigDir());
         }else{
             LOGGER.info(LOG_NOCONF);
         }
@@ -311,7 +311,7 @@ public final class JindolfMain {
      * @return アプリケーションのトップフレーム
      */
     private static JFrame buildMVC(AppSetting appSetting){
-        LandsModel model = new LandsModel();
+        LandsTreeModel model = new LandsTreeModel();
         WindowManager windowManager = new WindowManager();
         ActionManager actionManager = new ActionManager();
 
index bbfedb6..662d1e0 100644 (file)
@@ -35,7 +35,7 @@ import javax.swing.JOptionPane;
 public final class JreChecker {
 
     /** Jindolfが実行時に必要とするJREの版。 */
-    public static final String REQUIRED_JRE_VER = "1.7";
+    public static final String REQUIRED_JRE_VER = "1.8";
 
     /** 互換性エラーの終了コード。 */
     public static final int EXIT_CODE_INCOMPAT_JRE = 1;
@@ -63,6 +63,7 @@ public final class JreChecker {
 
     /**
      * クラス名に相当するクラスがロードできるか判定する。
+     *
      * @param klassName FQDNなクラス名
      * @return ロードできたらtrue
      */
@@ -81,96 +82,116 @@ public final class JreChecker {
 
     /**
      * JRE 1.1 相当のランタイムライブラリが提供されているか判定する。
+     *
      * @return 提供されているならtrue
      * @see java.io.Serializable
      */
-    public static boolean has11Runtime(){
+    public static boolean has1_1Runtime(){
         boolean result = hasClass("java.io.Serializable");
         return result;
     }
 
     /**
      * JRE 1.2 相当のランタイムライブラリが提供されているか判定する。
+     *
      * @return 提供されているならtrue
      * @see java.util.Iterator
      */
-    public static boolean has12Runtime(){
+    public static boolean has1_2Runtime(){
         boolean result;
-        if(has11Runtime()) result = hasClass("java.util.Iterator");
+        if(has1_1Runtime()) result = hasClass("java.util.Iterator");
         else               result = false;
         return result;
     }
 
     /**
      * JRE 1.3 相当のランタイムライブラリが提供されているか判定する。
+     *
      * @return 提供されているならtrue
      * @see java.util.TimerTask
      */
-    public static boolean has13Runtime(){
+    public static boolean has1_3Runtime(){
         boolean result;
-        if(has12Runtime()) result = hasClass("java.util.TimerTask");
+        if(has1_2Runtime()) result = hasClass("java.util.TimerTask");
         else               result = false;
         return result;
     }
 
     /**
      * JRE 1.4 相当のランタイムライブラリが提供されているか判定する。
+     *
      * @return 提供されているならtrue
      * @see java.lang.CharSequence
      */
-    public static boolean has14Runtime(){
+    public static boolean has1_4Runtime(){
         boolean result;
-        if(has13Runtime()) result = hasClass("java.lang.CharSequence");
+        if(has1_3Runtime()) result = hasClass("java.lang.CharSequence");
         else               result = false;
         return result;
     }
 
     /**
      * JRE 1.5 相当のランタイムライブラリが提供されているか判定する。
+     *
      * @return 提供されているならtrue
      * @see java.lang.Appendable
      */
-    public static boolean has15Runtime(){
+    public static boolean has1_5Runtime(){
         boolean result;
-        if(has14Runtime()) result = hasClass("java.lang.Appendable");
+        if(has1_4Runtime()) result = hasClass("java.lang.Appendable");
         else               result = false;
         return result;
     }
 
     /**
      * JRE 1.6 相当のランタイムライブラリが提供されているか判定する。
+     *
      * @return 提供されているならtrue
      * @see java.util.Deque
      */
-    public static boolean has16Runtime(){
+    public static boolean has1_6Runtime(){
         boolean result;
-        if(has15Runtime()) result = hasClass("java.util.Deque");
+        if(has1_5Runtime()) result = hasClass("java.util.Deque");
         else               result = false;
         return result;
     }
 
     /**
      * JRE 1.7 相当のランタイムライブラリが提供されているか判定する。
+     *
      * @return 提供されているならtrue
      * @see java.lang.AutoCloseable
      */
-    public static boolean has17Runtime(){
+    public static boolean has1_7Runtime(){
         boolean result;
-        if(has16Runtime()) result = hasClass("java.lang.AutoCloseable");
+        if(has1_6Runtime()) result = hasClass("java.lang.AutoCloseable");
         else               result = false;
         return result;
     }
 
-    // TODO JRE1.8 対応
+    /**
+     * JRE 1.8 相当のランタイムライブラリが提供されているか判定する。
+     *
+     * @return 提供されているならtrue
+     * @see java.lang.AutoCloseable
+     */
+    public static boolean has1_8Runtime(){
+        boolean result;
+        if(has1_7Runtime()) result = hasClass("java.util.stream.Stream");
+        else               result = false;
+        return result;
+    }
 
     /**
      * JREもしくは<code>java.lang</code>パッケージの
      * 仕様バージョンを返す。
+     *
      * <ol>
      * <li>システムプロパティ<code>java.specification.version</code>
      * <li>システムプロパティ<code>java.version</code>
      * <li><code>java.lang</code>パッケージの仕様バージョン
      * </ol>の順でバージョンが求められる。
+     *
      * @return 仕様バージョン文字列。不明ならnull
      */
     public static String getLangPkgSpec(){
@@ -199,7 +220,9 @@ public final class JreChecker {
 
     /**
      * JREのインストール情報を返す。
-     * システムプロパティ<code>java.home</code>の取得が試みられる。
+     *
+     * <p>システムプロパティ<code>java.home</code>の取得が試みられる。
+     *
      * @return インストール情報。不明ならnull
      */
     public static String getJreHome(){
@@ -216,6 +239,7 @@ public final class JreChecker {
 
     /**
      * 非互換エラーメッセージを組み立てる。
+     *
      * @return エラーメッセージ
      */
     public static String buildErrMessage(){
@@ -277,19 +301,19 @@ public final class JreChecker {
     }
 
     /**
-     * JRE環境をチェックする。(JRE1.7)
+     * JRE環境をチェックする。(JRE1.8)
      *
      * <p>もしJREの非互換性が検出されたらエラーメッセージを報告する。
      *
      * @return 互換性があれば0、無ければ非0
      */
     public static int checkJre(){
-        if(has17Runtime()) return 0;
+        if(has1_8Runtime()) return 0;
 
         String message = buildErrMessage();
         STDERR.println(message);
         STDERR.flush();
-        if(has12Runtime()){
+        if(has1_2Runtime()){
             showErrorDialog(message);
         }
 
index f360cbb..fad7176 100644 (file)
@@ -9,7 +9,6 @@ package jp.sfjp.jindolf;
 
 import java.awt.image.BufferedImage;
 import java.io.BufferedInputStream;
-import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -17,6 +16,7 @@ import java.io.LineNumberReader;
 import java.io.Reader;
 import java.net.URL;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.Properties;
 import javax.imageio.ImageIO;
 import javax.swing.Icon;
@@ -33,38 +33,33 @@ import jp.sfjp.jindolf.view.FlexiIcon;
  * リカバリの対象外とする。(ビルド工程の不手際扱い。)
  *
  * @see java.lang.Class#getResource
+ * @see java.lang.ClassLoader#getResource
  */
 public final class ResourceManager {
 
-    /** リソース名セパレータ文字。 */
-    public static final char RES_SEPCHAR = '/';
-    /** パッケージ名セパレータ文字。 */
-    public static final char PKG_SEPCHAR = '.';
-
-    /** リソース名セパレータ文字列。 */
-    public static final String RES_SEPARATOR =
-            Character.toString(RES_SEPCHAR);
-    /** パッケージ名セパレータ文字列。 */
-    public static final String PKG_SEPARATOR =
-            Character.toString(PKG_SEPCHAR);
-
     /**
      * デフォルトで用いられるルートパッケージ。
-     * 相対リソース名の起点となる。
+     *
+     * <p>相対リソース名の起点となる。
      */
     public static final Package DEF_ROOT_PACKAGE;
-    /** デフォルトで用いられるクラスローダ。 */
-    public static final ClassLoader DEF_LOADER;
 
-    private static final Charset CS_UTF8 = Charset.forName("UTF-8");
+    private static final Class<?> ROOT_KLASS = Jindolf.class;
+
+    private static final ClassLoader DEF_LOADER;
+
+    private static final char PKG_SEPCHAR = '.';
+    private static final char RES_SEPCHAR = '/';
+    private static final String RES_SEPARATOR =
+            Character.toString(RES_SEPCHAR);
+
+    private static final Charset CS_UTF8 = StandardCharsets.UTF_8;
 
     private static final int BTN_SZ = 24;
 
     static{
-        Class<?> rootKlass = Jindolf.class;
-
-        DEF_ROOT_PACKAGE = rootKlass.getPackage();
-        DEF_LOADER = rootKlass.getClassLoader();
+        DEF_ROOT_PACKAGE = ROOT_KLASS.getPackage();
+        DEF_LOADER = ROOT_KLASS.getClassLoader();
     }
 
 
@@ -79,16 +74,18 @@ public final class ResourceManager {
     /**
      * リソース名が絶対パスか否か判定する。
      *
-     * <p>リソース名が「/」で始まる場合絶対パスとみなされる。
+     * <p>リソース名が「/」で始まる場合、
+     * {@link Class}用の絶対パスとみなされる。
      *
-     * <p>ã\81\93ã\81®ã\83ªã\82½ã\83¼ã\82¹å\90\8dは{@link Class}用であって
-     * {@link ClassLoader}用ではない
+     * <p>ã\80\8c\80\8dã\81§å§\8bã\81¾ã\82\8bã\83ªã\82½ã\83¼ã\82¹å\90\8d表è¨\98は{@link Class}用であって
+     * {@link ClassLoader}では不要
      *
      * @param resPath リソース名
      * @return 絶対パスならtrueを返す。
      * @see java.lang.Class#getResource
+     * @see java.lang.ClassLoader#getResource
      */
-    public static boolean isAbsoluteResourcePath(String resPath){
+    private static boolean isAbsoluteResourcePath(String resPath){
         if (resPath.startsWith(RES_SEPARATOR)) return true;
         return false;
     }
@@ -104,19 +101,25 @@ public final class ResourceManager {
      * {@link Class}用ではないので、
      * 頭に「/」が付かない。
      *
+     * <p>パッケージ「com.example.test」の
+     * リソース名前置詞は「com/example/test/」
+     *
      * @param pkg パッケージ設定。nullは無名パッケージと認識される。
      * @return リソース名前置詞。無名パッケージの場合は空文字列が返る。
+     * @see java.lang.ClassLoader#getResource
      * @see java.lang.Class#getResource
      */
-    public static String getResourcePrefix(Package pkg){
+    private static String getResourcePrefix(Package pkg){
         if(pkg == null) return "";   // 無名パッケージ
 
         String pkgName = pkg.getName();
         String result = pkgName.replace(PKG_SEPCHAR, RES_SEPCHAR);
-        if(result.length() > 0){
+        if( ! result.isEmpty() ){
             result += RES_SEPARATOR;
         }
 
+        assert result.charAt(0) != RES_SEPCHAR;
+
         return result;
     }
 
@@ -124,6 +127,7 @@ public final class ResourceManager {
      * リソース名を用いて、
      * デフォルトのクラスローダとデフォルトのルートパッケージから
      * リソースのURLを取得する。
+     *
      * @param resPath リソース名
      * @return リソースのURL。リソースが見つからなければnull。
      */
@@ -134,35 +138,40 @@ public final class ResourceManager {
     /**
      * 任意のルートパッケージと相対リソース名を用いて、
      * デフォルトのクラスローダからリソースのURLを取得する。
+     *
      * @param rootPkg ルートパッケージ情報。
      *     「/」で始まる絶対リソース名が指定された場合は無視される。
      * @param resPath リソース名
      * @return リソースのURL。リソースが見つからなければnull。
      */
-    public static URL getResource(Package rootPkg, String resPath){
+    private static URL getResource(Package rootPkg, String resPath){
         return getResource(DEF_LOADER, rootPkg, resPath);
     }
 
     /**
      * 任意のルートパッケージと相対リソース名を用いて、
      * 任意のクラスローダからリソースのURLを取得する。
+     *
      * @param loader クラスローダ
      * @param rootPkg ルートパッケージ情報。
      *     「/」で始まる絶対リソース名が指定された場合は無視される。
      * @param resPath リソース名
      * @return リソースのURL。リソースが見つからなければnull。
      */
-    public static URL getResource(ClassLoader loader,
-                                    Package rootPkg,
-                                    String resPath ){
+    private static URL getResource(ClassLoader loader,
+                                   Package rootPkg,
+                                   String resPath ){
         String fullName;
         if(isAbsoluteResourcePath(resPath)){
-            fullName = resPath.substring(1);    // chop '/' heading
+            // chop '/' heading
+            fullName = resPath.substring(1);
         }else{
             String pfx = getResourcePrefix(rootPkg);
             fullName = pfx + resPath;
         }
 
+        assert ! isAbsoluteResourcePath(fullName);
+
         URL result = loader.getResource(fullName);
 
         return result;
@@ -172,6 +181,7 @@ public final class ResourceManager {
      * リソース名を用いて、
      * デフォルトのクラスローダとデフォルトのルートパッケージから
      * リソースの入力ストリームを取得する。
+     *
      * @param resPath リソース名
      * @return リソースの入力ストリーム。リソースが見つからなければnull。
      */
@@ -182,28 +192,30 @@ public final class ResourceManager {
     /**
      * 任意のルートパッケージと相対リソース名を用いて、
      * デフォルトのクラスローダからリソースの入力ストリームを取得する。
+     *
      * @param rootPkg ルートパッケージ情報。
      *     「/」で始まる絶対リソース名が指定された場合は無視される。
      * @param resPath リソース名
      * @return リソースの入力ストリーム。リソースが見つからなければnull。
      */
-    public static InputStream getResourceAsStream(Package rootPkg,
-                                                     String resPath ){
+    private static InputStream getResourceAsStream(Package rootPkg,
+                                                   String resPath ){
         return getResourceAsStream(DEF_LOADER, rootPkg, resPath);
     }
 
     /**
      * 任意のルートパッケージと相対リソース名を用いて、
      * 任意のクラスローダからリソースの入力ストリームを取得する。
+     *
      * @param loader クラスローダ
      * @param rootPkg ルートパッケージ情報。
      *     「/」で始まる絶対リソース名が指定された場合は無視される。
      * @param resPath リソース名
      * @return リソースの入力ストリーム。リソースが見つからなければnull。
      */
-    public static InputStream getResourceAsStream(ClassLoader loader,
-                                                     Package rootPkg,
-                                                     String resPath ){
+    private static InputStream getResourceAsStream(ClassLoader loader,
+                                                   Package rootPkg,
+                                                   String resPath ){
         URL url = getResource(loader, rootPkg, resPath);
         if(url == null) return null;
 
@@ -219,6 +231,7 @@ public final class ResourceManager {
 
     /**
      * リソース名を用いてイメージ画像を取得する。
+     *
      * @param resPath 画像リソース名
      * @return イメージ画像。リソースが見つからなければnull。
      */
@@ -238,6 +251,7 @@ public final class ResourceManager {
 
     /**
      * リソース名を用いてアイコン画像を取得する。
+     *
      * @param resPath アイコン画像リソース名
      * @return アイコン画像。リソースが見つからなければnull。
      */
@@ -276,24 +290,18 @@ public final class ResourceManager {
 
     /**
      * リソース名を用いてプロパティを取得する。
+     *
      * @param resPath プロパティファイルのリソース名
      * @return プロパティ。リソースが読み込めなければnull。
      */
     public static Properties getProperties(String resPath){
         InputStream is = getResourceAsStream(resPath);
         if(is == null) return null;
-        is = new BufferedInputStream(is);
 
         Properties properties = new Properties();
 
-        try{
-            properties.load(is);
-        }catch(IOException e){
-            properties = null;
-        }
-
-        try{
-            is.close();
+        try(InputStream pis = new BufferedInputStream(is)){
+            properties.load(pis);
         }catch(IOException e){
             properties = null;
         }
@@ -310,31 +318,23 @@ public final class ResourceManager {
      * @return テキスト。リソースが読み込めなければnull。
      */
     public static String getTextFile(String resPath){
-        InputStream is = getResourceAsStream(resPath);
+        InputStream is;
+        is = getResourceAsStream(resPath);
         if(is == null) return null;
         is = new BufferedInputStream(is);
 
         Reader reader = new InputStreamReader(is, CS_UTF8);
-        reader = new BufferedReader(reader);
-        LineNumberReader lineReader = new LineNumberReader(reader);
 
         StringBuilder result = new StringBuilder();
 
-        for(;;){
-            String line;
-            try{
+        try(LineNumberReader lineReader = new LineNumberReader(reader)){
+            for(;;){
+                String line;
                 line = lineReader.readLine();
-            }catch(IOException e){
-                result = null;
-                break;
+                if(line == null) break;
+                if(line.startsWith("#")) continue;
+                result.append(line).append('\n');
             }
-            if(line == null) break;
-            if(line.startsWith("#")) continue;
-            result.append(line).append('\n');
-        }
-
-        try{
-            lineReader.close();
         }catch(IOException e){
             result = null;
         }
index 373d209..8ffab6d 100644 (file)
@@ -9,14 +9,29 @@ package jp.sfjp.jindolf.config;
 
 import java.awt.Font;
 import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+import javax.imageio.ImageIO;
+import jp.sfjp.jindolf.data.Avatar;
 import jp.sfjp.jindolf.data.DialogPref;
 import jp.sfjp.jindolf.glyph.Font2Json;
 import jp.sfjp.jindolf.glyph.FontInfo;
 import jp.sfjp.jindolf.net.ProxyInfo;
+import jp.sfjp.jindolf.view.AvatarPics;
+import jp.sfjp.jindolf.view.LocalAvatarImg;
 import jp.sourceforge.jovsonz.JsBoolean;
 import jp.sourceforge.jovsonz.JsObject;
 import jp.sourceforge.jovsonz.JsPair;
+import jp.sourceforge.jovsonz.JsString;
+import jp.sourceforge.jovsonz.JsTypes;
 import jp.sourceforge.jovsonz.JsValue;
 
 /**
@@ -35,6 +50,12 @@ public class AppSetting{
     private static final String HASH_ALIGNBALOON = "alignBaloonWidth";
     private static final String HASH_PROXY       = "proxy";
 
+    private static final String MSG_NOIMG =
+            "画像ファイル{0}が読み込めないため"
+            + "{1}の表示に代替イメージを使います。";
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
 
     private final OptionInfo optInfo;
     private final ConfigStore configStore;
@@ -49,6 +70,10 @@ public class AppSetting{
     private JsValue loadedNetConfig;
     private JsValue loadedTalkConfig;
 
+    private final Map<String, BufferedImage> avatarFaceMap = new HashMap<>();
+    private final Map<String, BufferedImage> avatarBodyMap = new HashMap<>();
+
+
     /**
      * コンストラクタ。
      * @param info コマンドライン引数
@@ -334,6 +359,116 @@ public class AppSetting{
     }
 
     /**
+     * JSONをパースしAvatar-Image間マップに反映させる。
+     *
+     * @param json JSON構造
+     * @param map マップ
+     */
+    private void parseImgMap(JsObject json, Map<String, BufferedImage> map){
+        Path imgDir = this.configStore.getLocalImgDir();
+
+        List<JsPair> pairList = json.getPairList();
+        for(JsPair pair : pairList){
+            String avatarId = pair.getName();
+            JsValue value = pair.getValue();
+
+            if(value.getJsTypes() != JsTypes.STRING) continue;
+            JsString sVal = (JsString)value;
+            String imgName = sVal.toRawString();
+
+            Path imgPath = Paths.get(imgName);
+            Path full = imgDir.resolve(imgPath);
+            File file = full.toFile();
+            if(        ! file.isAbsolute()
+                    || ! file.exists()
+                    || ! file.isFile()
+                    || ! file.canRead() ){
+                String msg = MessageFormat.format(
+                        MSG_NOIMG, file.getPath(), avatarId
+                );
+                LOGGER.info(msg);
+                continue;
+            }
+
+            BufferedImage image;
+            try {
+                image = ImageIO.read(file);
+            }catch(IOException e){
+                String msg = MessageFormat.format(
+                        MSG_NOIMG, file.getPath(), avatarId
+                );
+                LOGGER.info(msg);
+                continue;
+            }
+
+            map.put(avatarId, image);
+        }
+
+        return;
+    }
+
+    /**
+     * ローカル画像設定をロードする。
+     */
+    private void loadLocalImageConfig(){
+        JsObject root = this.configStore.loadLocalImgConfig();
+        if(root == null) return;
+
+        JsValue faceConfig = root.getValue("avatarFace");
+        JsValue bodyConfig = root.getValue("avatarBody");
+        if(faceConfig.getJsTypes() != JsTypes.OBJECT) return;
+        if(bodyConfig.getJsTypes() != JsTypes.OBJECT) return;
+
+        JsObject jsonFace = (JsObject) faceConfig;
+        parseImgMap(jsonFace, this.avatarFaceMap);
+
+
+        JsObject jsonBody = (JsObject) bodyConfig;
+        parseImgMap(jsonBody, this.avatarBodyMap);
+
+        return;
+    }
+
+    /**
+     * ローカル代替イメージを画像キャッシュに反映させる。
+     *
+     * @param avatarPics 画像キャッシュ
+     */
+    public void applyLocalImage(AvatarPics avatarPics){
+        BufferedImage graveImage     = this.avatarFaceMap.get("tomb");
+        BufferedImage graveBodyImage = this.avatarBodyMap.get("tomb");
+
+        if(graveImage == null){
+            graveImage = LocalAvatarImg.getGraveImage();
+        }
+        if(graveBodyImage == null){
+            graveBodyImage = LocalAvatarImg.getGraveBodyImage();
+        }
+
+        avatarPics.setGraveImage(graveImage);
+        avatarPics.setGraveBodyImage(graveBodyImage);
+
+        for(Avatar avatar : Avatar.getPredefinedAvatarList()){
+            String avatarId = avatar.getIdentifier();
+
+            BufferedImage faceImage = this.avatarFaceMap.get(avatarId);
+            BufferedImage bodyImage = this.avatarBodyMap.get(avatarId);
+
+            if(faceImage == null){
+                faceImage = LocalAvatarImg.getAvatarFaceImage(avatarId);
+            }
+            if(bodyImage == null){
+                bodyImage = LocalAvatarImg.getAvatarBodyImage(avatarId);
+            }
+
+            avatarPics.setAvatarFaceImage(avatar, faceImage);
+            avatarPics.setAvatarBodyImage(avatar, bodyImage);
+        }
+
+        return;
+    }
+
+    /**
      * ネットワーク設定をセーブする。
      */
     private void saveNetConfig(){
@@ -392,6 +527,7 @@ public class AppSetting{
     public void loadConfig(){
         loadNetConfig();
         loadTalkConfig();
+        loadLocalImageConfig();
         return;
     }
 
index fe92f87..35efc47 100644 (file)
@@ -10,13 +10,19 @@ package jp.sfjp.jindolf.config;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.io.Writer;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import javax.swing.JDialog;
 import javax.swing.JOptionPane;
+import jp.sfjp.jindolf.ResourceManager;
 import jp.sfjp.jindolf.VerInfo;
 import jp.sfjp.jindolf.view.LockErrorPane;
 
@@ -31,7 +37,7 @@ public final class ConfigFile{
     private static final String JINCONF     = "Jindolf";
     private static final String JINCONF_DOT = ".jindolf";
     private static final String FILE_README = "README.txt";
-    private static final Charset CHARSET_README = Charset.forName("UTF-8");
+    private static final Charset CHARSET_README = StandardCharsets.UTF_8;
 
     private static final String MSG_POST =
             "<ul>"
@@ -48,22 +54,31 @@ public final class ConfigFile{
      * 隠れコンストラクタ。
      */
     private ConfigFile(){
-        super();
+        assert false;
         return;
     }
 
 
     /**
      * 暗黙的な設定格納ディレクトリを返す。
-     * 起動元JARファイルと同じディレクトリに、
+     *
+     * <ul>
+     *
+     * <li>起動元JARファイルと同じディレクトリに、
      * アクセス可能なディレクトリ"Jindolf"が
      * すでに存在していればそれを返す。
-     * 起動元JARファイルおよび"Jindolf"が発見できなければ、
+     *
+     * <li>起動元JARファイルおよび"Jindolf"が発見できなければ、
      * MacOSX環境の場合"~/Library/Application Support/Jindolf/"を返す。
      * Windows環境の場合"%USERPROFILE%\Jindolf\"を返す。
-     * それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
-     * 返すディレクトリが存在しているか否か、
+     *
+     * <li>それ以外の環境(Linux,etc?)の場合"~/.jindolf/"を返す。
+     *
+     * </ul>
+     *
+     * <p>返すディレクトリが存在しているか否か、
      * アクセス可能か否かは呼び出し元で判断せよ。
+     *
      * @return 設定格納ディレクトリ
      */
     public static File getImplicitConfigDirectory(){
@@ -91,14 +106,16 @@ public final class ConfigFile{
 
     /**
      * まだ存在しない設定格納ディレクトリを新規に作成する。
-     * エラーがあればダイアログ提示とともにVM終了する。
+     *
+     * <p>エラーがあればダイアログ提示とともにVM終了する。
+     *
      * @param confPath 設定格納ディレクトリ
      * @param isImplicitPath ディレクトリが暗黙的に指定されたものならtrue。
      * @return 新規に作成した設定格納ディレクトリ
      * @throws IllegalArgumentException すでにそのディレクトリは存在する。
      */
     public static File buildConfigDirectory(File confPath,
-                                               boolean isImplicitPath )
+                                            boolean isImplicitPath )
             throws IllegalArgumentException{
         if(confPath.exists()) throw new IllegalArgumentException();
 
@@ -154,9 +171,41 @@ public final class ConfigFile{
     }
 
     /**
+     * ローカル画像キャッシュディレクトリを作る。
+     *
+     * <p>作られたディレクトリ内に
+     * ファイルavatarCache.jsonが作られる。
+     *
+     * @param imgCacheDir ローカル画像キャッシュディレクトリ
+     */
+    public static void buildImageCacheDir(File imgCacheDir){
+        if(imgCacheDir.exists()) return;
+
+        String jsonRes = "resources/image/avatarCache.json";
+        InputStream is = ResourceManager.getResourceAsStream(jsonRes);
+        if(is == null) return;
+
+        imgCacheDir.mkdirs();
+        ConfigFile.checkAccessibility(imgCacheDir);
+
+        Path cachePath = imgCacheDir.toPath();
+        Path jsonLeaf = Paths.get("avatarCache.json");
+        Path path = cachePath.resolve(jsonLeaf);
+        try{
+            Files.copy(is, path);
+        }catch(IOException e){
+            abortCantAccessConfigDir(path.toFile());
+        }
+
+        return;
+    }
+
+    /**
      * 設定ディレクトリ操作の
      * 共通エラーメッセージ確認ダイアログを表示する。
-     * 閉じるまで待つ。
+     *
+     * <p>閉じるまで待つ。
+     *
      * @param seq メッセージ
      */
     private static void showErrorMessage(CharSequence seq){
@@ -170,7 +219,9 @@ public final class ConfigFile{
     /**
      * 設定ディレクトリ操作の
      * 共通エラーメッセージ確認ダイアログを表示する。
-     * 閉じるまで待つ。
+     *
+     * <p>閉じるまで待つ。
+     *
      * @param seq メッセージ
      */
     private static void showWarnMessage(CharSequence seq){
@@ -184,7 +235,9 @@ public final class ConfigFile{
     /**
      * 設定ディレクトリ操作の
      * 情報提示メッセージ確認ダイアログを表示する。
-     * 閉じるまで待つ。
+     *
+     * <p>閉じるまで待つ。
+     *
      * @param seq メッセージ
      */
     private static void showInfoMessage(CharSequence seq){
@@ -197,6 +250,7 @@ public final class ConfigFile{
 
     /**
      * ダイアログを表示し、閉じられるまで待つ。
+     *
      * @param pane ダイアログの元となるペイン
      */
     private static void showDialog(JOptionPane pane){
@@ -222,6 +276,7 @@ public final class ConfigFile{
     /**
      * 設定ディレクトリのルートファイルシステムもしくはドライブレターに
      * アクセスできないエラーをダイアログに提示し、VM終了する。
+     *
      * @param path 設定ディレクトリ
      * @param preMessage メッセージ前半
      */
@@ -242,6 +297,7 @@ public final class ConfigFile{
     /**
      * 設定ディレクトリの祖先に書き込めないエラーをダイアログで提示し、
      * VM終了する。
+     *
      * @param existsAncestor 存在するもっとも近い祖先
      * @param preMessage メッセージ前半
      */
@@ -262,6 +318,7 @@ public final class ConfigFile{
 
     /**
      * 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。
+     *
      * @param existsAncestor 存在するもっとも近い祖先
      * @param preMessage メッセージ前半
      * @return 生成してよいと指示があればtrue
@@ -312,6 +369,7 @@ public final class ConfigFile{
     /**
      * 設定ディレクトリが生成できないエラーをダイアログで提示し、
      * VM終了する。
+     *
      * @param path 生成できなかったディレクトリ
      */
     private static void abortCantBuildConfigDir(File path){
@@ -330,6 +388,7 @@ public final class ConfigFile{
     /**
      * 設定ディレクトリへアクセスできないエラーをダイアログで提示し、
      * VM終了する。
+     *
      * @param path アクセスできないディレクトリ
      */
     private static void abortCantAccessConfigDir(File path){
@@ -349,6 +408,7 @@ public final class ConfigFile{
 
     /**
      * ファイルに書き込めないエラーをダイアログで提示し、VM終了する。
+     *
      * @param file 書き込めなかったファイル
      */
     private static void abortCantWrite(File file){
@@ -365,7 +425,9 @@ public final class ConfigFile{
 
     /**
      * 指定されたディレクトリにREADMEファイルを生成する。
-     * 生成できなければダイアログ表示とともにVM終了する。
+     *
+     * <p>生成できなければダイアログ表示とともにVM終了する。
+     *
      * @param path READMEの格納ディレクトリ
      */
     private static void touchReadme(File path){
@@ -413,6 +475,7 @@ public final class ConfigFile{
     /**
      * 設定ディレクトリがアクセス可能でなければ
      * エラーダイアログを出してVM終了する。
+     *
      * @param confDir 設定ディレクトリ
      */
     public static void checkAccessibility(File confDir){
@@ -425,6 +488,7 @@ public final class ConfigFile{
 
     /**
      * センタリングされたファイル名表示のHTML表記を出力する。
+     *
      * @param path ファイル
      * @return HTML表記
      */
@@ -437,10 +501,13 @@ public final class ConfigFile{
 
     /**
      * ロックエラーダイアログの表示。
-     * 呼び出しから戻ってもまだロックオブジェクトが
+     *
+     * <p>呼び出しから戻ってもまだロックオブジェクトが
      * ロックファイルのオーナーでない場合、
      * 今後設定ディレクトリは一切使わずに起動を続行するものとする。
-     * ロックファイルの強制解除に失敗した場合はVM終了する。
+     *
+     * <p>ロックファイルの強制解除に失敗した場合はVM終了する。
+     *
      * @param lock エラーを起こしたロック
      */
     public static void confirmLockError(InterVMLock lock){
index 28b3e9c..720c5e6 100644 (file)
@@ -23,6 +23,9 @@ import java.io.OutputStreamWriter;
 import java.io.Reader;
 import java.io.Writer;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import jp.sourceforge.jovsonz.JsComposition;
@@ -39,60 +42,51 @@ public class ConfigStore {
 
     /** 検索履歴ファイル。 */
     public static final File HIST_FILE = new File("searchHistory.json");
-    /** 原稿ファイル。 */
-    public static final File DRAFT_FILE = new File("draft.json");
     /** ネットワーク設定ファイル。 */
     public static final File NETCONFIG_FILE = new File("netconfig.json");
     /** 台詞表示設定ファイル。 */
     public static final File TALKCONFIG_FILE = new File("talkconfig.json");
+    /** ローカル画像格納ディレクトリ。 */
+    public static final Path LOCALIMG_DIR = Paths.get("img");
+    /** ローカル画像設定ファイル。 */
+    public static final Path LOCALIMGCONFIG_PATH =
+            Paths.get("avatarCache.json");
 
     private static final String LOCKFILE = "lock";
 
-    private static final Charset CHARSET_JSON = Charset.forName("UTF-8");
+    private static final Charset CHARSET_JSON = StandardCharsets.UTF_8;
 
     private static final Logger LOGGER = Logger.getAnonymousLogger();
 
 
     private boolean useStoreFile;
     private boolean isImplicitPath;
-    private File configPath;
+    private File configDir;
 
 
     /**
      * コンストラクタ。
-     * @param useStoreFile 設定ディレクトリへの永続化機能を使うならtrue
-     * @param configPath 設定ディレクトリ。
-     *     設定ディレクトリを使わない場合は無視され、nullとして扱われる。
-     */
-    public ConfigStore(boolean useStoreFile, File configPath ){
-        this(useStoreFile, true, configPath);
-        return;
-    }
-
-    /**
-     * コンストラクタ。
-     * @param useStoreFile 設定ディレクトリへの永続化機能を使うならtrue
-     * @param isImplicitPath コマンドラインで指定されたディレクトリならfalse
-     * @param configPath 設定ディレクトリ。
-     *     設定ディレクトリを使わない場合は無視され、nullとして扱われる。
+     *
+     * @param useStoreFile 設定ディレクトリ内への
+     *     セーブデータ機能を使うならtrue
+     * @param isImplicitPath 起動コマンドラインから指定された
+     *     設定ディレクトリの場合false
+     * @param configDirPath 設定ディレクトリ。
+     *     設定ディレクトリを使わない場合は無視される。
      */
     public ConfigStore(boolean useStoreFile,
-                         boolean isImplicitPath,
-                         File configPath ){
+                       boolean isImplicitPath,
+                       File configDirPath ){
         super();
 
         this.useStoreFile = useStoreFile;
 
         if(this.useStoreFile){
             this.isImplicitPath = isImplicitPath;
+            this.configDir = configDirPath;
         }else{
             this.isImplicitPath = true;
-        }
-
-        if(this.useStoreFile){
-            this.configPath = configPath;
-        }else{
-            this.configPath = null;
+            this.configDir = null;
         }
 
         return;
@@ -101,6 +95,7 @@ public class ConfigStore {
 
     /**
      * 設定ディレクトリを使うか否か判定する。
+     *
      * @return 設定ディレクトリを使うならtrue。
      */
     public boolean useStoreFile(){
@@ -109,12 +104,24 @@ public class ConfigStore {
 
     /**
      * 設定ディレクトリを返す。
+     *
      * @return 設定ディレクトリ。設定ディレクトリを使わない場合はnull
      */
-    public File getConfigPath(){
-        File result;
-        if(this.useStoreFile) result = this.configPath;
-        else                  result = null;
+    public File getConfigDir(){
+        return this.configDir;
+    }
+
+    /**
+     * ローカル画像格納ディレクトリを返す。
+     *
+     * @return 格納ディレクトリ。格納ディレクトリを使わない場合はnull
+     */
+    public Path getLocalImgDir(){
+        if(this.configDir == null) return null;
+
+        Path configPath = this.configDir.toPath();
+        Path result = configPath.resolve(LOCALIMG_DIR);
+
         return result;
     }
 
@@ -126,13 +133,18 @@ public class ConfigStore {
     public void prepareConfigDir(){
         if( ! this.useStoreFile ) return;
 
-        if( ! this.configPath.exists() ){
+        if( ! this.configDir.exists() ){
             File created =
-                ConfigFile.buildConfigDirectory(this.configPath,
+                ConfigFile.buildConfigDirectory(this.configDir,
                                                 this.isImplicitPath );
             ConfigFile.checkAccessibility(created);
         }else{
-            ConfigFile.checkAccessibility(this.configPath);
+            ConfigFile.checkAccessibility(this.configDir);
+        }
+
+        File imgDir = new File(this.configDir, "img");
+        if( ! imgDir.exists()){
+            ConfigFile.buildImageCacheDir(imgDir);
         }
 
         return;
@@ -140,11 +152,14 @@ public class ConfigStore {
 
     /**
      * ロックファイルの取得を試みる。
+     *
+     * <p>ロックに失敗したが処理を続行する場合、
+     * 設定ディレクトリは使わないものとして続行する。
      */
     public void tryLock(){
         if( ! this.useStoreFile ) return;
 
-        File lockFile = new File(this.configPath, LOCKFILE);
+        File lockFile = new File(this.configDir, LOCKFILE);
         InterVMLock lock = new InterVMLock(lockFile);
 
         lock.tryLock();
@@ -153,7 +168,8 @@ public class ConfigStore {
             ConfigFile.confirmLockError(lock);
             if( ! lock.isFileOwner() ){
                 this.useStoreFile = false;
-                this.configPath = null;
+                this.isImplicitPath = true;
+                this.configDir = null;
             }
         }
 
@@ -162,6 +178,7 @@ public class ConfigStore {
 
     /**
      * 設定ディレクトリ上のOBJECT型JSONファイルを読み込む。
+     *
      * @param file JSONファイルの相対パス。
      * @return JSON object。
      *     設定ディレクトリを使わない設定、
@@ -178,6 +195,7 @@ public class ConfigStore {
 
     /**
      * 設定ディレクトリ上のJSONファイルを読み込む。
+     *
      * @param file JSONファイルの相対パス
      * @return JSON objectまたはarray。
      *     設定ディレクトリを使わない設定、
@@ -191,8 +209,8 @@ public class ConfigStore {
         if(file.isAbsolute()){
             absFile = file;
         }else{
-            if(this.configPath == null) return null;
-            absFile = new File(this.configPath, file.getPath());
+            if(this.configDir == null) return null;
+            absFile = new File(this.configDir, file.getPath());
             if( ! absFile.exists() ) return null;
             if( ! absFile.isAbsolute() ) return null;
         }
@@ -257,6 +275,7 @@ public class ConfigStore {
 
     /**
      * 文字ストリーム上のJSONデータを読み込む。
+     *
      * @param reader 文字ストリーム
      * @return JSON objectまたはarray。
      * @throws IOException 入力エラー
@@ -270,6 +289,7 @@ public class ConfigStore {
 
     /**
      * 設定ディレクトリ上のJSONファイルに書き込む。
+     *
      * @param file JSONファイルの相対パス
      * @param root JSON objectまたはarray
      * @return 正しくセーブが行われればtrue。
@@ -279,7 +299,7 @@ public class ConfigStore {
         if( ! this.useStoreFile ) return false;
 
         // TODO テンポラリファイルを用いたより安全なファイル更新
-        File absFile = new File(this.configPath, file.getPath());
+        File absFile = new File(this.configDir, file.getPath());
         String absPath = absFile.getPath();
 
         absFile.delete();
@@ -351,6 +371,7 @@ public class ConfigStore {
 
     /**
      * 文字ストリームにJSONデータを書き込む。
+     *
      * @param writer 文字ストリーム出力
      * @param root JSON objectまたはarray
      * @throws IOException 出力エラー
@@ -364,6 +385,7 @@ public class ConfigStore {
 
     /**
      * 検索履歴ファイルを読み込む。
+     *
      * @return 履歴データ。履歴を読まないもしくは読めない場合はnull
      */
     public JsObject loadHistoryConfig(){
@@ -372,16 +394,8 @@ public class ConfigStore {
     }
 
     /**
-     * 原稿ファイルを読み込む。
-     * @return 原稿データ。原稿を読まないもしくは読めない場合はnull
-     */
-    public JsObject loadDraftConfig(){
-        JsObject result = loadJsObject(DRAFT_FILE);
-        return result;
-    }
-
-    /**
      * ネットワーク設定ファイルを読み込む。
+     *
      * @return ネットワーク設定データ。
      *     設定を読まないもしくは読めない場合はnull
      */
@@ -392,6 +406,7 @@ public class ConfigStore {
 
     /**
      * 台詞表示設定ファイルを読み込む。
+     *
      * @return 台詞表示設定データ。
      *     設定を読まないもしくは読めない場合はnull
      */
@@ -401,27 +416,31 @@ public class ConfigStore {
     }
 
     /**
-     * 検索履歴ファイルに書き込む。
-     * @param root 履歴データ
-     * @return 書き込まなかったもしくは書き込めなかった場合はfalse
+     * ローカル画像設定ファイルを読み込む。
+     *
+     * @return ローカル画像設定データ。
+     *     設定を読まないもしくは読めない場合はnull
      */
-    public boolean saveHistoryConfig(JsComposition<?> root){
-        boolean result = saveJson(HIST_FILE, root);
+    public JsObject loadLocalImgConfig(){
+        Path path = LOCALIMG_DIR.resolve(LOCALIMGCONFIG_PATH);
+        JsObject result = loadJsObject(path.toFile());
         return result;
     }
 
     /**
-     * 原稿ファイルに書き込む。
-     * @param root 原稿データ
+     * 検索履歴ファイルに書き込む。
+     *
+     * @param root 履歴データ
      * @return 書き込まなかったもしくは書き込めなかった場合はfalse
      */
-    public boolean saveDraftConfig(JsComposition<?> root){
-        boolean result = saveJson(DRAFT_FILE, root);
+    public boolean saveHistoryConfig(JsComposition<?> root){
+        boolean result = saveJson(HIST_FILE, root);
         return result;
     }
 
     /**
      * ネットワーク設定ファイルに書き込む。
+     *
      * @param root ネットワーク設定
      * @return 書き込まなかったもしくは書き込めなかった場合はfalse
      */
@@ -432,6 +451,7 @@ public class ConfigStore {
 
     /**
      * 台詞表示設定ファイルに書き込む。
+     *
      * @param root 台詞表示設定
      * @return 書き込まなかったもしくは書き込めなかった場合はfalse
      */
index 7eb1b9b..4bbb20d 100644 (file)
@@ -11,7 +11,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import jp.sfjp.jindolf.util.StringUtils;
 
 /**
  * 発言アンカー。
@@ -186,7 +185,7 @@ public final class Anchor{
 
         /* G国アンカー */
         if(matcher.start(14) < matcher.end(14)){
-            int talkNo = StringUtils.parseInt(source, matcher, 14);
+            int talkNo = parseInt(source, matcher, 14);
             Anchor anchor = new Anchor(source, startPos, endPos, talkNo);
             return anchor;
         }
@@ -198,7 +197,7 @@ public final class Anchor{
             }else if(matcher.start(3) < matcher.end(3)){ // epilogue
                 day = EPILOGUEDAY;
             }else if(matcher.start(4) < matcher.end(4)){  // etc) "6d"
-                day = StringUtils.parseInt(source, matcher, 4);
+                day = parseInt(source, matcher, 4);
             }else{
                 assert false;
                 return null;
@@ -229,8 +228,8 @@ public final class Anchor{
             assert false;
             return null;
         }
-        int hour   = StringUtils.parseInt(source, matcher, hourGroup);
-        int minute = StringUtils.parseInt(source, matcher, minuteGroup);
+        int hour   = parseInt(source, matcher, hourGroup);
+        int minute = parseInt(source, matcher, minuteGroup);
 
         if(isPM && hour < 12) hour += 12;
         hour %= 24;
@@ -246,6 +245,49 @@ public final class Anchor{
     }
 
     /**
+     * 正規表現にマッチした領域を数値化する。
+     *
+     * @param seq 文字列
+     * @param matcher Matcher
+     * @param groupIndex 前方指定グループ番号
+     * @return 数値
+     * @throws IndexOutOfBoundsException 不正なグループ番号
+     */
+    static int parseInt(CharSequence seq,
+                        Matcher matcher,
+                        int groupIndex )
+            throws IndexOutOfBoundsException {
+        int startPos = matcher.start(groupIndex);
+        int endPos   = matcher.end(groupIndex);
+        return parseInt(seq, startPos, endPos);
+    }
+
+    /**
+     * 部分文字列を数値化する。
+     *
+     * @param seq 文字列
+     * @param startPos 範囲開始位置
+     * @param endPos 範囲終了位置
+     * @return パースした数値
+     * @throws IndexOutOfBoundsException 不正な位置指定
+     */
+    static int parseInt(CharSequence seq, int startPos, int endPos)
+            throws IndexOutOfBoundsException{
+        int result = 0;
+
+        for(int pos = startPos; pos < endPos; pos++){
+            char ch = seq.charAt(pos);
+            int digit = Character.digit(ch, 10);
+            if(digit < 0) break;
+            result *= 10;
+            result += digit;
+        }
+
+        return result;
+    }
+
+
+    /**
      * アンカーの含まれる文字列を返す。
      * @return アンカーの含まれる文字列
      */
index 4925e09..7b17f9d 100644 (file)
@@ -7,69 +7,50 @@
 
 package jp.sfjp.jindolf.data;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.RandomAccess;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.ParserConfigurationException;
-import jp.sfjp.jindolf.dxchg.XmlUtils;
+import java.util.stream.Collectors;
 import jp.sourceforge.jindolf.corelib.PreDefAvatar;
-import org.xml.sax.SAXException;
 
 /**
  * Avatar またの名をキャラクター。
+ *
+ * <p>ゲルトもAvatarである。
+ * 墓石は「Avatarの状態」であってAvatarそのものではない。
+ *
+ * <p>プロローグが終わり参加プレイヤーが固定されるまでの間、
+ * 複数のプレイヤーが同一Avatarを担当しうる。
+ *
+ * <p>Avatar同士は通し番号により一意に順序づけられる。
+ *
+ * <p>設計メモ: 未知のAvatar出現に備え、
+ * {@link PreDefAvatar}と分離したクラスとして設計された。
+ *
+ * <p>2020-03現在、Avatarは20インスタンスで固定。
+ * Z国含め未知のAvatarが追加されるケースは今後考慮しない。
  */
 public class Avatar implements Comparable<Avatar> {
 
     /** ゲルト。 */
     public static final Avatar AVATAR_GERD;
 
-    private static final List<Avatar>        AVATAR_LIST;
-    private static final Map<String, Avatar> AVATAR_MAP;
+    private static final List<Avatar>        AVATAR_LIST = buildAvatarList();
+    private static final Map<String, Avatar> AVATAR_FN_MAP = new HashMap<>();
+    private static final Map<String, Avatar> AVATAR_ID_MAP = new HashMap<>();
 
-    private static final Pattern AVATAR_PATTERN;
 
     static{
-        List<PreDefAvatar>  predefs;
-        try{
-            DocumentBuilder builder = XmlUtils.createDocumentBuilder();
-            predefs = PreDefAvatar.buildPreDefAvatarList(builder);
-        }catch(   IOException
-                | ParserConfigurationException
-                | SAXException
-                | URISyntaxException
-                e){
-            throw new ExceptionInInitializerError(e);
-        }
-
-        AVATAR_LIST = buildAvatarList(predefs);
-
-        AVATAR_MAP = new HashMap<>();
-        for(Avatar avatar : AVATAR_LIST){
+        AVATAR_LIST.forEach(avatar -> {
             String fullName = avatar.getFullName();
-            AVATAR_MAP.put(fullName, avatar);
-        }
+            String avatarId = avatar.getIdentifier();
+            AVATAR_FN_MAP.put(fullName, avatar);
+            AVATAR_ID_MAP.put(avatarId, avatar);
+        });
 
-        StringBuilder avatarGroupRegex = new StringBuilder();
-        for(Avatar avatar : AVATAR_LIST){
-            String fullName = avatar.getFullName();
-            if(avatarGroupRegex.length() > 0){
-                avatarGroupRegex.append('|');
-            }
-            avatarGroupRegex.append('(')
-                            .append(Pattern.quote(fullName))
-                            .append(')');
-        }
-        AVATAR_PATTERN = Pattern.compile(avatarGroupRegex.toString());
-
-        AVATAR_GERD = getPredefinedAvatar("楽天家 ゲルト");
+        AVATAR_GERD = getAvatarById("gerd");
 
         assert AVATAR_LIST instanceof RandomAccess;
         assert AVATAR_GERD != null;
@@ -81,20 +62,24 @@ public class Avatar implements Comparable<Avatar> {
     private final String fullName;
     private final int idNum;
     private final String identifier;
-    private final int hashNum;
 
 
     /**
-     * Avatarを生成する。
+     * constructor.
+     *
+     * <p>全ての引数は他のインスタンスに対しユニークでなければならない。
+     *
      * @param name 名前
      * @param jobTitle 職業名
      * @param idNum 通し番号
      * @param identifier 識別文字列
      */
     private Avatar(String name,
-                    String jobTitle,
-                    int idNum,
-                    String identifier ){
+                   String jobTitle,
+                   int idNum,
+                   String identifier ){
+        super();
+
         this.name = name.intern();
         this.jobTitle = jobTitle.intern();
         this.idNum = idNum;
@@ -102,69 +87,50 @@ public class Avatar implements Comparable<Avatar> {
 
         this.fullName = (this.jobTitle + " " + this.name).intern();
 
-        this.hashNum = this.fullName.hashCode() ^ this.idNum;
-
         return;
     }
 
+
     /**
-     * Avatarを生成する。
-     * @param fullName フルネーム
+     * Avatarリストを生成する。
+     *
+     * Avatarの全インスタンスはこのリストに含まれる。
+     *
+     * @return ソートされた定義済みAvatarのリスト
      */
-    // TODO 当面は呼ばれないはず。Z国とか向け。
-    public Avatar(String fullName){
-        this.fullName = fullName.intern();
-        this.idNum = -1;
-
-        String[] tokens = this.fullName.split("\\p{Blank}+", 2);
-        if(tokens.length == 1){
-            this.jobTitle = null;
-            this.name = this.fullName;
-        }else if(tokens.length == 2){
-            this.jobTitle = tokens[0].intern();
-            this.name = tokens[1].intern();
-        }else{
-            this.jobTitle = null;
-            this.name = null;
-            assert false;
-        }
-
-        this.identifier = "???".intern();
-
-        this.hashNum = this.fullName.hashCode() ^ this.idNum;
+    private static List<Avatar> buildAvatarList(){
+        List<Avatar> result;
 
-        return;
-    }
+        List<PreDefAvatar>  predefs = CoreData.getPreDefAvatarList();
+        result = predefs.stream()
+                .map(preDefAvatar -> toAvatar(preDefAvatar))
+                .sorted()
+                .collect(Collectors.toList());
 
+        result = Collections.unmodifiableList(result);
+
+        return result;
+    }
 
     /**
-     * 定義済みAvatar群の生成。
-     * @param predefs 定義済みAvatar元データ群
-     * @return ソートされた定義済みAvatarのリスト
+     * 定義済みAvatarからAvatarへの変換を行う。
+     *
+     * @param pre 定義済みAvatar
+     * @return Avatar
      */
-    private static List<Avatar> buildAvatarList(List<PreDefAvatar> predefs){
-        List<Avatar> result = new ArrayList<>(predefs.size());
-
-        for(PreDefAvatar preDefAvatar : predefs){
-            String shortName = preDefAvatar.getShortName();
-            String jobTitle  = preDefAvatar.getJobTitle();
-            int serialNo     = preDefAvatar.getSerialNo();
-            String avatarId  = preDefAvatar.getAvatarId();
-            Avatar avatar = new Avatar(shortName,
-                                       jobTitle,
-                                       serialNo,
-                                       avatarId );
-            result.add(avatar);
-        }
-
-        Collections.sort(result);
-        result = Collections.unmodifiableList(result);
+    private static Avatar toAvatar(PreDefAvatar pre){
+        String shortName = pre.getShortName();
+        String jobTitle  = pre.getJobTitle();
+        int serialNo     = pre.getSerialNo();
+        String avatarId  = pre.getAvatarId();
 
+        Avatar result = new Avatar(shortName, jobTitle, serialNo, avatarId);
         return result;
     }
 
     /**
      * 定義済みAvatar群のリストを返す。
+     *
      * @return Avatarのリスト
      */
     public static List<Avatar> getPredefinedAvatarList(){
@@ -172,52 +138,29 @@ public class Avatar implements Comparable<Avatar> {
     }
 
     /**
-     * 定義済みAvatarを返す。
-     * @param fullName Avatarのフルネーム
+     * フルネームに合致するAvatarを返す。
+     *
+     * @param fullNameArg Avatarのフルネーム
      * @return Avatar。フルネームが一致するAvatarが無ければnull
      */
-    // TODO 20キャラ程度ならListをなめる方が早いか?
-    public static Avatar getPredefinedAvatar(String fullName){
-        return AVATAR_MAP.get(fullName);
+    public static Avatar getAvatarByFullname(String fullNameArg){
+        return AVATAR_FN_MAP.get(fullNameArg);
     }
 
     /**
-     * 定義済みAvatarを返す。
-     * @param fullName Avatarのフルネーム
-     * @return Avatar。フルネームが一致するAvatarが無ければnull
+     * IDに合致するAvatarを返す。
+     *
+     * @param avatarId AvatarのID
+     * @return Avatar。IDが一致するAvatarが無ければnull
      */
-    public static Avatar getPredefinedAvatar(CharSequence fullName){
-        for(Avatar avatar : AVATAR_LIST){
-            String avatarName = avatar.getFullName();
-            if(avatarName.contentEquals(fullName)){
-                return avatar;
-            }
-        }
-        return null;
+    public static Avatar getAvatarById(String avatarId){
+        return AVATAR_ID_MAP.get(avatarId);
     }
 
-    /**
-     * 定義済みAvatar名に一致しないか調べる。
-     * @param matcher マッチャ
-     * @return 一致したAvatar。一致しなければnull。
-     */
-    public static Avatar lookingAtAvatar(Matcher matcher){
-        matcher.usePattern(AVATAR_PATTERN);
-
-        if( ! matcher.lookingAt() ) return null;
-        int groupCt = matcher.groupCount();
-        for(int group = 1; group <= groupCt; group++){
-            if(matcher.start(group) >= 0){
-                Avatar avatar = AVATAR_LIST.get(group - 1);
-                return avatar;
-            }
-        }
-
-        return null;
-    }
 
     /**
      * フルネームを取得する。
+     *
      * @return フルネーム
      */
     public String getFullName(){
@@ -226,6 +169,7 @@ public class Avatar implements Comparable<Avatar> {
 
     /**
      * 職業名を取得する。
+     *
      * @return 職業名
      */
     public String getJobTitle(){
@@ -234,6 +178,7 @@ public class Avatar implements Comparable<Avatar> {
 
     /**
      * 通常名を取得する。
+     *
      * @return 通常名
      */
     public String getName(){
@@ -242,6 +187,7 @@ public class Avatar implements Comparable<Avatar> {
 
     /**
      * 通し番号を返す。
+     *
      * @return 通し番号
      */
     public int getIdNum(){
@@ -249,7 +195,8 @@ public class Avatar implements Comparable<Avatar> {
     }
 
     /**
-     * 識別文字列を返す。
+     * AvatarID識別文字列を返す。
+     *
      * @return 識別文字列
      */
     public String getIdentifier(){
@@ -258,39 +205,7 @@ public class Avatar implements Comparable<Avatar> {
 
     /**
      * {@inheritDoc}
-     * @param obj {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public boolean equals(Object obj){
-        if(this == obj){
-            return true;
-        }
-
-        if( ! (obj instanceof Avatar) ){
-            return false;
-        }
-        Avatar other = (Avatar) obj;
-
-        boolean nameMatch = this.fullName.equals(other.fullName);
-        boolean idMatch = this.idNum == other.idNum;
-
-        if(nameMatch && idMatch) return true;
-
-        return false;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public int hashCode(){
-        return this.hashNum;
-    }
-
-    /**
-     * {@inheritDoc}
+     *
      * @return {@inheritDoc}
      */
     @Override
@@ -300,6 +215,7 @@ public class Avatar implements Comparable<Avatar> {
 
     /**
      * {@inheritDoc}
+     *
      * 通し番号順に順序づける。
      * @param avatar {@inheritDoc}
      * @return {@inheritDoc}
diff --git a/src/main/java/jp/sfjp/jindolf/data/CoreData.java b/src/main/java/jp/sfjp/jindolf/data/CoreData.java
new file mode 100644 (file)
index 0000000..e679858
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * core data
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.ParserConfigurationException;
+import jp.sfjp.jindolf.dxchg.XmlUtils;
+import jp.sourceforge.jindolf.corelib.LandDef;
+import jp.sourceforge.jindolf.corelib.PreDefAvatar;
+import org.xml.sax.SAXException;
+
+/**
+ * JinCore 各種固定データのロード。
+ *
+ * <p>I/Oはリソースへのアクセスを前提とし、異常系はリカバーしない。
+ */
+public final class CoreData{
+
+    private static final List<LandDef> LIST_LANDDEF;
+    private static final List<PreDefAvatar> LIST_PREDEFAVATAR;
+
+    static{
+        try{
+            LIST_LANDDEF = buildLandDefList();
+            LIST_PREDEFAVATAR = buildPreDefAvatarList();
+        }catch(   IOException
+                | URISyntaxException
+                | ParserConfigurationException
+                | SAXException e
+                ){
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+
+    /**
+     * hidden constructor.
+     */
+    private CoreData(){
+        assert false;
+    }
+
+
+    /**
+     * load and build LandDef list.
+     *
+     * @return LandDef list
+     * @throws IOException resource IO error
+     * @throws URISyntaxException xml error
+     * @throws ParserConfigurationException xml error
+     * @throws SAXException xml error
+     */
+    private static List<LandDef> buildLandDefList()
+            throws
+            IOException,
+            URISyntaxException,
+            ParserConfigurationException,
+            SAXException
+    {
+        DocumentBuilder builder = XmlUtils.createDocumentBuilder();
+        List<LandDef> result = LandDef.buildLandDefList(builder);
+        return result;
+    }
+
+    /**
+     * load and build PreDefAvatar list.
+     *
+     * @return PreDefAvatar list
+     * @throws IOException resource IO error
+     * @throws URISyntaxException xml error
+     * @throws ParserConfigurationException xml error
+     * @throws SAXException xml error
+     */
+    private static List<PreDefAvatar> buildPreDefAvatarList()
+            throws
+            IOException,
+            URISyntaxException,
+            ParserConfigurationException,
+            SAXException
+    {
+        DocumentBuilder builder = XmlUtils.createDocumentBuilder();
+        List<PreDefAvatar> result =
+                PreDefAvatar.buildPreDefAvatarList(builder);
+        return result;
+
+    }
+
+
+    /**
+     * return LandDef list.
+     *
+     * @return list
+     */
+    public static List<LandDef> getLandDefList(){
+        return LIST_LANDDEF;
+    }
+
+    /**
+     * return PreDefAvatar list.
+     *
+     * @return list
+     */
+    public static List<PreDefAvatar> getPreDefAvatarList(){
+        return LIST_PREDEFAVATAR;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/InterPlay.java b/src/main/java/jp/sfjp/jindolf/data/InterPlay.java
new file mode 100644 (file)
index 0000000..babf053
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * interplay
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data;
+
+/**
+ * Avatar間の行為関係。
+ *
+ * <p>行為を行う者と行為の対象者から構成される。
+ *
+ * <p>処刑投票、襲撃など。
+ */
+public class InterPlay {
+
+    private final Avatar byWhom;
+    private final Avatar target;
+
+
+    /**
+     * constructor.
+     *
+     * @param byWhom 行為者
+     * @param target 行為の対象
+     */
+    public InterPlay(Avatar byWhom, Avatar target){
+        super();
+        this.byWhom = byWhom;
+        this.target = target;
+        return;
+    }
+
+
+    /**
+     * 行為者を返す。
+     *
+     * @return 行為者
+     */
+    public Avatar getByWhom(){
+        return this.byWhom;
+    }
+
+    /**
+     * 行為対象を返す。
+     *
+     * @return 行為対象
+     */
+    public Avatar getTarget(){
+        return this.target;
+    }
+
+}
index ddef69c..e74b936 100644 (file)
@@ -11,42 +11,25 @@ import java.awt.image.BufferedImage;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import jp.osdn.jindolf.parser.HtmlAdapter;
-import jp.osdn.jindolf.parser.HtmlParseException;
-import jp.osdn.jindolf.parser.HtmlParser;
-import jp.osdn.jindolf.parser.PageType;
-import jp.osdn.jindolf.parser.SeqRange;
-import jp.osdn.jindolf.parser.content.DecodedContent;
-import jp.sfjp.jindolf.net.HtmlSequence;
 import jp.sfjp.jindolf.net.ServerAccess;
 import jp.sourceforge.jindolf.corelib.LandDef;
-import jp.sourceforge.jindolf.corelib.LandState;
-import jp.sourceforge.jindolf.corelib.VillageState;
 
 /**
  * いわゆる「国」。
  */
 public class Land {
 
-    // 古国ID
-    private static final String ID_VANILLAWOLF = "wolf";
-
     private static final Logger LOGGER = Logger.getAnonymousLogger();
 
 
     private final LandDef landDef;
     private final ServerAccess serverAccess;
-    private final HtmlParser parser = new HtmlParser();
-    private final VillageListHandler handler = new VillageListHandler();
 
     private final List<Village> villageList = new LinkedList<>();
 
@@ -69,89 +52,11 @@ public class Land {
         }
         this.serverAccess = new ServerAccess(url, this.landDef.getEncoding());
 
-        this.parser.setBasicHandler(this.handler);
-
         return;
     }
 
 
     /**
-     * クエリー文字列から特定キーの値を得る。
-     * クエリーの書式例:「{@literal a=b&c=d&e=f}」この場合キーcの値はd
-     * @param key キー
-     * @param allQuery クエリー
-     * @return 値
-     */
-    public static String getValueFromCGIQueries(String key,
-                                                   String allQuery){
-        String result = null;
-
-        String[] queries = allQuery.split("\\Q&\\E");
-
-        for(String pair : queries){
-            if(pair == null) continue;
-            String[] namevalue = pair.split("\\Q=\\E");
-            if(namevalue == null) continue;
-            if(namevalue.length != 2) continue;
-            String name  = namevalue[0];
-            String value = namevalue[1];
-            if(name == null) continue;
-            if( name.equals(key) ){
-                result = value;
-                if(result == null) continue;
-                if(result.length() <= 0) continue;
-                break;
-            }
-        }
-
-        return result;
-    }
-
-    /**
-     * AタグのHREF属性値からクエリー部を抽出する。
-     * 「{@literal &amp;}」は「{@literal &}」に解釈される。
-     * @param hrefValue HREF属性値
-     * @return クエリー文字列
-     */
-    public static String getRawQueryFromHREF(CharSequence hrefValue){
-        if(hrefValue == null) return null;
-
-        // HTML 4.01 B.2.2 rule
-        String pureHREF = hrefValue.toString().replace("&amp;", "&");
-
-        URI uri;
-        try{
-            uri = new URI(pureHREF);
-        }catch(URISyntaxException e){
-            LOGGER.warning(
-                     "不正なURI["
-                    + hrefValue
-                    + "]を検出しました");
-            return null;
-        }
-
-        String rawQuery = uri.getRawQuery();
-
-        return rawQuery;
-    }
-
-    /**
-     * AタグのHREF属性値から村IDを得る。
-     * @param hrefValue HREF値
-     * @return village 村ID
-     */
-    public static String getVillageIDFromHREF(CharSequence hrefValue){
-        String rawQuery = getRawQueryFromHREF(hrefValue);
-        if(rawQuery == null) return null;
-
-        String villageID = getValueFromCGIQueries("vid", rawQuery);
-        if(villageID == null) return null;
-        if(villageID.length() <= 0) return null;
-
-        return villageID;
-    }
-
-    /**
      * 国定義を得る。
      * @return 国定義
      */
@@ -240,66 +145,10 @@ public class Land {
     }
 
     /**
-     * 村一覧情報をダウンロードする。
-     * リスト元情報は国のトップページと村一覧ページ。
-     * 古国の場合は村一覧にアクセスせずトップページのみ。
-     * 古国以外で村建てをやめた国はトップページにアクセスしない。
-     * 村リストはVillageの実装に従いソートされる。重複する村は排除。
-     *
-     * @return ソートされた村一覧
-     * @throws java.io.IOException ネットワーク入出力の異常
-     */
-    public SortedSet<Village> downloadVillageList() throws IOException {
-        LandDef thisLand = getLandDef();
-        LandState state = thisLand.getLandState();
-        boolean isVanillaWolf = thisLand.getLandId().equals(ID_VANILLAWOLF);
-
-        ServerAccess server = getServerAccess();
-
-        // たまに同じ村が複数回出現するので注意!
-        SortedSet<Village> result = new TreeSet<>();
-
-        // トップページ
-        if(state.equals(LandState.ACTIVE) || isVanillaWolf){
-            HtmlSequence html = server.getHTMLTopPage();
-            DecodedContent content = html.getContent();
-            try{
-                this.parser.parseAutomatic(content);
-            }catch(HtmlParseException e){
-                LOGGER.log(Level.WARNING, "トップページを認識できない", e);
-            }
-            List<Village> list = this.handler.getVillageList();
-            if(list != null){
-                result.addAll(list);
-            }
-        }
-
-        // 村一覧ページ
-        if( ! isVanillaWolf ){
-            HtmlSequence html = server.getHTMLLandList();
-            DecodedContent content = html.getContent();
-            try{
-                this.parser.parseAutomatic(content);
-            }catch(HtmlParseException e){
-                LOGGER.log(Level.WARNING, "村一覧ページを認識できない", e);
-            }
-            List<Village> list = this.handler.getVillageList();
-            if(list != null){
-                result.addAll(list);
-            }
-        }
-
-        this.parser.reset();
-        this.handler.reset();
-
-        return result;
-    }
-
-    /**
      * 村リストを更新する。
      * @param vset ソート済みの村一覧
      */
-    public void updateVillageList(SortedSet<Village> vset){
+    public void updateVillageList(List<Village> vset){
         // TODO 村リスト更新のイベントリスナがあると便利か?
         this.villageList.clear();
         this.villageList.addAll(vset);
@@ -315,126 +164,4 @@ public class Land {
         return getLandDef().getLandName();
     }
 
-    /**
-     * 村一覧取得用ハンドラ。
-     */
-    private class VillageListHandler extends HtmlAdapter{
-
-        private List<Village> villageList = null;
-
-        /**
-         * コンストラクタ。
-         */
-        public VillageListHandler(){
-            super();
-            return;
-        }
-
-        /**
-         * 村一覧を返す。
-         * 再度パースを行うまで呼んではいけない。
-         * @return 村一覧
-         * @throws IllegalStateException パース前に呼び出された。
-         *     あるいはパース後すでにリセットされている。
-         */
-        public List<Village> getVillageList() throws IllegalStateException{
-            if(this.villageList == null){
-                throw new IllegalStateException("パースが必要です。");
-            }
-
-            List<Village> result = this.villageList;
-
-            return result;
-        }
-
-        /**
-         * リセットを行う。
-         * 村一覧は空になる。
-         */
-        public void reset(){
-            this.villageList = null;
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * 村一覧リストが初期化される。
-         * @param content {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void startParse(DecodedContent content)
-                throws HtmlParseException{
-            reset();
-            this.villageList = new LinkedList<>();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * 自動判定の結果がトップページでも村一覧ページでもなければ
-         * 例外を投げる。
-         * @param type {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
-         */
-        @Override
-        public void pageType(PageType type) throws HtmlParseException{
-            if(    type != PageType.VILLAGELIST_PAGE
-                && type != PageType.TOP_PAGE ){
-                throw new HtmlParseException(
-                        "トップページか村一覧ページが必要です。");
-            }
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param anchorRange {@inheritDoc}
-         * @param villageRange {@inheritDoc}
-         * @param hour {@inheritDoc}
-         * @param minute {@inheritDoc}
-         * @param villageState {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void villageRecord(DecodedContent content,
-                                    SeqRange anchorRange,
-                                    SeqRange villageRange,
-                                    int hour, int minute,
-                                    VillageState villageState)
-                throws HtmlParseException{
-            LandDef landdef = getLandDef();
-            LandState landState = landdef.getLandState();
-
-            CharSequence href = anchorRange.sliceSequence(content);
-            String villageID = getVillageIDFromHREF(href);
-            if(    villageID == null
-                || villageID.length() <= 0 ){
-                LOGGER.warning(
-                        "認識できないURL[" + href + "]に遭遇しました。");
-                return;
-            }
-
-            CharSequence fullVillageName =
-                    villageRange.sliceSequence(content);
-
-            // TODO 既に出来ているかもしれないVillageを再度作るのは無駄?
-            Village village = new Village(Land.this,
-                                          villageID,
-                                          fullVillageName.toString() );
-
-            if(landState == LandState.HISTORICAL){
-                village.setState(VillageState.GAMEOVER);
-            }else{
-                village.setState(villageState);
-            }
-
-            this.villageList.add(village);
-
-            return;
-        }
-
-    }
-
 }
@@ -7,32 +7,25 @@
 
 package jp.sfjp.jindolf.data;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.swing.event.EventListenerList;
 import javax.swing.event.TreeModelEvent;
 import javax.swing.event.TreeModelListener;
 import javax.swing.tree.TreeModel;
 import javax.swing.tree.TreePath;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.ParserConfigurationException;
-import jp.sfjp.jindolf.dxchg.XmlUtils;
 import jp.sourceforge.jindolf.corelib.LandDef;
-import org.xml.sax.SAXException;
 
 /**
  * 国の集合。あらゆるデータモデルの大元。
  * 国一覧と村一覧を管理。
  * JTreeのモデルも兼用。
  */
-public class LandsModel implements TreeModel{ // ComboBoxModelも付けるか?
+public class LandsTreeModel implements TreeModel{ // ComboBoxModelも付けるか?
 
     private static final String ROOT = "ROOT";
     private static final int SECTION_INTERVAL = 100;
@@ -55,7 +48,7 @@ public class LandsModel implements TreeModel{ // ComboBoxModelも付けるか?
      * コンストラクタ。
      * この時点ではまだ国一覧が読み込まれない。
      */
-    public LandsModel(){
+    public LandsTreeModel(){
         super();
         return;
     }
@@ -95,23 +88,12 @@ public class LandsModel implements TreeModel{ // ComboBoxModelも付けるか?
 
         this.landList.clear();
 
-        List<LandDef> landDefList;
-        try{
-            DocumentBuilder builder = XmlUtils.createDocumentBuilder();
-            landDefList = LandDef.buildLandDefList(builder);
-        }catch(   IOException
-                | SAXException
-                | URISyntaxException
-                | ParserConfigurationException
-                e){
-            LOGGER.log(Level.SEVERE, "failed to load land list", e);
-            return;
-        }
-
-        for(LandDef landDef : landDefList){
-            Land land = new Land(landDef);
+        List<LandDef> landDefList = CoreData.getLandDefList();
+        landDefList.stream().map((landDef) ->
+            new Land(landDef)
+        ).forEachOrdered((land) -> {
             this.landList.add(land);
-        }
+        });
 
         this.isLandListLoaded = true;
 
diff --git a/src/main/java/jp/sfjp/jindolf/data/Nominated.java b/src/main/java/jp/sfjp/jindolf/data/Nominated.java
new file mode 100644 (file)
index 0000000..13fd565
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * nominated with execution info
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data;
+
+/**
+ * 処刑投票の処刑先別集計。
+ */
+public class Nominated {
+
+    private final Avatar avatar;
+    private final int count;
+
+
+    /**
+     * constructor.
+     *
+     * @param avatar 処刑希望先Avatar
+     * @param count 処刑票数
+     */
+    public Nominated(Avatar avatar, int count){
+        super();
+        this.avatar = avatar;
+        this.count = count;
+        return;
+    }
+
+    /**
+     * 処刑希望先Avatarを返す。
+     *
+     * @return Avatar
+     */
+    public Avatar getAvatar(){
+        return this.avatar;
+    }
+
+    /**
+     * 処刑票数を返す。
+     *
+     * @return 票数
+     */
+    public int getCount(){
+        return this.count;
+    }
+
+}
index e06493d..32608bd 100644 (file)
@@ -7,34 +7,15 @@
 
 package jp.sfjp.jindolf.data;
 
-import java.io.IOException;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import jp.osdn.jindolf.parser.EntityConverter;
-import jp.osdn.jindolf.parser.HtmlAdapter;
-import jp.osdn.jindolf.parser.HtmlParseException;
-import jp.osdn.jindolf.parser.HtmlParser;
-import jp.osdn.jindolf.parser.PageType;
-import jp.osdn.jindolf.parser.SeqRange;
-import jp.osdn.jindolf.parser.content.DecodedContent;
-import jp.sfjp.jindolf.net.HtmlSequence;
-import jp.sfjp.jindolf.net.ServerAccess;
-import jp.sfjp.jindolf.util.StringUtils;
-import jp.sourceforge.jindolf.corelib.EventFamily;
-import jp.sourceforge.jindolf.corelib.GameRole;
 import jp.sourceforge.jindolf.corelib.LandDef;
 import jp.sourceforge.jindolf.corelib.PeriodType;
 import jp.sourceforge.jindolf.corelib.SysEventType;
-import jp.sourceforge.jindolf.corelib.TalkType;
-import jp.sourceforge.jindolf.corelib.Team;
-import jp.sourceforge.jindolf.corelib.VillageState;
 
 /**
  * いわゆる「日」。
@@ -44,29 +25,15 @@ import jp.sourceforge.jindolf.corelib.VillageState;
  * 人気のないプロローグなどで、
  * 24時間以上の期間を持つPeriodが生成される可能性の考慮が必要。
  */
-public class Period{
+public final class Period{
     // TODO Comparable も implement する?
 
-    private static final HtmlParser PARSER = new HtmlParser();
-    private static final PeriodHandler HANDLER =
-            new PeriodHandler();
-
-    private static final Logger LOGGER = Logger.getAnonymousLogger();
-
-    static{
-        PARSER.setBasicHandler   (HANDLER);
-        PARSER.setSysEventHandler(HANDLER);
-        PARSER.setTalkHandler    (HANDLER);
-    }
-
     private final Village homeVillage;
     private final PeriodType periodType;
     private final int day;
     private int limitHour;
     private int limitMinute;
     // TODO 更新月日も入れるべきか。
-    private String loginName;
-    private boolean isFullOpen = false;
 
     private final List<Topic> topicList = new LinkedList<>();
     private final List<Topic> unmodList =
@@ -74,50 +41,25 @@ public class Period{
 
 
     /**
-     * この Period が進行中の村の最新日で、
-     * 今まさに次々と発言が蓄積されているときは
-     * true になる。
-     * ※重要: Hot な Period は meslog クエリーを使ってダウンロードできない。
-     */
-    private boolean isHot;
-
-
-    /**
      * Periodを生成する。
-     * この段階では発言データのロードは行われない。
-     * デフォルトで非Hot状態。
+     *
+     * <p>この段階では発言データのロードは行われない。
+     *
      * @param homeVillage 所属するVillage
      * @param periodType Period種別
      * @param day Period通番
      * @throws java.lang.NullPointerException 引数にnullが渡された場合。
      */
     public Period(Village homeVillage,
-                   PeriodType periodType,
-                   int day)
-                   throws NullPointerException{
-        this(homeVillage, periodType, day, false);
-        return;
-    }
+                  PeriodType periodType,
+                  int day){
+        Objects.nonNull(homeVillage);
+        Objects.nonNull(periodType);
 
-    /**
-     * Periodを生成する。
-     * この段階では発言データのロードは行われない。
-     * @param homeVillage 所属するVillage
-     * @param periodType Period種別
-     * @param day Period通番
-     * @param isHot Hotか否か
-     * @throws java.lang.NullPointerException 引数にnullが渡された場合。
-     */
-    private Period(Village homeVillage,
-                    PeriodType periodType,
-                    int day,
-                    boolean isHot)
-                    throws NullPointerException{
-        if(    homeVillage == null
-            || periodType  == null ) throw new NullPointerException();
         if(day < 0){
             throw new IllegalArgumentException("Period day is too small !");
         }
+
         switch(periodType){
         case PROLOGUE:
             assert day == 0;
@@ -137,59 +79,13 @@ public class Period{
 
         unload();
 
-        this.isHot = isHot;
-
         return;
     }
 
 
     /**
-     * Periodを更新する。Topicのリストが更新される。
-     * @param period 日
-     * @param force trueなら強制再読み込み。
-     *     falseならまだ読み込んで無い時のみ読み込み。
-     * @throws IOException ネットワーク入力エラー
-     */
-    public static void parsePeriod(Period period, boolean force)
-            throws IOException{
-        if( ! force && period.hasLoaded() ) return;
-
-        Village village = period.getVillage();
-        Land land = village.getParentLand();
-        ServerAccess server = land.getServerAccess();
-
-        if(village.getState() != VillageState.PROGRESS){
-            period.isFullOpen = true;
-        }else if(period.getType() != PeriodType.PROGRESS){
-            period.isFullOpen = true;
-        }else{
-            period.isFullOpen = false;
-        }
-
-        HtmlSequence html = server.getHTMLPeriod(period);
-
-        period.topicList.clear();
-
-        boolean wasHot = period.isHot();
-
-        HANDLER.setPeriod(period);
-        DecodedContent content = html.getContent();
-        try{
-            PARSER.parseAutomatic(content);
-        }catch(HtmlParseException e){
-            LOGGER.log(Level.WARNING, "発言抽出に失敗", e);
-        }
-
-        if(wasHot && ! period.isHot() ){
-            parsePeriod(period, true);
-            return;
-        }
-
-        return;
-    }
-
-    /**
      * 所属する村を返す。
+     *
      * @return 村
      */
     public Village getVillage(){
@@ -198,6 +94,7 @@ public class Period{
 
     /**
      * Period種別を返す。
+     *
      * @return 種別
      */
     public PeriodType getType(){
@@ -206,9 +103,11 @@ public class Period{
 
     /**
      * Period通番を返す。
-     * プロローグは常に0番。
-     * n日目のゲーム進行日はn番
-     * エピローグは最後のゲーム進行日+1番
+     *
+     * <p>プロローグは常に0番。
+     * n日目のゲーム進行日はn番。
+     * エピローグは最後のゲーム進行日+1番。
+     *
      * @return Period通番
      */
     public int getDay(){
@@ -216,7 +115,20 @@ public class Period{
     }
 
     /**
+     * 更新時刻を設定する。
+     *
+     * @param hour 時
+     * @param minute 分
+     */
+    public void setLimit(int hour, int minute){
+        this.limitHour = hour;
+        this.limitMinute = minute;
+        return;
+    }
+
+    /**
      * 更新時刻の文字表記を返す。
+     *
      * @return 更新時刻の文字表記
      */
     public String getLimit(){
@@ -232,23 +144,8 @@ public class Period{
     }
 
     /**
-     * Hotか否か返す。
-     * @return Hotか否か
-     */
-    public boolean isHot(){
-        return this.isHot;
-    }
-
-    /**
-     * Hotか否か設定する。
-     * @param isHotArg Hot指定
-     */
-    public void setHot(boolean isHotArg){
-        this.isHot = isHotArg;
-    }
-
-    /**
      * プロローグか否か判定する。
+     *
      * @return プロローグならtrue
      */
     public boolean isPrologue(){
@@ -258,6 +155,7 @@ public class Period{
 
     /**
      * エピローグか否か判定する。
+     *
      * @return エピローグならtrue
      */
     public boolean isEpilogue(){
@@ -267,6 +165,7 @@ public class Period{
 
     /**
      * 進行日か否か判定する。
+     *
      * @return 進行日ならtrue
      */
     public boolean isProgress(){
@@ -276,6 +175,7 @@ public class Period{
 
     /**
      * このPeriodにアクセスするためのクエリーを生成する。
+     *
      * @return CGIに渡すクエリー
      */
     public String getCGIQuery(){
@@ -284,11 +184,6 @@ public class Period{
         Village village = getVillage();
         result.append(village.getCGIQuery());
 
-        if(isHot()){
-            result.append("&mes=all");   // 全表示指定
-            return result.toString();
-        }
-
         Land land = village.getParentLand();
         LandDef ldef = land.getLandDef();
 
@@ -336,7 +231,9 @@ public class Period{
 
     /**
      * Periodに含まれるTopicのリストを返す。
-     * このリストは上書き操作不能。
+     *
+     * <p>このリストは上書き操作不能。
+     *
      * @return Topicのリスト
      */
     public List<Topic> getTopicList(){
@@ -344,7 +241,16 @@ public class Period{
     }
 
     /**
+     * Topicのリスト内容を消す。
+     */
+    public void clearTopicList(){
+        this.topicList.clear();
+        return;
+    }
+
+    /**
      * Periodに含まれるTopicの総数を返す。
+     *
      * @return Topic総数
      */
     public int getTopics(){
@@ -353,10 +259,11 @@ public class Period{
 
     /**
      * Topicを追加する。
+     *
      * @param topic Topic
      * @throws java.lang.NullPointerException nullが渡された場合。
      */
-    protected void addTopic(Topic topic) throws NullPointerException{
+    public void addTopic(Topic topic) throws NullPointerException{
         if(topic == null) throw new NullPointerException();
         this.topicList.add(topic);
         return;
@@ -364,7 +271,9 @@ public class Period{
 
     /**
      * Periodのキャプション文字列を返す。
-     * 主な用途はタブ画面の耳のラベルなど。
+     *
+     * <p>主な用途はタブ画面の耳のラベルなど。
+     *
      * @return キャプション文字列
      */
     public String getCaption(){
@@ -390,15 +299,8 @@ public class Period{
     }
 
     /**
-     * このPeriodをダウンロードしたときのログイン名を返す。
-     * @return ログイン名。ログアウト中はnull。
-     */
-    public String getLoginName(){
-        return this.loginName;
-    }
-
-    /**
      * 公開発言番号にマッチする発言を返す。
+     *
      * @param talkNo 公開発言番号
      * @return 発言。見つからなければnull
      */
@@ -415,19 +317,12 @@ public class Period{
     }
 
     /**
-     * このPeriodの内容にゲーム進行上隠された部分がある可能性を判定する。
-     * @return 隠れた要素がありうるならfalse
-     */
-    public boolean isFullOpen(){
-        return this.isFullOpen;
-    }
-
-    /**
      * ロード済みか否かチェックする。
+     *
      * @return ロード済みならtrue
      */
     public boolean hasLoaded(){
-        return getTopics() > 0;
+        return ! this.topicList.isEmpty();
     }
 
     /**
@@ -436,10 +331,6 @@ public class Period{
     public void unload(){
         this.limitHour = 0;
         this.limitMinute = 0;
-        this.loginName = null;
-        this.isFullOpen = false;
-
-        this.isHot = false;
 
         this.topicList.clear();
 
@@ -448,8 +339,10 @@ public class Period{
 
     /**
      * 襲撃メッセージの有無を判定する。
-     * 決着が付くまで非狼陣営には見えない。
+     *
+     * <p>決着が付くまで非狼陣営には見えない。
      * 偽装GJでは狼にも見えない。
+     *
      * @return 襲撃メッセージがあればtrue
      */
     public boolean hasAssaultTried(){
@@ -469,6 +362,7 @@ public class Period{
 
     /**
      * 処刑されたAvatarを返す。
+     *
      * @return 処刑されたAvatar。突然死などなんらかの理由でいない場合はnull
      */
     public Avatar getExecutedAvatar(){
@@ -486,6 +380,7 @@ public class Period{
 
     /**
      * 投票に参加したAvatarの集合を返す。
+     *
      * @return 投票に参加したAvatarのSet
      */
     public Set<Avatar> getVoterSet(){
@@ -502,7 +397,9 @@ public class Period{
 
     /**
      * 任意のタイプのシステムイベントを返す。
-     * 複数存在する場合、返すのは最初の一つだけ。
+     *
+     * <p>複数存在する場合、返すのは最初の一つだけ。
+     *
      * @param type イベントタイプ
      * @return システムイベント
      */
@@ -516,735 +413,4 @@ public class Period{
         return null;
     }
 
-    /**
-     * Periodパース用ハンドラ。
-     */
-    private static class PeriodHandler extends HtmlAdapter{
-
-        private static final int TALKTYPE_NUM = TalkType.values().length;
-
-        private final EntityConverter converter =
-                new EntityConverter(true);
-        // TODO: SMP面文字に彩色対応するまでの暫定措置
-
-        private final Map<Avatar, int[]> countMap =
-                new HashMap<>();
-
-        private Period period = null;
-
-        private TalkType talkType;
-        private Avatar avatar;
-        private int talkNo;
-        private String anchorId;
-        private int talkHour;
-        private int talkMinute;
-        private DecodedContent talkContent = null;
-
-        private EventFamily eventFamily;
-        private SysEventType sysEventType;
-        private DecodedContent eventContent = null;
-        private final List<Avatar> avatarList = new LinkedList<>();
-        private final List<GameRole> roleList = new LinkedList<>();
-        private final List<Integer> integerList = new LinkedList<>();
-        private final List<CharSequence>  charseqList =
-            new LinkedList<>();
-
-        /**
-         * コンストラクタ。
-         */
-        public PeriodHandler(){
-            super();
-            return;
-        }
-
-        /**
-         * パース結果を格納するPeriodを設定する。
-         * @param period Period
-         */
-        public void setPeriod(Period period){
-            this.period = period;
-            return;
-        }
-
-        /**
-         * 文字列断片からAvatarを得る。
-         * 村に未登録のAvatarであればついでに登録される。
-         * @param content 文字列
-         * @param range 文字列内のAvatarフルネームを示す領域
-         * @return Avatar
-         */
-        private Avatar toAvatar(DecodedContent content, SeqRange range){
-            Village village = this.period.getVillage();
-            String fullName = this.converter
-                                  .convert(content, range)
-                                  .toString();
-            Avatar result = village.getAvatar(fullName);
-            if(result == null){
-                result = new Avatar(fullName);
-                village.addAvatar(result);
-            }
-
-            return result;
-        }
-
-        /**
-         * Avatar別、会話種ごとに発言回数をカウントする。
-         * 1から始まる。
-         * @param targetAvatar 対象Avatar
-         * @param targetType 対象会話種
-         * @return カウント数
-         */
-        private int countUp(Avatar targetAvatar, TalkType targetType){
-            int[] countArray = this.countMap.get(targetAvatar);
-            if(countArray == null){
-                countArray = new int[TALKTYPE_NUM];
-                this.countMap.put(targetAvatar, countArray);
-            }
-            int count = ++countArray[targetType.ordinal()];
-            return count;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void startParse(DecodedContent content)
-                throws HtmlParseException{
-            this.period.loginName = null;
-            this.period.topicList.clear();
-            this.countMap.clear();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param loginRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void loginName(DecodedContent content, SeqRange loginRange)
-                throws HtmlParseException{
-            DecodedContent loginName =
-                    this.converter.convert(content, loginRange);
-
-            this.period.loginName = loginName.toString();
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param type {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void pageType(PageType type) throws HtmlParseException{
-            if(type != PageType.PERIOD_PAGE){
-                throw new HtmlParseException(
-                        "意図しないページを読み込もうとしました。");
-            }
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param month {@inheritDoc}
-         * @param day {@inheritDoc}
-         * @param hour {@inheritDoc}
-         * @param minute {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void commitTime(int month, int day, int hour, int minute)
-                throws HtmlParseException{
-            this.period.limitHour   = hour;
-            this.period.limitMinute = minute;
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * 自分へのリンクが無いかチェックする。
-         * 自分へのリンクが見つかればこのPeriodを非Hotにする。
-         * 自分へのリンクがあるということは、
-         * 今読んでるHTMLは別のPeriodのために書かれたものということ。
-         * 考えられる原因は、HotだったPeriodがゲーム進行に従い
-         * Hotでなくなったこと。
-         * @param content {@inheritDoc}
-         * @param anchorRange {@inheritDoc}
-         * @param periodType {@inheritDoc}
-         * @param day {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void periodLink(DecodedContent content,
-                                SeqRange anchorRange,
-                                PeriodType periodType,
-                                int day)
-                throws HtmlParseException{
-
-            if(this.period.getType() != periodType) return;
-
-            if(    periodType == PeriodType.PROGRESS
-                && this.period.getDay() != day ){
-                return;
-            }
-
-            if( ! anchorRange.isValid() ) return;
-
-            this.period.setHot(false);
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void startTalk() throws HtmlParseException{
-            this.talkType = null;
-            this.avatar = null;
-            this.talkNo = -1;
-            this.anchorId = null;
-            this.talkHour = -1;
-            this.talkMinute = -1;
-            this.talkContent = new DecodedContent(100 + 1);
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param type {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void talkType(TalkType type)
-                throws HtmlParseException{
-            this.talkType = type;
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void talkAvatar(DecodedContent content, SeqRange avatarRange)
-                throws HtmlParseException{
-            this.avatar = toAvatar(content, avatarRange);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param hour {@inheritDoc}
-         * @param minute {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void talkTime(int hour, int minute)
-                throws HtmlParseException{
-            this.talkHour = hour;
-            this.talkMinute = minute;
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param tno {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void talkNo(int tno) throws HtmlParseException{
-            this.talkNo = tno;
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param idRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void talkId(DecodedContent content, SeqRange idRange)
-                throws HtmlParseException{
-            this.anchorId = content.subSequence(idRange.getStartPos(),
-                                                idRange.getEndPos()   )
-                                   .toString();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param textRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void talkText(DecodedContent content, SeqRange textRange)
-                throws HtmlParseException{
-            this.converter.append(this.talkContent, content, textRange);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void talkBreak()
-                throws HtmlParseException{
-            this.talkContent.append('\n');
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void endTalk() throws HtmlParseException{
-            Talk talk = new Talk(this.period,
-                                 this.talkType,
-                                 this.avatar,
-                                 this.talkNo,
-                                 this.anchorId,
-                                 this.talkHour, this.talkMinute,
-                                 this.talkContent );
-
-            int count = countUp(this.avatar, this.talkType);
-            talk.setCount(count);
-
-            this.period.addTopic(talk);
-
-            this.talkType = null;
-            this.avatar = null;
-            this.talkNo = -1;
-            this.anchorId = null;
-            this.talkHour = -1;
-            this.talkMinute = -1;
-            this.talkContent = null;
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param family {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void startSysEvent(EventFamily family)
-                throws HtmlParseException{
-            this.eventFamily = family;
-            this.sysEventType = null;
-            this.eventContent = new DecodedContent();
-            this.avatarList.clear();
-            this.roleList.clear();
-            this.integerList.clear();
-            this.charseqList.clear();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param type {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventType(SysEventType type)
-                throws HtmlParseException{
-            this.sysEventType = type;
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param contentRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventContent(DecodedContent content,
-                                      SeqRange contentRange)
-                throws HtmlParseException{
-            this.converter.append(this.eventContent, content, contentRange);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param anchorRange {@inheritDoc}
-         * @param contentRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventContentAnchor(DecodedContent content,
-                                             SeqRange anchorRange,
-                                             SeqRange contentRange)
-                throws HtmlParseException{
-            this.converter.append(this.eventContent, content, contentRange);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventContentBreak() throws HtmlParseException{
-            this.eventContent.append('\n');
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param entryNo {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventOnStage(DecodedContent content,
-                                      int entryNo,
-                                      SeqRange avatarRange)
-                throws HtmlParseException{
-            Avatar newAvatar = toAvatar(content, avatarRange);
-            this.integerList.add(entryNo);
-            this.avatarList.add(newAvatar);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param role {@inheritDoc}
-         * @param num {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventOpenRole(GameRole role, int num)
-                throws HtmlParseException{
-            this.roleList.add(role);
-            this.integerList.add(num);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventMurdered(DecodedContent content,
-                                       SeqRange avatarRange)
-                throws HtmlParseException{
-            Avatar murdered = toAvatar(content, avatarRange);
-            this.avatarList.add(murdered);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventSurvivor(DecodedContent content,
-                                       SeqRange avatarRange)
-                throws HtmlParseException{
-            Avatar survivor = toAvatar(content, avatarRange);
-            this.avatarList.add(survivor);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param voteByRange {@inheritDoc}
-         * @param voteToRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventCounting(DecodedContent content,
-                                       SeqRange voteByRange,
-                                       SeqRange voteToRange)
-                throws HtmlParseException{
-            if(voteByRange.isValid()){
-                Avatar voteBy = toAvatar(content, voteByRange);
-                this.avatarList.add(voteBy);
-            }
-            Avatar voteTo = toAvatar(content, voteToRange);
-            this.avatarList.add(voteTo);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param voteByRange {@inheritDoc}
-         * @param voteToRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventCounting2(DecodedContent content,
-                                        SeqRange voteByRange,
-                                        SeqRange voteToRange)
-                throws HtmlParseException{
-            sysEventCounting(content, voteByRange, voteToRange);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventSuddenDeath(DecodedContent content,
-                                           SeqRange avatarRange)
-                throws HtmlParseException{
-            Avatar suddenDeath = toAvatar(content, avatarRange);
-            this.avatarList.add(suddenDeath);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @param anchorRange {@inheritDoc}
-         * @param loginRange {@inheritDoc}
-         * @param isLiving {@inheritDoc}
-         * @param role {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventPlayerList(DecodedContent content,
-                                          SeqRange avatarRange,
-                                          SeqRange anchorRange,
-                                          SeqRange loginRange,
-                                          boolean isLiving,
-                                          GameRole role )
-                throws HtmlParseException{
-            Avatar who = toAvatar(content, avatarRange);
-
-            CharSequence anchor;
-            if(anchorRange.isValid()){
-                anchor = this.converter.convert(content, anchorRange);
-            }else{
-                anchor = "";
-            }
-            CharSequence account = this.converter
-                                       .convert(content, loginRange);
-
-            Integer liveOrDead;
-            if(isLiving) liveOrDead = 1;
-            else         liveOrDead = 0;
-
-            this.avatarList.add(who);
-            this.charseqList.add(anchor);
-            this.charseqList.add(account);
-            this.integerList.add(liveOrDead);
-            this.roleList.add(role);
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @param votes {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventExecution(DecodedContent content,
-                                        SeqRange avatarRange,
-                                        int votes )
-                throws HtmlParseException{
-            Avatar who = toAvatar(content, avatarRange);
-
-            this.avatarList.add(who);
-            this.integerList.add(votes);
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param hour {@inheritDoc}
-         * @param minute {@inheritDoc}
-         * @param minLimit {@inheritDoc}
-         * @param maxLimit {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventAskEntry(int hour, int minute,
-                                       int minLimit, int maxLimit)
-                throws HtmlParseException{
-            this.integerList.add(hour * 60 + minute);
-            this.integerList.add(minLimit);
-            this.integerList.add(maxLimit);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param hour {@inheritDoc}
-         * @param minute {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventAskCommit(int hour, int minute)
-                throws HtmlParseException{
-            this.integerList.add(hour * 60 + minute);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param avatarRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventNoComment(DecodedContent content,
-                                        SeqRange avatarRange)
-                throws HtmlParseException{
-            Avatar noComAvatar = toAvatar(content, avatarRange);
-            this.avatarList.add(noComAvatar);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param winner {@inheritDoc}
-         * @param hour {@inheritDoc}
-         * @param minute {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventStayEpilogue(Team winner, int hour, int minute)
-                throws HtmlParseException{
-            GameRole role = null;
-
-            switch(winner){
-            case VILLAGE: role = GameRole.INNOCENT; break;
-            case WOLF:    role = GameRole.WOLF;     break;
-            case HAMSTER: role = GameRole.HAMSTER;  break;
-            default: assert false; break;
-            }
-
-            this.roleList.add(role);
-            this.integerList.add(hour * 60 + minute);
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param guardByRange {@inheritDoc}
-         * @param guardToRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventGuard(DecodedContent content,
-                                    SeqRange guardByRange,
-                                    SeqRange guardToRange)
-                throws HtmlParseException{
-            Avatar guardBy = toAvatar(content, guardByRange);
-            Avatar guardTo = toAvatar(content, guardToRange);
-            this.avatarList.add(guardBy);
-            this.avatarList.add(guardTo);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param judgeByRange {@inheritDoc}
-         * @param judgeToRange {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void sysEventJudge(DecodedContent content,
-                                    SeqRange judgeByRange,
-                                    SeqRange judgeToRange)
-                throws HtmlParseException{
-            Avatar judgeBy = toAvatar(content, judgeByRange);
-            Avatar judgeTo = toAvatar(content, judgeToRange);
-            this.avatarList.add(judgeBy);
-            this.avatarList.add(judgeTo);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void endSysEvent() throws HtmlParseException{
-            SysEvent event = new SysEvent();
-            event.setEventFamily(this.eventFamily);
-            event.setSysEventType(this.sysEventType);
-            event.setContent(this.eventContent);
-            event.addAvatarList(this.avatarList);
-            event.addRoleList(this.roleList);
-            event.addIntegerList(this.integerList);
-            event.addCharSequenceList(this.charseqList);
-
-            this.period.addTopic(event);
-
-            if(    this.sysEventType == SysEventType.MURDERED
-                || this.sysEventType == SysEventType.NOMURDER ){
-                for(Topic topic : this.period.topicList){
-                    if( ! (topic instanceof Talk) ) continue;
-                    Talk talk = (Talk) topic;
-                    if(talk.getTalkType() != TalkType.WOLFONLY) continue;
-                    if( ! StringUtils
-                         .isTerminated(talk.getDialog(),
-                                       "!\u0020今日がお前の命日だ!") ){
-                        continue;
-                    }
-                    talk.setCount(-1);
-                    this.countMap.clear();
-                }
-            }
-
-            this.eventFamily = null;
-            this.sysEventType = null;
-            this.eventContent = null;
-            this.avatarList.clear();
-            this.roleList.clear();
-            this.integerList.clear();
-            this.charseqList.clear();
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void endParse() throws HtmlParseException{
-            return;
-        }
-
-        // TODO 村名のチェックは不要か?
-    }
-
 }
index d9f44a6..bae4620 100644 (file)
@@ -12,6 +12,7 @@ import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 import jp.osdn.jindolf.parser.content.DecodedContent;
 import jp.sourceforge.jindolf.corelib.EventFamily;
 import jp.sourceforge.jindolf.corelib.GameRole;
@@ -33,6 +34,13 @@ public class SysEvent implements Topic{
     private final List<Integer>  integerList = new LinkedList<>();
     private final List<CharSequence>  charseqList =
             new LinkedList<>();
+    /** for playerList and onStage. */
+    private final List<Player> playerList = new LinkedList<>();
+    /** for execution. */
+    private final List<Nominated> nominatedList = new LinkedList<>();
+    /** for vote, judge, counting, etc. */
+    private final List<InterPlay> interPlayList = new LinkedList<>();
+
 
     /**
      * コンストラクタ。
@@ -139,6 +147,39 @@ public class SysEvent implements Topic{
     }
 
     /**
+     * Playerリストを返す。
+     *
+     * @return Playerリスト
+     */
+    public List<Player> getPlayerList(){
+        List<Player> result =
+                Collections.unmodifiableList(this.playerList);
+        return result;
+    }
+
+    /**
+     * Nominatedリストを返す。
+     *
+     * @return Nominatedリスト
+     */
+    public List<Nominated> getNominatedList(){
+        List<Nominated> result =
+                Collections.unmodifiableList(this.nominatedList);
+        return result;
+    }
+
+    /**
+     * InterPlayリストを返す。
+     *
+     * @return InterPlayリスト
+     */
+    public List<InterPlay> getInterPlayList(){
+        List<InterPlay> result =
+                Collections.unmodifiableList(this.interPlayList);
+        return result;
+    }
+
+    /**
      * Avatar一覧を追加する。
      * @param list Avatar一覧
      */
@@ -175,6 +216,35 @@ public class SysEvent implements Topic{
     }
 
     /**
+     * Player一覧を追加する。
+     *
+     * @param list Player一覧
+     */
+    public void addPlayerList(List<Player> list){
+        this.playerList.addAll(list);
+        return;
+    }
+
+    /**
+     * Nominated一覧を追加する。
+     *
+     * @param list Nominated一覧
+     */
+    public void addNominatedList(List<Nominated> list){
+        this.nominatedList.addAll(list);
+        return;
+    }
+
+    /**
+     * InterPlay一覧を追加する。
+     * @param list InterPlay一覧
+     */
+    public void addInterPlayList(List<InterPlay> list){
+        this.interPlayList.addAll(list);
+        return;
+    }
+
+    /**
      * システムイベントを解析し、処刑されたAvatarを返す。
      * G国運用中の時点で、処刑者が出るのはCOUNTINGとEXECUTIONのみ。
      * @return 処刑されたAvatar。いなければnull
@@ -182,27 +252,11 @@ public class SysEvent implements Topic{
     public Avatar getExecutedAvatar(){
         Avatar result = null;
 
-        int avatarNum;
-        Avatar lastAvatar;
-
         switch(this.sysEventType){
         case COUNTING:
-            if(this.avatarList.isEmpty()) return null;
-            avatarNum = this.avatarList.size();
-            if(avatarNum % 2 != 0){
-                lastAvatar = this.avatarList.get(avatarNum - 1);
-                result = lastAvatar;
-            }
-            break;
         case EXECUTION:
-            if(this.avatarList.isEmpty()) return null;
-            avatarNum = this.avatarList.size();
-            List<Integer> intList = getIntegerList();
-            int intNum = intList.size();
-            assert intNum > 0;
-            if(avatarNum != intNum || intList.get(intNum - 1) <= 0){
-                lastAvatar = this.avatarList.get(avatarNum - 1);
-                result = lastAvatar;
+            if( ! this.avatarList.isEmpty()){
+                result = this.avatarList.get(0);
             }
             break;
         case COUNTING2:
@@ -231,15 +285,11 @@ public class SysEvent implements Topic{
             return result;
         }
 
-        int size = this.avatarList.size();
-        assert size >= 2;
-        int limit = size - 1;
-        if(size % 2 != 0) limit--;
+        Set<Avatar> voterSet = this.interPlayList.stream()
+                .map(interPlay -> interPlay.getByWhom())
+                .collect(Collectors.toSet());
 
-        for(int idx = 0; idx <= limit; idx += 2){
-            Avatar avatar = this.avatarList.get(idx);
-            result.add(avatar);
-        }
+        result.addAll(voterSet);
 
         return result;
     }
index 7b966c3..c82db23 100644 (file)
@@ -7,6 +7,9 @@
 
 package jp.sfjp.jindolf.data;
 
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Objects;
 import jp.sourceforge.jindolf.corelib.TalkType;
 
 /**
@@ -14,20 +17,35 @@ import jp.sourceforge.jindolf.corelib.TalkType;
  */
 public class Talk implements Topic{
 
+    private static final String MEINICHI_LAST =
+            "!\u0020今日がお前の命日だ!";
+
+    private static final Map<TalkType, String> COLOR_MAP;
+
+    static{
+        COLOR_MAP = new EnumMap<>(TalkType.class);
+        COLOR_MAP.put(TalkType.PUBLIC,   "白");
+        COLOR_MAP.put(TalkType.PRIVATE,  "灰");
+        COLOR_MAP.put(TalkType.WOLFONLY, "赤");
+        COLOR_MAP.put(TalkType.GRAVE,    "青");
+    }
+
+
     private final Period homePeriod;
     private final TalkType talkType;
     private final Avatar avatar;
-    private final int talkNo;
     private final String messageID;
     private final int hour;
     private final int minute;
-    private final CharSequence dialog;
-    private final int charNum;
+    private int charNum;
+    private int talkNo;
+    private CharSequence dialog;
     private int count = -1;
 
 
     /**
      * Talkの生成。
+     *
      * @param homePeriod 発言元Period
      * @param talkType 発言種別
      * @param avatar Avatar
@@ -72,27 +90,49 @@ public class Talk implements Topic{
 
     /**
      * 会話種別から色名への変換を行う。
+     *
      * @param type 会話種別
      * @return 色名
      */
     public static String encodeColorName(TalkType type){
-        String result;
+        Objects.requireNonNull(type);
+        String result = COLOR_MAP.get(type);
+        return result;
+    }
 
-        switch(type){
-        case PUBLIC:   result = "白"; break;
-        case PRIVATE:  result = "灰"; break;
-        case WOLFONLY: result = "赤"; break;
-        case GRAVE:    result = "青"; break;
-        default:
-            assert false;
-            return null;
+    /**
+     * ある文字列の末尾が別の文字列に一致するか判定する。
+     *
+     * @param target 判定対象
+     * @param term 末尾文字
+     * @return 一致すればtrue
+     * @throws java.lang.NullPointerException 引数がnull
+     * @see String#endsWith(String)
+     */
+    static boolean isTerminated(CharSequence target,
+                                CharSequence term)
+            throws NullPointerException{
+        Objects.requireNonNull(target);
+        Objects.requireNonNull(term);
+
+        int targetLength = target.length();
+        int termLength   = term  .length();
+
+        int offset = targetLength - termLength;
+        if(offset < 0) return false;
+
+        for(int pos = 0; pos < termLength; pos++){
+            char targetch = target.charAt(offset + pos);
+            char termch   = term  .charAt(0      + pos);
+            if(targetch != termch) return false;
         }
 
-        return result;
+        return true;
     }
 
     /**
      * 発言が交わされたPeriodを返す。
+     *
      * @return Period
      */
     public Period getPeriod(){
@@ -101,6 +141,7 @@ public class Talk implements Topic{
 
     /**
      * 発言種別を得る。
+     *
      * @return 種別
      */
     public TalkType getTalkType(){
@@ -109,6 +150,7 @@ public class Talk implements Topic{
 
     /**
      * 墓下発言か否か判定する。
+     *
      * @return 墓下発言ならtrue
      */
     public boolean isGrave(){
@@ -116,8 +158,10 @@ public class Talk implements Topic{
     }
 
     /**
-     * 発言種別ごとにその日(Period)の累積発言回数を返す。
-     * 1から始まる。
+     * 各Avatarの発言種別ごとにその日(Period)の累積発言回数を返す。
+     *
+     * <p>システム生成の襲撃予告の場合は負の値となる。
+     *
      * @return 累積発言回数。
      */
     public int getTalkCount(){
@@ -126,7 +170,9 @@ public class Talk implements Topic{
 
     /**
      * 発言文字数を返す。
-     * 改行(\n)は1文字。
+     *
+     * <p>改行(\n)は1文字。
+     *
      * @return 文字数
      */
     public int getTotalChars(){
@@ -135,6 +181,7 @@ public class Talk implements Topic{
 
     /**
      * 発言元Avatarを得る。
+     *
      * @return 発言元Avatar
      */
     public Avatar getAvatar(){
@@ -143,7 +190,9 @@ public class Talk implements Topic{
 
     /**
      * 公開発言番号を取得する。
-     * 公開発言番号が割り振られてなければ0以下の値を返す。
+     *
+     * <p>公開発言番号が割り振られてなければ0以下の値を返す。
+     *
      * @return 公開発言番号
      */
     public int getTalkNo(){
@@ -151,7 +200,20 @@ public class Talk implements Topic{
     }
 
     /**
+     * 公開発言番号を設定する。
+     *
+     * <p>0以下の値は公開発言番号を持たないと判断される。
+     *
+     * @param talkNo 公開発言番号
+     */
+    public void setTalkNo(int talkNo){
+        this.talkNo = talkNo;
+        return;
+    }
+
+    /**
      * 公開発言番号の有無を返す。
+     *
      * @return 公開発言番号が割り当てられているならtrueを返す。
      */
     public boolean hasTalkNo(){
@@ -161,6 +223,7 @@ public class Talk implements Topic{
 
     /**
      * メッセージIDを取得する。
+     *
      * @return メッセージID
      */
     public String getMessageID(){
@@ -169,6 +232,7 @@ public class Talk implements Topic{
 
     /**
      * メッセージIDからエポック秒(ms)に変換する。
+     *
      * @return GMT 1970-01-01 00:00:00 からのエポック秒(ms)
      */
     public long getTimeFromID(){
@@ -179,6 +243,7 @@ public class Talk implements Topic{
 
     /**
      * 発言時を取得する。
+     *
      * @return 発言時
      */
     public int getHour(){
@@ -187,6 +252,7 @@ public class Talk implements Topic{
 
     /**
      * 発言分を取得する。
+     *
      * @return 発言分
      */
     public int getMinute(){
@@ -195,6 +261,7 @@ public class Talk implements Topic{
 
     /**
      * 会話データを取得する。
+     *
      * @return 会話データ
      */
     public CharSequence getDialog(){
@@ -202,7 +269,21 @@ public class Talk implements Topic{
     }
 
     /**
+     * 会話データを設定する。
+     *
+     * @param seq 会話データ
+     */
+    public void setDialog(CharSequence seq){
+        this.dialog = seq;
+        this.charNum = this.dialog.length();
+        return;
+    }
+
+    /**
      * 発言種別ごとの発言回数を設定する。
+     *
+     * <p>システム生成の襲撃予告では負の値を入れれば良い。
+     *
      * @param count 発言回数
      */
     public void setCount(int count){
@@ -212,7 +293,9 @@ public class Talk implements Topic{
 
     /**
      * この会話を識別するためのアンカー文字列を生成する。
-     * 例えば「3d09:56」など。
+     *
+     * <p>例えば「3d09:56」など。
+     *
      * @return アンカー文字列
      */
     public String getAnchorNotation(){
@@ -228,7 +311,9 @@ public class Talk implements Topic{
 
     /**
      * この会話を識別するためのG国用アンカー文字列を発言番号から生成する。
-     * 例えば「{@literal >>172}」など。
+     *
+     * <p>例えば「{@literal >>172}」など。
+     *
      * @return アンカー文字列。発言番号がなければ空文字列。
      */
     public String getAnchorNotation_G(){
@@ -237,23 +322,67 @@ public class Talk implements Topic{
     }
 
     /**
+     * 会話テキスト本文が襲撃予告たりうるか判定する。
+     *
+     * <p>Period開始時の襲撃予告の文面はシステムが生成する文書であり、
+     * 狼プレイヤーの投稿に由来しない。
+     *
+     * <p>「! 今日がお前の命日だ!」で終わる赤ログは
+     * 襲撃予告の可能性がある。
+     *
+     * <p>
+     * {@link jp.sourceforge.jindolf.corelib.SysEventType#MURDERED}
+     * もしくは
+     * {@link jp.sourceforge.jindolf.corelib.SysEventType#NOMURDER}
+     * の前に該当する赤ログが出現すれば、それは襲撃予告と断定して良い。
+     *
+     * @return 襲撃予告のテキストの可能性があるならtrue
+     */
+    public boolean isMurderNotice(){
+        boolean isWolf;
+        isWolf = this.talkType == TalkType.WOLFONLY;
+        if( ! isWolf) return false;
+
+        boolean meinichida;
+        meinichida = isTerminated(getDialog(), MEINICHI_LAST);
+        if( ! meinichida) return false;
+
+        return true;
+    }
+
+    /**
      * {@inheritDoc}
-     * 会話のString表現を返す。
+     *
+     * <p>会話のString表現を返す。
      * 実体参照やHTMLタグも含まれる。
+     *
      * @return 会話のString表現
      */
     @Override
     public String toString(){
-        StringBuilder result = new StringBuilder();
-
-        result.append(this.avatar.getFullName());
-
-        if     (this.talkType == TalkType.PUBLIC)   result.append(" says ");
-        else if(this.talkType == TalkType.PRIVATE)  result.append(" think ");
-        else if(this.talkType == TalkType.WOLFONLY) result.append(" howl ");
-        else if(this.talkType == TalkType.GRAVE)    result.append(" groan ");
+        String fullName = this.avatar.getFullName();
+
+        String verb;
+        switch (this.talkType) {
+        case PUBLIC:
+            verb=" says ";
+            break;
+        case PRIVATE:
+            verb=" think ";
+            break;
+        case WOLFONLY:
+            verb=" howl ";
+            break;
+        case GRAVE:
+            verb=" groan ";
+            break;
+        default:
+            assert false;
+            return null;
+        }
 
-        result.append(this.dialog);
+        StringBuilder result = new StringBuilder();
+        result.append(fullName).append(verb).append(this.dialog);
 
         return result.toString();
     }
index 79f23c7..7777aa9 100644 (file)
@@ -7,53 +7,22 @@
 
 package jp.sfjp.jindolf.data;
 
-import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.text.MessageFormat;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import jp.osdn.jindolf.parser.HtmlAdapter;
-import jp.osdn.jindolf.parser.HtmlParseException;
-import jp.osdn.jindolf.parser.HtmlParser;
-import jp.osdn.jindolf.parser.PageType;
-import jp.osdn.jindolf.parser.SeqRange;
-import jp.osdn.jindolf.parser.content.DecodedContent;
-import jp.sfjp.jindolf.net.HtmlSequence;
-import jp.sfjp.jindolf.net.ServerAccess;
-import jp.sfjp.jindolf.util.GUIUtils;
+import jp.sfjp.jindolf.view.AvatarPics;
 import jp.sourceforge.jindolf.corelib.LandDef;
-import jp.sourceforge.jindolf.corelib.LandState;
-import jp.sourceforge.jindolf.corelib.PeriodType;
 import jp.sourceforge.jindolf.corelib.VillageState;
 
 /**
  * いわゆる「村」。
  */
-public class Village implements Comparable<Village> {
+public class Village{
 
     private static final int GID_MIN = 3;
 
-    private static final Comparator<Village> VILLAGE_COMPARATOR =
-            new VillageComparator();
-
-    private static final HtmlParser PARSER = new HtmlParser();
-    private static final VillageHeadHandler HANDLER =
-            new VillageHeadHandler();
-
-    private static final Logger LOGGER = Logger.getAnonymousLogger();
-
-    static{
-        PARSER.setBasicHandler   (HANDLER);
-        PARSER.setSysEventHandler(HANDLER);
-        PARSER.setTalkHandler    (HANDLER);
-    }
-
 
     private final Land parentLand;
     private final String villageID;
@@ -76,18 +45,14 @@ public class Village implements Comparable<Village> {
     private final Map<String, Avatar> avatarMap =
             new HashMap<>();
 
-    private final Map<Avatar, BufferedImage> faceImageMap =
-            new HashMap<>();
-    private final Map<Avatar, BufferedImage> bodyImageMap =
-            new HashMap<>();
-    private final Map<Avatar, BufferedImage> faceMonoImageMap =
-            new HashMap<>();
-    private final Map<Avatar, BufferedImage> bodyMonoImageMap =
-            new HashMap<>();
+    private final AvatarPics avatarPics;
+
+    private boolean isLocalArchive = false;
 
 
     /**
      * Villageを生成する。
+     *
      * @param parentLand Villageの所属する国
      * @param villageID 村のID
      * @param villageName 村の名前
@@ -101,51 +66,15 @@ public class Village implements Comparable<Village> {
         this.isValid = this.parentLand.getLandDef()
                        .isValidVillageId(this.villageIDNum);
 
-        return;
-    }
-
-
-    /**
-     * 村同士を比較するためのComparatorを返す。
-     * @return Comparatorインスタンス
-     */
-    public static Comparator<Village> comparator(){
-        return VILLAGE_COMPARATOR;
-    }
-
-    /**
-     * 人狼BBSサーバからPeriod一覧情報が含まれたHTMLを取得し、
-     * Periodリストを更新する。
-     * @param village 村
-     * @throws java.io.IOException ネットワーク入出力の異常
-     */
-    public static synchronized void updateVillage(Village village)
-            throws IOException{
-        Land land = village.getParentLand();
-        LandDef landDef = land.getLandDef();
-        LandState landState = landDef.getLandState();
-        ServerAccess server = land.getServerAccess();
-
-        HtmlSequence html;
-        if(landState == LandState.ACTIVE){
-            html = server.getHTMLBoneHead(village);
-        }else{
-            html = server.getHTMLVillage(village);
-        }
-
-        DecodedContent content = html.getContent();
-        HANDLER.setVillage(village);
-        try{
-            PARSER.parseAutomatic(content);
-        }catch(HtmlParseException e){
-            LOGGER.log(Level.WARNING, "村の状態が不明", e);
-        }
+        this.avatarPics = new AvatarPics(this.parentLand);
 
         return;
     }
 
+
     /**
      * 所属する国を返す。
+     *
      * @return 村の所属する国(Land)
      */
     public Land getParentLand(){
@@ -154,6 +83,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 村のID文字列を返す。
+     *
      * @return 村ID
      */
     public String getVillageID(){
@@ -162,6 +92,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 村のID数値を返す。
+     *
      * @return 村ID
      */
     public int getVillageIDNum(){
@@ -170,6 +101,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 村の名前を返す。
+     *
      * @return 村の名前
      */
     public String getVillageName(){
@@ -193,6 +125,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 村の長い名前を返す。
+     *
      * @return 村の長い名前
      */
     public String getVillageFullName(){
@@ -201,6 +134,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 村の状態を返す。
+     *
      * @return 村の状態
      */
     public VillageState getState(){
@@ -209,6 +143,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 村の状態を設定する。
+     *
      * @param state 村の状態
      */
     public void setState(VillageState state){
@@ -217,7 +152,18 @@ public class Village implements Comparable<Village> {
     }
 
     /**
+     * 日程及び更新時刻を持っているか判定する。
+     *
+     * @return 日程が不明ならtrue
+     */
+    public boolean hasSchedule(){
+        boolean result = ! this.periodList.isEmpty();
+        return result;
+    }
+
+    /**
      * プロローグを返す。
+     *
      * @return プロローグ
      */
     public Period getPrologue(){
@@ -229,6 +175,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * エピローグを返す。
+     *
      * @return エピローグ
      */
     public Period getEpilogue(){
@@ -240,6 +187,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 指定された日付の進行日を返す。
+     *
      * @param day 日付
      * @return Period
      */
@@ -253,6 +201,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * PROGRESS状態のPeriodの総数を返す。
+     *
      * @return PROGRESS状態のPeriod総数
      */
     public int getProgressDays(){
@@ -265,7 +214,9 @@ public class Village implements Comparable<Village> {
 
     /**
      * 指定されたPeriodインデックスのPeriodを返す。
-     * プロローグやエピローグへのアクセスも可能。
+     *
+     * <p>プロローグやエピローグへのアクセスも可能。
+     *
      * @param day Periodインデックス
      * @return Period
      */
@@ -275,6 +226,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 指定されたアンカーの対象のPeriodを返す。
+     *
      * @param anchor アンカー
      * @return Period
      */
@@ -294,6 +246,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * Period総数を返す。
+     *
      * @return Period総数
      */
     public int getPeriodSize(){
@@ -302,6 +255,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * Periodへのリストを返す。
+     *
      * @return Periodのリスト。
      */
     public List<Period> getPeriodList(){
@@ -309,163 +263,70 @@ public class Village implements Comparable<Village> {
     }
 
     /**
-     * 指定した名前で村に登録されているAvatarを返す。
+     * 指定したフルネームで村に登録されているAvatarを返す。
+     *
      * @param fullName Avatarの名前
      * @return Avatar
      */
     public Avatar getAvatar(String fullName){
-        // TODO CharSequenceにできない?
-        Avatar avatar;
-
-        avatar = Avatar.getPredefinedAvatar(fullName);
-        if( avatar != null ){
-            preloadAvatarFace(avatar);
-            return avatar;
-        }
-
-        avatar = this.avatarMap.get(fullName);
-        if( avatar != null ){
-            preloadAvatarFace(avatar);
-            return avatar;
-        }
-
-        return null;
-    }
-
-    /**
-     * Avatarの顔画像を事前にロードする。
-     * @param avatar Avatar
-     */
-    private void preloadAvatarFace(Avatar avatar){
-        if(this.faceImageMap.get(avatar) != null) return;
-
-        Land land = getParentLand();
-        LandDef landDef = land.getLandDef();
-
-        String template = landDef.getFaceURITemplate();
-        int serialNo = avatar.getIdNum();
-        String uri = MessageFormat.format(template, serialNo);
-
-        BufferedImage image = land.downloadImage(uri);
-        if(image == null) image = GUIUtils.getNoImage();
-
-        this.faceImageMap.put(avatar, image);
-
-        return;
+        Avatar avatar = this.avatarMap.get(fullName);
+        return avatar;
     }
 
     /**
      * Avatarを村に登録する。
+     *
      * @param avatar Avatar
      */
-    // 未知のAvatar出現時の処理が不完全
     public void addAvatar(Avatar avatar){
         if(avatar == null) return;
-        String fullName = avatar.getFullName();
-        this.avatarMap.put(fullName, avatar);
 
-        preloadAvatarFace(avatar);
+        String fullName = avatar.getFullName();
+        if(this.avatarMap.get(fullName) != null) return;
 
+        this.avatarMap.put(fullName, avatar);
         return;
     }
 
     /**
-     * 村に登録されたAvatarの顔イメージを返す。
-     * @param avatar Avatar
-     * @return 顔イメージ
-     */
-    // TODO 失敗したらプロローグを強制読み込みして再トライしたい
-    public BufferedImage getAvatarFaceImage(Avatar avatar){
-        return this.faceImageMap.get(avatar);
-    }
-
-    /**
-     * 村に登録されたAvatarの全身像イメージを返す。
-     * @param avatar Avatar
-     * @return 全身イメージ
-     */
-    public BufferedImage getAvatarBodyImage(Avatar avatar){
-        BufferedImage result;
-        result = this.bodyImageMap.get(avatar);
-        if(result != null) return result;
-
-        Land land = getParentLand();
-        LandDef landDef = land.getLandDef();
-
-        String template = landDef.getBodyURITemplate();
-        int serialNo = avatar.getIdNum();
-        String uri = MessageFormat.format(template, serialNo);
-
-        result = land.downloadImage(uri);
-        if(result == null) result = GUIUtils.getNoImage();
-
-        this.bodyImageMap.put(avatar, result);
-
-        return result;
-    }
-
-    /**
-     * 村に登録されたAvatarのモノクロ顔イメージを返す。
-     * @param avatar Avatar
-     * @return 顔イメージ
-     */
-    public BufferedImage getAvatarFaceMonoImage(Avatar avatar){
-        BufferedImage result;
-        result = this.faceMonoImageMap.get(avatar);
-        if(result == null){
-            result = getAvatarFaceImage(avatar);
-            result = GUIUtils.createMonoImage(result);
-            this.faceMonoImageMap.put(avatar, result);
-        }
-        return result;
-    }
-
-    /**
-     * 村に登録されたAvatarの全身像イメージを返す。
-     * @param avatar Avatar
-     * @return 全身イメージ
-     */
-    public BufferedImage getAvatarBodyMonoImage(Avatar avatar){
-        BufferedImage result;
-        result = this.bodyMonoImageMap.get(avatar);
-        if(result == null){
-            result = getAvatarBodyImage(avatar);
-            result = GUIUtils.createMonoImage(result);
-            this.bodyMonoImageMap.put(avatar, result);
-        }
-        return result;
-    }
-
-    /**
-     * 国に登録された墓イメージを返す。
-     * @return 墓イメージ
+     * Avatar画像管理を返す。
+     *
+     * @return 画像管理
      */
-    public BufferedImage getGraveImage(){
-        BufferedImage result = getParentLand().getGraveIconImage();
-        return result;
-    }
-
-    /**
-     * 国に登録された墓イメージ(大)を返す。
-     * @return 墓イメージ(大)
-     */
-    public BufferedImage getGraveBodyImage(){
-        BufferedImage result = getParentLand().getGraveBodyImage();
-        return result;
+    public AvatarPics getAvatarPics(){
+        return this.avatarPics;
     }
 
     /**
      * 村にアクセスするためのCGIクエリーを返す。
+     *
      * @return CGIクエリー
      */
-    public CharSequence getCGIQuery(){
+    public String getCGIQuery(){
         StringBuilder result = new StringBuilder();
         result.append("?vid=").append(getVillageID());
-        return result;
+        return result.toString();
+    }
+
+    /**
+     * 次回更新時を設定する。
+     *
+     * @param month 月
+     * @param day 日
+     * @param hour 時
+     * @param minute 分
+     */
+    public void setLimit(int month, int day, int hour, int minute){
+        this.limitMonth = month;
+        this.limitDay = day;
+        this.limitHour = hour;
+        this.limitMinute = minute;
+        return;
     }
 
     /**
      * 次回更新月を返す。
+     *
      * @return 更新月(1-12)
      */
     public int getLimitMonth(){
@@ -474,6 +335,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 次回更新日を返す。
+     *
      * @return 更新日(1-31)
      */
     public int getLimitDay(){
@@ -482,6 +344,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 次回更新時を返す。
+     *
      * @return 更新時(0-23)
      */
     public int getLimitHour(){
@@ -490,6 +353,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 次回更新分を返す。
+     *
      * @return 更新分(0-59)
      */
     public int getLimitMinute(){
@@ -498,6 +362,7 @@ public class Village implements Comparable<Village> {
 
     /**
      * 有効な村か否か判定する。
+     *
      * @return 無効な村ならfalse
      */
     public boolean isValid(){
@@ -506,13 +371,15 @@ public class Village implements Comparable<Village> {
 
     /**
      * Periodリストの指定したインデックスにPeriodを上書きする。
-     * リストのサイズと同じインデックスを指定する事が許される。
+     *
+     * <p>リストのサイズと同じインデックスを指定する事が許される。
      * その場合の動作はList.addと同じ。
+     *
      * @param index Periodリストのインデックス。
      * @param period 上書きするPeriod
      * @throws java.lang.IndexOutOfBoundsException インデックスの指定がおかしい
      */
-    private void setPeriod(int index, Period period)
+    public void setPeriod(int index, Period period)
             throws IndexOutOfBoundsException{
         int listSize = this.periodList.size();
         if(index == listSize){
@@ -527,17 +394,16 @@ public class Village implements Comparable<Village> {
 
     /**
      * アンカーに一致する会話(Talk)のリストを取得する。
+     *
      * @param anchor アンカー
      * @return Talkのリスト
-     * @throws java.io.IOException おそらくネットワークエラー
      */
-    public List<Talk> getTalkListFromAnchor(Anchor anchor)
-            throws IOException{
+    public List<Talk> getTalkListFromAnchor(Anchor anchor){
         List<Talk> result = new LinkedList<>();
 
         /* G国アンカー対応 */
         if(anchor.hasTalkNo()){
-            // 事前に全Periodがロードされているのが前提
+            // äº\8bå\89\8dã\81«å\85¨Periodã\81®å\85¨ä¼\9a話ã\81\8cã\83­ã\83¼ã\83\89ã\81\95ã\82\8cã\81¦ã\81\84ã\82\8bã\81®ã\81\8cå\89\8dæ\8f\90
             for(Period period : this.periodList){
                 Talk talk = period.getNumberedTalk(anchor.getTalkNo());
                 if(talk == null) continue;
@@ -549,7 +415,7 @@ public class Village implements Comparable<Village> {
         Period anchorPeriod = getPeriod(anchor);
         if(anchorPeriod == null) return result;
 
-        Period.parsePeriod(anchorPeriod, false);
+        // 事前にアンカー対象Periodの全会話がロードされているのが前提
 
         for(Topic topic : anchorPeriod.getTopicList()){
             if( ! (topic instanceof Talk) ) continue;
@@ -572,52 +438,30 @@ public class Village implements Comparable<Village> {
     }
 
     /**
-     * {@inheritDoc}
-     * 二つの村を順序付ける。
-     * @param village {@inheritDoc}
-     * @return {@inheritDoc}
+     * この村がローカルなアーカイブに由来するものであるか判定する。
+     *
+     * @return ローカルなアーカイブによる村であればtrue
      */
-    @Override
-    public int compareTo(Village village){
-        int cmpResult = VILLAGE_COMPARATOR.compare(this, village);
-        return cmpResult;
+    public boolean isLocalArchive(){
+        return this.isLocalArchive;
     }
 
     /**
-     * {@inheritDoc}
-     * 二つの村が等しいか調べる。
-     * @param obj {@inheritDoc}
-     * @return {@inheritDoc}
+     * この村がローカルなアーカイブに由来するものであるか設定する。
+     *
+     * @param flag ローカルなアーカイブによる村であればtrue
      */
-    @Override
-    public boolean equals(Object obj){
-        if(obj == null) return false;
-        if( ! (obj instanceof Village) ) return false;
-        Village village = (Village) obj;
-
-        if( getParentLand() != village.getParentLand() ) return false;
-
-        int cmpResult = compareTo(village);
-        if(cmpResult == 0) return true;
-        return false;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public int hashCode(){
-        int homeHash = getParentLand().hashCode();
-        int vidHash = getVillageID().hashCode();
-        int result = homeHash ^ vidHash;
-        return result;
+    public void setLocalArchive(boolean flag){
+        this.isLocalArchive = flag;
+        return;
     }
 
     /**
      * {@inheritDoc}
-     * 村の文字列表現を返す。
+     *
+     * <p>村の文字列表現を返す。
      * 村の名前と等しい。
+     *
      * @return 村の名前
      */
     @Override
@@ -625,277 +469,4 @@ public class Village implements Comparable<Village> {
         return getVillageFullName();
     }
 
-
-    /**
-     * Period一覧取得用ハンドラ。
-     */
-    private static class VillageHeadHandler extends HtmlAdapter{
-
-        private Village village = null;
-
-        private boolean hasPrologue;
-        private boolean hasProgress;
-        private boolean hasEpilogue;
-        private boolean hasDone;
-        private int maxProgress;
-
-        /**
-         * コンストラクタ。
-         */
-        public VillageHeadHandler(){
-            super();
-            return;
-        }
-
-        /**
-         * 更新対象の村を設定する。
-         * @param village 村
-         */
-        public void setVillage(Village village){
-            this.village = village;
-            return;
-        }
-
-        /**
-         * リセットを行う。
-         */
-        public void reset(){
-            this.hasPrologue = false;
-            this.hasProgress = false;
-            this.hasEpilogue = false;
-            this.hasDone = false;
-            this.maxProgress = 0;
-            return;
-        }
-
-        /**
-         * パース結果から村の状態を算出する。
-         * @return 村の状態
-         */
-        public VillageState getVillageState(){
-            if(this.hasDone){
-                return VillageState.GAMEOVER;
-            }else if(this.hasEpilogue){
-                return VillageState.EPILOGUE;
-            }else if(this.hasProgress){
-                return VillageState.PROGRESS;
-            }else if(this.hasPrologue){
-                return VillageState.PROLOGUE;
-            }
-
-            return VillageState.UNKNOWN;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void startParse(DecodedContent content)
-                throws HtmlParseException{
-            reset();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * 自動判定の結果が日ページでなければ例外を投げる。
-         * @param type {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
-         */
-        @Override
-        public void pageType(PageType type) throws HtmlParseException{
-            if(type != PageType.PERIOD_PAGE){
-                throw new HtmlParseException(
-                        "日ページが必要です。");
-            }
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param month {@inheritDoc}
-         * @param day {@inheritDoc}
-         * @param hour {@inheritDoc}
-         * @param minute {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void commitTime(int month, int day,
-                                int hour, int minute)
-                throws HtmlParseException{
-            this.village.limitMonth  = month;
-            this.village.limitDay    = day;
-            this.village.limitHour   = hour;
-            this.village.limitMinute = minute;
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param content {@inheritDoc}
-         * @param anchorRange {@inheritDoc}
-         * @param periodType {@inheritDoc}
-         * @param day {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void periodLink(DecodedContent content,
-                                SeqRange anchorRange,
-                                PeriodType periodType,
-                                int day)
-                throws HtmlParseException{
-            if(periodType == null){
-                this.hasDone = true;
-                return;
-            }
-
-            switch(periodType){
-            case PROLOGUE:
-                this.hasPrologue = true;
-                break;
-            case PROGRESS:
-                this.hasProgress = true;
-                this.maxProgress = day;
-                break;
-            case EPILOGUE:
-                this.hasEpilogue = true;
-                break;
-            default:
-                assert false;
-                break;
-            }
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @throws HtmlParseException {@inheritDoc}
-         */
-        @Override
-        public void endParse() throws HtmlParseException{
-            Land land = this.village.getParentLand();
-            LandDef landDef = land.getLandDef();
-            LandState landState = landDef.getLandState();
-
-            VillageState villageState = getVillageState();
-            if(villageState == VillageState.UNKNOWN){
-                this.village.setState(villageState);
-                this.village.periodList.clear();
-                LOGGER.warning("村の状況を読み取れません");
-                return;
-            }
-
-            if(landState == LandState.ACTIVE){
-                this.village.setState(villageState);
-            }else{
-                this.village.setState(VillageState.GAMEOVER);
-            }
-
-            modifyPeriodList();
-
-            return;
-        }
-
-        /**
-         * 抽出したリンク情報に伴いPeriodリストを更新する。
-         * まだPeriodデータのロードは行われない。
-         * ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
-         * 既存のPeriodリストが伸張する場合がある。
-         */
-        private void modifyPeriodList(){
-            Period lastPeriod = null;
-
-            if(this.hasPrologue){
-                Period prologue = this.village.getPrologue();
-                if(prologue == null){
-                    lastPeriod = new Period(this.village,
-                                            PeriodType.PROLOGUE, 0);
-                    this.village.setPeriod(0, lastPeriod);
-                }else{
-                    lastPeriod = prologue;
-                }
-            }
-
-            if(this.hasProgress){
-                for(int day = 1; day <= this.maxProgress; day++){
-                    Period progress = this.village.getProgress(day);
-                    if(progress == null){
-                        lastPeriod = new Period(this.village,
-                                                PeriodType.PROGRESS, day);
-                        this.village.setPeriod(day, lastPeriod);
-                    }else{
-                        lastPeriod = progress;
-                    }
-                }
-            }
-
-            if(this.hasEpilogue){
-                Period epilogue = this.village.getEpilogue();
-                if(epilogue == null){
-                    lastPeriod = new Period(this.village,
-                                            PeriodType.EPILOGUE,
-                                            this.maxProgress +1);
-                    this.village.setPeriod(this.maxProgress +1, lastPeriod);
-                }else{
-                    lastPeriod = epilogue;
-                }
-            }
-
-            assert this.village.getPeriodSize() > 0;
-            assert lastPeriod != null;
-
-            // 念のためチョップ。
-            // リロードで村が縮むわけないじゃん。みんな大げさだなあ
-            while(this.village.periodList.getLast() != lastPeriod){
-                this.village.periodList.removeLast();
-            }
-
-            if(this.village.getState() != VillageState.GAMEOVER){
-                lastPeriod.setHot(true);
-            }
-
-            return;
-        }
-
-    }
-
-
-    /**
-     * 村同士を比較するためのComparator。
-     */
-    private static class VillageComparator implements Comparator<Village> {
-
-        /**
-         * コンストラクタ。
-         */
-        public VillageComparator(){
-            super();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param v1 {@inheritDoc}
-         * @param v2 {@inheritDoc}
-         * @return {@inheritDoc}
-         */
-        @Override
-        public int compare(Village v1, Village v2){
-            int v1Num;
-            if(v1 == null) v1Num = Integer.MIN_VALUE;
-            else           v1Num = v1.getVillageIDNum();
-
-            int v2Num;
-            if(v2 == null) v2Num = Integer.MIN_VALUE;
-            else           v2Num = v2.getVillageIDNum();
-
-            return v1Num - v2Num;
-        }
-
-    }
-
 }
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/PeriodHandler.java b/src/main/java/jp/sfjp/jindolf/data/html/PeriodHandler.java
new file mode 100644 (file)
index 0000000..114c2d6
--- /dev/null
@@ -0,0 +1,976 @@
+/*
+ * period handler
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import jp.osdn.jindolf.parser.EntityConverter;
+import jp.osdn.jindolf.parser.HtmlAdapter;
+import jp.osdn.jindolf.parser.HtmlParseException;
+import jp.osdn.jindolf.parser.PageType;
+import jp.osdn.jindolf.parser.SeqRange;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sfjp.jindolf.data.Avatar;
+import jp.sfjp.jindolf.data.InterPlay;
+import jp.sfjp.jindolf.data.Nominated;
+import jp.sfjp.jindolf.data.Period;
+import jp.sfjp.jindolf.data.Player;
+import jp.sfjp.jindolf.data.SysEvent;
+import jp.sfjp.jindolf.data.Talk;
+import jp.sfjp.jindolf.data.Topic;
+import jp.sfjp.jindolf.data.Village;
+import jp.sourceforge.jindolf.corelib.Destiny;
+import jp.sourceforge.jindolf.corelib.EventFamily;
+import jp.sourceforge.jindolf.corelib.GameRole;
+import jp.sourceforge.jindolf.corelib.PeriodType;
+import jp.sourceforge.jindolf.corelib.SysEventType;
+import jp.sourceforge.jindolf.corelib.TalkType;
+import jp.sourceforge.jindolf.corelib.Team;
+
+
+/**
+ * 各日(Period)のHTMLをパースし、
+ * 会話やイベントの通知を受け取るためのハンドラ。
+ *
+ * <p>パース終了時には、
+ * あらかじめ指定したPeriodインスタンスに
+ * 会話やイベントのリストが適切に更新される。
+ *
+ * <p>各種ビューが対応するまでの間、Unicodeの非BMP面文字には代替文字で対処。
+ *
+ * <p>※ 人狼BBS:G国におけるG2087村のエピローグが終了した段階で、
+ * 人狼BBSは過去ログの提供しか行っていない。
+ * だがこのクラスには進行中の村の各日をパースするための
+ * 冗長な処理が若干残っている。
+ */
+class PeriodHandler extends HtmlAdapter {
+
+    private static final int TALKTYPE_NUM = TalkType.values().length;
+
+    private final EntityConverter converter =
+            new EntityConverter(true);
+    // TODO: 非BMP面文字に対応するまでの暫定措置
+
+    /** 非別、Avatar別、会話種別の会話通し番号。 */
+    private final Map<Avatar, int[]> countMap =
+            new HashMap<>();
+
+    private Period period = null;
+
+    private TalkType talkType;
+    private Avatar avatar;
+    private int talkNo;
+    private String anchorId;
+    private int talkHour;
+    private int talkMinute;
+    private DecodedContent talkContent = null;
+
+    private EventFamily eventFamily;
+    private SysEventType sysEventType;
+    private DecodedContent eventContent = null;
+    private final List<Avatar> avatarList = new LinkedList<>();
+    private final List<GameRole> roleList = new LinkedList<>();
+    private final List<Integer> integerList = new LinkedList<>();
+    private final List<CharSequence>  charseqList =
+        new LinkedList<>();
+    private final List<Player> playerList = new LinkedList<>();
+    private final List<Nominated> nominatedList = new LinkedList<>();
+    private final List<InterPlay> interPlayList = new LinkedList<>();
+
+
+    /**
+     * コンストラクタ。
+     */
+    PeriodHandler(){
+        super();
+        return;
+    }
+
+
+    /**
+     * 更新対象のPeriodを設定する。
+     *
+     * @param period Period
+     */
+    void setPeriod(Period period){
+        this.period = period;
+        reset();
+        return;
+    }
+
+    /**
+     * フルネーム文字列からAvatarインスタンスを得る。
+     *
+     * <p>村に未登録のAvatarであればついでに登録される。
+     *
+     * @param content 文字列
+     * @param range 文字列内のAvatarフルネームを示す領域
+     * @return Avatar
+     */
+    private Avatar toAvatar(DecodedContent content, SeqRange range){
+        Village village = this.period.getVillage();
+        String fullName = this.converter
+                              .convert(content, range)
+                              .toString();
+        Avatar result = village.getAvatar(fullName);
+        if(result == null){
+            result = Avatar.getAvatarByFullname(fullName);
+            village.addAvatar(result);
+        }
+
+        return result;
+    }
+
+    /**
+     * パース中の各種コンテキストをリセットする。
+     */
+    void reset(){
+        this.countMap.clear();
+
+        resetTalkContext();
+        resetEventContext();
+
+        return;
+    }
+
+    /**
+     * パース中の会話コンテキストをリセットする。
+     */
+    private void resetTalkContext(){
+        this.talkType = null;
+        this.avatar = null;
+        this.talkNo = -1;
+        this.anchorId = null;
+        this.talkHour = -1;
+        this.talkMinute = -1;
+        this.talkContent = null;
+        return;
+    }
+
+    /**
+     * パース中のイベントコンテキストをリセットする。
+     */
+    private void resetEventContext(){
+        this.eventFamily = null;
+        this.sysEventType = null;
+        this.eventContent = null;
+        this.avatarList.clear();
+        this.roleList.clear();
+        this.integerList.clear();
+        this.charseqList.clear();
+        this.playerList.clear();
+        this.nominatedList.clear();
+        this.interPlayList.clear();
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param content {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void startParse(DecodedContent content)
+            throws HtmlParseException{
+        reset();
+
+        this.period.clearTopicList();
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>各PeriodのHTML上部にあるログイン名が通知されたのなら、
+     * それはPOSTやCookieを使ってのログインに成功したと言うこと。
+     *
+     * <p>ログイン名中の文字実体参照は展開される。
+     *
+     * <p>※ 2020-02現在、人狼BBS各国へのログインは無意味。
+     *
+     * @param content {@inheritDoc}
+     * @param loginRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void loginName(DecodedContent content, SeqRange loginRange)
+            throws HtmlParseException{
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>受信したHTMLがPeriodページでないのならパースを中止する。
+     *
+     * @param type {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void pageType(PageType type) throws HtmlParseException{
+        if(type != PageType.PERIOD_PAGE){
+            throw new HtmlParseException(
+                    "意図しないページを読み込もうとしました。");
+        }
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>月日の通知は無視される。
+     *
+     * @param month {@inheritDoc}
+     * @param day {@inheritDoc}
+     * @param hour {@inheritDoc}
+     * @param minute {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void commitTime(int month, int day, int hour, int minute)
+            throws HtmlParseException{
+        this.period.setLimit(hour, minute);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>このPeriodが進行中(Hot!)か否か判定する。
+     *
+     * <p>PeriodのHTML内に自分自身へのリンクが無いかチェックする。
+     * 自分へのリンクが見つかればこのPeriodを非Hotにする。
+     * 自分へのリンクがあるということは、
+     * 今受信しているHTMLは別のPeriodから辿るために書かれたものということ。
+     *
+     * <p>原因としては、HotだったPeriodがゲーム進行に従い
+     * Hotでなくなったことなどが考えられる。
+     *
+     * <p>各Periodの種別と日は、
+     * 村情報受信を通じて事前に設定されていなければならない。
+     *
+     * <p>※ 2020-02現在、HotなPeriodを受信する機会はないはず。
+     *
+     * @param content {@inheritDoc}
+     * @param anchorRange {@inheritDoc}
+     * @param periodType {@inheritDoc}
+     * @param day {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void periodLink(DecodedContent content,
+                           SeqRange anchorRange,
+                           PeriodType periodType,
+                           int day )
+            throws HtmlParseException{
+        if(this.period.getType() != periodType) return;
+
+        boolean isProgress = periodType == PeriodType.PROGRESS;
+        boolean dayMatch = this.period.getDay() == day;
+        if(isProgress && ! dayMatch){
+            return;
+        }
+
+        if( ! anchorRange.isValid() ) return;
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void startTalk() throws HtmlParseException{
+        resetTalkContext();
+        this.talkContent = new DecodedContent(100 + 1);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param type {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void talkType(TalkType type)
+            throws HtmlParseException{
+        this.talkType = type;
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void talkAvatar(DecodedContent content, SeqRange avatarRange)
+            throws HtmlParseException{
+        this.avatar = toAvatar(content, avatarRange);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param hour {@inheritDoc}
+     * @param minute {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void talkTime(int hour, int minute)
+            throws HtmlParseException{
+        this.talkHour = hour;
+        this.talkMinute = minute;
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param tno {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void talkNo(int tno) throws HtmlParseException{
+        this.talkNo = tno;
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param content {@inheritDoc}
+     * @param idRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void talkId(DecodedContent content, SeqRange idRange)
+            throws HtmlParseException{
+        this.anchorId = content.subSequence(idRange.getStartPos(),
+                                            idRange.getEndPos()   )
+                               .toString();
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>会話中の文字実体参照は展開される。
+     *
+     * @param content {@inheritDoc}
+     * @param textRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void talkText(DecodedContent content, SeqRange textRange)
+            throws HtmlParseException{
+        this.converter.append(this.talkContent, content, textRange);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void talkBreak()
+            throws HtmlParseException{
+        this.talkContent.append('\n');
+        return;
+    }
+
+    /**
+     * 日別、Avatar別、会話種ごとに発言回数をインクリメントする。
+     *
+     * @param targetAvatar 対象Avatar
+     * @param targetType 対象会話種
+     * @return 現時点でのカウント数
+     */
+    private int countUp(Avatar targetAvatar, TalkType targetType){
+        int[] avatarCount = this.countMap.get(targetAvatar);
+        if(avatarCount == null){
+            avatarCount = new int[TALKTYPE_NUM];
+            this.countMap.put(targetAvatar, avatarCount);
+        }
+
+        int typeIdx = targetType.ordinal();
+        int count = ++avatarCount[typeIdx];
+
+        return count;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>パース中の各種コンテキストから会話を組み立て、
+     * Periodに追加する。
+     *
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void endTalk() throws HtmlParseException{
+        Talk talk = new Talk(this.period,
+                             this.talkType,
+                             this.avatar,
+                             this.talkNo,
+                             this.anchorId,
+                             this.talkHour, this.talkMinute,
+                             this.talkContent );
+
+        int count = countUp(this.avatar, this.talkType);
+        talk.setCount(count);
+
+        this.period.addTopic(talk);
+
+        resetTalkContext();
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param family {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void startSysEvent(EventFamily family)
+            throws HtmlParseException{
+        resetEventContext();
+
+        this.eventFamily = family;
+        this.eventContent = new DecodedContent();
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param type {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventType(SysEventType type)
+            throws HtmlParseException{
+        this.sysEventType = type;
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>イベント文字列中の文字実体参照は展開される。
+     *
+     * @param content {@inheritDoc}
+     * @param contentRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventContent(DecodedContent content,
+                                SeqRange contentRange)
+            throws HtmlParseException{
+        this.converter.append(this.eventContent, content, contentRange);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>イベント文内Aタグ内容の文字実体参照は展開される。
+     * HREF属性値は無視される
+     *
+     * @param content {@inheritDoc}
+     * @param anchorRange {@inheritDoc}
+     * @param contentRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventContentAnchor(DecodedContent content,
+                                      SeqRange anchorRange,
+                                      SeqRange contentRange)
+            throws HtmlParseException{
+        this.converter.append(this.eventContent, content, contentRange);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventContentBreak() throws HtmlParseException{
+        this.eventContent.append('\n');
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Avatarリストの先頭にAvatarが、
+     * intリストの先頭にエントリー番号が入る。
+     *
+     * @param content {@inheritDoc}
+     * @param entryNo {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventOnStage(DecodedContent content,
+                                int entryNo,
+                                SeqRange avatarRange)
+            throws HtmlParseException{
+        Avatar newAvatar = toAvatar(content, avatarRange);
+        Player player = new Player();
+        player.setAvatar(newAvatar);
+        player.setEntryNo(entryNo);
+        this.playerList.add(player);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>役職者数開示に伴い役職リストとintリストに一件ずつ追加される。
+     *
+     * @param role {@inheritDoc}
+     * @param num {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventOpenRole(GameRole role, int num)
+            throws HtmlParseException{
+        this.roleList.add(role);
+        this.integerList.add(num);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>噛み及びハム溶けに伴いAvatarリストに1件ずつ追加される。
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventMurdered(DecodedContent content,
+                                 SeqRange avatarRange)
+            throws HtmlParseException{
+        Avatar murdered = toAvatar(content, avatarRange);
+        this.avatarList.add(murdered);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>生存者表示に伴いAvatarリストに1件ずつ追加される。
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventSurvivor(DecodedContent content,
+                                 SeqRange avatarRange)
+            throws HtmlParseException{
+        Avatar survivor = toAvatar(content, avatarRange);
+        this.avatarList.add(survivor);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>G国以外での処刑に伴い、
+     * 投票元と投票先の順でAvatarリストに追加される。
+     *
+     * <p>被処刑者がいればAvatarリストの最後に追加される。
+     *
+     * @param content {@inheritDoc}
+     * @param voteByRange {@inheritDoc}
+     * @param voteToRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventCounting(DecodedContent content,
+                                 SeqRange voteByRange,
+                                 SeqRange voteToRange)
+            throws HtmlParseException{
+        if( ! voteByRange.isValid()){
+            Avatar victim = toAvatar(content, voteToRange);
+            this.avatarList.add(victim);
+            return;
+        }
+
+        Avatar voteBy = toAvatar(content, voteByRange);
+        Avatar voteTo = toAvatar(content, voteToRange);
+
+        InterPlay interPlay = new InterPlay(voteBy, voteTo);
+        this.interPlayList.add(interPlay);
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>G国処刑に伴い、
+     * 投票元と投票先の順でAvatarリストに追加される。
+     *
+     * @param content {@inheritDoc}
+     * @param voteByRange {@inheritDoc}
+     * @param voteToRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventCounting2(DecodedContent content,
+                                  SeqRange voteByRange,
+                                  SeqRange voteToRange)
+            throws HtmlParseException{
+        sysEventCounting(content, voteByRange, voteToRange);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Avatarリストの先頭に突然死者が入る。
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventSuddenDeath(DecodedContent content,
+                                    SeqRange avatarRange)
+            throws HtmlParseException{
+        Avatar suddenDeath = toAvatar(content, avatarRange);
+        this.avatarList.add(suddenDeath);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventCheckout(DecodedContent content,
+                                 SeqRange avatarRange)
+            throws HtmlParseException {
+        Avatar checkouted = toAvatar(content, avatarRange);
+        this.avatarList.add(checkouted);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventVanish(DecodedContent content,
+                               SeqRange avatarRange)
+            throws HtmlParseException {
+        Avatar vanished = toAvatar(content, avatarRange);
+        this.avatarList.add(vanished);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>プレイヤー情報開示に伴い、
+     * Avatarリストに1件、
+     * 文字列リストにURLとプレイヤー名の2件、
+     * intリストに生死(1or0)が1件、
+     * Roleリストに役職が1件追加される。
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @param anchorRange {@inheritDoc}
+     * @param loginRange {@inheritDoc}
+     * @param isLiving {@inheritDoc}
+     * @param role {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventPlayerList(DecodedContent content,
+                                   SeqRange avatarRange,
+                                   SeqRange anchorRange,
+                                   SeqRange loginRange,
+                                   boolean isLiving,
+                                   GameRole role )
+            throws HtmlParseException{
+        Avatar who = toAvatar(content, avatarRange);
+
+        CharSequence anchor;
+        if(anchorRange.isValid()){
+            anchor = this.converter.convert(content, anchorRange);
+        }else{
+            anchor = "";
+        }
+        CharSequence account = this.converter
+                                   .convert(content, loginRange);
+
+        Player player = new Player();
+
+        player.setAvatar(who);
+        player.setRole(role);
+        player.setIdName(account.toString());
+        player.setUrlText(anchor.toString());
+        if(isLiving){
+            player.setObitDay(-1);
+            player.setDestiny(Destiny.ALIVE);
+        }
+
+        this.playerList.add(player);
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>G国処刑に伴い、被処刑者がいればAvatarリストに1件追加される。
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @param votes {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventExecution(DecodedContent content,
+                                  SeqRange avatarRange,
+                                  int votes )
+            throws HtmlParseException{
+        Avatar who = toAvatar(content, avatarRange);
+
+        if(votes <= 0){
+            this.avatarList.add(who);
+        }else{
+            Nominated nominated = new Nominated(who, votes);
+            this.nominatedList.add(nominated);
+        }
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>エントリー促しに伴い、
+     * intリストに分数、最小メンバ数、最大メンバ数の3件が設定される。
+     *
+     * @param hour {@inheritDoc}
+     * @param minute {@inheritDoc}
+     * @param minLimit {@inheritDoc}
+     * @param maxLimit {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventAskEntry(int hour, int minute,
+                                 int minLimit, int maxLimit)
+            throws HtmlParseException{
+        this.integerList.add(hour * 60 + minute);
+        this.integerList.add(minLimit);
+        this.integerList.add(maxLimit);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>エントリー完了に伴い、分数をintリストに設定する。
+     *
+     * @param hour {@inheritDoc}
+     * @param minute {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventAskCommit(int hour, int minute)
+            throws HtmlParseException{
+        this.integerList.add(hour * 60 + minute);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>未発言者一覧に伴い、
+     * 未発言者はAvatarリストへ1件ずつ追加される。
+     *
+     * @param content {@inheritDoc}
+     * @param avatarRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventNoComment(DecodedContent content,
+                                  SeqRange avatarRange)
+            throws HtmlParseException{
+        Avatar noComAvatar = toAvatar(content, avatarRange);
+        this.avatarList.add(noComAvatar);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>決着発表に伴い、
+     * Roleリストに勝者が1件、intリスト分数が1件設定される。
+     *
+     * <p>村勝利の場合は素村役職が用いられる。
+     *
+     * @param winner {@inheritDoc}
+     * @param hour {@inheritDoc}
+     * @param minute {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventStayEpilogue(Team winner, int hour, int minute)
+            throws HtmlParseException{
+        GameRole role = null;
+
+        switch(winner){
+        case VILLAGE: role = GameRole.INNOCENT; break;
+        case WOLF:    role = GameRole.WOLF;     break;
+        case HAMSTER: role = GameRole.HAMSTER;  break;
+        default: assert false; break;
+        }
+
+        this.roleList.add(role);
+        this.integerList.add(hour * 60 + minute);
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>護衛に伴い、Avatarリストに護衛元1件と護衛先1件が設定される。
+     *
+     * @param content {@inheritDoc}
+     * @param guardByRange {@inheritDoc}
+     * @param guardToRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventGuard(DecodedContent content,
+                              SeqRange guardByRange,
+                              SeqRange guardToRange)
+            throws HtmlParseException{
+        Avatar guardBy = toAvatar(content, guardByRange);
+        Avatar guardTo = toAvatar(content, guardToRange);
+        InterPlay interPlay = new InterPlay(guardBy, guardTo);
+        this.interPlayList.add(interPlay);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>占いに伴い、
+     * 占い元が1件、占い先が1件Avatarリストに設定される。
+     *
+     * @param content {@inheritDoc}
+     * @param judgeByRange {@inheritDoc}
+     * @param judgeToRange {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void sysEventJudge(DecodedContent content,
+                              SeqRange judgeByRange,
+                              SeqRange judgeToRange)
+            throws HtmlParseException{
+        Avatar judgeBy = toAvatar(content, judgeByRange);
+        Avatar judgeTo = toAvatar(content, judgeToRange);
+        InterPlay interPlay = new InterPlay(judgeBy, judgeTo);
+        this.interPlayList.add(interPlay);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>パースの完了した1件のイベントインスタンスを
+     * Periodに追加する。
+     *
+     * <p>襲撃もしくは襲撃なしのイベントの前に、
+     * 「今日がお前の命日だ!」で終わる赤ログが出現した場合、
+     * 赤カウントに含めない。
+     *
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void endSysEvent() throws HtmlParseException{
+        SysEvent event = new SysEvent();
+        event.setEventFamily(this.eventFamily);
+        event.setSysEventType(this.sysEventType);
+        event.setContent(this.eventContent);
+        event.addAvatarList(this.avatarList);
+        event.addRoleList(this.roleList);
+        event.addIntegerList(this.integerList);
+        event.addCharSequenceList(this.charseqList);
+        event.addPlayerList(this.playerList);
+        event.addNominatedList(this.nominatedList);
+        event.addInterPlayList(this.interPlayList);
+
+        this.period.addTopic(event);
+
+        boolean isMurderResult =
+                   this.sysEventType == SysEventType.MURDERED
+                || this.sysEventType == SysEventType.NOMURDER;
+
+        if(isMurderResult){
+            for(Topic topic : this.period.getTopicList()){
+                if( ! (topic instanceof Talk) ) continue;
+                Talk talk = (Talk) topic;
+                if(talk.isMurderNotice()){
+                    talk.setCount(-1);
+                    this.countMap.clear();
+                    break;
+                }
+            }
+        }
+
+        resetEventContext();
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void endParse() throws HtmlParseException{
+        reset();
+        return;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/PeriodLoader.java b/src/main/java/jp/sfjp/jindolf/data/html/PeriodLoader.java
new file mode 100644 (file)
index 0000000..a024919
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * period loader
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jp.osdn.jindolf.parser.HtmlParseException;
+import jp.osdn.jindolf.parser.HtmlParser;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sfjp.jindolf.data.Land;
+import jp.sfjp.jindolf.data.Period;
+import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.net.HtmlSequence;
+import jp.sfjp.jindolf.net.ServerAccess;
+
+/**
+ * 人狼各国のHTTPサーバから各村の個別の日(Period)をHTMLで取得する。
+ *
+ * <p>Periodには、プレイヤー同士の会話や
+ * システムが自動生成するメッセージが正しい順序で納められる。
+ */
+public final class PeriodLoader {
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+
+    /**
+     * hidden constructor.
+     */
+    private PeriodLoader(){
+        assert false;
+    }
+
+
+    /**
+     * Periodを更新する。Topicのリストが更新される。
+     *
+     * @param period 日
+     * @param force trueなら強制再読み込み。
+     *     falseならまだ読み込んで無い時のみ読み込み。
+     * @throws IOException ネットワーク入力エラー
+     */
+    public static void parsePeriod(Period period, boolean force)
+            throws IOException{
+        if( ! force && period.hasLoaded() ) return;
+
+        Village village = period.getVillage();
+
+        Land land = village.getParentLand();
+        ServerAccess server = land.getServerAccess();
+
+        HtmlSequence html = server.getHTMLPeriod(period);
+        DecodedContent content = html.getContent();
+
+        period.clearTopicList();
+
+        HtmlParser parser = new HtmlParser();
+        PeriodHandler handler = new PeriodHandler();
+        parser.setBasicHandler   (handler);
+        parser.setSysEventHandler(handler);
+        parser.setTalkHandler    (handler);
+
+        handler.setPeriod(period);
+        try{
+            parser.parseAutomatic(content);
+        }catch(HtmlParseException e){
+            LOGGER.log(Level.WARNING, "発言抽出に失敗", e);
+        }
+
+        parser.reset();
+        handler.reset();
+
+        return;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/VillageInfoHandler.java b/src/main/java/jp/sfjp/jindolf/data/html/VillageInfoHandler.java
new file mode 100644 (file)
index 0000000..984b5c3
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ * village info handler
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+import java.util.logging.Logger;
+import jp.osdn.jindolf.parser.HtmlAdapter;
+import jp.osdn.jindolf.parser.HtmlParseException;
+import jp.osdn.jindolf.parser.PageType;
+import jp.osdn.jindolf.parser.SeqRange;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sfjp.jindolf.data.Land;
+import jp.sfjp.jindolf.data.Period;
+import jp.sfjp.jindolf.data.Village;
+import jp.sourceforge.jindolf.corelib.LandDef;
+import jp.sourceforge.jindolf.corelib.LandState;
+import jp.sourceforge.jindolf.corelib.PeriodType;
+import jp.sourceforge.jindolf.corelib.VillageState;
+
+
+/**
+ * 各村のHTMLをパースし、村情報や日程の通知を受け取るためのハンドラ。
+ *
+ * <p>パース終了時には、
+ * あらかじめ指定したVillageインスタンスに
+ * 更新時刻などの村情報が適切に更新される。
+ *
+ * <p>日程は空Periodのリストに反映されるが各Periodのロードはまだ行われない。
+ *
+ * <p>※人狼BBS:G国におけるG2087村のエピローグが終了した段階で、
+ * 人狼BBSは過去ログの提供しか行っていない。
+ * だがこのクラスには進行中の村をパースするための冗長な処理が若干残っている。
+ */
+class VillageInfoHandler extends HtmlAdapter {
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+
+    private Village village = null;
+
+    private boolean hasPrologue;
+    private boolean hasProgress;
+    private boolean hasEpilogue;
+
+    private boolean hasDone;
+    private int maxProgress;
+
+
+    /**
+     * コンストラクタ。
+     */
+    VillageInfoHandler(){
+        super();
+        return;
+    }
+
+
+    /**
+     * 更新対象の村インスタンスを設定する。
+     *
+     * @param village 村インスタンス
+     */
+    void setVillage(Village village){
+        this.village = village;
+        reset();
+        return;
+    }
+
+    /**
+     * 各種進行コンテキストのリセットを行う。
+     */
+    void reset() {
+        this.hasPrologue = false;
+        this.hasProgress = false;
+        this.hasEpilogue = false;
+        this.hasDone = false;
+        this.maxProgress = 0;
+        return;
+    }
+
+    /**
+     * パース結果から村の状態を算出する。
+     *
+     * @return 村の状態
+     */
+    private VillageState getVillageState() {
+        VillageState result = VillageState.UNKNOWN;
+
+        if(this.hasDone){
+            result = VillageState.GAMEOVER;
+        }else if(this.hasEpilogue){
+            result = VillageState.EPILOGUE;
+        }else if(this.hasProgress){
+            result = VillageState.PROGRESS;
+        }else if(this.hasPrologue){
+            result = VillageState.PROLOGUE;
+        }
+
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param content {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void startParse(DecodedContent content)
+            throws HtmlParseException {
+        reset();
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>HTML自動判定の結果が村の日程ページでなければ例外を投げ、
+     * パースを中止する。
+     *
+     * @param type {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
+     */
+    @Override
+    public void pageType(PageType type) throws HtmlParseException {
+        if(type != PageType.PERIOD_PAGE){
+            throw new HtmlParseException("日ページが必要です。");
+        }
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>更新時刻の通知を受け取る。
+     * 更新時刻はVillageインスタンスへ反映される。
+     *
+     * @param month {@inheritDoc}
+     * @param day {@inheritDoc}
+     * @param hour {@inheritDoc}
+     * @param minute {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void commitTime(int month, int day, int hour, int minute)
+            throws HtmlParseException {
+        this.village.setLimit(month, day, hour, minute);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>日程ページから各Period(日)へのリンクHTML出現の通知を受け取る。
+     * Villageインスタンスの進行状況へ反映される。
+     *
+     * @param content {@inheritDoc}
+     * @param anchorRange {@inheritDoc}
+     * @param periodType {@inheritDoc}
+     * @param day {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void periodLink(DecodedContent content,
+                           SeqRange anchorRange,
+                           PeriodType periodType,
+                           int day)
+            throws HtmlParseException {
+        if(periodType == null){
+            this.hasDone = true;
+            return;
+        }
+
+        switch(periodType){
+        case PROLOGUE:
+            this.hasPrologue = true;
+            break;
+        case PROGRESS:
+            this.hasProgress = true;
+            this.maxProgress = day;
+            break;
+        case EPILOGUE:
+            this.hasEpilogue = true;
+            break;
+        default:
+            assert false;
+            break;
+        }
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>パース終了時の処理を行う。
+     *
+     * <p>村としての体裁に矛盾が検出されると、
+     * 例外を投げパースを中断する。
+     *
+     * <p>村の進行に従い空Periodのリストを生成する。
+     *
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void endParse() throws HtmlParseException {
+        VillageState villageState = getVillageState();
+        if(villageState == VillageState.UNKNOWN){
+            this.village.setState(villageState);
+            LOGGER.warning("村の状況を読み取れません");
+            return;
+        }
+
+        Land land = this.village.getParentLand();
+        LandDef landDef = land.getLandDef();
+        LandState landState = landDef.getLandState();
+
+        if(landState != LandState.ACTIVE){
+            villageState = VillageState.GAMEOVER;
+        }
+        this.village.setState(villageState);
+
+        modifyPeriodList();
+
+        return;
+    }
+
+    /**
+     * 抽出したPeriod別リンク情報に伴い空Periodリストを準備する。
+     *
+     * <p>まだPeriodデータのロードは行われない。
+     *
+     * <p>ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
+     * 既存のPeriodリストが伸張する場合がある。
+     */
+    private void modifyPeriodList() {
+        Period lastPeriod = null;
+        if(this.hasPrologue){
+            Period prologue = this.village.getPrologue();
+            if(prologue == null){
+                lastPeriod =
+                        new Period(this.village,
+                                   PeriodType.PROLOGUE,
+                                   0 );
+                this.village.setPeriod(0, lastPeriod);
+            }else{
+                lastPeriod = prologue;
+            }
+        }
+
+        if(this.hasProgress){
+            for(int day = 1; day <= this.maxProgress; day++){
+                Period progress = this.village.getProgress(day);
+                if(progress == null){
+                    lastPeriod =
+                            new Period(this.village,
+                                       PeriodType.PROGRESS,
+                                       day );
+                    this.village.setPeriod(day, lastPeriod);
+                }else{
+                    lastPeriod = progress;
+                }
+            }
+        }
+
+        if(this.hasEpilogue){
+            Period epilogue = this.village.getEpilogue();
+            if(epilogue == null){
+                int epilogueDay = this.maxProgress + 1;
+                lastPeriod =
+                        new Period(this.village,
+                                   PeriodType.EPILOGUE,
+                                   epilogueDay );
+                this.village.setPeriod(epilogueDay, lastPeriod);
+            } else {
+                lastPeriod = epilogue;
+            }
+        }
+
+        assert this.village.getPeriodSize() > 0;
+        assert lastPeriod != null;
+
+        return;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/VillageInfoLoader.java b/src/main/java/jp/sfjp/jindolf/data/html/VillageInfoLoader.java
new file mode 100644 (file)
index 0000000..e0a465d
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * village information loader
+ *
+ * License : The MIT License
+ * Copyright(c) 2008 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jp.osdn.jindolf.parser.HtmlParseException;
+import jp.osdn.jindolf.parser.HtmlParser;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sfjp.jindolf.data.Land;
+import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.net.HtmlSequence;
+import jp.sfjp.jindolf.net.ServerAccess;
+import jp.sourceforge.jindolf.corelib.LandDef;
+import jp.sourceforge.jindolf.corelib.LandState;
+
+/**
+ * 人狼各国のHTTPサーバから個別の村の村情報をHTMLで取得する。
+ *
+ * <p>村情報には村毎の更新時刻、日程、進行状況などが含まれる。
+ *
+ * <p>各Periodの会話はまだロードされない。
+ */
+public final class VillageInfoLoader {
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+
+    /**
+     * Hidden constructor.
+     */
+    private VillageInfoLoader() {
+        assert false;
+    }
+
+
+    /**
+     * 人狼BBSサーバから
+     * HTMLで記述された各村の村情報ページをダウンロードする。
+     *
+     * <p>村情報ページのURLは各国の状態及び村の進行状況により異なる。
+     *
+     * <p>※ G国HISTORICAL運用移行に伴い、
+     * 2020-02の時点で進行中の村はもはや存在しないため、
+     * 若干の冗長なコードが残存する。
+     *
+     * <p>例: G1000村(エピローグ終了状態)の村情報ページは
+     * <a href="http://www.wolfg.x0.com/index.rb?vid=1000">
+     * http://www.wolfg.x0.com/index.rb?vid=1000</a>
+     *
+     * @param village 村
+     * @return HTML文書
+     * @throws IOException 入出力エラー
+     */
+    private static DecodedContent loadVillageInfo(Village village)
+            throws IOException{
+        Land land = village.getParentLand();
+        ServerAccess server = land.getServerAccess();
+        LandDef landDef = land.getLandDef();
+        LandState landState = landDef.getLandState();
+
+        HtmlSequence html;
+        if(landState == LandState.ACTIVE){
+            html = server.getHTMLBoneHead(village);
+        }else{
+            html = server.getHTMLVillage(village);
+        }
+
+        DecodedContent content = html.getContent();
+
+        return content;
+    }
+
+    /**
+     * 人狼BBSサーバから各村のPeriod一覧情報が含まれたHTML(村情報)を取得し、
+     * 更新時刻や日程、空PeriodのリストをVillageインスタンスに設定する。
+     *
+     * @param village 村
+     * @throws java.io.IOException ネットワーク入出力の異常
+     */
+    public static void updateVillageInfo(Village village)
+            throws IOException{
+        DecodedContent content = loadVillageInfo(village);
+
+        HtmlParser parser = new HtmlParser();
+        VillageInfoHandler handler = new VillageInfoHandler();
+        parser.setBasicHandler   (handler);
+        parser.setSysEventHandler(handler);
+        parser.setTalkHandler    (handler);
+
+        handler.setVillage(village);
+        try{
+            parser.parseAutomatic(content);
+        }catch(HtmlParseException e){
+            LOGGER.log(Level.WARNING, "村の状態が不明", e);
+        }
+
+        parser.reset();
+        handler.reset();
+
+        return;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/VillageListHandler.java b/src/main/java/jp/sfjp/jindolf/data/html/VillageListHandler.java
new file mode 100644 (file)
index 0000000..446907e
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * village list handler
+ *
+ * License : The MIT License
+ * Copyright(c) 2008 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import jp.osdn.jindolf.parser.HtmlAdapter;
+import jp.osdn.jindolf.parser.HtmlParseException;
+import jp.osdn.jindolf.parser.PageType;
+import jp.osdn.jindolf.parser.SeqRange;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sourceforge.jindolf.corelib.VillageState;
+
+/**
+ * 各国の村一覧HTMLをパースし、村一覧通知を受け取るためのハンドラ。
+ *
+ * <p>パース終了時には村一覧リストが完成する。
+ */
+class VillageListHandler extends HtmlAdapter{
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+    private static final String ERR_ILLEGALPAGE =
+            "トップページか村一覧ページが必要です。";
+    private static final String ERR_URI =
+            "認識できないURL[{0}]に遭遇しました。";
+
+    private static final Pattern REG_VID = Pattern.compile(
+            "\\Qindex.rb?vid=\\E" + "([1-9][0-9]*)" + "\\Q&amp;\\E");
+
+
+    private List<VillageRecord> villageRecords = new LinkedList<>();
+
+
+    /**
+     * コンストラクタ。
+     */
+    VillageListHandler() {
+        super();
+        this.villageRecords = new LinkedList<>();
+        return;
+    }
+
+
+    /**
+     * HTMLのAタグ内HREF属性値から村IDを得る。
+     *
+     * <p>G国も含め村IDは0以外から始まる1以上の10進数。
+     *
+     * @param hrefValue HREF属性値
+     * @return 村ID。見つからなければnull。
+     */
+    static String parseVidFromHref(CharSequence hrefValue){
+        Matcher matcher = REG_VID.matcher(hrefValue);
+        boolean match = matcher.lookingAt();
+        if(!match) return null;
+
+        String result = matcher.group(1);
+        assert result.length() > 0;
+
+        return result;
+    }
+
+
+    /**
+     * パース結果の村一覧を返す。
+     *
+     * @return 村一覧
+     */
+    List<VillageRecord> getVillageRecords(){
+        return this.villageRecords;
+    }
+
+    /**
+     * リセットを行う。
+     *
+     * <p>村一覧リストは空になる。
+     */
+    void reset() {
+        this.villageRecords = new LinkedList<>();
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>パース開始通知を受け、村一覧リストを初期化する。
+     *
+     * @param content {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void startParse(DecodedContent content) throws HtmlParseException {
+        reset();
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>ページ自動判定の結果の通知を受け、
+     * パース対象HTMLがトップページでも村一覧ページでもなければ
+     * 例外を投げパースを中止させる。
+     *
+     * @param type {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
+     */
+    @Override
+    public void pageType(PageType type) throws HtmlParseException {
+        if(        type != PageType.VILLAGELIST_PAGE
+                && type != PageType.TOP_PAGE ){
+            throw new HtmlParseException(ERR_ILLEGALPAGE);
+        }
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>村URL出現の通知を受け、村一覧リストに村を追加する。
+     *
+     * @param content {@inheritDoc}
+     * @param anchorRange {@inheritDoc}
+     * @param villageRange {@inheritDoc}
+     * @param hour {@inheritDoc}
+     * @param minute {@inheritDoc}
+     * @param villageState {@inheritDoc}
+     * @throws HtmlParseException {@inheritDoc}
+     */
+    @Override
+    public void villageRecord(DecodedContent content,
+                              SeqRange anchorRange,
+                              SeqRange villageRange,
+                              int hour, int minute,
+                              VillageState villageState)
+            throws HtmlParseException {
+        CharSequence href = anchorRange.sliceSequence(content);
+        String villageID = parseVidFromHref(href);
+        if(villageID == null){
+            LOGGER.log(Level.WARNING, ERR_URI, href);
+            return;
+        }
+
+        CharSequence fullVillageName = villageRange.sliceSequence(content);
+
+        VillageRecord record =
+                new VillageRecord(villageID,
+                                  fullVillageName.toString(),
+                                  villageState );
+
+        this.villageRecords.add(record);
+
+        return;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/VillageListLoader.java b/src/main/java/jp/sfjp/jindolf/data/html/VillageListLoader.java
new file mode 100644 (file)
index 0000000..d20b802
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * village list loader
+ *
+ * License : The MIT License
+ * Copyright(c) 2008 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jp.osdn.jindolf.parser.HtmlParseException;
+import jp.osdn.jindolf.parser.HtmlParser;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sfjp.jindolf.data.Land;
+import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.net.HtmlSequence;
+import jp.sfjp.jindolf.net.ServerAccess;
+import jp.sourceforge.jindolf.corelib.LandDef;
+import jp.sourceforge.jindolf.corelib.LandState;
+import jp.sourceforge.jindolf.corelib.VillageState;
+
+/**
+ * 人狼各国のHTTPサーバから村一覧リストを取得する。
+ */
+public final class VillageListLoader {
+
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+    // 古国ID
+    private static final String ID_VANILLAWOLF = "wolf";
+
+    private static final List<VillageRecord> EMPTY_LIST =
+            Collections.emptyList();
+
+
+    /**
+     * Hidden constructor.
+     */
+    private VillageListLoader() {
+        assert false;
+    }
+
+
+    /**
+     * 村一覧リストをサーバからダウンロードする。
+     *
+     * <p>リスト元情報は国のトップページと村一覧ページ。
+     *
+     * <p>古国(wolf)の場合は村一覧にアクセスせずトップページのみ。
+     * 古国以外で村建てをやめた国はトップページにアクセスしない。
+     *
+     * <p>戻される村一覧不変リストはソート済みで重複がない。
+     *
+     * @param land 国
+     * @return 村一覧の不変リスト
+     * @throws java.io.IOException ネットワーク入出力の異常
+     */
+    public static List<Village> loadVillageList(Land land)
+            throws IOException{
+        SortedSet<VillageRecord> records = loadVillageRecords(land);
+
+        LandDef landDef = land.getLandDef();
+        LandState landState = landDef.getLandState();
+        boolean isHistorical = landState == LandState.HISTORICAL;
+
+        List<Village> result = new ArrayList<>(records.size());
+
+        for(VillageRecord record : records){
+            String id = record.getVillageId();
+            String fullVillageName = record.getFullVillageName();
+
+            VillageState status;
+            if(isHistorical){
+                status = VillageState.GAMEOVER;
+            }else{
+                status = record.getVillageStatus();
+            }
+
+            Village village = new Village(land, id, fullVillageName);
+            village.setState(status);
+
+            result.add(village);
+        }
+
+        result = Collections.unmodifiableList(result);
+
+        return result;
+    }
+
+    /**
+     * 村一覧リストを各国サーバからダウンロードする。
+     *
+     * <p>リスト元情報は国のトップページと村一覧ページ。
+     *
+     * <p>古国(wolf)の場合は村一覧にアクセスせずトップページのみ。
+     * 古国以外で村建てをやめた国はトップページにアクセスしない。
+     *
+     * <p>戻される村一覧セットは順序づけられており重複はない。
+     *
+     * @param land 国
+     * @return 村一覧セット
+     * @throws java.io.IOException ネットワーク入出力の異常
+     */
+    private static SortedSet<VillageRecord> loadVillageRecords(Land land)
+            throws IOException{
+        LandDef landDef = land.getLandDef();
+        boolean isVanillaWolf = landDef.getLandId().equals(ID_VANILLAWOLF);
+        LandState state = landDef.getLandState();
+
+        boolean needTopPage =
+                state.equals(LandState.ACTIVE) || isVanillaWolf;
+        boolean hasVillageList = ! isVanillaWolf;
+
+        ServerAccess server = land.getServerAccess();
+
+        // 昇順ソートと重複排除処理。 重複例) B国116村
+        SortedSet<VillageRecord> result = new TreeSet<>();
+
+        // トップページ
+        if(needTopPage){
+            List<VillageRecord> recList = EMPTY_LIST;
+            HtmlSequence html = server.getHTMLTopPage();
+            try{
+                recList = parseVillageRecords(html);
+            }catch(HtmlParseException e){
+                LOGGER.log(Level.WARNING, "トップページを認識できない", e);
+            }
+            result.addAll(recList);
+        }
+
+        // 村一覧ページ
+        if(hasVillageList){
+            List<VillageRecord> recList = EMPTY_LIST;
+            HtmlSequence html = server.getHTMLLandList();
+            try{
+                recList = parseVillageRecords(html);
+            }catch(HtmlParseException e){
+                LOGGER.log(Level.WARNING, "村一覧ページを認識できない", e);
+            }
+            result.addAll(recList);
+        }
+
+        return result;
+    }
+
+    /**
+     * HTMLをパースし村一覧リストを返す。
+     *
+     * @param html HTML文書
+     * @return 村一覧リスト
+     * @throws HtmlParseException HTMLパースエラーによるパース停止
+     */
+    private static List<VillageRecord> parseVillageRecords(HtmlSequence html)
+            throws HtmlParseException{
+        HtmlParser parser = new HtmlParser();
+        VillageListHandler handler = new VillageListHandler();
+        parser.setBasicHandler(handler);
+
+        DecodedContent content = html.getContent();
+        parser.parseAutomatic(content);
+
+        List<VillageRecord> result = handler.getVillageRecords();
+
+        parser.reset();
+        handler.reset();
+
+        return result;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/VillageRecord.java b/src/main/java/jp/sfjp/jindolf/data/html/VillageRecord.java
new file mode 100644 (file)
index 0000000..efea2b5
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * village record
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+import jp.sourceforge.jindolf.corelib.VillageState;
+
+/**
+ * Village record on HTML.
+ */
+class VillageRecord implements Comparable<VillageRecord> {
+
+    private final String villageId;
+    private final String fullVillageName;
+    private final VillageState villageStatus;
+
+    private final int villageIdNum;
+
+
+    /**
+     * Constructor.
+     *
+     * @param villageId village id on CGI query
+     * @param fullVillageName full village name
+     * @param villageStatus village status
+     */
+    VillageRecord(String villageId,
+                  String fullVillageName,
+                  VillageState villageStatus ){
+        super();
+
+        this.villageId = villageId;
+        this.fullVillageName = fullVillageName;
+        this.villageStatus = villageStatus;
+
+        this.villageIdNum = Integer.parseInt(villageId);
+
+        return;
+    }
+
+    /**
+     * return village id on CGI query.
+     *
+     * @return village id
+     */
+    String getVillageId(){
+        return this.villageId;
+    }
+
+    /**
+     * return long village name.
+     *
+     * @return long village name
+     */
+    String getFullVillageName(){
+        return this.fullVillageName;
+    }
+
+    /**
+     * return village status.
+     *
+     * @return village status
+     */
+    VillageState getVillageStatus(){
+        return this.villageStatus;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>村IDの自然数順に順序づける。
+     *
+     * @param rec {@inheritDoc}
+     * @return {@inheritDoc}
+     */
+    @Override
+    public int compareTo(VillageRecord rec) {
+        int result = this.villageIdNum - rec.villageIdNum;
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return this.villageId.hashCode();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param obj {@inheritDoc}
+     * @return {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if(this == obj) return true;
+        if(obj == null) return false;
+
+        if(! (obj instanceof VillageRecord)) return false;
+        VillageRecord other = (VillageRecord) obj;
+
+        return this.villageId.equals(other.villageId);
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/html/package-info.java b/src/main/java/jp/sfjp/jindolf/data/html/package-info.java
new file mode 100644 (file)
index 0000000..c67ada7
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * package info
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+/**
+ * 人狼BBSサーバから受信したHTMLデータから、
+ * JinParserなどを用いて各種データモデルを生成するクラス群。
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+/* EOF */
diff --git a/src/main/java/jp/sfjp/jindolf/data/xml/BotherHandler.java b/src/main/java/jp/sfjp/jindolf/data/xml/BotherHandler.java
new file mode 100644 (file)
index 0000000..e6e383c
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * XML custom error-handler
+ *
+ * License : The MIT License
+ * Copyright(c) 2010 MikuToga Partners
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+/**
+ * 自製エラーハンドラ。
+ *
+ * <p>例外を渡されれば即投げ返す。
+ */
+public final class BotherHandler implements ErrorHandler{
+
+    /**
+     * 唯一のシングルトン。
+     */
+    public static final ErrorHandler HANDLER = new BotherHandler();
+
+
+    /**
+     * 隠しコンストラクタ。
+     */
+    private BotherHandler(){
+        super();
+        return;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param exception {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void error(SAXParseException exception) throws SAXException{
+        throw exception;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param exception {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void fatalError(SAXParseException exception) throws SAXException{
+        throw exception;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param exception {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void warning(SAXParseException exception) throws SAXException{
+        throw exception;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/xml/ElemTag.java b/src/main/java/jp/sfjp/jindolf/data/xml/ElemTag.java
new file mode 100644 (file)
index 0000000..9c6da11
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * village XML file element tags
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+import java.util.HashMap;
+import java.util.Map;
+import jp.sourceforge.jindolf.corelib.SysEventType;
+
+/**
+ * XMLファイルのタグ要素名デコーダ。
+ */
+public enum ElemTag {
+
+    VILLAGE("village"),
+    AVATARLIST("avatarList"),
+    AVATAR("avatar"),
+    AVATARREF("avatarRef"),
+    PERIOD("period"),
+
+    TALK("talk"),
+    LI("li"),
+    RAWDATA("rawdata"),
+
+    STARTENTRY("startEntry", SysEventType.STARTENTRY),
+    ONSTAGE("onStage", SysEventType.ONSTAGE),
+    STARTMIRROR("startMirror", SysEventType.STARTMIRROR),
+    OPENROLE("openRole", SysEventType.OPENROLE),
+    MURDERED("murdered", SysEventType.MURDERED),
+    STARTASSAULT("startAssault", SysEventType.STARTASSAULT),
+    SURVIVOR("survivor", SysEventType.SURVIVOR),
+    COUNTING("counting", SysEventType.COUNTING),
+    SUDDENDEATH("suddenDeath", SysEventType.SUDDENDEATH),
+    NOMURDER("noMurder", SysEventType.NOMURDER),
+    WINVILLAGE("winVillage", SysEventType.WINVILLAGE),
+    WINWOLF("winWolf", SysEventType.WINWOLF),
+    WINHAMSTER("winHamster", SysEventType.WINHAMSTER),
+    PLAYERLIST("playerList", SysEventType.PLAYERLIST),
+    PANIC("panic", SysEventType.PANIC),
+    EXECUTION("execution", SysEventType.EXECUTION),
+    VANISH("vanish", SysEventType.VANISH),
+    CHECKOUT("checkout", SysEventType.CHECKOUT),
+    SHORTMEMBER("shortMember", SysEventType.SHORTMEMBER),
+    ASKENTRY("askEntry", SysEventType.ASKENTRY),
+    ASKCOMMIT("askCommit", SysEventType.ASKCOMMIT),
+    NOCOMMENT("noComment", SysEventType.NOCOMMENT),
+    STAYEPILOGUE("stayEpilogue", SysEventType.STAYEPILOGUE),
+    GAMEOVER("gameOver", SysEventType.GAMEOVER),
+    JUDGE("judge", SysEventType.JUDGE),
+    GUARD("guard", SysEventType.GUARD),
+    COUNTING2("counting2", SysEventType.COUNTING2),
+    ASSAULT("assault", SysEventType.ASSAULT),
+
+    ROLEHEADS("roleHeads"),
+    VOTE("vote"),
+    PLAYERINFO("playerInfo"),
+    NOMINATED("nominated"),
+    ;
+
+
+    private final String name;
+    private final SysEventType sysEventType;
+
+
+    /**
+     * constructor.
+     *
+     * @param name element name
+     * @param isSysEvent true if SysEvent
+     */
+    ElemTag(String name, SysEventType type){
+        this.name = name;
+        this.sysEventType = type;
+        return;
+    }
+
+    /**
+     * constructor.
+     *
+     * <p>It's not SysEvent.
+     *
+     * @param name element name
+     */
+    ElemTag(String name){
+        this(name, null);
+        return;
+    }
+
+
+    /**
+     * get ElemTag map with name-space Prefixed key.
+     *
+     * @param pfx prefix
+     * @return ElemTag
+     */
+    public static Map<String, ElemTag> getQNameMap(String pfx){
+        Map<String, ElemTag> result = new HashMap<>();
+
+        String lead;
+        if(pfx.isEmpty()){
+            lead = "";
+        }else{
+            lead = pfx + ":";
+        }
+
+        for(ElemTag tag : values()){
+            String key = lead + tag.name;
+            key = key.intern();
+            result.put(key, tag);
+        }
+
+        return result;
+    }
+
+
+    /**
+     * タグがSysEventか判定する。
+     *
+     * @return SysEventならtrue
+     */
+    public boolean isSysEventTag(){
+        return this.sysEventType != null;
+    }
+
+    /**
+     * return SysEventType.
+     *
+     * @return SysEventType. true if not SystemEvent.
+     */
+    public SysEventType getSystemEventType(){
+        return this.sysEventType;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/xml/NoopEntityResolver.java b/src/main/java/jp/sfjp/jindolf/data/xml/NoopEntityResolver.java
new file mode 100644 (file)
index 0000000..638df4e
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * No-operation Entity Resolver for XML.
+ *
+ * License : The MIT License
+ * Copyright(c) 2019 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+import java.io.Reader;
+import java.io.StringReader;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+
+/**
+ * No-operation Entity Resolver implementation for preventing XXE.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/XML_external_entity_attack">
+ *     XML external entity attack (Wikipedia)
+ *     </a>
+ */
+public final class NoopEntityResolver implements EntityResolver{
+
+    /** Singleton resolver. */
+    public static final EntityResolver NOOP_RESOLVER =
+            new NoopEntityResolver();
+
+
+    /**
+     * Constructor.
+     */
+    private NoopEntityResolver(){
+        super();
+        return;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Prevent any external entity reference XXE.
+     *
+     * @param publicId {@inheritDoc}
+     * @param systemId {@inheritDoc}
+     * @return empty input source
+     */
+    @Override
+    public InputSource resolveEntity(String publicId, String systemId){
+        Reader emptyReader = new StringReader("");
+        InputSource source = new InputSource(emptyReader);
+
+        source.setPublicId(publicId);
+        source.setSystemId(systemId);
+
+        return source;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/xml/VillageHandler.java b/src/main/java/jp/sfjp/jindolf/data/xml/VillageHandler.java
new file mode 100644 (file)
index 0000000..7e8c144
--- /dev/null
@@ -0,0 +1,952 @@
+/*
+ * village handler
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sfjp.jindolf.data.Avatar;
+import jp.sfjp.jindolf.data.CoreData;
+import jp.sfjp.jindolf.data.InterPlay;
+import jp.sfjp.jindolf.data.Land;
+import jp.sfjp.jindolf.data.Nominated;
+import jp.sfjp.jindolf.data.Period;
+import jp.sfjp.jindolf.data.Player;
+import jp.sfjp.jindolf.data.SysEvent;
+import jp.sfjp.jindolf.data.Talk;
+import jp.sfjp.jindolf.data.Topic;
+import jp.sfjp.jindolf.data.Village;
+import jp.sourceforge.jindolf.corelib.Destiny;
+import jp.sourceforge.jindolf.corelib.EventFamily;
+import jp.sourceforge.jindolf.corelib.GameRole;
+import jp.sourceforge.jindolf.corelib.PeriodType;
+import jp.sourceforge.jindolf.corelib.SysEventType;
+import jp.sourceforge.jindolf.corelib.TalkType;
+import jp.sourceforge.jindolf.corelib.VillageState;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+
+/**
+ * VillageのXMLパーサ本体。
+ *
+ * <p>パース中の各種コンテキストを保持する。
+ */
+public class VillageHandler implements ContentHandler{
+
+    private static final String NS_JINARCHIVE =
+            "http://jindolf.sourceforge.jp/xml/ns/501";
+
+    private static final int TALKTYPE_NUM = TalkType.values().length;
+
+
+    private String nsPfx;
+
+    private Land land;
+    private Village village;
+    private Period period;
+
+    private Talk talk;
+    private int talkNo;
+
+    private SysEvent sysEvent;
+
+    private boolean inLine;
+    private final StringBuilder content = new StringBuilder(250);
+
+    /** 非別、Avatar別、会話種別の会話通し番号。 */
+    private final Map<Avatar, int[]> countMap =
+            new HashMap<>();
+
+    private final Map<String, Avatar> idAvatarMap = new HashMap<>();
+    private final List<Avatar> avatarList = new LinkedList<>();
+    private final List<Player> playerList = new LinkedList<>();
+    private final List<Nominated> nominatedList = new LinkedList<>();
+    private final List<InterPlay> interPlayList = new LinkedList<>();
+
+    private Map<String, ElemTag> qNameMap = ElemTag.getQNameMap("");
+
+
+    /**
+     * constructor.
+     */
+    public VillageHandler(){
+        super();
+        return;
+    }
+
+
+    /**
+     * 属性値を得る。
+     *
+     * @param atts 属性集合
+     * @param attrName 属性名
+     * @return 属性値。なければnull。
+     */
+    private static String attrValue(Attributes atts, String attrName){
+        String result = atts.getValue("", attrName);
+        return result;
+    }
+
+
+    /**
+     * decode ElemTag.
+     *
+     * @param uri URI of namespace
+     * @param localName local name
+     * @param qName Qname
+     * @return
+     */
+    private ElemTag decodeElemTag(String uri,
+                                  String localName,
+                                  String qName){
+        ElemTag result = this.qNameMap.get(qName);
+        return result;
+    }
+
+    /**
+     * パースした結果のVillageを返す。
+     *
+     * @return 村。
+     */
+    public Village getVillage(){
+        return this.village;
+    }
+
+    /**
+     * パース開始前の準備。
+     */
+    private void resetBefore(){
+        this.idAvatarMap.clear();
+        this.countMap.clear();
+        this.content.setLength(0);
+        this.avatarList.clear();
+        this.playerList.clear();
+        this.nominatedList.clear();
+        this.interPlayList.clear();
+        this.inLine = false;
+        return;
+    }
+
+    /**
+     * パース開始後の後始末。
+     */
+    private void resetAfter(){
+        this.idAvatarMap.clear();
+        this.countMap.clear();
+        this.content.setLength(0);
+        this.avatarList.clear();
+        this.playerList.clear();
+        this.nominatedList.clear();
+        this.interPlayList.clear();
+        this.nsPfx = null;
+        this.land = null;
+        this.period = null;
+        return;
+    }
+
+    /**
+     * village要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startVillage(Attributes atts){
+        String landId = attrValue(atts, "landId");
+        String vid    = attrValue(atts, "vid");
+        String name   = attrValue(atts, "fullName");
+        String state  = attrValue(atts, "state");
+
+        this.land = CoreData.getLandDefList().stream()
+                .filter(landDef -> landDef.getLandId().equals(landId))
+                .map(landDef -> new Land(landDef))
+                .findFirst().get();
+
+        this.village = new Village(this.land, vid, name);
+
+        this.village.setState(VillageState.GAMEOVER);
+
+        this.talkNo = 0;
+        this.period = null;
+
+        return;
+    }
+
+    /**
+     * avatar要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startAvatar(Attributes atts){
+        String avatarId    = attrValue(atts, "avatarId");
+        String fullName    = attrValue(atts, "fullName");
+        String shortName   = attrValue(atts, "shortName");
+        String faceIconUri = attrValue(atts, "faceIconURI");
+
+        Avatar avatar = Avatar.getAvatarByFullname(fullName);
+
+        this.village.addAvatar(avatar);
+        this.idAvatarMap.put(avatarId, avatar);
+
+        return;
+    }
+
+    /**
+     * period要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startPeriod(Attributes atts) throws SAXException{
+        String typeAttr = attrValue(atts, "type");
+        String dayAttr  = attrValue(atts, "day");
+
+        PeriodType periodType = XmlDecoder.decodePeriodType(typeAttr);
+        int day = Integer.parseInt(dayAttr);
+
+        this.period = new Period(this.village, periodType, day);
+
+        // append tail
+        int periodIdx = this.village.getPeriodSize();
+        this.village.setPeriod(periodIdx, this.period);
+
+        return;
+    }
+
+    /**
+     * period要素終了の受信。
+     */
+    private void endPeriod(){
+        this.countMap.clear();
+        return;
+    }
+
+    /**
+     * 日別、Avatar別、会話種ごとに発言回数をインクリメントする。
+     *
+     * @param targetAvatar 対象Avatar
+     * @param targetType 対象会話種
+     * @return 現時点でのカウント数
+     */
+    private int countUp(Avatar targetAvatar, TalkType targetType){
+        int[] avatarCount = this.countMap.get(targetAvatar);
+        if(avatarCount == null){
+            avatarCount = new int[TALKTYPE_NUM];
+            this.countMap.put(targetAvatar, avatarCount);
+        }
+
+        int typeIdx = targetType.ordinal();
+        int count = ++avatarCount[typeIdx];
+
+        return count;
+    }
+
+    /**
+     * talk要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startTalk(Attributes atts) throws SAXException{
+        String type     = attrValue(atts, "type");
+        String avatarId = attrValue(atts, "avatarId");
+        String xname    = attrValue(atts, "xname");
+        String time     = attrValue(atts, "time");
+
+        TalkType talkType = XmlDecoder.decodeTalkType(type);
+        Avatar talkAvatar = this.idAvatarMap.get(avatarId);
+        int hour   = XmlDecoder.decodeHour  (time);
+        int minute = XmlDecoder.decodeMinute(time);
+
+        this.talk = new Talk(
+                this.period,
+                talkType, talkAvatar,
+                0, xname,
+                hour, minute,
+                ""
+        );
+
+        int count = countUp(talkAvatar, talkType);
+        this.talk.setCount(count);
+
+        this.content.setLength(0);
+
+        return;
+    }
+
+    /**
+     * talk要素終了の受信。
+     */
+    private void endTalk(){
+        int no = 0;
+        if(this.talk.getTalkType() == TalkType.PUBLIC){
+            no = ++this.talkNo;
+        }
+
+        String dialog = this.content.toString();
+        this.content.setLength(0);
+
+        this.talk.setTalkNo(no);
+        this.talk.setDialog(dialog);
+
+        this.period.addTopic(this.talk);
+
+        return;
+    }
+
+    /**
+     * li要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startLi(Attributes atts){
+        this.inLine = true;
+        if(this.content.length() > 0){
+            this.content.append('\n');
+        }
+        return;
+    }
+
+    /**
+     * li要素終了の受信。
+     */
+    private void endLi(){
+        this.inLine = false;
+        return;
+    }
+
+    /**
+     * playerList要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startPlayerList(Attributes atts){
+        this.playerList.clear();
+        return;
+    }
+
+    /**
+     * playerList要素終了を受信する。
+     */
+    private void endPlayerList(){
+        this.sysEvent.addPlayerList(this.playerList);
+        this.playerList.clear();
+        return;
+    }
+
+    /**
+     * execution要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startExecution(Attributes atts){
+        String victimId = attrValue(atts, "victim");
+
+        if(victimId != null){
+            Avatar victim = this.idAvatarMap.get(victimId);
+            List<Avatar> single = Collections.singletonList(victim);
+            this.sysEvent.addAvatarList(single);
+        }
+
+        return;
+    }
+
+    /**
+     * execution要素終了を受信する。
+     */
+    private void endExecution(){
+        this.sysEvent.addNominatedList(this.nominatedList);
+        this.nominatedList.clear();
+        return;
+    }
+
+    /**
+     * counting要素終了を受信する。
+     */
+    private void endCounting(){
+        this.sysEvent.addInterPlayList(this.interPlayList);
+        this.interPlayList.clear();
+        return;
+    }
+
+    /**
+     * counting2要素終了を受信する。
+     */
+    private void endCounting2(){
+        endCounting();
+        return;
+    }
+
+    /**
+     * murdered要素終了を受信する。
+     */
+    private void endMurdered(){
+        this.sysEvent.addAvatarList(this.avatarList);
+        this.avatarList.clear();
+        return;
+    }
+
+    /**
+     * survivor要素終了を受信する。
+     */
+    private void endSurvivor(){
+        this.sysEvent.addAvatarList(this.avatarList);
+        this.avatarList.clear();
+        return;
+    }
+
+    /**
+     * suddenDeath要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startSuddenDeath(Attributes atts){
+        startSimpleAvatarAttr(atts);
+        return;
+    }
+
+    /**
+     * vanish要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startVanish(Attributes atts){
+        startSimpleAvatarAttr(atts);
+        return;
+    }
+
+    /**
+     * checkout要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startCheckOut(Attributes atts){
+        startSimpleAvatarAttr(atts);
+        return;
+    }
+
+    /**
+     * Avatar参照を一つだけ含む要素を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startSimpleAvatarAttr(Attributes atts){
+        String avatarId = attrValue(atts, "avatarId");
+        Avatar avatar = this.idAvatarMap.get(avatarId);
+        List<Avatar> single = Collections.singletonList(avatar);
+        this.sysEvent.addAvatarList(single);
+        return;
+    }
+
+    /**
+     * counting要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startCounting(Attributes atts){
+        String victimId = attrValue(atts, "victim");
+        if(victimId == null) return;
+        Avatar victim = this.idAvatarMap.get(victimId);
+        List<Avatar> single = Collections.singletonList(victim);
+        this.sysEvent.addAvatarList(single);
+        return;
+    }
+
+    /**
+     * judge要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startJudge(Attributes atts){
+        startSimpleInterPlay(atts);
+        return;
+    }
+
+    /**
+     * guard要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startGuard(Attributes atts){
+        startSimpleInterPlay(atts);
+        return;
+    }
+
+    /**
+     * SystemEvent内の単一InterPlayを抽出する。
+     *
+     * @param atts 属性
+     */
+    private void startSimpleInterPlay(Attributes atts){
+        String byWhomId = attrValue(atts, "byWhom");
+        String targetId = attrValue(atts, "target");
+
+        Avatar byWhom = this.idAvatarMap.get(byWhomId);
+        Avatar target = this.idAvatarMap.get(targetId);
+
+        InterPlay interPlay = new InterPlay(byWhom, target);
+        List<InterPlay> single = Collections.singletonList(interPlay);
+
+        this.sysEvent.addInterPlayList(single);
+
+        return;
+    }
+
+    /**
+     * SysEvent 開始の受信。
+     *
+     * @param type SysEvent種別
+     */
+    private void startSysEvent(ElemTag tag, Attributes atts){
+        this.sysEvent = new SysEvent();
+
+        SysEventType type = tag.getSystemEventType();
+        EventFamily eventFamily = type.getEventFamily();
+        this.sysEvent.setSysEventType(type);
+        this.sysEvent.setEventFamily(eventFamily);
+
+        if(this.sysEvent.getSysEventType() == SysEventType.ASSAULT){
+            startAssault(atts);
+        }else{
+            switch(tag){
+                case ONSTAGE:
+                    startOnStage(atts);
+                    break;
+                case PLAYERLIST:
+                    startPlayerList(atts);
+                    break;
+                case EXECUTION:
+                    startExecution(atts);
+                    break;
+                case SUDDENDEATH:
+                    startSuddenDeath(atts);
+                    break;
+                case COUNTING:
+                    startCounting(atts);
+                    break;
+                case JUDGE:
+                    startJudge(atts);
+                    break;
+                case GUARD:
+                    startGuard(atts);
+                    break;
+                case VANISH:
+                    startVanish(atts);
+                    break;
+                case CHECKOUT:
+                    startCheckOut(atts);
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        this.content.setLength(0);
+
+        return;
+    }
+
+    /**
+     * SysEvent 終了の受信。
+     *
+     * @return パースしたSysEvent。
+     */
+    private void endSysEvent(){
+        Topic topic;
+        SysEventType eventType = this.sysEvent.getSysEventType();
+        if(eventType == SysEventType.ASSAULT){
+            endAssault();
+            topic = this.talk;
+        }else{
+            switch(eventType){
+                case PLAYERLIST:
+                    endPlayerList();
+                    break;
+                case EXECUTION:
+                    endExecution();
+                    break;
+                case COUNTING:
+                    endCounting();
+                    break;
+                case COUNTING2:
+                    endCounting2();
+                    break;
+                case MURDERED:
+                    endMurdered();
+                    break;
+                case SURVIVOR:
+                    endSurvivor();
+                    break;
+                default:
+                    break;
+            }
+
+            DecodedContent decoded = new DecodedContent(this.content);
+            this.sysEvent.setContent(decoded);
+
+            topic = this.sysEvent;
+        }
+
+        this.period.addTopic(topic);
+
+        this.content.setLength(0);
+        this.sysEvent = null;
+
+        return;
+    }
+
+    /**
+     * assault要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startAssault(Attributes atts){
+        String byWhom = attrValue(atts, "byWhom");
+        String xname  = attrValue(atts, "xname");
+        String time   = attrValue(atts, "time");
+
+        Avatar talkAvatar = this.idAvatarMap.get(byWhom);
+
+        int hour   = XmlDecoder.decodeHour  (time);
+        int minute = XmlDecoder.decodeMinute(time);
+
+        this.talk = new Talk(
+                this.period,
+                TalkType.WOLFONLY, talkAvatar,
+                0, xname,
+                hour, minute,
+                ""
+        );
+
+        return;
+    }
+
+    /**
+     * assault要素終了の受信。
+     */
+    private void endAssault(){
+        String dialog = this.content.toString();
+        this.content.setLength(0);
+
+        this.talk.setDialog(dialog);
+
+        return;
+    }
+
+    /**
+     * onStage要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startOnStage(Attributes atts){
+        String entryNo  = attrValue(atts, "entryNo");
+        String avatarId = attrValue(atts, "avatarId");
+
+        Avatar avatar = this.idAvatarMap.get(avatarId);
+        int entry = Integer.parseInt(entryNo);
+
+        Player player = new Player();
+        player.setAvatar(avatar);
+        player.setEntryNo(entry);
+
+        this.sysEvent.addPlayerList(Collections.singletonList(player));
+
+        return;
+    }
+
+    /**
+     * playerInfo要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startPlayerInfo(Attributes atts) throws SAXException{
+        String playerId = attrValue(atts, "playerId");
+        String avatarId = attrValue(atts, "avatarId");
+        String survive = attrValue(atts, "survive");
+        String role = attrValue(atts, "role");
+        String uri = attrValue(atts, "uri");
+
+        Avatar avatar = this.idAvatarMap.get(avatarId);
+        GameRole gameRole = XmlDecoder.decodeRole(role);
+        if(uri == null) uri = "";
+
+        Player player = new Player();
+
+        player.setAvatar(avatar);
+        player.setRole(gameRole);
+        player.setIdName(playerId);
+        player.setUrlText(uri);
+        if("true".equals(survive) || "1".equals(survive)){
+            player.setObitDay(-1);
+            player.setDestiny(Destiny.ALIVE);
+        }
+
+        this.playerList.add(player);
+
+        return;
+    }
+
+    /**
+     * nominated要素開始の受信。
+     *
+     * @param atts 属性
+     */
+    private void startNominated(Attributes atts){
+        String avatarId = attrValue(atts, "avatarId");
+        String countTxt = attrValue(atts, "count");
+
+        Avatar avatar = this.idAvatarMap.get(avatarId);
+        int count = Integer.parseInt(countTxt);
+
+        Nominated nominated = new Nominated(avatar, count);
+        this.nominatedList.add(nominated);
+
+        return;
+    }
+
+    /**
+     * vote要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startVote(Attributes atts){
+        String byWhomId = attrValue(atts, "byWhom");
+        String targetId = attrValue(atts, "target");
+
+        Avatar byWhom = this.idAvatarMap.get(byWhomId);
+        Avatar target = this.idAvatarMap.get(targetId);
+
+        InterPlay interPlay = new InterPlay(byWhom, target);
+        this.interPlayList.add(interPlay);
+
+        return;
+    }
+
+    /**
+     * avatarRef要素開始を受信する。
+     *
+     * @param atts 属性
+     */
+    private void startAvatarRef(Attributes atts){
+        String avatarId = attrValue(atts, "avatarId");
+        Avatar avatar = this.idAvatarMap.get(avatarId);
+        this.avatarList.add(avatar);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param locator {@inheritDoc}
+     */
+    @Override
+    public void setDocumentLocator(Locator locator) {
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void startDocument() throws SAXException {
+        resetBefore();
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void endDocument() throws SAXException {
+        resetAfter();
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param prefix {@inheritDoc}
+     * @param uri {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void startPrefixMapping(String prefix, String uri)
+            throws SAXException {
+        if(NS_JINARCHIVE.equals(uri)){
+            this.nsPfx = prefix;
+            this.qNameMap = ElemTag.getQNameMap(this.nsPfx);
+        }
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param prefix {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void endPrefixMapping(String prefix)
+            throws SAXException {
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param uri {@inheritDoc}
+     * @param localName {@inheritDoc}
+     * @param qName {@inheritDoc}
+     * @param atts {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void startElement(String uri,
+                             String localName,
+                             String qName,
+                             Attributes atts)
+            throws SAXException {
+        ElemTag tag = decodeElemTag(uri, localName, qName);
+        if(tag == null) return;
+
+        if(tag.isSysEventTag()){
+            startSysEvent(tag, atts);
+            return;
+        }
+
+        switch(tag){
+            case VILLAGE:
+                startVillage(atts);
+                break;
+            case AVATAR:
+                startAvatar(atts);
+                break;
+            case PERIOD:
+                startPeriod(atts);
+                break;
+            case TALK:
+                startTalk(atts);
+                break;
+            case LI:
+                startLi(atts);
+                break;
+            case PLAYERINFO:
+                startPlayerInfo(atts);
+                break;
+            case NOMINATED:
+                startNominated(atts);
+                break;
+            case VOTE:
+                startVote(atts);
+                break;
+            case AVATARREF:
+                startAvatarRef(atts);
+                break;
+            default:
+                break;
+        }
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param uri {@inheritDoc}
+     * @param localName {@inheritDoc}
+     * @param qName {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void endElement(String uri, String localName, String qName)
+            throws SAXException {
+        ElemTag tag = decodeElemTag(uri, localName, qName);
+        if(tag == null) return;
+
+        if(tag.isSysEventTag()){
+            endSysEvent();
+            return;
+        }
+
+        switch(tag){
+            case PERIOD:
+                endPeriod();
+                break;
+            case TALK:
+                endTalk();
+                break;
+            case LI:
+                endLi();
+                break;
+            default:
+                break;
+        }
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param ch {@inheritDoc}
+     * @param start {@inheritDoc}
+     * @param length {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void characters(char[] ch, int start, int length)
+            throws SAXException {
+        if(!this.inLine) return;
+        this.content.append(ch, start, length);
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param ch {@inheritDoc}
+     * @param start {@inheritDoc}
+     * @param length {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void ignorableWhitespace(char[] ch, int start, int length)
+            throws SAXException {
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param target {@inheritDoc}
+     * @param data {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void processingInstruction(String target, String data)
+            throws SAXException {
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param name {@inheritDoc}
+     * @throws SAXException {@inheritDoc}
+     */
+    @Override
+    public void skippedEntity(String name) throws SAXException {
+        return;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/xml/VillageLoader.java b/src/main/java/jp/sfjp/jindolf/data/xml/VillageLoader.java
new file mode 100644 (file)
index 0000000..b1d3fdc
--- /dev/null
@@ -0,0 +1,255 @@
+/*
+ * village loader
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.validation.Schema;
+import jp.sfjp.jindolf.data.Village;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+import org.xml.sax.XMLReader;
+
+/**
+ * JinArchiverなどでXMLファイルにアーカイブされた人狼BBSの村プレイ記録を
+ * 読み取る。
+ */
+public class VillageLoader {
+
+    private static final String F_DISALLOW_DOCTYPE_DECL =
+            "http://apache.org/xml/features/disallow-doctype-decl";
+    private static final String F_EXTERNAL_GENERAL_ENTITIES =
+            "http://xml.org/sax/features/external-general-entities";
+    private static final String F_EXTERNAL_PARAMETER_ENTITIES =
+            "http://xml.org/sax/features/external-parameter-entities";
+    private static final String F_LOAD_EXTERNAL_DTD =
+            "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+    private static final String F_NAMESPACE =
+            "http://xml.org/sax/features/namespaces";
+    private static final String F_NAMESPACEPFX =
+            "http://xml.org/sax/features/namespace-prefixes";
+
+
+    /**
+     * constructor.
+     */
+    private VillageLoader(){
+        assert false;
+        return;
+    }
+
+
+    /**
+     * XMLファイルをパースする。
+     *
+     * @param xmlFile XMLファイル
+     * @return 村
+     * @throws IOException I/Oエラー
+     * @throws SAXException XMLの形式エラー
+     */
+    public static Village parseVillage(File xmlFile)
+            throws IOException, SAXException{
+        Objects.nonNull(xmlFile);
+
+        boolean isNormal;
+        isNormal = xmlFile.isFile()
+                && xmlFile.exists()
+                && xmlFile.canRead();
+        if(!isNormal){
+            throw new IOException(xmlFile.getPath() + "を読み込むことができません");
+        }
+
+        Path path = xmlFile.toPath();
+
+        Village result = parseVillage(path);
+        return result;
+    }
+
+    /**
+     * XMLファイルをパースする。
+     *
+     * @param path XMLファイルのPath
+     * @return 村
+     * @throws IOException I/Oエラー
+     * @throws SAXException XMLの形式エラー
+     */
+    public static Village parseVillage(Path path)
+            throws IOException, SAXException{
+        Objects.nonNull(path);
+
+        path = path.normalize();
+
+        boolean isNormal;
+        isNormal = Files.exists(path)
+                && Files.isRegularFile(path)
+                && Files.isReadable(path);
+        if(!isNormal){
+            throw new IOException(path.toString() + "を読み込むことができません");
+        }
+
+        Village result;
+        try(InputStream is = pathToStream(path)){
+            result = parseVillage(is);
+        }
+
+        return result;
+    }
+
+    /**
+     * Pathから入力ストリームを得る。
+     *
+     * @param path Path
+     * @return バッファリングされた入力ストリーム
+     * @throws IOException I/Oエラー
+     */
+    private static InputStream pathToStream(Path path) throws IOException{
+        InputStream is;
+        is = Files.newInputStream(path);
+        is = new BufferedInputStream(is, 4*1024);
+        return is;
+    }
+
+    /**
+     * XML入力をパースする。
+     *
+     * @param istream XML入力
+     * @return 村
+     * @throws IOException I/Oエラー
+     * @throws SAXException XMLの形式エラー
+     */
+    public static Village parseVillage(InputStream istream)
+            throws IOException, SAXException{
+        InputSource isource = new InputSource(istream);
+        Village result = parseVillage(isource);
+        return result;
+    }
+
+    /**
+     * XML入力をパースする。
+     *
+     * @param isource XML入力
+     * @return 村
+     * @throws IOException I/Oエラー
+     * @throws SAXException XMLの形式エラー
+     */
+    public static Village parseVillage(InputSource isource)
+            throws IOException, SAXException{
+        XMLReader reader = buildReader();
+        VillageHandler handler = new VillageHandler();
+        reader.setContentHandler(handler);
+
+        reader.parse(isource);
+
+        Village result = handler.getVillage();
+        return result;
+    }
+
+    /**
+     * SAXパーサファクトリを生成する。
+     *
+     * <ul>
+     * <li>XML名前空間機能は有効になる。
+     * <li>DTDによる形式検証は無効となる。
+     * <li>XIncludeによる差し込み機能は無効となる。
+     * </ul>
+     *
+     * @param schema スキーマ
+     * @return ファクトリ
+     * @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html">
+     *     XML External Entity Prevention Cheat Sheet
+     * </a>
+     */
+    private static SAXParserFactory buildFactory(Schema schema){
+        SAXParserFactory factory = SAXParserFactory.newInstance();
+
+        factory.setNamespaceAware(true);
+        factory.setValidating(false);
+        factory.setXIncludeAware(false);
+
+        try{
+            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+            //factory.setFeature(F_DISALLOW_DOCTYPE_DECL, true);
+            factory.setFeature(F_EXTERNAL_GENERAL_ENTITIES, false);
+            factory.setFeature(F_EXTERNAL_PARAMETER_ENTITIES, false);
+            factory.setFeature(F_LOAD_EXTERNAL_DTD, false);
+            factory.setFeature(F_NAMESPACE, true);
+            factory.setFeature(F_NAMESPACEPFX, true);
+        }catch(   ParserConfigurationException
+                | SAXNotRecognizedException
+                | SAXNotSupportedException e ){
+            assert false;
+            throw new AssertionError(e);
+        }
+
+        factory.setSchema(schema);
+
+        return factory;
+    }
+
+    /**
+     * SAXパーサを生成する。
+     *
+     * @param schema スキーマ
+     * @return SAXパーサ
+     */
+    private static SAXParser buildParser(Schema schema){
+        SAXParserFactory factory = buildFactory(schema);
+
+        SAXParser parser;
+        try{
+            parser = factory.newSAXParser();
+        }catch(ParserConfigurationException | SAXException e){
+            assert false;
+            throw new AssertionError(e);
+        }
+
+        try{
+            parser.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+            parser.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+        }catch(SAXNotRecognizedException | SAXNotSupportedException e){
+            assert false;
+            throw new AssertionError(e);
+        }
+
+        return parser;
+    }
+
+    /**
+     * XMLリーダを生成する。
+     *
+     * @return XMLリーダ
+     */
+    private static XMLReader buildReader(){
+        SAXParser parser = buildParser((Schema)null);
+
+        XMLReader reader;
+        try{
+            reader = parser.getXMLReader();
+        }catch(SAXException e){
+            assert false;
+            throw new AssertionError(e);
+        }
+
+        reader.setEntityResolver(NoopEntityResolver.NOOP_RESOLVER);
+        reader.setErrorHandler(BotherHandler.HANDLER);
+
+        return reader;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/xml/XmlDecoder.java b/src/main/java/jp/sfjp/jindolf/data/xml/XmlDecoder.java
new file mode 100644 (file)
index 0000000..ae8fe6c
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * XML decoders
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+import jp.sourceforge.jindolf.corelib.GameRole;
+import jp.sourceforge.jindolf.corelib.PeriodType;
+import jp.sourceforge.jindolf.corelib.TalkType;
+import org.xml.sax.SAXException;
+
+/**
+ * XML values decoders.
+ */
+public final class XmlDecoder {
+
+    /**
+     * hidden constructor.
+     */
+    private XmlDecoder(){
+        assert false;
+    }
+
+
+    /**
+     * Period種別をデコードする。
+     *
+     * @param type 属性値
+     * @return Period種別
+     * @throws SAXException 不正な属性値
+     */
+    public static PeriodType decodePeriodType(String type)
+            throws SAXException {
+        PeriodType result;
+
+        switch (type) {
+            case "prologue":
+                result = PeriodType.PROLOGUE;
+                break;
+            case "progress":
+                result = PeriodType.PROGRESS;
+                break;
+            case "epilogue":
+                result = PeriodType.EPILOGUE;
+                break;
+            default:
+                assert false;
+                throw new SAXException("invalid period type:" + type);
+        }
+
+        return result;
+    }
+
+    /**
+     * 会話種別をデコードする。
+     *
+     * @param type 属性値
+     * @return 会話種別
+     * @throws SAXException 不正な属性値
+     */
+    public static TalkType decodeTalkType(String type)
+            throws SAXException {
+        TalkType result;
+
+        switch (type) {
+            case "public":
+                result = TalkType.PUBLIC;
+                break;
+            case "wolf":
+                result = TalkType.WOLFONLY;
+                break;
+            case "private":
+                result = TalkType.PRIVATE;
+                break;
+            case "grave":
+                result = TalkType.GRAVE;
+                break;
+            default:
+                assert false;
+                throw new SAXException("invalid talk type: " + type);
+        }
+
+        return result;
+    }
+
+    /**
+     * hour値をデコードする。
+     *
+     * <p>例: 22:49:00+09:00 の 22
+     *
+     * @param txt 属性値
+     * @return hour値
+     */
+    public static int decodeHour(String txt) {
+        int d1 = txt.charAt(0) - '0';
+        int d0 = txt.charAt(1) - '0';
+        int result = d1 * 10 + d0;
+        return result;
+    }
+
+    /**
+     * minute値をデコードする。
+     *
+     * <p>例: 22:49:00+09:00 の 49
+     *
+     * @param txt 属性値
+     * @return minute値
+     */
+    public static int decodeMinute(String txt) {
+        int d1 = txt.charAt(3) - '0';
+        int d0 = txt.charAt(4) - '0';
+        int result = d1 * 10 + d0;
+        return result;
+    }
+
+    /**
+     * roleをデコードする。
+     *
+     * @param role role属性値
+     * @return GameRole種別
+     * @throws SAXException 不正な値
+     */
+    static GameRole decodeRole(String role) throws SAXException {
+        GameRole result;
+
+        switch (role) {
+            case "innocent":
+                result = GameRole.INNOCENT;
+                break;
+            case "wolf":
+                result = GameRole.WOLF;
+                break;
+            case "seer":
+                result = GameRole.SEER;
+                break;
+            case "shaman":
+                result = GameRole.SHAMAN;
+                break;
+            case "madman":
+                result = GameRole.MADMAN;
+                break;
+            case "hunter":
+                result = GameRole.HUNTER;
+                break;
+            case "frater":
+                result = GameRole.FRATER;
+                break;
+            case "hamster":
+                result = GameRole.HAMSTER;
+                break;
+            default:
+                assert false;
+                throw new SAXException("invalid role: " + role);
+        }
+
+        return result;
+    }
+
+}
diff --git a/src/main/java/jp/sfjp/jindolf/data/xml/package-info.java b/src/main/java/jp/sfjp/jindolf/data/xml/package-info.java
new file mode 100644 (file)
index 0000000..9d1c510
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * package info
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+/**
+ * JinArchiverなどでXMLファイルにアーカイブされた人狼BBSの村プレイ記録から、
+ * 各種データモデルを生成するクラス群。
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+/* EOF */
index 1a81f60..8c4582b 100644 (file)
@@ -563,7 +563,7 @@ public class WebIPCDialog extends JDialog {
         }
 
         /**
-         * D&Dに成功したらダイアログを閉じる。
+         * D&amp;Dに成功したらダイアログを閉じる。
          *
          * <p>{@inheritDoc}
          *
index 2dfed6b..c3e3d12 100644 (file)
@@ -35,7 +35,7 @@ import jp.sourceforge.jindolf.corelib.GameRole;
 /**
  * まちゅ氏運営のまとめサイト(wolfbbs)に関する諸々。
  *
- * PukiWikiベース。
+ * <p>PukiWikiベース。
  *
  * @see <a href="https://wolfbbs.jp/">まとめサイト</a>
  * @see <a href="https://pukiwiki.osdn.jp/">PukiWiki</a>
@@ -341,7 +341,7 @@ public final class WolfBBS{
     /**
      * 数値参照文字に変換された文字を追加する。
      *
-     * 例){@literal 'D' => "&#x44;}"
+     * <p>例){@literal 'D' => "&#x44;}"
      *
      * @param app 追加対象
      * @param ch 1文字
@@ -364,7 +364,7 @@ public final class WolfBBS{
     /**
      * 任意の文字を数値参照文字列に変換する。
      *
-     * 例){@literal 'D' => "&#x44;"}
+     * <p>例){@literal 'D' => "&#x44;"}
      *
      * @param ch 文字
      * @return 変換後の文字列
@@ -383,7 +383,7 @@ public final class WolfBBS{
     /**
      * ColorのRGB各成分をWikiカラー表記に変換する。
      *
-     * α成分は無視される。
+     * <p>α成分は無視される。
      *
      * @param color 色
      * @return Wikiカラー表記
diff --git a/src/main/java/jp/sfjp/jindolf/editor/BalloonBorder.java b/src/main/java/jp/sfjp/jindolf/editor/BalloonBorder.java
deleted file mode 100644 (file)
index 13601e7..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * baloon border
- *
- * License : The MIT License
- * Copyright(c) 2008 olyutorskii
- */
-
-package jp.sfjp.jindolf.editor;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.Insets;
-import java.awt.LayoutManager;
-import java.awt.RenderingHints;
-import javax.swing.JComponent;
-import javax.swing.border.Border;
-
-/**
- * フキダシ風Border。
- */
-public class BalloonBorder implements Border{
-
-    private static final int RADIUS = 5;
-
-
-    /**
-     * コンストラクタ。
-     */
-    public BalloonBorder(){
-        super();
-        return;
-    }
-
-
-    /**
-     * 隙間が透明なフキダシ装飾を任意のコンポーネントに施す。
-     * @param inner 装飾対象のコンポーネント
-     * @return 装飾されたコンポーネント
-     */
-    public static JComponent decorateTransparentBorder(JComponent inner){
-        JComponent result = new TransparentContainer(inner);
-
-        Border border = new BalloonBorder();
-        result.setBorder(border);
-
-        return result;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param comp {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public Insets getBorderInsets(Component comp){
-        Insets insets = new Insets(RADIUS, RADIUS, RADIUS, RADIUS);
-        return insets;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 必ずfalseを返す(このBorderは透明)。
-     * @return {@inheritDoc}
-     */
-    @Override
-    public boolean isBorderOpaque(){
-        return false;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param comp {@inheritDoc}
-     * @param g {@inheritDoc}
-     * @param x {@inheritDoc}
-     * @param y {@inheritDoc}
-     * @param width {@inheritDoc}
-     * @param height {@inheritDoc}
-     */
-    @Override
-    public void paintBorder(Component comp,
-                              Graphics g,
-                              int x, int y,
-                              int width, int height ){
-        final int diameter = RADIUS * 2;
-        final int innerWidth  = width - diameter;
-        final int innerHeight = height - diameter;
-
-        Graphics2D g2d = (Graphics2D) g;
-
-        Color bgColor = comp.getBackground();
-        g2d.setColor(bgColor);
-
-        Object antiAliaseHint =
-                g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
-        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
-                             RenderingHints.VALUE_ANTIALIAS_ON );
-
-        g2d.fillRect(x + RADIUS, y,
-                   innerWidth, RADIUS);
-        g2d.fillRect(x, y + RADIUS,
-                   RADIUS, innerHeight);
-        g2d.fillRect(x + RADIUS + innerWidth, y + RADIUS,
-                   RADIUS, innerHeight);
-        g2d.fillRect(x + RADIUS, y + RADIUS + innerHeight,
-                   innerWidth, RADIUS);
-
-        int right = 90;  // 90 degree right angle
-
-        g2d.fillArc(x + innerWidth, y,
-                  diameter, diameter, right * 0, right);
-        g2d.fillArc(x, y,
-                  diameter, diameter, right * 1, right);
-        g2d.fillArc(x, y + innerHeight,
-                  diameter, diameter, right * 2, right);
-        g2d.fillArc(x + innerWidth, y + innerHeight,
-                  diameter, diameter, right * 3, right);
-
-        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antiAliaseHint);
-
-        return;
-    }
-
-    /**
-     * 透明コンテナ。
-     * 1つの子を持ち、背景色操作を委譲する。
-     * つまりこのコンテナにBorderを設定すると子の背景色が反映される。
-     */
-    @SuppressWarnings("serial")
-    private static class TransparentContainer extends JComponent{
-
-        private final JComponent inner;
-
-        /**
-         * コンストラクタ。
-         * @param inner 内部コンポーネント
-         */
-        public TransparentContainer(JComponent inner){
-            super();
-
-            this.inner = inner;
-
-            setOpaque(false);
-
-            LayoutManager layout = new BorderLayout();
-            setLayout(layout);
-            add(this.inner, BorderLayout.CENTER);
-
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * 子の背景色を返す。
-         * @return {@inheritDoc}
-         */
-        @Override
-        public Color getBackground(){
-            Color bg = this.inner.getBackground();
-            return bg;
-        }
-
-        /**
-         * {@inheritDoc}
-         * 背景色指定をフックし、子の背景色を指定する。
-         * @param bg {@inheritDoc}
-         */
-        @Override
-        public void setBackground(Color bg){
-            this.inner.setBackground(bg);
-            return;
-        }
-    }
-
-}
diff --git a/src/main/java/jp/sfjp/jindolf/editor/EditArray.java b/src/main/java/jp/sfjp/jindolf/editor/EditArray.java
deleted file mode 100644 (file)
index fbe8783..0000000
+++ /dev/null
@@ -1,741 +0,0 @@
-/*
- * エディタ集合の操作
- *
- * License : The MIT License
- * Copyright(c) 2008 olyutorskii
- */
-
-package jp.sfjp.jindolf.editor;
-
-import java.awt.Dimension;
-import java.awt.EventQueue;
-import java.awt.Font;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.LayoutManager;
-import java.awt.Rectangle;
-import java.awt.event.FocusEvent;
-import java.awt.event.FocusListener;
-import java.util.ArrayList;
-import java.util.List;
-import javax.swing.JPanel;
-import javax.swing.Scrollable;
-import javax.swing.SwingConstants;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.Document;
-import javax.swing.text.JTextComponent;
-import javax.swing.text.NavigationFilter;
-import javax.swing.text.Position.Bias;
-
-/**
- * エディタ集合の操作。
- * ※ このクラスはすべてシングルスレッドモデルで作られている。
- */
-@SuppressWarnings("serial")
-public class EditArray extends JPanel
-                       implements Scrollable,
-                                  FocusListener {
-
-    private static final int MAX_EDITORS = 50;
-
-    private final List<TalkEditor> editorList = new ArrayList<>();
-    private boolean onAdjusting = false;
-
-    private final NavigationFilter keyNavigator = new CustomNavigation();
-    private final DocumentListener documentListener = new DocWatcher();
-
-    private TalkEditor activeEditor;
-
-    private Font textFont;
-
-    /**
-     * コンストラクタ。
-     */
-    public EditArray(){
-        super();
-
-        setOpaque(false);
-
-        LayoutManager layout = new GridBagLayout();
-        setLayout(layout);
-
-        TalkEditor firstEditor = incrementTalkEditor();
-        setActiveEditor(firstEditor);
-
-        return;
-    }
-
-    /**
-     * 個別エディタの生成を行う。
-     * @return エディタ
-     */
-    private TalkEditor createTalkEditor(){
-        TalkEditor editor = new TalkEditor();
-        editor.setNavigationFilter(this.keyNavigator);
-        editor.addTextFocusListener(this);
-        Document document = editor.getDocument();
-        document.addDocumentListener(this.documentListener);
-
-        if(this.textFont == null){
-            this.textFont = editor.getTextFont();
-        }else{
-            editor.setTextFont(this.textFont);
-        }
-
-        return editor;
-    }
-
-    /**
-     * エディタ集合を一つ増やす。
-     * @return 増えたエディタ
-     */
-    private TalkEditor incrementTalkEditor(){
-        TalkEditor editor = createTalkEditor();
-
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        constraints.gridx = 0;
-        constraints.gridy = GridBagConstraints.RELATIVE;
-
-        constraints.gridwidth = GridBagConstraints.REMAINDER;
-        constraints.gridheight = 1;
-
-        constraints.weightx = 1.0;
-        constraints.weighty = 0.0;
-
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        constraints.anchor = GridBagConstraints.NORTHEAST;
-
-        add(editor, constraints);
-
-        this.editorList.add(editor);
-
-        int sequenceNumber = this.editorList.size();
-        editor.setSequenceNumber(sequenceNumber);
-
-        return editor;
-    }
-
-    /**
-     * 1から始まる通し番号指定でエディタを取得する。
-     * 存在しない通し番号が指定された場合は新たにエディタが追加される。
-     * @param sequenceNumber 通し番号
-     * @return エディタ
-     */
-    private TalkEditor getTalkEditor(int sequenceNumber){
-        while(this.editorList.size() < sequenceNumber){
-            incrementTalkEditor();
-        }
-
-        TalkEditor result = this.editorList.get(sequenceNumber - 1);
-
-        return result;
-    }
-
-    /**
-     * 指定したエディタの次の通し番号を持つエディタを返す。
-     * エディタがなければ追加される。
-     * @param editor エディタ
-     * @return 次のエディタ
-     */
-    private TalkEditor nextEditor(TalkEditor editor){
-        int sequenceNumber = editor.getSequenceNumber();
-        TalkEditor nextEditor = getTalkEditor(sequenceNumber + 1);
-        return nextEditor;
-    }
-
-    /**
-     * 指定したエディタの前の通し番号を持つエディタを返す。
-     * @param editor エディタ
-     * @return 前のエディタ。
-     *     最初のエディタ(通し番号1)が指定されればnullを返す。
-     */
-    private TalkEditor prevEditor(TalkEditor editor){
-        int sequenceNumber = editor.getSequenceNumber();
-        if(sequenceNumber <= 1) return null;
-        TalkEditor prevEditor = getTalkEditor(sequenceNumber - 1);
-        return prevEditor;
-    }
-
-    /**
-     * 指定したエディタがエディタ集合の最後のエディタか判定する。
-     * @param editor エディタ
-     * @return 最後のエディタならtrue
-     */
-    private boolean isLastEditor(TalkEditor editor){
-        int seqNo = editor.getSequenceNumber();
-        int size = this.editorList.size();
-        if(seqNo >= size) return true;
-        return false;
-    }
-
-    /**
-     * Documentからその持ち主であるエディタを取得する。
-     * @param document Documentインスタンス
-     * @return 持ち主のエディタ。見つからなければnull。
-     */
-    private TalkEditor getEditorFromDocument(Document document){
-        for(TalkEditor editor : this.editorList){
-            if(editor.getDocument() == document) return editor;
-        }
-        return null;
-    }
-
-    /**
-     * エディタ集合から任意のエディタを除く。
-     * ただし最初のエディタは消去不可。
-     * @param editor エディタ
-     */
-    private void removeEditor(TalkEditor editor){
-        if(editor.getParent() != this) return;
-
-        int seqNo = editor.getSequenceNumber();
-        if(seqNo <= 1) return;
-        TalkEditor prevEditor = prevEditor(editor);
-        if(editor.isActive()){
-            setActiveEditor(prevEditor);
-        }
-        if(editor.hasEditorFocus()){
-            prevEditor.requestEditorFocus();
-        }
-
-        this.editorList.remove(seqNo - 1);
-
-        editor.setNavigationFilter(null);
-        editor.removeTextFocusListener(this);
-        Document document = editor.getDocument();
-        document.removeDocumentListener(this.documentListener);
-        editor.clearText();
-
-        remove(editor);
-        revalidate();
-
-        int renumber = 1;
-        for(TalkEditor newEditor : this.editorList){
-            newEditor.setSequenceNumber(renumber++);
-        }
-
-        return;
-    }
-
-    /**
-     * エディタ間文字調整タスクをディスパッチスレッドとして事後投入する。
-     * エディタ間文字調整タスクが実行中であれば何もしない。
-     * きっかけとなったエディタ上でIME操作が確定していなければ何もしない。
-     * @param triggerEvent ドキュメント変更イベント
-     */
-    private void detachAdjustTask(DocumentEvent triggerEvent){
-        if(this.onAdjusting) return;
-
-        Document document = triggerEvent.getDocument();
-        final TalkEditor triggerEditor = getEditorFromDocument(document);
-        if(triggerEditor.onIMEoperation()) return;
-
-        this.onAdjusting = true;
-
-        EventQueue.invokeLater(new Runnable(){
-            @Override
-            public void run(){
-                try{
-                    adjustTask(triggerEditor);
-                }finally{
-                    EditArray.this.onAdjusting = false;
-                }
-                return;
-            }
-        });
-
-        return;
-    }
-
-    /**
-     * エディタ間文字調整タスク本体。
-     * @param triggerEditor タスク実行のきっかけとなったエディタ
-     */
-    private void adjustTask(TalkEditor triggerEditor){
-        int initCaretPos = triggerEditor.getCaretPosition();
-
-        TalkEditor newFocus = null;
-        int newCaretPos = -1;
-
-        TalkEditor current = triggerEditor;
-        for(;;){
-            TalkEditor next;
-
-            if( ! isLastEditor(current) ){
-                next = nextEditor(current);
-                String nextContents = next.getText();
-                int nextLength = nextContents.length();
-
-                current.appendTail(nextContents);
-                String rest = current.chopRest();
-                int restLength;
-                if(rest == null) restLength = 0;
-                else             restLength = rest.length();
-
-                int chopLength = nextLength - restLength;
-                if(chopLength > 0){
-                    next.chopHead(chopLength);
-                }else if(chopLength < 0){
-                    rest = rest.substring(0, -chopLength);
-                    next.appendHead(rest);
-                }else{
-                    if(newFocus == null){
-                        newFocus = current;
-                        newCaretPos = initCaretPos;
-                    }
-                    break;
-                }
-            }else{
-                String rest = current.chopRest();
-                if(rest == null || this.editorList.size() >= MAX_EDITORS){
-                    if(newFocus == null){
-                        newFocus = current;
-                        if(current.getTextLength() >= initCaretPos){
-                            newCaretPos = initCaretPos;
-                        }else{
-                            newCaretPos = current.getTextLength();
-                        }
-                    }
-                    break;
-                }
-                next = nextEditor(current);
-                next.appendHead(rest);
-            }
-
-            if(newFocus == null){
-                int currentLength = current.getTextLength();
-                if(initCaretPos >= currentLength){
-                    initCaretPos -= currentLength;
-                }else{
-                    newFocus = current;
-                    newCaretPos = initCaretPos;
-                }
-            }
-
-            current = next;
-        }
-
-        if(newFocus != null){
-            newFocus.requestEditorFocus();
-            newFocus.setCaretPosition(newCaretPos);
-        }
-
-        adjustEditorsTail();
-
-        return;
-    }
-
-    /**
-     * エディタ集合末尾の空エディタを切り詰める。
-     * ただし最初のエディタ(通し番号1)は削除されない。
-     * フォーカスを持つエディタが削除された場合は、
-     * 削除されなかった最後のエディタにフォーカスが移る。
-     */
-    private void adjustEditorsTail(){
-        int editorNum = this.editorList.size();
-        if(editorNum <= 0) return;
-        TalkEditor lastEditor = this.editorList.get(editorNum - 1);
-
-        TalkEditor prevlostEditor = null;
-
-        boolean lostFocusedEditor = false;
-
-        for(;;){
-            int textLength = lastEditor.getTextLength();
-            int seqNo = lastEditor.getSequenceNumber();
-
-            if(lostFocusedEditor){
-                prevlostEditor = lastEditor;
-            }
-
-            if(textLength > 0) break;
-            if(seqNo <= 1) break;
-
-            if(lastEditor.hasEditorFocus()) lostFocusedEditor = true;
-            removeEditor(lastEditor);
-
-            lastEditor = prevEditor(lastEditor); // TODO ちょっと変
-        }
-
-        if(prevlostEditor != null){
-            int textLength = prevlostEditor.getTextLength();
-            prevlostEditor.requestEditorFocus();
-            prevlostEditor.setCaretPosition(textLength);
-        }
-
-        return;
-    }
-
-    /**
-     * フォーカスを持つエディタを取得する。
-     * @return エディタ
-     */
-    public TalkEditor getFocusedTalkEditor(){
-        for(TalkEditor editor : this.editorList){
-            if(editor.hasEditorFocus()) return editor;
-        }
-        return null;
-    }
-
-    /**
-     * フォーカスを持つエディタの次エディタがあればフォーカスを移し、
-     * カレット位置を0にする。
-     */
-    // TODO エディタのスクロール位置調整が必要。
-    public void forwardEditor(){
-        TalkEditor editor = getFocusedTalkEditor();
-        if(isLastEditor(editor)) return;
-        TalkEditor next = nextEditor(editor);
-        next.setCaretPosition(0);
-        next.requestEditorFocus();
-        return;
-    }
-
-    /**
-     * フォーカスを持つエディタの前エディタがあればフォーカスを移し、
-     * カレット位置を末尾に置く。
-     */
-    public void backwardEditor(){
-        TalkEditor editor = getFocusedTalkEditor();
-        TalkEditor prev = prevEditor(editor);
-        if(prev == null) return;
-        int length = prev.getTextLength();
-        prev.setCaretPosition(length);
-        prev.requestEditorFocus();
-        return;
-    }
-
-    /**
-     * 任意のエディタをアクティブにする。
-     * 同時にアクティブなエディタは一つのみ。
-     * @param editor アクティブにするエディタ
-     */
-    private void setActiveEditor(TalkEditor editor){
-        if(this.activeEditor != null){
-            this.activeEditor.setActive(false);
-        }
-
-        this.activeEditor = editor;
-
-        if(this.activeEditor != null){
-            this.activeEditor.setActive(true);
-        }
-
-        fireChangeActive();
-
-        return;
-    }
-
-    /**
-     * アクティブなエディタを返す。
-     * @return アクティブなエディタ。
-     */
-    public TalkEditor getActiveEditor(){
-        return this.activeEditor;
-    }
-
-    /**
-     * 全発言を連結した文字列を返す。
-     * @return 連結文字列
-     */
-    public CharSequence getAllText(){
-        StringBuilder result = new StringBuilder();
-
-        for(TalkEditor editor : this.editorList){
-            String text = editor.getText();
-            result.append(text);
-        }
-
-        return result;
-    }
-
-    /**
-     * 先頭エディタの0文字目から字を詰め込む。
-     * 2番目移行のエディタへはみ出すかもしれない。
-     * @param seq 詰め込む文字列
-     */
-    public void setAllText(CharSequence seq){
-        TalkEditor firstEditor = getTalkEditor(1);
-        Document doc = firstEditor.getDocument();
-        try{
-            doc.insertString(0, seq.toString(), null);
-        }catch(BadLocationException e){
-            assert false;
-        }
-        return;
-    }
-
-    /**
-     * 全エディタをクリアする。
-     */
-    public void clearAllEditor(){
-        int editorNum = this.editorList.size();
-        if(editorNum <= 0) return;
-
-        TalkEditor lastEditor = this.editorList.get(editorNum - 1);
-        for(;;){
-            removeEditor(lastEditor);
-            lastEditor = prevEditor(lastEditor);
-            if(lastEditor == null) break;
-        }
-
-        TalkEditor firstEditor = getTalkEditor(1);
-        firstEditor.clearText();
-        setActiveEditor(firstEditor);
-
-        return;
-    }
-
-    /**
-     * テキスト編集用フォントを指定する。
-     * @param textFont フォント
-     */
-    public void setTextFont(Font textFont){
-        this.textFont = textFont;
-        for(TalkEditor editor : this.editorList){
-            editor.setTextFont(this.textFont);
-            editor.repaint();
-        }
-        revalidate();
-        return;
-    }
-
-    /**
-     * テキスト編集用フォントを取得する。
-     * @return フォント
-     */
-    public Font getTextFont(){
-        return this.textFont;
-    }
-
-    /**
-     * アクティブエディタ変更通知用リスナの登録。
-     * @param listener リスナ
-     */
-    public void addChangeListener(ChangeListener listener){
-        this.listenerList.add(ChangeListener.class, listener);
-        return;
-    }
-
-    /**
-     * アクティブエディタ変更通知用リスナの削除。
-     * @param listener リスナ
-     */
-    public void removeChangeListener(ChangeListener listener){
-        this.listenerList.remove(ChangeListener.class, listener);
-        return;
-    }
-
-    /**
-     * アクティブエディタ変更通知を行う。
-     */
-    private void fireChangeActive(){
-        ChangeEvent event = new ChangeEvent(this);
-
-        ChangeListener[] listeners =
-                this.listenerList.getListeners(ChangeListener.class);
-        for(ChangeListener listener : listeners){
-            listener.stateChanged(event);
-        }
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * エディタのフォーカス取得とともにアクティブ状態にする。
-     * @param event {@inheritDoc}
-     */
-    @Override
-    public void focusGained(FocusEvent event){
-        Object source = event.getSource();
-        if( ! (source instanceof JTextComponent) ) return;
-        JTextComponent textComp = (JTextComponent) source;
-
-        Document document = textComp.getDocument();
-        TalkEditor editor = getEditorFromDocument(document);
-
-        setActiveEditor(editor);
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param event {@inheritDoc}
-     */
-    @Override
-    public void focusLost(FocusEvent event){
-        // NOTHING
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public Dimension getPreferredScrollableViewportSize(){
-        Dimension result = getPreferredSize();
-        return result;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 横スクロールバーを極力出さないようレイアウトでがんばる。
-     * @return {@inheritDoc}
-     */
-    @Override
-    public boolean getScrollableTracksViewportWidth(){
-        return true;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 縦スクロールバーを出しても良いのでレイアウトでがんばらない。
-     * @return {@inheritDoc}
-     */
-    @Override
-    public boolean getScrollableTracksViewportHeight(){
-        return false;
-    }
-
-    /**
-     *  {@inheritDoc}
-     * @param visibleRect {@inheritDoc}
-     * @param orientation {@inheritDoc}
-     * @param direction {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public int getScrollableBlockIncrement(Rectangle visibleRect,
-                                           int orientation,
-                                           int direction ){
-        if(orientation == SwingConstants.VERTICAL){
-            return visibleRect.height;
-        }
-        return 10;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param visibleRect {@inheritDoc}
-     * @param orientation {@inheritDoc}
-     * @param direction {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public int getScrollableUnitIncrement(Rectangle visibleRect,
-                                          int orientation,
-                                          int direction ){
-        return 30; // TODO フォント高の1.5倍くらい?
-    }
-
-    /**
-     * エディタ内のカーソル移動を監視するための、
-     * カスタム化したナビゲーションフィルター。
-     * 必要に応じてエディタ間カーソル移動を行う。
-     */
-    private class CustomNavigation extends NavigationFilter{
-
-        /**
-         * コンストラクタ。
-         */
-        public CustomNavigation(){
-            super();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * カーソル移動が行き詰まった場合、
-         * 隣接するエディタ間でカーソル移動を行う。
-         * @param text {@inheritDoc}
-         * @param pos {@inheritDoc}
-         * @param bias {@inheritDoc}
-         * @param direction {@inheritDoc}
-         * @param biasRet {@inheritDoc}
-         * @return {@inheritDoc}
-         * @throws javax.swing.text.BadLocationException {@inheritDoc}
-         */
-        @Override
-        public int getNextVisualPositionFrom(JTextComponent text,
-                                                 int pos,
-                                                 Bias bias,
-                                                 int direction,
-                                                 Bias[] biasRet )
-                                                 throws BadLocationException {
-            int result = super.getNextVisualPositionFrom(text,
-                                                         pos,
-                                                         bias,
-                                                         direction,
-                                                         biasRet );
-            if(result != pos) return result;
-
-            switch(direction){
-            case SwingConstants.WEST:
-            case SwingConstants.NORTH:
-                backwardEditor();
-                break;
-            case SwingConstants.EAST:
-            case SwingConstants.SOUTH:
-                forwardEditor();
-                break;
-            default:
-                assert false;
-            }
-
-            return result;
-        }
-    }
-
-    /**
-     * エディタの内容変更を監視し、随時エディタ間調整を行う。
-     */
-    private class DocWatcher implements DocumentListener{
-
-        /**
-         * コンストラクタ。
-         */
-        public DocWatcher(){
-            super();
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void changedUpdate(DocumentEvent event){
-            detachAdjustTask(event);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void insertUpdate(DocumentEvent event){
-            detachAdjustTask(event);
-            return;
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param event {@inheritDoc}
-         */
-        @Override
-        public void removeUpdate(DocumentEvent event){
-            detachAdjustTask(event);
-            return;
-        }
-    }
-
-}
diff --git a/src/main/java/jp/sfjp/jindolf/editor/TalkEditor.java b/src/main/java/jp/sfjp/jindolf/editor/TalkEditor.java
deleted file mode 100644 (file)
index 17caa8f..0000000
+++ /dev/null
@@ -1,551 +0,0 @@
-/*
- * 原稿作成支援エディタ
- *
- * License : The MIT License
- * Copyright(c) 2008 olyutorskii
- */
-
-package jp.sfjp.jindolf.editor;
-
-import java.awt.Color;
-import java.awt.Dimension;
-import java.awt.Font;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.awt.LayoutManager;
-import java.awt.Rectangle;
-import java.awt.event.FocusListener;
-import javax.swing.BorderFactory;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.border.Border;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.Document;
-import javax.swing.text.NavigationFilter;
-import javax.swing.text.PlainDocument;
-import javax.swing.text.Segment;
-import jp.sfjp.jindolf.dxchg.TextPopup;
-
-/**
- * 原稿作成支援エディタ。
- * 文字数行数管理などを行う。
- * 200文字もしくは10行まで入力可能。
- * ※ 10回目に出現する改行文字は許される。
- * ※ 2010-11-27以降、G国では5行制限が10行に緩和された。
- */
-@SuppressWarnings("serial")
-public class TalkEditor
-        extends JPanel
-        implements DocumentListener {
-
-    private static final int MAX_CHARS = 200;
-    private static final int MAX_LINES = 10;
-
-    private static final Color COLOR_ACTIVATED = Color.GRAY;
-
-
-    private final PlainDocument document = new PlainDocument();
-
-    private int sequenceNumber;
-
-    private boolean isActive = false;
-
-    private final JLabel seqCount = new JLabel();
-    private final JLabel talkStat = new JLabel();
-    private final TextEditor textEditor = new TextEditor();
-
-    private Font textFont;
-
-
-    /**
-     * コンストラクタ。
-     * 通し番号は0が指定される。
-     */
-    public TalkEditor(){
-        this(0);
-        return;
-    }
-
-    /**
-     * コンストラクタ。
-     * @param seqNumber 通し番号
-     */
-    @SuppressWarnings("LeakingThisInConstructor")
-    private TalkEditor(int seqNumber){
-        super();
-
-        setOpaque(true);
-
-        this.document.addDocumentListener(this);
-
-        this.seqCount.setForeground(Color.WHITE);
-        this.talkStat.setForeground(Color.WHITE);
-        this.seqCount.setOpaque(false);
-        this.talkStat.setOpaque(false);
-
-        this.textEditor.setMargin(new Insets(3, 3, 3, 3));
-        this.textEditor.setDocument(this.document);
-
-        JPopupMenu popup = new TextPopup();
-        this.textEditor.setComponentPopupMenu(popup);
-
-        this.textFont = this.textEditor.getFont();
-
-        setSequenceNumber(seqNumber);
-        updateStat();
-        setActive(false);
-
-        design();
-
-        return;
-    }
-
-
-    /**
-     * 指定された文字列の指定された位置から、
-     * 最大何文字まで1発言におさめる事ができるか判定する。
-     * @param source 検査対象
-     * @param start 検査開始位置
-     * @return 1発言に納めていい長さ。
-     */
-    public static int choplimit(CharSequence source, int start){
-        int length = source.length();
-        if(start >= length) return 0;
-
-        int chars = 0;
-        int lines = 0;
-
-        for(int pos = start; pos < length; pos++){
-            chars++;
-            if(chars >= MAX_CHARS) break;
-            char ch = source.charAt(pos);
-            if(ch == '\n'){
-                lines++;
-                if(lines >= MAX_LINES) break;
-            }
-        }
-
-        return chars;
-    }
-
-    /**
-     * レイアウトを行う。
-     */
-    private void design(){
-        LayoutManager layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        setLayout(layout);
-
-        constraints.gridx = 0;
-        constraints.gridy = 0;
-        constraints.gridwidth = 1;
-        constraints.gridheight = 1;
-        constraints.weightx = 0.0;
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.NONE;
-        constraints.anchor = GridBagConstraints.NORTHWEST;
-        constraints.insets = new Insets(0, 0, 1, 3);
-        add(this.seqCount, constraints);
-
-        constraints.gridx = 1;
-        constraints.gridy = 0;
-        constraints.gridwidth = 1;
-        constraints.gridheight = 2;
-        constraints.weightx = 1.0;
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        constraints.anchor = GridBagConstraints.NORTHWEST;
-        constraints.insets = new Insets(0, 0, 0, 0);
-        JComponent decorated =
-                BalloonBorder.decorateTransparentBorder(this.textEditor);
-        add(decorated, constraints);
-
-        constraints.gridx = 0;
-        constraints.gridy = 1;
-        constraints.gridwidth = 1;
-        constraints.gridheight = 1;
-        constraints.weightx = 0.0;
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.NONE;
-        constraints.anchor = GridBagConstraints.NORTHEAST;
-        constraints.insets = new Insets(0, 0, 0, 3);
-        add(this.talkStat, constraints);
-
-        Border border = BorderFactory.createEmptyBorder(5, 5, 5, 5);
-        setBorder(border);
-
-        return;
-    }
-
-    /**
-     * テキスト編集用フォントを指定する。
-     * @param textFont フォント
-     */
-    public void setTextFont(Font textFont){
-        this.textFont = textFont;
-        this.textEditor.setFont(this.textFont);
-        this.textEditor.repaint();
-        revalidate();
-        return;
-    }
-
-    /**
-     * テキスト編集用フォントを取得する。
-     * @return フォント
-     */
-    public Font getTextFont(){
-        return this.textFont;
-    }
-
-    /**
-     * テキストコンポーネントにNavigationFilterを設定する。
-     * @param navigator ナビゲーションフィルタ
-     */
-    public void setNavigationFilter(NavigationFilter navigator){
-        this.textEditor.setNavigationFilter(navigator);
-        return;
-    }
-
-    /**
-     * 通し番号を取得する。
-     * @return 通し番号
-     */
-    public int getSequenceNumber(){
-        return this.sequenceNumber;
-    }
-
-    /**
-     * 通し番号を設定する。
-     * @param seqNumber 通し番号
-     */
-    public void setSequenceNumber(int seqNumber){
-        this.sequenceNumber = seqNumber;
-        String seqText = "=== #" + this.sequenceNumber + " ===";
-        this.seqCount.setText(seqText);
-        return;
-    }
-
-    /**
-     * Documentを取得する。
-     * @return 管理中のDocument
-     */
-    public Document getDocument(){
-        return this.document;
-    }
-
-    /**
-     * 現在のエディタの文字列内容を取得する。
-     * @return 文字列内容
-     */
-    public String getText(){
-        int length = this.document.getLength();
-        String result = "";
-        try{
-            result = this.document.getText(0, length);
-        }catch(BadLocationException e){
-            assert false;
-        }
-        return result;
-    }
-
-    /**
-     * 現在のエディタの文字列長を取得する。
-     * @return 文字列長
-     */
-    public int getTextLength(){
-        return this.document.getLength();
-    }
-
-    /**
-     * 現在の行数を取得する。
-     * ※ 改行文字の総数より一つ多い場合もある。
-     * @return 行数
-     */
-    private int getTextLines(){
-        int lines = 0;
-
-        Segment segment = new Segment();
-        segment.setPartialReturn(true);
-
-        boolean hasLineContents = false;
-        int pos = 0;
-        int remain = getTextLength();
-        while(remain > 0){
-            try{
-                this.document.getText(pos, remain, segment);
-            }catch(BadLocationException e){
-                assert false;
-            }
-
-            for(;;){
-                char ch = segment.current();
-                if(ch == Segment.DONE) break;
-
-                if(ch == '\n'){
-                    if( ! hasLineContents ){
-                        lines++;
-                    }
-                    hasLineContents = false;
-                }else if( ! hasLineContents ){
-                    hasLineContents = true;
-                    lines++;
-                }
-
-                segment.next();
-            }
-
-            pos    += segment.count;
-            remain -= segment.count;
-        }
-
-        return lines;
-    }
-
-    /**
-     * エディタ先頭に文字列を挿入する。
-     * @param text 挿入文字列
-     */
-    public void appendHead(CharSequence text){
-        if(text == null) return;
-        if(text.length() <= 0) return;
-
-        try{
-            this.document.insertString(0, text.toString(), null);
-        }catch(BadLocationException e){
-            assert false;
-        }
-
-        return;
-    }
-
-    /**
-     * エディタ末尾に文字列を追加する。
-     * @param text 追加文字列
-     */
-    public void appendTail(CharSequence text){
-        if(text == null) return;
-        if(text.length() <= 0) return;
-
-        int offset = getTextLength();
-        try{
-            this.document.insertString(offset, text.toString(), null);
-        }catch(BadLocationException e){
-            assert false;
-        }
-
-        return;
-    }
-
-    /**
-     * エディタ先頭から文字列を削除する。
-     * @param chopLength 削除文字数
-     */
-    public void chopHead(int chopLength){
-        if(chopLength <= 0) return;
-
-        int modLength;
-        int textLength = getTextLength();
-        if(chopLength > textLength) modLength = textLength;
-        else                        modLength = chopLength;
-
-        try{
-            this.document.remove(0, modLength);
-        }catch(BadLocationException e){
-            assert false;
-        }
-
-        return;
-    }
-
-    /**
-     * テキストを空にする。
-     */
-    public void clearText(){
-        int textLength = this.document.getLength();
-        try{
-            this.document.remove(0, textLength);
-        }catch(BadLocationException e){
-            assert false;
-        }
-        return;
-    }
-
-    /**
-     * アクティブ状態を指定する。
-     * アクティブ状態の場合、背景色が変わる。
-     * @param isActiveArg trueならアクティブ状態
-     */
-    public void setActive(boolean isActiveArg){
-        this.isActive = isActiveArg;
-
-        if(this.isActive){
-            setOpaque(true);
-            setBackground(COLOR_ACTIVATED);
-            Dimension size = getSize();
-            Rectangle bounds = new Rectangle(size);
-            scrollRectToVisible(bounds);
-            this.textEditor.scrollCaretToVisible();
-        }else{
-            setOpaque(false);
-        }
-
-        repaint();
-
-        return;
-    }
-
-    /**
-     * アクティブ状態を取得する。
-     * @return アクティブ状態ならtrue
-     */
-    public boolean isActive(){
-        return this.isActive;
-    }
-
-    /**
-     * エディタが現在IME操作中か判定する。
-     * @return IME操作中ならtrue
-     */
-    public boolean onIMEoperation(){
-        boolean result = this.textEditor.onIMEoperation();
-        return result;
-    }
-
-    /**
-     * エディタの現在のカーソル位置を取得する。
-     * @return 0から始まるカーソル位置
-     */
-    public int getCaretPosition(){
-        int caretPos = this.textEditor.getCaretPosition();
-        return caretPos;
-    }
-
-    /**
-     * エディタのカーソル位置を設定する。
-     * @param pos 0から始まるカーソル位置
-     * @throws java.lang.IllegalArgumentException 範囲外のカーソル位置指定
-     */
-    public void setCaretPosition(int pos) throws IllegalArgumentException{
-        this.textEditor.setCaretPosition(pos);
-        return;
-    }
-
-    /**
-     * 集計情報表示(文字数、行数)を更新する。
-     */
-    private void updateStat(){
-        if(onIMEoperation()) return;
-
-        int charTotal = getTextLength();
-        int lineNumber = getTextLines();
-
-        StringBuilder statistics = new StringBuilder();
-        statistics.append(charTotal)
-                  .append("字 ")
-                  .append(lineNumber)
-                  .append("行");
-        this.talkStat.setText(statistics.toString());
-
-        return;
-    }
-
-    /**
-     * このテキストエディタに収まらない末尾文章を切り出す。
-     * @return 最小の末尾文書。余裕があり切り出す必要がなければnullを返す。
-     */
-    public String chopRest(){
-        String text = getText();
-        int textLength = getTextLength();
-        int choppedlen = choplimit(text, 0);
-
-        int restLength = textLength - choppedlen;
-        if(restLength <= 0) return null;
-
-        String rest = null;
-        try{
-            rest = this.document.getText(choppedlen, restLength);
-            this.document.remove(choppedlen, restLength);
-        }catch(BadLocationException e){
-            assert false;
-        }
-
-        return rest;
-    }
-
-    /**
-     * テキストエディタにフォーカスを設定する。
-     * @return 絶対失敗する場合はfalse
-     */
-    public boolean requestEditorFocus(){
-        boolean result = this.textEditor.requestFocusInWindow();
-        return result;
-    }
-
-    /**
-     * テキストエディタがフォーカスを保持しているか判定する。
-     * @return フォーカスを保持していればtrue
-     */
-    public boolean hasEditorFocus(){
-        boolean result = this.textEditor.hasFocus();
-        return result;
-    }
-
-    /**
-     * 子エディタのフォーカス監視リスナを登録する。
-     * @param listener FocusListener
-     */
-    public void addTextFocusListener(FocusListener listener){
-        this.textEditor.addFocusListener(listener);
-        return;
-    }
-
-    /**
-     * 子エディタからフォーカス監視リスナを外す。
-     * @param listener FocusListener
-     */
-    public void removeTextFocusListener(FocusListener listener){
-        this.textEditor.removeFocusListener(listener);
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 集計情報を更新する。
-     * @param event {@inheritDoc}
-     */
-    // TODO いつ呼ばれるのか不明
-    @Override
-    public void changedUpdate(DocumentEvent event){
-        updateStat();
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 集計情報を更新する。
-     * @param event {@inheritDoc}
-     */
-    @Override
-    public void insertUpdate(DocumentEvent event){
-        updateStat();
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 集計情報を更新する。
-     * @param event {@inheritDoc}
-     */
-    @Override
-    public void removeUpdate(DocumentEvent event){
-        updateStat();
-        return;
-    }
-
-}
diff --git a/src/main/java/jp/sfjp/jindolf/editor/TalkPreview.java b/src/main/java/jp/sfjp/jindolf/editor/TalkPreview.java
deleted file mode 100644 (file)
index 9a04c43..0000000
+++ /dev/null
@@ -1,516 +0,0 @@
-/*
- * 発言エディットパネル
- *
- * License : The MIT License
- * Copyright(c) 2008 olyutorskii
- */
-
-package jp.sfjp.jindolf.editor;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Container;
-import java.awt.Font;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.awt.LayoutManager;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.io.File;
-import javax.swing.BorderFactory;
-import javax.swing.JButton;
-import javax.swing.JComponent;
-import javax.swing.JFrame;
-import javax.swing.JLabel;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.JScrollPane;
-import javax.swing.JSplitPane;
-import javax.swing.JViewport;
-import javax.swing.border.BevelBorder;
-import javax.swing.border.Border;
-import javax.swing.border.TitledBorder;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-import javax.swing.text.JTextComponent;
-import jp.sfjp.jindolf.dxchg.ClipboardAction;
-import jp.sfjp.jindolf.dxchg.TextPopup;
-import jp.sfjp.jindolf.glyph.FontInfo;
-import jp.sfjp.jindolf.util.GUIUtils;
-import jp.sourceforge.jovsonz.JsArray;
-import jp.sourceforge.jovsonz.JsComposition;
-import jp.sourceforge.jovsonz.JsObject;
-import jp.sourceforge.jovsonz.JsString;
-import jp.sourceforge.jovsonz.JsTypes;
-import jp.sourceforge.jovsonz.JsValue;
-
-/**
- * 発言エディットパネル。
- */
-@SuppressWarnings("serial")
-public class TalkPreview extends JFrame
-        implements ActionListener, ChangeListener{
-
-    /** 原稿ファイル。 */
-    public static final File DRAFT_FILE = new File("draft.json");
-
-    private static final Color COLOR_EDITORBACK = Color.BLACK;
-
-    private final JTextComponent freeMemo = new TextEditor();
-
-    private final EditArray editArray = new EditArray();
-
-    private final JButton cutButton      = new JButton("カット");
-    private final JButton copyButton     = new JButton("コピー");
-    private final JButton clearButton    = new JButton("クリア");
-    private final JButton cutAllButton   = new JButton("全カット");
-    private final JButton copyAllButton  = new JButton("全コピー");
-    private final JButton clearAllButton = new JButton("全クリア");
-    private final JButton closeButton    = new JButton("閉じる");
-    private final TitledBorder numberBorder =
-            BorderFactory.createTitledBorder("");
-    private final JComponent singleGroup = buildSingleGroup();
-    private final JComponent multiGroup  = buildMultiGroup();
-    private final JLabel letsBrowser =
-            new JLabel("投稿はWebブラウザからどうぞ");
-
-    private JsObject loadedDraft = null;
-
-    /**
-     * コンストラクタ。
-     */
-    @SuppressWarnings("LeakingThisInConstructor")
-    public TalkPreview(){
-        super();
-
-        GUIUtils.modifyWindowAttributes(this, true, false, true);
-
-        setDefaultCloseOperation(HIDE_ON_CLOSE);
-
-        this.cutButton      .addActionListener(this);
-        this.copyButton     .addActionListener(this);
-        this.clearButton    .addActionListener(this);
-        this.cutAllButton   .addActionListener(this);
-        this.copyAllButton  .addActionListener(this);
-        this.clearAllButton .addActionListener(this);
-        this.closeButton    .addActionListener(this);
-
-        this.editArray.addChangeListener(this);
-
-        Container content = getContentPane();
-        design(content);
-
-        setBorderNumber(1);
-
-        return;
-    }
-
-    /**
-     * レイアウトを行う。
-     * @param content コンテナ
-     */
-    private void design(Container content){
-        JComponent freeNotePanel = buildFreeNotePanel();
-
-        JScrollPane scrollPane  = new JScrollPane();
-        scrollPane.setHorizontalScrollBarPolicy(
-                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
-        );
-        JViewport viewPort = new JViewport();
-        viewPort.setBackground(COLOR_EDITORBACK);
-        viewPort.setView(this.editArray);
-        scrollPane.setViewport(viewPort);
-
-        LayoutManager layout;
-        Border border;
-
-        JComponent editPanel = new JPanel();
-        layout = new BorderLayout();
-        editPanel.setLayout(layout);
-        editPanel.add(scrollPane, BorderLayout.CENTER);
-        JComponent buttonPanel = buildButtonPanel();
-        editPanel.add(buttonPanel, BorderLayout.EAST);
-        border = BorderFactory.createTitledBorder("発言編集");
-        editPanel.setBorder(border);
-
-        JSplitPane split = new JSplitPane();
-        split.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
-        split.setContinuousLayout(false);
-        split.setDividerSize(10);
-        split.setDividerLocation(200);
-        split.setOneTouchExpandable(true);
-        split.setLeftComponent(freeNotePanel);
-        split.setRightComponent(editPanel);
-
-        Border inside  = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
-        Border outside = BorderFactory.createEmptyBorder(2, 5, 2, 2);
-        border = BorderFactory.createCompoundBorder(inside, outside);
-        this.letsBrowser.setBorder(border);
-
-        layout = new BorderLayout();
-        content.setLayout(layout);
-        content.add(split, BorderLayout.CENTER);
-        content.add(this.letsBrowser, BorderLayout.SOUTH);
-
-        return;
-    }
-
-    /**
-     * ボタン群を生成する。
-     * @return ボタン群
-     */
-    private JComponent buildButtonPanel(){
-        JPanel panel = new JPanel();
-
-        LayoutManager layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        panel.setLayout(layout);
-
-        constraints.weightx = 1.0;
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.NONE;
-        constraints.anchor = GridBagConstraints.WEST;
-        constraints.gridx = 1;
-        constraints.gridy = GridBagConstraints.RELATIVE;
-        constraints.gridwidth = 1;
-        constraints.gridheight = 1;
-
-
-        constraints.insets = new Insets(3, 3, 3, 3);
-        panel.add(this.singleGroup, constraints);
-
-        constraints.insets = new Insets(10, 3, 3, 3);
-        panel.add(this.multiGroup, constraints);
-
-        constraints.weighty = 1.0;
-        constraints.anchor = GridBagConstraints.SOUTH;
-        constraints.insets = new Insets(3, 3, 10, 3);
-        panel.add(this.closeButton, constraints);
-
-        return panel;
-    }
-
-    /**
-     * アクティブ発言操作ボタン群を生成する。
-     * @return ボタン群
-     */
-    private JComponent buildSingleGroup(){
-        JComponent panel = new JPanel();
-
-        LayoutManager layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        panel.setLayout(layout);
-
-        constraints.weightx = 1.0;
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        constraints.gridx = 1;
-        constraints.gridy = GridBagConstraints.RELATIVE;
-        constraints.gridwidth = 1;
-        constraints.gridheight = 1;
-        constraints.insets = new Insets(3, 3, 3, 3);
-
-        panel.add(this.cutButton,   constraints);
-        panel.add(this.copyButton,  constraints);
-        panel.add(this.clearButton, constraints);
-
-        panel.setBorder(this.numberBorder);
-
-        return panel;
-    }
-
-    /**
-     * 全発言操作ボタン群を生成する。
-     * @return ボタン群
-     */
-    private JComponent buildMultiGroup(){
-        JComponent panel = new JPanel();
-
-        LayoutManager layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        panel.setLayout(layout);
-
-        constraints.weightx = 1.0;
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        constraints.gridx = 1;
-        constraints.gridy = GridBagConstraints.RELATIVE;
-        constraints.gridwidth = 1;
-        constraints.gridheight = 1;
-        constraints.insets = new Insets(3, 3, 3, 3);
-
-        panel.add(this.cutAllButton,   constraints);
-        panel.add(this.copyAllButton,  constraints);
-        panel.add(this.clearAllButton, constraints);
-
-        Border border = BorderFactory.createTitledBorder("全発言を");
-        panel.setBorder(border);
-
-        return panel;
-    }
-
-    /**
-     * フリーノート部を生成する。
-     * @return フリーノート部
-     */
-    private JComponent buildFreeNotePanel(){
-        Insets margin = new Insets(3, 3, 3, 3);
-        this.freeMemo.setMargin(margin);
-        JPopupMenu popup = new TextPopup();
-        this.freeMemo.setComponentPopupMenu(popup);
-
-        JScrollPane scrollPane = new JScrollPane();
-        scrollPane.setHorizontalScrollBarPolicy(
-                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
-        );
-        JViewport viewPort = new JViewport();
-        viewPort.setView(this.freeMemo);
-        scrollPane.setViewport(viewPort);
-
-        JComponent panel = new JPanel();
-
-        LayoutManager layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        panel.setLayout(layout);
-
-        constraints.weightx = 1.0;
-        constraints.weighty = 1.0;
-        constraints.fill = GridBagConstraints.BOTH;
-        constraints.insets = new Insets(1, 1, 1, 1);
-        panel.add(scrollPane, constraints);
-
-        Border border = BorderFactory.createTitledBorder("フリーメモ");
-        panel.setBorder(border);
-
-        return panel;
-    }
-
-    /**
-     * アクティブ発言の通し番号表示を更新。
-     * @param num 通し番号
-     */
-    private void setBorderNumber(int num){
-        String title = "発言#"+num+" を";
-        this.numberBorder.setTitle(title);
-        this.singleGroup.revalidate();
-        this.singleGroup.repaint();
-        return;
-    }
-
-    /**
-     * テキスト編集用フォントを指定する。
-     * 描画属性は無視される。
-     * @param fontInfo フォント設定
-     */
-    public void setFontInfo(FontInfo fontInfo){
-        setTextFont(fontInfo.getFont());
-        return;
-    }
-
-    /**
-     * テキスト編集用フォントを指定する。
-     * @param textFont フォント
-     */
-    public void setTextFont(Font textFont){
-        this.freeMemo.setFont(textFont);
-        this.editArray.setTextFont(textFont);
-        return;
-    }
-
-    /**
-     * テキスト編集用フォントを取得する。
-     * @return フォント
-     */
-    public Font getTextFont(){
-        return this.editArray.getTextFont();
-    }
-
-    /**
-     * 発言クリア操作の確認ダイアログを表示する。
-     * @return OKなら0, Cancelなら2
-     */
-    private int warnClear(){
-        int result = JOptionPane.showConfirmDialog(
-                this,
-                "本当に発言をクリアしてもよいですか?",
-                "発言クリア確認",
-                JOptionPane.OK_CANCEL_OPTION,
-                JOptionPane.QUESTION_MESSAGE );
-        return result;
-    }
-
-    /**
-     * JSON形式の原稿情報を返す。
-     * @return JSON形式の原稿情報
-     */
-    public JsObject getJson(){
-        JsObject result = new JsObject();
-        JsString memo = new JsString(this.freeMemo.getText());
-        result.putValue("freeMemo", memo);
-
-        JsArray array = new JsArray();
-        JsString text = new JsString(this.editArray.getAllText());
-        array.add(text);
-        result.putValue("drafts", array);
-
-        return result;
-    }
-
-    /**
-     * JSON形式の原稿情報を反映させる。
-     * @param root JSON形式の原稿情報。nullが来たら何もしない
-     */
-    public void putJson(JsObject root){
-        if(root == null) return;
-
-        JsValue value;
-
-        value = root.getValue("freeMemo");
-        if(value.getJsTypes() == JsTypes.STRING){
-            JsString memo = (JsString) value;
-            this.freeMemo.setText(memo.toRawString());
-        }
-
-        value = root.getValue("drafts");
-        if(value.getJsTypes() != JsTypes.ARRAY) return;
-        JsArray array = (JsArray) value;
-
-        StringBuilder draftAll = new StringBuilder();
-        for(JsValue elem : array){
-            if(elem.getJsTypes() != JsTypes.STRING) continue;
-            JsString draft = (JsString) elem;
-            draftAll.append(draft.toRawString());
-        }
-        this.editArray.clearAllEditor();
-        this.editArray.setAllText(draftAll);
-
-        this.loadedDraft = root;
-
-        return;
-    }
-
-    /**
-     * 起動時の原稿と等価か判定する。
-     * @param conf 比較対象
-     * @return 等価ならtrue
-     */
-    public boolean hasConfChanged(JsComposition<?> conf){
-        if(this.loadedDraft != null){
-            if(this.loadedDraft.equals(conf)) return true;
-        }
-        return false;
-    }
-
-    /**
-     * アクティブな発言をカットしクリップボードへコピーする。
-     */
-    private void actionCutActive(){
-        actionCopyActive();
-        actionClearActive(false);
-        return;
-    }
-
-    /**
-     * アクティブな発言をクリップボードにコピーする。
-     */
-    private void actionCopyActive(){
-        TalkEditor activeEditor = this.editArray.getActiveEditor();
-        if(activeEditor == null) return;
-
-        CharSequence text = activeEditor.getText();
-        ClipboardAction.copyToClipboard(text);
-
-        return;
-    }
-
-    /**
-     * アクティブな発言をクリアする。
-     * @param confirm trueなら確認ダイアログを出す
-     */
-    private void actionClearActive(boolean confirm){
-        if(confirm && warnClear() != 0 ) return;
-
-        TalkEditor activeEditor = this.editArray.getActiveEditor();
-        if(activeEditor == null) return;
-
-        activeEditor.clearText();
-
-        return;
-    }
-
-    /**
-     * 全発言をカットしクリップボードへコピーする。
-     */
-    private void actionCutAll(){
-        actionCopyAll();
-        actionClearAll(false);
-        return;
-    }
-
-    /**
-     * 全発言をクリップボードにコピーする。
-     */
-    private void actionCopyAll(){
-        CharSequence text = this.editArray.getAllText();
-        ClipboardAction.copyToClipboard(text);
-        return;
-    }
-
-    /**
-     * 全発言をクリアする。
-     * @param confirm trueなら確認ダイアログを出す
-     */
-    private void actionClearAll(boolean confirm){
-        if(confirm && warnClear() != 0 ) return;
-        this.editArray.clearAllEditor();
-        return;
-    }
-
-    /**
-     * 上位ウィンドウをクローズする。
-     */
-    private void actionClose(){
-        setVisible(false);
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 各種ボタン操作の処理。
-     * @param event {@inheritDoc}
-     */
-    @Override
-    public void actionPerformed(ActionEvent event){
-        Object source = event.getSource();
-        if     (source == this.cutButton)      actionCutActive();
-        else if(source == this.copyButton)     actionCopyActive();
-        else if(source == this.clearButton)    actionClearActive(true);
-        else if(source == this.cutAllButton)   actionCutAll();
-        else if(source == this.copyAllButton)  actionCopyAll();
-        else if(source == this.clearAllButton) actionClearAll(true);
-        else if(source == this.closeButton)    actionClose();
-        return;
-    }
-
-    /**
-     * アクティブなエディタが変更された時の処理。
-     * @param event イベント情報
-     */
-    @Override
-    public void stateChanged(ChangeEvent event){
-        TalkEditor activeEditor = this.editArray.getActiveEditor();
-        int seqNo = activeEditor.getSequenceNumber();
-        setBorderNumber(seqNo);
-        return;
-    }
-
-    // TODO アンドゥ・リドゥ機能
-    // TODO バルーンの雰囲気を選択できるようにしたい。(白、赤、青、灰)
-    // TODO アンカーの表記揺れの指摘
-}
diff --git a/src/main/java/jp/sfjp/jindolf/editor/TextEditor.java b/src/main/java/jp/sfjp/jindolf/editor/TextEditor.java
deleted file mode 100644 (file)
index 6ada0d2..0000000
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- * 原稿作成支援用テキストコンポーネント
- *
- * License : The MIT License
- * Copyright(c) 2008 olyutorskii
- */
-
-package jp.sfjp.jindolf.editor;
-
-import java.awt.Rectangle;
-import java.awt.event.InputMethodEvent;
-import java.awt.event.InputMethodListener;
-import java.nio.CharBuffer;
-import java.text.AttributedCharacterIterator;
-import javax.swing.JTextArea;
-import javax.swing.text.AbstractDocument;
-import javax.swing.text.AttributeSet;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.Document;
-import javax.swing.text.DocumentFilter;
-import javax.swing.text.DocumentFilter.FilterBypass;
-import javax.swing.text.PlainDocument;
-
-/**
- * 原稿作成支援用テキストコンポーネント。
- */
-@SuppressWarnings("serial")
-public class TextEditor extends JTextArea
-        implements InputMethodListener {
-
-    private static final int MAX_DOCUMENT = 10 * 1000;
-
-    private final DocumentFilter documentFilter = new CustomFilter();
-
-    private boolean onIMEoperation = false;
-
-    /**
-     * コンストラクタ。
-     */
-    @SuppressWarnings("LeakingThisInConstructor")
-    public TextEditor(){
-        super();
-
-        setLineWrap(true);
-        setWrapStyleWord(false);
-
-        Document document = new PlainDocument();
-        setDocument(document);
-
-        addInputMethodListener(this);
-
-        return;
-    }
-
-    /**
-     * エディタが現在IME操作中か判定する。
-     * @return IME操作中ならtrue
-     */
-    public boolean onIMEoperation(){
-        return this.onIMEoperation;
-    }
-
-    /**
-     * 現在のカーソルが表示されるようスクロールエリアを操作する。
-     */
-    public void scrollCaretToVisible(){
-        int caretPosition = getCaretPosition();
-
-        Rectangle caretBounds;
-        try{
-            caretBounds = modelToView(caretPosition);
-        }catch(BadLocationException e){
-            assert false;
-            return;
-        }
-
-        scrollRectToVisible(caretBounds);
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * Document変更をフックしてフィルタを仕込む。
-     * @param document {@inheritDoc}
-     */
-    @Override
-    public final void setDocument(Document document){
-        Document oldDocument = getDocument();
-        if(oldDocument instanceof AbstractDocument){
-            AbstractDocument abstractDocument =
-                    (AbstractDocument) oldDocument;
-            abstractDocument.setDocumentFilter(null);
-        }
-
-        super.setDocument(document);
-
-        if(document instanceof AbstractDocument){
-            AbstractDocument abstractDocument = (AbstractDocument) document;
-            abstractDocument.setDocumentFilter(this.documentFilter);
-        }
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * このエディタ中の指定領域が表示されるようスクロールエリアを操作する。
-     * キーボードフォーカスを保持しないときは無視。
-     * @param rect {@inheritDoc}
-     */
-    @Override
-    public void scrollRectToVisible(Rectangle rect){
-        if( ! hasFocus() ) return;
-        super.scrollRectToVisible(rect);
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param event {@inheritDoc}
-     */
-    @Override
-    public void caretPositionChanged(InputMethodEvent event){
-        // NOTHING
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * このテキストエディタで現在IMEの変換中か否か判定する処理を含む。
-     * @param event {@inheritDoc}
-     */
-    @Override
-    public void inputMethodTextChanged(InputMethodEvent event){
-        int committed = event.getCommittedCharacterCount();
-        AttributedCharacterIterator aci = event.getText();
-        if(aci == null){
-            this.onIMEoperation = false;
-            return;
-        }
-        int begin = aci.getBeginIndex();
-        int end   = aci.getEndIndex();
-        int span = end - begin;
-
-        if(committed >= span) this.onIMEoperation = false;
-        else                  this.onIMEoperation = true;
-
-        return;
-    }
-
-    /**
-     * 入力文字列に制限を加えるDocumentFilter。
-     * \n,\f 以外の制御文字はタブも含め入力禁止。
-     * U+FFFF はjava.textパッケージで特別扱いなのでこれも入力禁止。
-     * ※ ただしIME操作中は制限なし。
-     */
-    private class CustomFilter extends DocumentFilter{
-
-        /**
-         * コンストラクタ。
-         */
-        public CustomFilter(){
-            super();
-            return;
-        }
-
-        /**
-         * 入力禁止文字の判定。
-         * @param ch 検査対象文字
-         * @return 入力禁止ならfalse。ただしIME操作中は必ずtrue。
-         */
-        private boolean isValid(char ch){
-            if(onIMEoperation()) return true;
-
-            if(ch == '\n') return true;
-//          if(ch == '\f') return true;
-
-            if(ch == '\uffff')             return false;
-            if(Character.isISOControl(ch)) return false;
-
-//          if( ! CodeX0208.isValid(ch) ) return false;
-            if(Character.isHighSurrogate(ch)) return false;
-            if(Character.isLowSurrogate(ch) ) return false;
-
-            return true;
-        }
-
-        /**
-         * 与えられた文字列から入力禁止文字を除いた文字列に変換する。
-         * @param input 検査対象文字列
-         * @return 除去済み文字列
-         */
-        private String filter(CharSequence input){
-            if(onIMEoperation()) return input.toString();
-
-            int length = input.length();
-            CharBuffer buf = CharBuffer.allocate(length);
-
-            for(int pos = 0; pos < length; pos++){
-                char ch = input.charAt(pos);
-                if(ch == '\u2211') ch = '\u03a3'; // Σ変換
-                if(ch == '\u00ac') ch = '\uffe2'; // ¬変換
-//              if(ch ==  0x005c ) ch = '\u00a5'; // バックスラッシュから円へ
-                if(isValid(ch)) buf.append(ch);
-            }
-
-            buf.flip();
-            return buf.toString();
-        }
-
-        /**
-         * {@inheritDoc}
-         * @param fb {@inheritDoc}
-         * @param offset {@inheritDoc}
-         * @param text {@inheritDoc}
-         * @param attrs {@inheritDoc}
-         * @throws javax.swing.text.BadLocationException {@inheritDoc}
-         */
-        @Override
-        public void insertString(FilterBypass fb,
-                                 int offset,
-                                 String text,
-                                 AttributeSet attrs)
-                                 throws BadLocationException{
-            String filtered = filter(text);
-
-            if( ! onIMEoperation() ){
-                Document document = fb.getDocument();
-                int docLength = document.getLength();
-                int rest = MAX_DOCUMENT - docLength;
-                if(rest < 0){
-                    return;
-                }else if(rest < filtered.length()){
-                    filtered = filtered.substring(0, rest);
-                }
-            }
-
-            fb.insertString(offset, filtered, attrs);
-
-            return;
-        }
-
-        /**
-         *  {@inheritDoc}
-         * @param fb {@inheritDoc}
-         * @param offset {@inheritDoc}
-         * @param length {@inheritDoc}
-         * @param text {@inheritDoc}
-         * @param attrs {@inheritDoc}
-         * @throws javax.swing.text.BadLocationException {@inheritDoc}
-         */
-        @Override
-        public void replace(FilterBypass fb,
-                            int offset,
-                            int length,
-                            String text,
-                            AttributeSet attrs)
-                            throws BadLocationException{
-            String filtered = filter(text);
-
-            if( ! onIMEoperation() ){
-                Document document = fb.getDocument();
-                int docLength = document.getLength();
-                docLength -= length;
-                int rest = MAX_DOCUMENT - docLength;
-                if(rest < 0){
-                    return;
-                }else if(rest < filtered.length()){
-                    filtered = filtered.substring(0, rest);
-                }
-            }
-
-            fb.replace(offset, length, filtered, attrs);
-
-            return;
-        }
-    }
-
-    // TODO 禁則チェック。20文字を超える長大なブレーク禁止文字列の出現の監視。
-    // TODO 連続したホワイトスペースに対する警告。
-    // TODO 先頭もしくは末尾のホワイトスペース出現に対する警告。
-    // TODO 改行記号の表示
-    // TODO 改発言記号の導入
-}
diff --git a/src/main/java/jp/sfjp/jindolf/editor/package-info.java b/src/main/java/jp/sfjp/jindolf/editor/package-info.java
deleted file mode 100644 (file)
index 0ba1728..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * パッケージ情報
- *
- * License : The MIT License
- * Copyright(c) 2011 olyutorskii
- */
-
-/**
- * 発言エディタ関連の一連のクラス。
- */
-
-package jp.sfjp.jindolf.editor;
-
-/* EOF */
index 309cf46..5740437 100644 (file)
@@ -21,6 +21,7 @@ import jp.sfjp.jindolf.data.DialogPref;
 import jp.sfjp.jindolf.data.Period;
 import jp.sfjp.jindolf.data.Talk;
 import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.view.AvatarPics;
 
 /**
  * アンカー描画。
@@ -90,13 +91,14 @@ public class AnchorDraw extends AbstractTextRow{
     private BufferedImage getFaceImage(){
         Period period = this.talk.getPeriod();
         Village village = period.getVillage();
+        AvatarPics avatarPics = village.getAvatarPics();
 
         BufferedImage image;
         if(this.talk.isGrave()){
-            image = village.getGraveImage();
+            image = avatarPics.getGraveImage();
         }else{
             Avatar avatar = this.talk.getAvatar();
-            image = village.getAvatarFaceImage(avatar);
+            image = avatarPics.getAvatarFaceImage(avatar);
         }
 
         return image;
index b2bc712..239e3c0 100644 (file)
@@ -23,7 +23,7 @@ import java.awt.event.ComponentListener;
 import java.awt.event.MouseEvent;
 import java.awt.font.FontRenderContext;
 import java.io.IOException;
-import java.util.EventListener;
+import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
@@ -31,6 +31,7 @@ import java.util.regex.Pattern;
 import javax.swing.AbstractAction;
 import javax.swing.Action;
 import javax.swing.ActionMap;
+import javax.swing.Icon;
 import javax.swing.InputMap;
 import javax.swing.JComponent;
 import javax.swing.JMenuItem;
@@ -39,7 +40,6 @@ import javax.swing.JTextField;
 import javax.swing.KeyStroke;
 import javax.swing.Scrollable;
 import javax.swing.SwingConstants;
-import javax.swing.event.EventListenerList;
 import javax.swing.event.MouseInputListener;
 import javax.swing.text.DefaultEditorKit;
 import jp.sfjp.jindolf.data.Anchor;
@@ -56,14 +56,22 @@ import jp.sfjp.jindolf.view.ActionManager;
 import jp.sfjp.jindolf.view.TopicFilter;
 
 /**
- * 発言表示画面。
+ * 会話表示画面。
  *
- * <p>表示に影響する要因は、Periodの中身、LayoutManagerによるサイズ変更、
- * フォント属性の指定、フィルタリング操作、ドラッギングによる文字列選択操作、
- * 文字列検索および検索ナビゲーション。
+ * <p>担当責務は
+ * <ul>
+ * <li>会話表示</li>
+ * <li>フィルタによる表示絞り込み</li>
+ * <li>検索結果ハイライト</li>
+ * <li>テキストのドラッグ選択</li>
+ * <li>アンカー選択処理</li>
+ * <li>テキストのCopyAndPaste</li>
+ * <li>ポップアップメニュー</li>
+ * </ul>
+ * など
  */
 @SuppressWarnings("serial")
-public class Discussion extends JComponent
+public final class Discussion extends JComponent
         implements Scrollable, MouseInputListener, ComponentListener{
 
     private static final Color COLOR_NORMALBG = Color.BLACK;
@@ -72,6 +80,7 @@ public class Discussion extends JComponent
     private static final int MARGINTOP    =  50;
     private static final int MARGINBOTTOM = 100;
 
+
     private Period period;
     private final List<TextRow> rowList       = new LinkedList<>();
     private final List<TalkDraw> talkDrawList = new LinkedList<>();
@@ -91,15 +100,14 @@ public class Discussion extends JComponent
     private int lastWidth = -1;
 
     private final DiscussionPopup popup = new DiscussionPopup();
+    private Talk activeTalk;
+    private Anchor activeAnchor;
 
-    private final EventListenerList thisListenerList =
-            new EventListenerList();
-
-    private final Action copySelectedAction =
-            new ProxyAction(ActionManager.CMD_COPY);
 
     /**
-     * 発言表示画面を作成する。
+     * コンストラクタ。
+     *
+     * <p>会話表示画面を作成する。
      */
     @SuppressWarnings("LeakingThisInConstructor")
     public Discussion(){
@@ -122,9 +130,8 @@ public class Discussion extends JComponent
 
         setComponentPopupMenu(this.popup);
 
-        updateInputMap();
-        ActionMap actionMap = getActionMap();
-        actionMap.put(DefaultEditorKit.copyAction, this.copySelectedAction);
+        modifyInputMap();
+        modifyActionMap();
 
         setColorDesign();
 
@@ -133,7 +140,8 @@ public class Discussion extends JComponent
 
     /**
      * 描画設定の更新。
-     * FontRenderContextが更新された後は必ず呼び出す必要がある。
+     *
+     * <p>FontRenderContextが更新された後は必ず呼び出す必要がある。
      */
     private void updateRenderingHints(){
         Object textAliaseValue;
@@ -177,6 +185,7 @@ public class Discussion extends JComponent
 
     /**
      * フォント描画設定を変更する。
+     *
      * @param newFontInfo フォント設定
      */
     public void setFontInfo(FontInfo newFontInfo){
@@ -198,8 +207,9 @@ public class Discussion extends JComponent
     }
 
     /**
-     * 発言表示設定を変更する。
-     * @param newPref 発言表示設定
+     * 会話表示設定を変更する。
+     *
+     * @param newPref 会話表示設定
      */
     public void setDialogPref(DialogPref newPref){
         this.dialogPref = newPref;
@@ -225,6 +235,7 @@ public class Discussion extends JComponent
 
     /**
      * 現在のPeriodを返す。
+     *
      * @return 現在のPeriod
      */
     public Period getPeriod(){
@@ -233,7 +244,9 @@ public class Discussion extends JComponent
 
     /**
      * Periodを更新する。
-     * 新しいPeriodの表示内容はまだ反映されない。
+     *
+     * <p>新しいPeriodの表示内容はまだ反映されない。
+     *
      * @param period 新しいPeriod
      */
     public final void setPeriod(Period period){
@@ -287,8 +300,9 @@ public class Discussion extends JComponent
     }
 
     /**
-     * 発言フィルタを設定する。
-     * @param filter 発言フィルタ
+     * 会話フィルタを設定する。
+     *
+     * @param filter 会話フィルタ
      */
     public void setTopicFilter(TopicFilter filter){
         this.topicFilter = filter;
@@ -297,7 +311,7 @@ public class Discussion extends JComponent
     }
 
     /**
-     * 発言フィルタを適用する。
+     * 会話フィルタを適用する。
      */
     public void filtering(){
         if(    this.topicFilter != null
@@ -321,6 +335,7 @@ public class Discussion extends JComponent
 
     /**
      * 検索パターンを取得する。
+     *
      * @return 検索パターン
      */
     public RegexPattern getRegexPattern(){
@@ -329,6 +344,7 @@ public class Discussion extends JComponent
 
     /**
      * 与えられた正規表現にマッチする文字列をハイライト描画する。
+     *
      * @param newPattern 検索パターン
      * @return ヒット件数
      */
@@ -486,6 +502,7 @@ public class Discussion extends JComponent
     /**
      * 指定した領域に若干の上下マージンを付けて
      * スクロールウィンドウに表示させる。
+     *
      * @param rectangle 指定領域
      */
     private void scrollRectWithMargin(Rectangle rectangle){
@@ -510,6 +527,7 @@ public class Discussion extends JComponent
 
     /**
      * 指定した矩形がフィルタリング対象か判定する。
+     *
      * @param row 矩形
      * @return フィルタリング対象ならtrue
      */
@@ -541,7 +559,9 @@ public class Discussion extends JComponent
 
     /**
      * 幅を設定する。
-     * 全子TextRowがリサイズされる。
+     *
+     * <p>全子TextRowがリサイズされる。
+     *
      * @param width コンポーネント幅
      */
     private void setWidth(int width){
@@ -559,8 +579,10 @@ public class Discussion extends JComponent
 
     /**
      * 子TextRowの縦位置レイアウトを行う。
-     * フィルタリングが反映される。
-     * TextRowは必要に応じて移動させられるがリサイズされることはない。
+     *
+     * <p>フィルタリングが反映される。
+     *
+     * <p>TextRowは必要に応じて移動させられるがリサイズされることはない。
      */
     private void layoutVertical(){
         Rectangle unionRect = null;
@@ -613,6 +635,9 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
+     * <p>描画処理
+     *
      * @param g {@inheritDoc}
      */
     @Override
@@ -637,6 +662,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @return {@inheritDoc}
      */
     @Override
@@ -646,6 +672,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @return {@inheritDoc}
      */
     @Override
@@ -655,6 +682,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @return {@inheritDoc}
      */
     @Override
@@ -664,6 +692,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param visibleRect {@inheritDoc}
      * @param orientation {@inheritDoc}
      * @param direction {@inheritDoc}
@@ -681,6 +710,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param visibleRect {@inheritDoc}
      * @param orientation {@inheritDoc}
      * @param direction {@inheritDoc}
@@ -694,9 +724,11 @@ public class Discussion extends JComponent
     }
 
     /**
-     * 任意の発言の表示が占める画面領域を返す。
-     * 発言がフィルタリング対象の時はnullを返す。
-     * @param talk 発言
+     * 任意の会話の表示が占める画面領域を返す。
+     *
+     * <p>会話がフィルタリング対象の時はnullを返す。
+     *
+     * @param talk 会話
      * @return 領域
      */
     public Rectangle getTalkBounds(Talk talk){
@@ -715,6 +747,7 @@ public class Discussion extends JComponent
 
     /**
      * ドラッグ処理を行う。
+     *
      * @param from ドラッグ開始位置
      * @param to 現在のドラッグ位置
      */
@@ -743,9 +776,10 @@ public class Discussion extends JComponent
     }
 
     /**
-     * 与えられた点座標を包含する発言を返す。
+     * 与えられた点座標を包含する会話を返す。
+     *
      * @param pt 点座標(JComponent基準)
-     * @return 点座標を含む発言。含む発言がなければnullを返す。
+     * @return 点座標を含む会話。含む会話がなければnullを返す。
      */
     // TODO 二分探索とかしたい。
     private TalkDraw getHittedTalkDraw(Point pt){
@@ -759,6 +793,7 @@ public class Discussion extends JComponent
 
     /**
      * アンカークリック動作の処理。
+     *
      * @param pt クリックポイント
      */
     private void hitAnchor(Point pt){
@@ -779,6 +814,7 @@ public class Discussion extends JComponent
 
     /**
      * 検索マッチ文字列クリック動作の処理。
+     *
      * @param pt クリックポイント
      */
     private void hitRegex(Point pt){
@@ -796,8 +832,11 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
-     * アンカーヒット処理を行う。
-     * MouseInputListenerを参照せよ。
+     *
+     * <p>アンカーヒット処理を行う。
+     *
+     * <p>MouseInputListenerを参照せよ。
+     *
      * @param event {@inheritDoc}
      */
     // TODO 距離判定がシビアすぎ
@@ -814,6 +853,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -824,6 +864,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -833,7 +874,9 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
-     * ドラッグ開始処理を行う。
+     *
+     * <p>ドラッグ開始処理を行う。
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -850,7 +893,9 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
-     * ドラッグ終了処理を行う。
+     *
+     * <p>ドラッグ終了処理を行う。
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -863,7 +908,9 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
-     * ドラッグ処理を行う。
+     *
+     * <p>ドラッグ処理を行う。
+     *
      * @param event {@inheritDoc}
      */
     // TODO ドラッグ範囲がビューポートを超えたら自動的にスクロールしてほしい。
@@ -877,6 +924,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -886,6 +934,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -895,6 +944,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -904,6 +954,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -913,6 +964,7 @@ public class Discussion extends JComponent
 
     /**
      * {@inheritDoc}
+     *
      * @param event {@inheritDoc}
      */
     @Override
@@ -931,6 +983,7 @@ public class Discussion extends JComponent
 
     /**
      * 選択文字列を返す。
+     *
      * @return 選択文字列
      */
     public CharSequence getSelected(){
@@ -953,7 +1006,8 @@ public class Discussion extends JComponent
 
     /**
      * 選択文字列をクリップボードにコピーする。
-     * @return 選択文字列
+     *
+     * @return 選択文字列。なければnull
      */
     public CharSequence copySelected(){
         CharSequence selected = getSelected();
@@ -963,13 +1017,13 @@ public class Discussion extends JComponent
     }
 
     /**
-     * 矩形の示す一発言をクリップボードにコピーする。
+     * 一会話全体の文字列内容をクリップボードにコピーする。
+     *
      * @return コピーした文字列
      */
     public CharSequence copyTalk(){
-        TalkDraw talkDraw = this.popup.lastPopupedTalkDraw;
-        if(talkDraw == null) return null;
-        Talk talk = talkDraw.getTalk();
+        Talk talk = getActiveTalk();
+        if(talk == null) return null;
 
         StringBuilder selected = new StringBuilder();
 
@@ -994,189 +1048,224 @@ public class Discussion extends JComponent
     }
 
     /**
-     * ポップアップメニュートリガ座標に発言があればそれを返す。
-     * @return 発言
+     * ポップアップメニュートリガなどの要因による
+     * 特定の会話への指示があればそれを返す。
+     *
+     * @return 会話
+     */
+    public Talk getActiveTalk(){
+        return this.activeTalk;
+    }
+
+    /**
+     * ポップアップメニュートリガなどの要因による
+     * 特定の会話への指示があればそれを設定する。
+     *
+     * @param talk 会話
      */
-    public Talk getPopupedTalk(){
-        TalkDraw talkDraw = this.popup.lastPopupedTalkDraw;
-        if(talkDraw == null) return null;
-        Talk talk = talkDraw.getTalk();
-        return talk;
+    public void setActiveTalk(Talk talk){
+        this.activeTalk = talk;
+        return;
     }
 
     /**
-     * ポップアップメニュートリガ座標にアンカーがあればそれを返す。
+     * ポップアップメニュートリガなどの要因による
+     * 特定のアンカーへの指示があればそれを返す。
+     *
      * @return アンカー
      */
-    public Anchor getPopupedAnchor(){
-        return this.popup.lastPopupedAnchor;
+    public Anchor getActiveAnchor(){
+        return this.activeAnchor;
+    }
+
+    /**
+     * ポップアップメニュートリガなどの要因による
+     * 特定のアンカーへの指示があればそれを設定する。
+     *
+     * @param anchor アンカー
+     */
+    public void setActiveAnchor(Anchor anchor){
+        this.activeAnchor = anchor;
+        return;
     }
 
     /**
      * {@inheritDoc}
+     *
+     * <p>キーバインディング設定が変わる可能性あり。
      */
     @Override
     public void updateUI(){
         super.updateUI();
-        this.popup.updateUI();
-
-        updateInputMap();
-
+        modifyInputMap();
         return;
     }
 
     /**
      * COPY処理を行うキーの設定をJTextFieldから流用する。
-     * おそらくはCtrl-C。MacならCommand-Cかも。
+     *
+     * <p>おそらくはCtrl-C。MacならCommand-Cかも。
+     *
+     * <p>キー設定はLookAndFeelにより異なる可能性がある。
      */
-    private void updateInputMap(){
+    private void modifyInputMap(){
         InputMap thisInputMap = getInputMap();
 
+        JComponent sampleComp = new JTextField();
         InputMap sampleInputMap;
-        sampleInputMap = new JTextField().getInputMap();
+        sampleInputMap = sampleComp.getInputMap();
+
         KeyStroke[] strokes = sampleInputMap.allKeys();
-        for(KeyStroke stroke : strokes){
-            Object bind = sampleInputMap.get(stroke);
-            if(bind.equals(DefaultEditorKit.copyAction)){
-                thisInputMap.put(stroke, DefaultEditorKit.copyAction);
-            }
-        }
+        Arrays.stream(strokes).filter((stroke) -> {
+            Object binding = sampleInputMap.get(stroke);
+            return DefaultEditorKit.copyAction.equals(binding);
+        }).forEach((stroke) ->
+            thisInputMap.put(stroke, DefaultEditorKit.copyAction)
+        );
+
+        return;
+    }
 
+    /**
+     * COPY処理を行うActionを割り当てる。
+     */
+    private void modifyActionMap(){
+        ActionMap actionMap = getActionMap();
+        Action copyAction = new CopySelAction();
+        actionMap.put(DefaultEditorKit.copyAction, copyAction);
         return;
     }
 
     /**
      * ActionListenerを追加する。
+     *
+     * <p>通知対象となるActionは、
+     * COPYキー操作によって選択文字列からクリップボードへの
+     * コピーが指示される場合とポップアップメニューの押下。
+     *
      * @param listener リスナー
      */
     public void addActionListener(ActionListener listener){
-        this.thisListenerList.add(ActionListener.class, listener);
-
-        this.popup.menuCopy       .addActionListener(listener);
-        this.popup.menuSelTalk    .addActionListener(listener);
-        this.popup.menuJumpAnchor .addActionListener(listener);
-        this.popup.menuWebTalk    .addActionListener(listener);
-        this.popup.menuSummary    .addActionListener(listener);
-
+        this.listenerList.add(ActionListener.class, listener);
+        this.popup.addActionListener(listener);
         return;
     }
 
     /**
      * ActionListenerを削除する。
+     *
      * @param listener リスナー
      */
     public void removeActionListener(ActionListener listener){
-        this.thisListenerList.remove(ActionListener.class, listener);
-
-        this.popup.menuCopy       .removeActionListener(listener);
-        this.popup.menuSelTalk    .removeActionListener(listener);
-        this.popup.menuJumpAnchor .removeActionListener(listener);
-        this.popup.menuWebTalk    .removeActionListener(listener);
-        this.popup.menuSummary    .removeActionListener(listener);
-
+        this.listenerList.remove(ActionListener.class, listener);
+        this.popup.removeActionListener(listener);
         return;
     }
 
     /**
      * ActionListenerを列挙する。
+     *
      * @return すべてのActionListener
      */
     public ActionListener[] getActionListeners(){
-        return this.thisListenerList.getListeners(ActionListener.class);
+        return this.listenerList.getListeners(ActionListener.class);
+    }
+
+    /**
+     * 登録済みリスナに対しActionEventを発火する。
+     *
+     * <p>対象となるActionは、
+     * COPYキー操作によって選択文字列からクリップボードへの
+     * コピーが指示される場合のみ。
+     *
+     * @param event イベント
+     */
+    protected void fireActionPerformed(ActionEvent event) {
+        Object source  = event.getSource();
+        int id         = event.getID();
+        String actcmd  = event.getActionCommand();
+        long when      = event.getWhen();
+        int modifiers  = event.getModifiers();
+
+        ActionEvent newEvent =
+                new ActionEvent(source, id, actcmd, when, modifiers);
+
+        for(ActionListener listener : getActionListeners()){
+            listener.actionPerformed(newEvent);
+        }
+
+        return;
     }
 
     /**
      * AnchorHitListenerを追加する。
+     *
      * @param listener リスナー
      */
     public void addAnchorHitListener(AnchorHitListener listener){
-        this.thisListenerList.add(AnchorHitListener.class, listener);
+        this.listenerList.add(AnchorHitListener.class, listener);
         return;
     }
 
     /**
      * AnchorHitListenerを削除する。
+     *
      * @param listener リスナー
      */
     public void removeAnchorHitListener(AnchorHitListener listener){
-        this.thisListenerList.remove(AnchorHitListener.class, listener);
+        this.listenerList.remove(AnchorHitListener.class, listener);
         return;
     }
 
     /**
      * AnchorHitListenerを列挙する。
+     *
      * @return すべてのAnchorHitListener
      */
     public AnchorHitListener[] getAnchorHitListeners(){
-        return this.thisListenerList.getListeners(AnchorHitListener.class);
+        return this.listenerList.getListeners(AnchorHitListener.class);
     }
 
-    /**
-     * {@inheritDoc}
-     * @param <T> {@inheritDoc}
-     * @param listenerType {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public <T extends EventListener> T[] getListeners(Class<T> listenerType){
-        T[] result;
-        result = this.thisListenerList.getListeners(listenerType);
-
-        if(result.length <= 0){
-            result = super.getListeners(listenerType);
-        }
-
-        return result;
-    }
 
     /**
-     * キーボード入力用ダミーAction。
+     * COPYキーボード操作受信用ダミーAction。
+     *
+     * <p>COPYキー押下イベントはCOPYコマンド実行イベントに変換され、
+     * {@link Discussion}のリスナへ通知される。
      */
-    private class ProxyAction extends AbstractAction{
-
-        private final String command;
+    private class CopySelAction extends AbstractAction{
 
         /**
          * コンストラクタ。
-         * @param command コマンド
-         * @throws NullPointerException 引数がnull
          */
-        public ProxyAction(String command) throws NullPointerException{
+        CopySelAction() throws NullPointerException{
             super();
-            if(command == null) throw new NullPointerException();
-            this.command = command;
             return;
         }
 
         /**
          * {@inheritDoc}
-         * @param event {@inheritDoc}
+         *
+         * @param event COPYキーボード押下イベント
          */
         @Override
         public void actionPerformed(ActionEvent event){
-            Object source  = event.getSource();
-            int id         = event.getID();
-            String actcmd  = this.command;
-            long when      = event.getWhen();
-            int modifiers  = event.getModifiers();
-
-            for(ActionListener listener : getActionListeners()){
-                ActionEvent newEvent = new ActionEvent(source,
-                                                       id,
-                                                       actcmd,
-                                                       when,
-                                                       modifiers );
-                listener.actionPerformed(newEvent);
-            }
-
+            ActionEvent newEvent =
+                    new ActionEvent(
+                            this,
+                            ActionEvent.ACTION_PERFORMED,
+                            ActionManager.CMD_COPY
+                    );
+            fireActionPerformed(newEvent);
             return;
         }
+
     }
 
     /**
      * ポップアップメニュー。
      */
-    private class DiscussionPopup extends JPopupMenu{
+    private static final class DiscussionPopup extends JPopupMenu{
 
         private final JMenuItem menuCopy =
                 new JMenuItem("選択範囲をコピー");
@@ -1189,15 +1278,26 @@ public class Discussion extends JComponent
         private final JMenuItem menuSummary =
                 new JMenuItem("発言を集計...");
 
-        private TalkDraw lastPopupedTalkDraw;
-        private Anchor lastPopupedAnchor;
 
         /**
          * コンストラクタ。
          */
-        public DiscussionPopup(){
+        DiscussionPopup(){
             super();
 
+            design();
+            setCommand();
+
+            Icon icon = GUIUtils.getWWWIcon();
+            this.menuWebTalk.setIcon(icon);
+
+            return;
+        }
+
+        /**
+         * メニューのデザイン。
+         */
+        private void design(){
             add(this.menuCopy);
             add(this.menuSelTalk);
             addSeparator();
@@ -1205,67 +1305,119 @@ public class Discussion extends JComponent
             add(this.menuWebTalk);
             addSeparator();
             add(this.menuSummary);
+            return;
+        }
 
+        /**
+         * アクションコマンドの設定。
+         */
+        private void setCommand(){
             this.menuCopy
-                .setActionCommand(ActionManager.CMD_COPY);
+                    .setActionCommand(ActionManager.CMD_COPY);
             this.menuSelTalk
-                .setActionCommand(ActionManager.CMD_COPYTALK);
+                    .setActionCommand(ActionManager.CMD_COPYTALK);
             this.menuJumpAnchor
-                .setActionCommand(ActionManager.CMD_JUMPANCHOR);
+                    .setActionCommand(ActionManager.CMD_JUMPANCHOR);
             this.menuWebTalk
-                .setActionCommand(ActionManager.CMD_WEBTALK);
+                    .setActionCommand(ActionManager.CMD_WEBTALK);
             this.menuSummary
-                .setActionCommand(ActionManager.CMD_DAYSUMMARY);
-
-            this.menuWebTalk.setIcon(GUIUtils.getWWWIcon());
-
+                    .setActionCommand(ActionManager.CMD_DAYSUMMARY);
             return;
         }
 
         /**
          * {@inheritDoc}
-         * @param comp {@inheritDoc}
+         *
+         * <p>状況に応じてボタン群をマスクする。
+         *
+         * <p>ポップアップクリックの対象となった会話やアンカーを記録する。
+         *
+         * @param invoker {@inheritDoc}
          * @param x {@inheritDoc}
          * @param y {@inheritDoc}
          */
         @Override
-        public void show(Component comp, int x, int y){
+        public void show(Component invoker, int x, int y){
+            if( ! (invoker instanceof Discussion) ){
+                super.show(invoker, x, y);
+                return;
+            }
+            Discussion dis = (Discussion) invoker;
+
             Point point = new Point(x, y);
 
-            this.lastPopupedTalkDraw = getHittedTalkDraw(point);
-            if(this.lastPopupedTalkDraw != null){
-                this.menuSelTalk.setEnabled(true);
-                this.menuWebTalk.setEnabled(true);
-            }else{
-                this.menuSelTalk.setEnabled(false);
-                this.menuWebTalk.setEnabled(false);
-            }
+            boolean talkPointed = false;
+            Talk activeTalk = null;
+            Anchor activeAnchor = null;
 
-            if(this.lastPopupedTalkDraw != null){
-                this.lastPopupedAnchor =
-                        this.lastPopupedTalkDraw.getAnchor(point);
-            }else{
-                this.lastPopupedAnchor = null;
+            TalkDraw popupTalkDraw = dis.getHittedTalkDraw(point);
+            if(popupTalkDraw != null){
+                talkPointed = true;
+                activeTalk   = popupTalkDraw.getTalk();
+                activeAnchor = popupTalkDraw.getAnchor(point);
             }
 
-            if(this.lastPopupedAnchor != null){
-                this.menuJumpAnchor.setEnabled(true);
-            }else{
-                this.menuJumpAnchor.setEnabled(false);
-            }
+            dis.setActiveTalk(activeTalk);
+            dis.setActiveAnchor(activeAnchor);
 
-            if(getSelected() != null){
-                this.menuCopy.setEnabled(true);
-            }else{
-                this.menuCopy.setEnabled(false);
-            }
+            boolean anchorPointed = activeAnchor != null;
+            boolean hasSelectedText = dis.getSelected() != null;
+
+            this.menuSelTalk    .setEnabled(talkPointed);
+            this.menuWebTalk    .setEnabled(talkPointed);
+            this.menuJumpAnchor .setEnabled(anchorPointed);
+            this.menuCopy       .setEnabled(hasSelectedText);
+
+            super.show(invoker, x, y);
+
+            return;
+        }
+
+        /**
+         * ActionListenerを追加する。
+         *
+         * <p>受信対象はポップアップメニュー押下。
+         *
+         * @param listener リスナー
+         */
+        public void addActionListener(ActionListener listener){
+            this.listenerList.add(ActionListener.class, listener);
 
-            super.show(comp, x, y);
+            this.menuCopy       .addActionListener(listener);
+            this.menuSelTalk    .addActionListener(listener);
+            this.menuJumpAnchor .addActionListener(listener);
+            this.menuWebTalk    .addActionListener(listener);
+            this.menuSummary    .addActionListener(listener);
 
             return;
         }
+
+        /**
+         * ActionListenerを削除する。
+         *
+         * @param listener リスナー
+         */
+        public void removeActionListener(ActionListener listener){
+            this.listenerList.remove(ActionListener.class, listener);
+
+            this.menuCopy       .removeActionListener(listener);
+            this.menuSelTalk    .removeActionListener(listener);
+            this.menuJumpAnchor .removeActionListener(listener);
+            this.menuWebTalk    .removeActionListener(listener);
+            this.menuSummary    .removeActionListener(listener);
+
+            return;
+        }
+
+        /**
+         * ActionListenerを列挙する。
+         *
+         * @return すべてのActionListener
+         */
+        public ActionListener[] getActionListeners(){
+            return this.listenerList.getListeners(ActionListener.class);
+        }
+
     }
 
-    // TODO シンプルモードの追加
-    // Period変更を追跡するリスナ化
 }
index d6cbf2d..34d19cf 100644 (file)
@@ -27,6 +27,7 @@ import jp.sfjp.jindolf.data.Period;
 import jp.sfjp.jindolf.data.Talk;
 import jp.sfjp.jindolf.data.Village;
 import jp.sfjp.jindolf.util.GUIUtils;
+import jp.sfjp.jindolf.view.AvatarPics;
 import jp.sourceforge.jindolf.corelib.TalkType;
 
 /**
@@ -269,6 +270,7 @@ public class TalkDraw extends AbstractTextRow{
      */
     private BufferedImage getFaceImage(){
         Village village = this.talk.getPeriod().getVillage();
+        AvatarPics avatarPics = village.getAvatarPics();
         Avatar avatar = this.talk.getAvatar();
 
         boolean useBodyImage = this.dialogPref.useBodyImage();
@@ -278,22 +280,22 @@ public class TalkDraw extends AbstractTextRow{
         if(this.talk.isGrave()){
             if(useMonoImage){
                 if(useBodyImage){
-                    image = village.getAvatarBodyMonoImage(avatar);
+                    image = avatarPics.getAvatarBodyMonoImage(avatar);
                 }else{
-                    image = village.getAvatarFaceMonoImage(avatar);
+                    image = avatarPics.getAvatarFaceMonoImage(avatar);
                 }
             }else{
                 if(useBodyImage){
-                    image = village.getGraveBodyImage();
+                    image = avatarPics.getGraveBodyImage();
                 }else{
-                    image = village.getGraveImage();
+                    image = avatarPics.getGraveImage();
                 }
             }
         }else{
             if(useBodyImage){
-                image = village.getAvatarBodyImage(avatar);
+                image = avatarPics.getAvatarBodyImage(avatar);
             }else{
-                image = village.getAvatarFaceImage(avatar);
+                image = avatarPics.getAvatarFaceImage(avatar);
             }
         }
 
@@ -693,11 +695,12 @@ public class TalkDraw extends AbstractTextRow{
         int total = 0;
 
         total += this.dialog.setRegex(searchRegex);
-/*
+        /*
         for(AnchorDraw anchorDraw : this.anchorTalks){
             total += anchorDraw.setRegex(searchRegex);
         }
-*/ // TODO よくわからんので保留
+        */
+        // TODO よくわからんので保留
         return total;
     }
 
diff --git a/src/main/java/jp/sfjp/jindolf/net/AuthManager.java b/src/main/java/jp/sfjp/jindolf/net/AuthManager.java
deleted file mode 100644 (file)
index cc0d4c1..0000000
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * manage authentification info
- *
- * License : The MIT License
- * Copyright(c) 2012 olyutorskii
- */
-
-package jp.sfjp.jindolf.net;
-
-import java.io.UnsupportedEncodingException;
-import java.net.CookieHandler;
-import java.net.CookieManager;
-import java.net.CookieStore;
-import java.net.HttpCookie;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.util.List;
-
-/**
- * Cookieを用いた人狼BBSサーバとの認証管理を行う。
- *
- * <p>2012-10現在、サポートするのはG国のみ。
- *
- * <p>2012-10より、Cookie "uniqID" の送出も必要になった模様。
- */
-public class AuthManager{
-
-    /** ログアウト用のPOSTデータ。 */
-    public static final String POST_LOGOUT;
-
-    private static final String COOKIE_LOGIN = "login";
-    private static final String ENC_POST = "UTF-8";
-    private static final String PARAM_REDIR;
-
-    private static final CookieManager COOKIE_MANAGER;
-
-    static{
-        PARAM_REDIR = "cgi_param=" + encodeForm4Post("&#bottom");
-        POST_LOGOUT = "cmd=logout" + '&' + PARAM_REDIR;
-
-        COOKIE_MANAGER = new CookieManager();
-        CookieHandler.setDefault(COOKIE_MANAGER);
-    }
-
-
-    private final URI baseURI;
-
-
-    /**
-     * コンストラクタ。
-     * @param bbsUri 人狼BBSサーバURI
-     * @throws NullPointerException 引数がnull
-     */
-    public AuthManager(URI bbsUri) throws NullPointerException{
-        if(bbsUri == null) throw new NullPointerException();
-        this.baseURI = bbsUri;
-        return;
-    }
-
-    /**
-     * コンストラクタ。
-     * @param bbsUrl 人狼BBSサーバURL
-     * @throws NullPointerException 引数がnull
-     * @throws IllegalArgumentException 不正な書式
-     */
-    public AuthManager(URL bbsUrl)
-            throws NullPointerException, IllegalArgumentException{
-        this(urlToUri(bbsUrl));
-        return;
-    }
-
-
-    /**
-     * URLからURIへ変換する。
-     * 書式に関する例外は非チェック例外へ変換される。
-     * @param url URL
-     * @return URI
-     * @throws NullPointerException 引数がnull
-     * @throws IllegalArgumentException 不正な書式
-     */
-    private static URI urlToUri(URL url)
-            throws NullPointerException, IllegalArgumentException{
-        if(url == null) throw new NullPointerException();
-
-        URI uri;
-        try{
-            uri = url.toURI();
-        }catch(URISyntaxException e){
-            throw new IllegalArgumentException(e);
-        }
-
-        return uri;
-    }
-
-    /**
-     * 与えられた文字列に対し
-     * 「application/x-www-form-urlencoded」符号化を行う。
-     *
-     * <p>この符号化はHTTPのPOSTメソッドで必要になる。
-     * この処理は、一般的なPC用Webブラウザにおける、
-     * HTML文書のFORMタグに伴うsubmit処理を模倣する。
-     *
-     * <p>生成文字列はUS-ASCIIの範疇に収まる。はず。
-     *
-     * @param formData 元の文字列
-     * @return 符号化された文字列
-     * @see java.net.URLEncoder
-     * @see
-     * <a href="http://tools.ietf.org/html/rfc1866#section-8.2.1">
-     * RFC1866 8.2.1
-     * </a>
-     */
-    public static String encodeForm4Post(String formData){
-        if(formData == null){
-            return null;
-        }
-
-        String result;
-        try{
-            result = URLEncoder.encode(formData, ENC_POST);
-        }catch(UnsupportedEncodingException e){
-            assert false;
-            result = null;
-        }
-
-        return result;
-    }
-
-    /**
-     * 配列版{@link #encodeForm4Post(java.lang.String)}。
-     * @param formData 元の文字列
-     * @return 符号化された文字列
-     * @see #encodeForm4Post(java.lang.String)
-     */
-    public static String encodeForm4Post(char[] formData){
-        return encodeForm4Post(new String(formData));
-    }
-
-    /**
-     * ログイン用POSTデータを生成する。
-     * @param userID 人狼BBSアカウント名
-     * @param password パスワード
-     * @return POSTデータ
-     */
-    public static String buildLoginPostData(String userID, char[] password){
-        String id = encodeForm4Post(userID);
-        if(id == null || id.isEmpty()){
-            return null;
-        }
-
-        String pw = encodeForm4Post(password);
-        if(pw == null || pw.isEmpty()){
-            return null;
-        }
-
-        StringBuilder postData = new StringBuilder();
-        postData.append("cmd=login");
-        postData.append('&').append(PARAM_REDIR);
-        postData.append('&').append("user_id=").append(id);
-        postData.append('&').append("password=").append(pw);
-
-        String result = postData.toString();
-        return result;
-    }
-
-
-    /**
-     * 人狼BBSサーバのベースURIを返す。
-     * @return ベースURI
-     */
-    public URI getBaseURI(){
-        return this.baseURI;
-    }
-
-    /**
-     * 人狼BBSサーバ管轄下の全Cookieを列挙する。
-     * 他サーバ由来のCookie群との区別はCookieドメイン名で判断される。
-     * @param cookieStore Cookie記憶域
-     * @return 人狼BBSサーバ管轄下のCookieのリスト
-     */
-    public List<HttpCookie> getCookieList(CookieStore cookieStore){
-        List<HttpCookie> cookieList = cookieStore.get(this.baseURI);
-        return cookieList;
-    }
-
-    /**
-     * 認証Cookieを取得する。
-     *
-     * <p>G国での認証Cookie名は"login"。
-     *
-     * <p>※ 2012-10より"uniqID"も増えた模様だが判定には使わない。
-     *
-     * @param cookieStore Cookie記憶域
-     * @return 認証Cookie。認証された状態に無いときはnull。
-     */
-    public HttpCookie getAuthCookie(CookieStore cookieStore){
-        List<HttpCookie> cookieList = getCookieList(cookieStore);
-        for(HttpCookie cookie : cookieList){
-            String cookieName = cookie.getName();
-            if(COOKIE_LOGIN.equals(cookieName)) return cookie;
-        }
-
-        return null;
-    }
-
-    /**
-     * 現在ログイン中か否か判別する。
-     * @return ログイン中ならtrue
-     */
-    public boolean hasLoggedIn(){
-        assert COOKIE_MANAGER == CookieHandler.getDefault();
-        CookieStore cookieStore = COOKIE_MANAGER.getCookieStore();
-
-        HttpCookie authCookie = getAuthCookie(cookieStore);
-        if(authCookie == null){
-            clearAuthentication();
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * 認証情報をクリアする。
-     */
-    public void clearAuthentication(){
-        assert COOKIE_MANAGER == CookieHandler.getDefault();
-        CookieStore cookieStore = COOKIE_MANAGER.getCookieStore();
-
-        HttpCookie authCookie = getAuthCookie(cookieStore);
-        if(authCookie != null){
-            cookieStore.remove(this.baseURI, authCookie);
-        }
-
-        return;
-    }
-
-}
index 90ebdad..e3a5c11 100644 (file)
@@ -11,6 +11,7 @@ import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URLConnection;
 import java.text.MessageFormat;
+import java.util.Arrays;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import jp.sfjp.jindolf.VerInfo;
@@ -52,6 +53,7 @@ public final class HttpUtils{
 
     /**
      * ネットワークのスループット報告用文字列を生成する。
+     *
      * @param size 転送サイズ(バイト数)
      * @param nano 所要時間(ナノ秒)
      * @return スループット文字列。引数のいずれかが0以下なら空文字列
@@ -80,6 +82,7 @@ public final class HttpUtils{
 
     /**
      * HTTPセッションの各種結果を文字列化する。
+     *
      * @param conn HTTPコネクション
      * @param size 転送サイズ
      * @param nano 転送に要したナノ秒
@@ -117,55 +120,49 @@ public final class HttpUtils{
     }
 
     /**
-     * ユーザエージェント名を返す。
-     * @return ユーザエージェント名
+     * HTTP UserAgent名を返す。
+     *
+     * @return UserAgent名
+     * @see <a href="https://tools.ietf.org/html/rfc7231#section-5.5.3">
+     * User-Agent</a>
      */
     public static String getUserAgentName(){
         StringBuilder result = new StringBuilder();
-        result.append(VerInfo.TITLE).append("/").append(VerInfo.VERSION);
+        result.append(VerInfo.TITLE).append('/').append(VerInfo.VERSION);
 
         StringBuilder rawComment = new StringBuilder();
-        if(EnvInfo.OS_NAME != null){
-            if(rawComment.length() > 0) rawComment.append("; ");
-            rawComment.append(EnvInfo.OS_NAME);
-        }
-        if(EnvInfo.OS_VERSION != null){
-            if(rawComment.length() > 0) rawComment.append("; ");
-            rawComment.append(EnvInfo.OS_VERSION);
-        }
-        if(EnvInfo.OS_ARCH != null){
-            if(rawComment.length() > 0) rawComment.append("; ");
-            rawComment.append(EnvInfo.OS_ARCH);
-        }
-        if(EnvInfo.JAVA_VENDOR != null){
-            if(rawComment.length() > 0) rawComment.append("; ");
-            rawComment.append(EnvInfo.JAVA_VENDOR);
-        }
-        if(EnvInfo.JAVA_VERSION != null){
-            if(rawComment.length() > 0) rawComment.append("; ");
-            rawComment.append(EnvInfo.JAVA_VERSION);
+        Arrays.asList(
+            EnvInfo.OS_NAME,
+            EnvInfo.OS_VERSION,
+            EnvInfo.OS_ARCH,
+            EnvInfo.JAVA_VENDOR,
+            EnvInfo.JAVA_VERSION
+        ).stream().forEachOrdered(info -> {
+            if(rawComment.length() > 0) rawComment.append(";\u0020");
+            rawComment.append(info);
+        });
+
+        if(rawComment.length() > 0){
+            CharSequence comment = escapeHttpComment(rawComment);
+            result.append('\u0020').append(comment);
         }
 
-        CharSequence comment = escapeHttpComment(rawComment);
-        if(comment != null) result.append(" ").append(comment);
-
         return result.toString();
     }
 
     /**
      * 与えられた文字列からHTTPコメントを生成する。
+     *
      * @param comment コメント
      * @return HTTPコメント
      */
     public static String escapeHttpComment(CharSequence comment){
-        if(comment == null) return null;
-        if(comment.length() <= 0) return null;
-
         String result = comment.toString();
+
         result = result.replaceAll("\\(", "\\\\(");
         result = result.replaceAll("\\)", "\\\\)");
         result = result.replaceAll("[\\u0000-\\u001f]", "?");
-        result = result.replaceAll("[\\u007f-\\uffff]", "?");
+        result = result.replaceAll("[\\u007f-\\x{10ffff}]", "?");
         result = "(" + result + ")";
 
         return result;
@@ -173,6 +170,7 @@ public final class HttpUtils{
 
     /**
      * HTTP応答からCharsetを取得する。
+     *
      * @param connection HTTP接続
      * @return Charset文字列
      */
@@ -184,6 +182,7 @@ public final class HttpUtils{
 
     /**
      * ContentTypeからCharsetを取得する。
+     *
      * @param contentType ContentType
      * @return Charset文字列
      */
index 30b80e8..93762f4 100644 (file)
@@ -35,6 +35,7 @@ import jp.sfjp.jindolf.util.Monodizer;
 
 /**
  * プロクシサーバ選択画面。
+ *
  * @see <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>
  * @see <a href="http://www.ietf.org/rfc/rfc1928.txt">RFC1928</a>
  */
@@ -64,6 +65,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * コンストラクタ。
+     *
      * @param proxyInfo プロクシ設定
      */
     public ProxyChooser(ProxyInfo proxyInfo){
@@ -100,6 +102,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * ポート番号選択肢を生成する。
+     *
      * @return ポート番号選択肢
      */
     private static ComboBoxModel<Integer> buildPortRecommender(){
@@ -115,6 +118,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * レイアウトを行う。
+     *
      * @param content コンテナ
      */
     private void design(Container content){
@@ -143,6 +147,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * サーバ情報パネルを生成する。
+     *
      * @return サーバ情報パネル
      */
     private JComponent buildServerPanel(){
@@ -177,13 +182,6 @@ public class ProxyChooser extends JPanel implements ItemListener{
         constraints.anchor = GridBagConstraints.NORTHWEST;
         panel.add(this.port, constraints);
 
-        String warn =
-                "<html>※ このプロクシサーバは本当に信頼できますか?<br>"
-                + "あなたが人狼BBSにログインしている間、<br>"
-                + "あなたのパスワードは平文状態のまま<br>"
-                + "このプロクシサーバ上を何度も通過します。</html>";
-        panel.add(new JLabel(warn), constraints);
-
         Border border = BorderFactory.createTitledBorder("プロクシサーバ情報");
         panel.setBorder(border);
 
@@ -192,6 +190,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * プロクシの種別を返す。
+     *
      * @return プロクシ種別
      */
     protected Proxy.Type getType(){
@@ -204,7 +203,9 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * サーバ名を返す。
-     * プロクシのホスト名として妥当なものか否かは検証されない。
+     *
+     * <p>プロクシのホスト名として妥当なものか否かは検証されない。
+     *
      * @return サーバ名
      */
     protected String getHostName(){
@@ -215,7 +216,9 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * ポート番号を返す。
-     * 番号の体をなしていなければゼロを返す。
+     *
+     * <p>番号の体をなしていなければゼロを返す。
+     *
      * @return ポート番号
      */
     protected int getPort(){
@@ -238,6 +241,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * サーバへのソケットアドレスを生成する。
+     *
      * @return ソケットアドレス
      */
     protected InetSocketAddress getInetSocketAddress(){
@@ -246,6 +250,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * プロクシ設定を返す。
+     *
      * @return プロクシ設定
      */
     public ProxyInfo getProxyInfo(){
@@ -255,7 +260,9 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * プロクシ設定を設定する。
-     * UIに反映される。
+     *
+     * <p>UIに反映される。
+     *
      * @param proxyInfo プロクシ設定。nullなら直接接続と解釈される。
      */
     public final void setProxyInfo(ProxyInfo proxyInfo){
@@ -287,6 +294,7 @@ public class ProxyChooser extends JPanel implements ItemListener{
 
     /**
      * プロクシ種別ボタン操作の受信。
+     *
      * @param event ボタン操作イベント
      */
     @Override
index a19fff1..7f65db8 100644 (file)
@@ -12,14 +12,12 @@ import io.bitbucket.olyutorskii.jiocema.DecodeNotifier;
 import java.awt.image.BufferedImage;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.lang.ref.SoftReference;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.Proxy;
 import java.net.URL;
 import java.nio.charset.Charset;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.logging.Logger;
@@ -33,29 +31,46 @@ import jp.sfjp.jindolf.data.Village;
 
 /**
  * 国ごとの人狼BBSサーバとの通信を一手に引き受ける。
+ *
+ * <p>受信対象はHTMLと各種アイコンJPEG画像。
+ *
+ * <p>プロクシサーバを介したHTTP通信を管理する。
+ *
+ * <p>国ごとに文字コードを保持し、HTML文書のデコードに用いられる。
+ *
+ * <p>画像(40種強)はキャッシュ管理が行われる。
+ *
+ * <p>※ 2020-02現在、進行中の村は存在しないゆえ、
+ * Cookie認証処理は削除された。
+ *
+ * <p>最後にHTTP受信が行われた時刻を保持する。
  */
 public class ServerAccess{
 
+    private static final int BUFLEN_CONTENT = 200 * 1024;
+
     private static final String USER_AGENT = HttpUtils.getUserAgentName();
     private static final String JINRO_CGI = "./index.rb";
+
     private static final
             Map<String, SoftReference<BufferedImage>> IMAGE_CACHE;
+    private static final Object CACHE_LOCK = new Object();
 
     private static final Logger LOGGER = Logger.getAnonymousLogger();
-    private static final String ENC_POST = "UTF-8";
 
     static{
-        Map<String, SoftReference<BufferedImage>> cache =
-                new HashMap<>();
-        IMAGE_CACHE = Collections.synchronizedMap(cache);
+        IMAGE_CACHE = new HashMap<>();
     }
 
 
     private final URL baseURL;
-    private final AuthManager authManager;
 
     private final Charset charset;
+    private final boolean isSJIS;
+    private final boolean isUTF8;
+
     private Proxy proxy = Proxy.NO_PROXY;
+
     private long lastServerMs;
     private long lastLocalMs;
     private long lastSystemMs;
@@ -63,7 +78,9 @@ public class ServerAccess{
 
     /**
      * 人狼BBSサーバとの接続管理を生成する。
-     * この時点ではまだ通信は行われない。
+     *
+     * <p>この時点ではまだ通信は行われない。
+     *
      * @param baseURL 国別のベースURL
      * @param charset 国のCharset
      * @throws IllegalArgumentException 不正なURL
@@ -73,15 +90,32 @@ public class ServerAccess{
         super();
 
         this.baseURL = baseURL;
-        this.authManager = new AuthManager(this.baseURL);
         this.charset = charset;
 
+        String charsetName = this.charset.name();
+        if("Shift_JIS".equalsIgnoreCase(charsetName)){
+            this.isSJIS = true;
+            this.isUTF8 = false;
+        }else if("UTF-8".equalsIgnoreCase(charsetName)){
+            this.isSJIS = false;
+            this.isUTF8 = true;
+        }else{
+            throw new IllegalArgumentException(charsetName);
+        }
+        assert this.isSJIS ^ this.isUTF8;
+
         return;
     }
 
 
     /**
      * 画像キャッシュを検索する。
+     *
+     * <p>キーは画像URL文字列。
+     *
+     * <p>ソフト参照オブジェクトの解放などにより、
+     * キャッシュの状況は変化する。
+     *
      * @param key キー
      * @return キャッシュされた画像。キャッシュされていなければnull。
      */
@@ -90,31 +124,33 @@ public class ServerAccess{
 
         BufferedImage image;
 
-        synchronized(IMAGE_CACHE){
+        // atomic get and remove
+        synchronized(CACHE_LOCK){
             SoftReference<BufferedImage> ref = IMAGE_CACHE.get(key);
             if(ref == null) return null;
 
-            Object referent = ref.get();
-            if(referent == null){
+            image = ref.get();
+            if(image == null){
                 IMAGE_CACHE.remove(key);
-                return null;
             }
-
-            image = (BufferedImage) referent;
         }
 
         return image;
     }
 
     /**
-     * 画像キャッシュに登録する。
+     * キャッシュに画像を登録する。
+     *
+     * <p>キーは画像URL文字列。
+     *
      * @param key キー
      * @param image キャッシュしたい画像。
      */
     private static void putImageCache(String key, BufferedImage image){
         if(key == null || image == null) return;
 
-        synchronized(IMAGE_CACHE){
+        // atomic get and put
+        synchronized(CACHE_LOCK){
             if(getImageCache(key) != null) return;
             SoftReference<BufferedImage> ref =
                     new SoftReference<>(image);
@@ -124,8 +160,10 @@ public class ServerAccess{
         return;
     }
 
+
     /**
-     * HTTP-Proxyを返す。
+     * HTTP通信に使われるProxyを返す。
+     *
      * @return HTTP-Proxy
      */
     public Proxy getProxy(){
@@ -133,7 +171,8 @@ public class ServerAccess{
     }
 
     /**
-     * HTTP-Proxyを設定する。
+     * HTTP通信に使われるProxyを設定する。
+     *
      * @param proxy HTTP-Proxy。nullならProxyなしと解釈される。
      */
     public void setProxy(Proxy proxy){
@@ -144,6 +183,7 @@ public class ServerAccess{
 
     /**
      * 国のベースURLを返す。
+     *
      * @return ベースURL
      */
     public URL getBaseURL(){
@@ -152,7 +192,8 @@ public class ServerAccess{
 
     /**
      * 与えられたクエリーとCGIのURLから新たにURLを合成する。
-     * @param query クエリー
+     *
+     * @param query ?から始まるクエリー
      * @return 新たなURL
      */
     protected URL getQueryURL(String query){
@@ -171,7 +212,32 @@ public class ServerAccess{
     }
 
     /**
-     * エンコーディングされた入力ストリームから文字列を生成する。
+     * 指定された村の最新PeriodのHTMLデータのURLを取得する。
+     *
+     * @param village 村
+     * @return URL
+     */
+    public URL getVillageURL(Village village){
+        String query = village.getCGIQuery();
+        URL url = getQueryURL(query);
+        return url;
+    }
+
+    /**
+     * 指定されたPeriodのHTMLデータのURLを取得する。
+     *
+     * @param period 日
+     * @return URL
+     */
+    public URL getPeriodURL(Period period){
+        String query = period.getCGIQuery();
+        URL url = getQueryURL(query);
+        return url;
+    }
+
+    /**
+     * エンコーディングされた入力ストリームからHTML文字列を受信する。
+     *
      * @param istream 入力ストリーム
      * @return 文字列
      * @throws java.io.IOException 入出力エラー(おそらくネットワーク関連)
@@ -180,12 +246,12 @@ public class ServerAccess{
             throws IOException{
         DecodeNotifier decoder;
         ContentBuilder builder;
-        if(this.charset.name().equalsIgnoreCase("Shift_JIS")){
+        if(this.isSJIS){
             decoder = new SjisNotifier();
-            builder = new ContentBuilderSJ(200 * 1024);
-        }else if(this.charset.name().equalsIgnoreCase("UTF-8")){
+            builder = new ContentBuilderSJ(BUFLEN_CONTENT);
+        }else if(this.isUTF8){
             decoder = new DecodeNotifier(this.charset.newDecoder());
-            builder = new ContentBuilder(200 * 1024);
+            builder = new ContentBuilder(BUFLEN_CONTENT);
         }else{
             assert false;
             return null;
@@ -205,20 +271,8 @@ public class ServerAccess{
     }
 
     /**
-     * 与えられたクエリーを用いてHTMLデータを取得する。
-     * @param query HTTP-GET クエリー
-     * @return HTMLデータ
-     * @throws java.io.IOException ネットワークエラー
-     */
-    protected HtmlSequence downloadHTML(String query)
-            throws IOException{
-        URL url = getQueryURL(query);
-        HtmlSequence result = downloadHTML(url);
-        return result;
-    }
-
-    /**
      * 与えられたURLを用いてHTMLデータを取得する。
+     *
      * @param url URL
      * @return HTMLデータ
      * @throws java.io.IOException ネットワークエラー
@@ -251,10 +305,11 @@ public class ServerAccess{
             return null;
         }
 
-        InputStream stream = TallyInputStream.getInputStream(connection);
-        DecodedContent html = downloadHTMLStream(stream);
+        DecodedContent html;
+        try(InputStream is = TallyInputStream.getInputStream(connection)){
+            html = downloadHTMLStream(is);
+        }
 
-        stream.close();
         connection.disconnect();
 
         HtmlSequence hseq = new HtmlSequence(url, datems, html);
@@ -263,97 +318,22 @@ public class ServerAccess{
     }
 
     /**
-     * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。
-     * @param url 画像URL文字列
-     * @return 画像イメージ
-     * @throws java.io.IOException ネットワークエラー
-     */
-    public BufferedImage downloadImage(String url) throws IOException{
-        URL absolute;
-        try{
-            URL base = getBaseURL();
-            absolute = new URL(base, url);
-        }catch(MalformedURLException e){
-            assert false;
-            return null;
-        }
-
-        BufferedImage image;
-        image = getImageCache(absolute.toString());
-        if(image != null) return image;
-
-        HttpURLConnection connection =
-                (HttpURLConnection) absolute.openConnection(this.proxy);
-        connection.setRequestProperty("Accept", "*/*");
-        connection.setRequestProperty("User-Agent", USER_AGENT);
-        connection.setUseCaches(true);
-        connection.setInstanceFollowRedirects(true);
-        connection.setDoInput(true);
-        connection.setRequestMethod("GET");
-
-        connection.connect();
-
-        int responseCode       = connection.getResponseCode();
-        if(responseCode != HttpURLConnection.HTTP_OK){
-            String logMessage =  "イメージのダウンロードに失敗しました。";
-            logMessage += HttpUtils.formatHttpStat(connection, 0, 0);
-            LOGGER.warning(logMessage);
-            return null;
-        }
-
-        InputStream stream = TallyInputStream.getInputStream(connection);
-        image = ImageIO.read(stream);
-        stream.close();
-
-        connection.disconnect();
-
-        putImageCache(absolute.toString(), image);
-
-        return image;
-    }
-
-    /**
-     * 指定された認証情報をPOSTする。
-     * @param authData 認証情報
-     * @return 認証情報が受け入れられたらtrue
+     * 与えられたクエリーを用いてHTMLデータを取得する。
+     *
+     * @param query HTTP-GET クエリー
+     * @return HTMLデータ
      * @throws java.io.IOException ネットワークエラー
      */
-    protected boolean postAuthData(String authData) throws IOException{
-        URL url = getQueryURL("");
-        HttpURLConnection connection =
-                (HttpURLConnection) url.openConnection(this.proxy);
-        connection.setRequestProperty("Accept", "*/*");
-        connection.setRequestProperty("User-Agent", USER_AGENT);
-        connection.setUseCaches(false);
-        connection.setInstanceFollowRedirects(false);
-        connection.setDoInput(true);
-        connection.setDoOutput(true);
-        connection.setRequestMethod("POST");
-
-        byte[] authBytes = authData.getBytes(ENC_POST);
-
-        OutputStream os = TallyOutputStream.getOutputStream(connection);
-        os.write(authBytes);
-        os.flush();
-        os.close();
-
-        updateLastAccess(connection);
-
-        connection.disconnect();
-
-        if( ! this.authManager.hasLoggedIn() ){
-            String logMessage =  "認証情報の送信に失敗しました。";
-            LOGGER.warning(logMessage);
-            return false;
-        }
-
-        LOGGER.info("正しく認証が行われました。");
-
-        return true;
+    protected HtmlSequence downloadHTML(String query)
+            throws IOException{
+        URL url = getQueryURL(query);
+        HtmlSequence result = downloadHTML(url);
+        return result;
     }
 
     /**
      * トップページのHTMLデータを取得する。
+     *
      * @return HTMLデータ
      * @throws java.io.IOException ネットワークエラー
      */
@@ -363,6 +343,9 @@ public class ServerAccess{
 
     /**
      * 国に含まれる村一覧HTMLデータを取得する。
+     *
+     * <p>wolf国には存在しない。
+     *
      * @return HTMLデータ
      * @throws java.io.IOException ネットワークエラー
      */
@@ -372,20 +355,24 @@ public class ServerAccess{
 
     /**
      * 指定された村のPeriod一覧のHTMLデータを取得する。
-     * 現在ゲーム進行中の村にも可能。
+     *
+     * <p>現在ゲーム進行中の村にも可能。
      * ※ 古国では使えないよ!
+     *
      * @param village 村
      * @return HTMLデータ
      * @throws java.io.IOException ネットワークエラー
      */
     public HtmlSequence getHTMLBoneHead(Village village) throws IOException{
-        String villageID = village.getVillageID();
-        return downloadHTML("?vid=" + villageID + "&meslog=");
+        String query = village.getCGIQuery();
+        return downloadHTML(query + "&meslog=");
     }
 
     /**
      * 指定された村の最新PeriodのHTMLデータをロードする。
-     * 既にGAMEOVERの村ではPeriod一覧のHTMLデータとなる。
+     *
+     * <p>既にGAMEOVERの村ではPeriod一覧のHTMLデータとなる。
+     *
      * @param village 村
      * @return HTMLデータ
      * @throws java.io.IOException ネットワークエラー
@@ -396,18 +383,8 @@ public class ServerAccess{
     }
 
     /**
-     * 指定された村の最新PeriodのHTMLデータのURLを取得する。
-     * @param village 村
-     * @return URL
-     */
-    public URL getVillageURL(Village village){
-        String villageID = village.getVillageID();
-        URL url = getQueryURL("?vid=" + villageID);
-        return url;
-    }
-
-    /**
      * 指定されたPeriodのHTMLデータをロードする。
+     *
      * @param period Period
      * @return HTMLデータ
      * @throws java.io.IOException ネットワークエラー
@@ -418,79 +395,68 @@ public class ServerAccess{
     }
 
     /**
-     * 指定されたPeriodのHTMLデータのURLを取得する。
-     * @param period 日
-     * @return URL
-     */
-    public URL getPeriodURL(Period period){
-        String query = period.getCGIQuery();
-        URL url = getQueryURL(query);
-        return url;
-    }
-
-    /**
-     * 最終アクセス時刻を更新する。
-     * @param connection HTTP接続
-     * @return リソース送信時刻
-     */
-    public long updateLastAccess(HttpURLConnection connection){
-        this.lastServerMs = connection.getDate();
-        this.lastLocalMs = System.currentTimeMillis();
-        this.lastSystemMs = System.nanoTime() / (1000 * 1000);
-        return this.lastServerMs;
-    }
-
-    /**
-     * 与えられたユーザIDとパスワードでログイン処理を行う。
-     * @param userID ユーザID
-     * @param password パスワード
-     * @return ログインに成功すればtrue
+     * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。
+     *
+     * @param url 画像URL文字列
+     * @return 画像イメージ
      * @throws java.io.IOException ネットワークエラー
      */
-    public final boolean login(String userID, char[] password)
-            throws IOException{
-        if(this.authManager.hasLoggedIn()){
-            return true;
-        }
-
-        String postText = AuthManager.buildLoginPostData(userID, password);
-        boolean result;
+    public BufferedImage downloadImage(String url) throws IOException{
+        URL absolute;
         try{
-            result = postAuthData(postText);
-        }catch(IOException e){
-            this.authManager.clearAuthentication();
-            throw e;
+            URL base = getBaseURL();
+            absolute = new URL(base, url);
+        }catch(MalformedURLException e){
+            assert false;
+            return null;
         }
 
-        return result;
-    }
+        String urlTxt = absolute.toString();
+        BufferedImage image;
+        image = getImageCache(urlTxt);
+        if(image != null) return image;
 
-    /**
-     * ログアウト処理を行う。
-     * @throws java.io.IOException ネットワーク入出力エラー
-     */
-    public void logout() throws IOException{
-        if( ! this.authManager.hasLoggedIn() ){
-            return;
+        HttpURLConnection connection =
+                (HttpURLConnection) absolute.openConnection(this.proxy);
+        connection.setRequestProperty("Accept", "*/*");
+        connection.setRequestProperty("User-Agent", USER_AGENT);
+        connection.setUseCaches(true);
+        connection.setInstanceFollowRedirects(true);
+        connection.setDoInput(true);
+        connection.setRequestMethod("GET");
+
+        connection.connect();
+
+        int responseCode       = connection.getResponseCode();
+        if(responseCode != HttpURLConnection.HTTP_OK){
+            String logMessage =  "イメージのダウンロードに失敗しました。";
+            logMessage += HttpUtils.formatHttpStat(connection, 0, 0);
+            LOGGER.warning(logMessage);
+            return null;
         }
 
-        try{
-            postAuthData(AuthManager.POST_LOGOUT);
-        }finally{
-            this.authManager.clearAuthentication();
+        try(InputStream is = TallyInputStream.getInputStream(connection)){
+            image = ImageIO.read(is);
         }
 
-        return;
+        connection.disconnect();
+
+        putImageCache(urlTxt, image);
+
+        return image;
     }
-    // TODO シャットダウンフックでログアウトさせようかな…
 
     /**
-     * ログイン中か否か判定する。
-     * @return ログイン中ならtrue
+     * 最終アクセス時刻を更新する。
+     *
+     * @param connection HTTP接続
+     * @return リソース送信時刻
      */
-    public boolean hasLoggedIn(){
-        boolean result = this.authManager.hasLoggedIn();
-        return result;
+    public long updateLastAccess(HttpURLConnection connection){
+        this.lastServerMs = connection.getDate();
+        this.lastLocalMs = System.currentTimeMillis();
+        this.lastSystemMs = System.nanoTime() / (1000 * 1000);
+        return this.lastServerMs;
     }
 
 }
index 901f5d2..35155b4 100644 (file)
@@ -50,6 +50,7 @@ import jp.sfjp.jindolf.data.Topic;
 import jp.sfjp.jindolf.data.Village;
 import jp.sfjp.jindolf.glyph.TalkDraw;
 import jp.sfjp.jindolf.util.GUIUtils;
+import jp.sfjp.jindolf.view.AvatarPics;
 import jp.sourceforge.jindolf.corelib.TalkType;
 
 /**
@@ -451,8 +452,9 @@ public class DaySummary extends JDialog
                 Avatar avatar = (Avatar) value;
 
                 Village village = DaySummary.this.period.getVillage();
-                Image image = village.getAvatarFaceImage(avatar);
-                if(image == null) image = village.getGraveImage();
+                AvatarPics avatarPics = village.getAvatarPics();
+                Image image = avatarPics.getAvatarFaceImage(avatar);
+                if(image == null) image = avatarPics.getGraveImage();
                 if(image != null){
                     ImageIcon icon = new ImageIcon(image);
                     setIcon(icon);
index 1d66056..d0e37e0 100644 (file)
@@ -22,13 +22,15 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import jp.sfjp.jindolf.VerInfo;
 import jp.sfjp.jindolf.data.Avatar;
+import jp.sfjp.jindolf.data.InterPlay;
 import jp.sfjp.jindolf.data.Period;
 import jp.sfjp.jindolf.data.Player;
 import jp.sfjp.jindolf.data.SysEvent;
 import jp.sfjp.jindolf.data.Talk;
-import jp.sfjp.jindolf.data.Topic;
 import jp.sfjp.jindolf.data.Village;
 import jp.sfjp.jindolf.dxchg.FaceIconSet;
 import jp.sfjp.jindolf.dxchg.WolfBBS;
@@ -109,40 +111,38 @@ public class GameSummary{
 
     /**
      * プレイヤーのリストから役職バランス文字列を得る。
-     * ex) "村村占霊狂狼"
+     *
+     * <p>ex) "村村占霊狂狼"
+     *
      * @param players プレイヤーのリスト
      * @return 役職バランス文字列
      */
     public static String getRoleBalanceSequence(List<Player> players){
-        List<GameRole> roleList = new LinkedList<>();
-        for(Player player : players){
-            GameRole role = player.getRole();
-            roleList.add(role);
-        }
-        Collections.sort(roleList, GameRole.getPowerBalanceComparator());
-
         StringBuilder result = new StringBuilder();
-        for(GameRole role : roleList){
-            char ch = role.getShortName();
-            result.append(ch);
-        }
+
+        players.stream()
+                .map(player -> player.getRole())
+                .sorted(GameRole.getPowerBalanceComparator())
+                .map(role -> role.getShortName())
+                .forEachOrdered(ch -> result.append(ch));
 
         return result.toString();
     }
 
+
     /**
      * サマライズ処理。
      */
     private void summarize(){
         buildEventMap();
+        buildPlayerList();
 
         summarizeTime();
         summarizeWinner();
-        summarizePlayers();
 
-        for(Period period : this.village.getPeriodList()){
+        this.village.getPeriodList().forEach(period -> {
             summarizePeriod(period);
-        }
+        });
 
         summarizeJudge();
         summarizeGuard();
@@ -154,20 +154,58 @@ public class GameSummary{
      * SysEventの種別ごとに集計する。
      */
     private void buildEventMap(){
-        for(SysEventType type : SysEventType.values()){
-            List<SysEvent> eventList = new LinkedList<>();
-            this.eventMap.put(type, eventList);
-        }
+        Stream.of(SysEventType.values())
+                .forEach(type -> {
+                    this.eventMap.put(type, new LinkedList<>());
+                });
+
+        this.village.getPeriodList().stream()
+                .flatMap(period -> period.getTopicList().stream())
+                .filter(topic -> (topic instanceof SysEvent) )
+                .map(topic -> (SysEvent) topic)
+                .forEachOrdered(event -> {
+                    SysEventType type = event.getSysEventType();
+                    List<SysEvent> eventList = this.eventMap.get(type);
+                    eventList.add(event);
+                });
 
-        for(Period period : this.village.getPeriodList()){
-            for(Topic topic : period.getTopicList()){
-                if( ! (topic instanceof SysEvent) ) continue;
-                SysEvent event = (SysEvent) topic;
-                SysEventType type = event.getSysEventType();
-                List<SysEvent> eventList = this.eventMap.get(type);
-                eventList.add(event);
-            }
-        }
+        return;
+    }
+
+    /**
+     * playerListイベントとonStageイベントの内容を付き合わせ、
+     * Player一覧情報を完成させる。
+     *
+     * <p>プロローグで失踪したPlayerは対象外。
+     */
+    private void buildPlayerList(){
+        List<SysEvent> playerListEventList =
+                this.eventMap.get(SysEventType.PLAYERLIST);
+        assert playerListEventList.size() == 1;
+        SysEvent playerListEvent = playerListEventList.get(0);
+
+        List<Player> players = playerListEvent.getPlayerList();
+        players.forEach( player -> {
+            Avatar avatar = player.getAvatar();
+            this.playerMap.put(avatar, player);
+            this.playerList.add(player);
+        });
+
+        List<SysEvent> onStageEventList =
+                this.eventMap.get(SysEventType.ONSTAGE);
+        onStageEventList.stream()
+                .map(onStageEvent -> onStageEvent.getPlayerList().get(0))
+                .forEachOrdered(onStagePlayer -> {
+                    Avatar avatar = onStagePlayer.getAvatar();
+                    Player listPlayer = this.playerMap.get(avatar);
+                    if(listPlayer != null){
+                        int entryNo = onStagePlayer.getEntryNo();
+                        listPlayer.setEntryNo(entryNo);
+                    }else{
+                        assert true;
+                        // プレイヤー失踪?
+                    }
+                });
 
         return;
     }
@@ -199,117 +237,68 @@ public class GameSummary{
     }
 
     /**
-     * 参加者集計。
-     */
-    private void summarizePlayers(){
-        List<SysEvent> eventList;
-
-        List<Avatar>       avatarList;
-        List<GameRole>     roleList;
-        List<Integer>      integerList;
-        List<CharSequence> textList;
-
-        eventList = this.eventMap.get(SysEventType.ONSTAGE);
-        for(SysEvent event : eventList){
-            avatarList  = event.getAvatarList();
-            integerList = event.getIntegerList();
-            Avatar onstageAvatar = avatarList.get(0);
-            Player onstagePlayer = registPlayer(onstageAvatar);
-            onstagePlayer.setEntryNo(integerList.get(0));
-        }
-
-        eventList = this.eventMap.get(SysEventType.PLAYERLIST);
-        assert eventList.size() == 1;
-        SysEvent event = eventList.get(0);
-
-        avatarList  = event.getAvatarList();
-        roleList    = event.getRoleList();
-        integerList = event.getIntegerList();
-        textList    = event.getCharSequenceList();
-        int avatarNum = avatarList.size();
-        for(int idx = 0; idx < avatarNum; idx++){
-            Avatar avatar = avatarList.get(idx);
-            GameRole role = roleList.get(idx);
-            CharSequence urlText = textList.get(idx * 2);
-            CharSequence idName  = textList.get(idx * 2 + 1);
-            int liveOrDead = integerList.get(idx);
-
-            Player player = registPlayer(avatar);
-            player.setRole(role);
-            player.setUrlText(urlText.toString());
-            player.setIdName(idName.toString());
-            if(liveOrDead != 0){        // 生存
-                player.setObitDay(-1);
-                player.setDestiny(Destiny.ALIVE);
-            }
-
-            this.playerList.add(player);
-        }
-
-        return;
-    }
-
-    /**
      * Periodのサマライズ。
+     *
      * @param period Period
      */
     private void summarizePeriod(Period period){
         int day = period.getDay();
-        for(Topic topic : period.getTopicList()){
-            if(topic instanceof SysEvent){
-                SysEvent sysEvent = (SysEvent) topic;
-                summarizeDestiny(day, sysEvent);
-            }
-        }
+
+        period.getTopicList().stream()
+                .filter(topic -> topic instanceof SysEvent)
+                .map(topic -> (SysEvent) topic)
+                .forEachOrdered(sysEvent -> {
+                    summarizeDestiny(day, sysEvent);
+                });
 
         return;
     }
 
     /**
      * 各プレイヤー運命のサマライズ。
+     *
      * @param day 日
      * @param sysEvent システムイベント
      */
     private void summarizeDestiny(int day, SysEvent sysEvent){
-        List<Avatar>  avatarList  = sysEvent.getAvatarList();
-        List<Integer> integerList = sysEvent.getIntegerList();
-
-        int avatarTotal = avatarList.size();
-        Avatar lastAvatar = null;
-        if(avatarTotal > 0) lastAvatar = avatarList.get(avatarTotal - 1);
+        List<Avatar> deadAvatars = new LinkedList<>();
+        Destiny destiny;
 
         SysEventType eventType = sysEvent.getSysEventType();
+
         switch(eventType){
+        case COUNTING:   // G国COUNTING2は運命に関係なし
         case EXECUTION:  // G国のみ
-            if(integerList.get(avatarTotal - 1) > 0) break;  // 処刑無し
-            Player executedPl = registPlayer(lastAvatar);
-            executedPl.setDestiny(Destiny.EXECUTED);
-            executedPl.setObitDay(day);
+            Avatar executedAvatar = sysEvent.getExecutedAvatar();
+            if(executedAvatar == null) return;
+            deadAvatars.add(executedAvatar);
+            destiny = Destiny.EXECUTED;
             break;
         case SUDDENDEATH:
+            List<Avatar>  avatarList = sysEvent.getAvatarList();
             Avatar suddenDeathAvatar = avatarList.get(0);
-            Player suddenDeathPlayer = registPlayer(suddenDeathAvatar);
-            suddenDeathPlayer.setDestiny(Destiny.SUDDENDEATH);
-            suddenDeathPlayer.setObitDay(day);
-            break;
-        case COUNTING:  // G国COUNTING2は運命に関係なし
-            if(avatarTotal % 2 == 0) break;  // 処刑無し
-            Player executedPlayer = registPlayer(lastAvatar);
-            executedPlayer.setDestiny(Destiny.EXECUTED);
-            executedPlayer.setObitDay(day);
+            deadAvatars.add(suddenDeathAvatar);
+            destiny = Destiny.SUDDENDEATH;
             break;
         case MURDERED:
-            for(Avatar avatar : avatarList){
-                Player player = registPlayer(avatar);
-                player.setDestiny(Destiny.EATEN);
-                player.setObitDay(day);
-            }
             // TODO E国ハム溶け処理は後回し
+            sysEvent.getAvatarList().stream()
+                    .forEach(avatar -> {
+                        deadAvatars.add(avatar);
+                    });
+            destiny = Destiny.EATEN;
             break;
         default:
-            break;
+            return;
         }
 
+        deadAvatars.stream()
+                .map(avatar -> getPlayer(avatar))
+                .forEach(player -> {
+                    player.setDestiny(destiny);
+                    player.setObitDay(day);
+                });
+
         return;
     }
 
@@ -317,21 +306,17 @@ public class GameSummary{
      * 会話時刻のサマライズ。
      */
     private void summarizeTime(){
-        for(Period period : this.village.getPeriodList()){
-            for(Topic topic : period.getTopicList()){
-                if( ! (topic instanceof Talk) ) continue;
-                Talk talk = (Talk) topic;
-
-                long epoch = talk.getTimeFromID();
-
-                if(this.talk1stTimeMs  < 0) this.talk1stTimeMs  = epoch;
-                if(this.talkLastTimeMs < 0) this.talkLastTimeMs = epoch;
-
-                if(epoch < this.talk1stTimeMs ) this.talk1stTimeMs  = epoch;
-                if(epoch > this.talkLastTimeMs) this.talkLastTimeMs = epoch;
-            }
-        }
-
+        this.village.getPeriodList().stream()
+                .flatMap(period -> period.getTopicList().stream())
+                .filter(topic -> topic instanceof Talk)
+                .mapToLong(topic -> ((Talk) topic).getTimeFromID())
+                .forEach(epoch -> {
+                    if(this.talk1stTimeMs  < 0) this.talk1stTimeMs  = epoch;
+                    if(this.talkLastTimeMs < 0) this.talkLastTimeMs = epoch;
+
+                    if(epoch < this.talk1stTimeMs ) this.talk1stTimeMs  = epoch;
+                    if(epoch > this.talkLastTimeMs) this.talkLastTimeMs = epoch;
+                });
         return;
     }
 
@@ -342,9 +327,10 @@ public class GameSummary{
         List<SysEvent> eventList = this.eventMap.get(SysEventType.JUDGE);
 
         for(SysEvent event : eventList){
-            List<Avatar> avatarList  = event.getAvatarList();
-            Avatar avatar = avatarList.get(1);
-            Player seered = getPlayer(avatar);
+            List<InterPlay> interPlayList = event.getInterPlayList();
+            InterPlay judge = interPlayList.get(0);
+            Avatar target = judge.getTarget();
+            Player seered = getPlayer(target);
             GameRole role = seered.getRole();
             switch(role){
             case WOLF:    this.ctScryWolf++;    break;
@@ -359,6 +345,7 @@ public class GameSummary{
 
     /**
      * 占い師の活動を文字列化する。
+     *
      * @return 占い師の活動
      */
     public CharSequence dumpSeerActivity(){
@@ -407,9 +394,10 @@ public class GameSummary{
 
         eventList = this.eventMap.get(SysEventType.GUARD);
         for(SysEvent event : eventList){
-            List<Avatar> avatarList = event.getAvatarList();
-            Avatar avatar = avatarList.get(1);
-            Player guarded = getPlayer(avatar);
+            List<InterPlay> interPlayList = event.getInterPlayList();
+            InterPlay guard = interPlayList.get(0);
+            Avatar target = guard.getTarget();
+            Player guarded = getPlayer(target);
             GameRole guardedRole = guarded.getRole();
             switch(guardedRole){
             case WOLF:    this.ctGuardWolf++;    break;
@@ -419,15 +407,16 @@ public class GameSummary{
             }
         }
 
-        for(Period period : this.village.getPeriodList()){
+        this.village.getPeriodList().forEach(period -> {
             summarizeGjPeriod(period);
-        }
+        });
 
         return;
     }
 
     /**
      * 狩人GJの日ごとの集計。
+     *
      * @param period 日
      */
     private void summarizeGjPeriod(Period period){
@@ -468,7 +457,8 @@ public class GameSummary{
         if(sysEvent == null) return;
 
         if(hasAssaultTried){
-            Avatar guarded = sysEvent.getAvatarList().get(1);
+            InterPlay inter = sysEvent.getInterPlayList().get(0);
+            Avatar guarded = inter.getTarget();
             Player guardedPlayer = getPlayer(guarded);
             GameRole guardedRole = guardedPlayer.getRole();
             switch(guardedRole){
@@ -485,6 +475,7 @@ public class GameSummary{
 
     /**
      * 狩人の活動を文字列化する。
+     *
      * @return 狩人の活動
      */
     public CharSequence dumpHunterActivity(){
@@ -558,6 +549,7 @@ public class GameSummary{
 
     /**
      * 処刑概観を文字列化する。
+     *
      * @return 文字列化した処刑概観
      */
     public CharSequence dumpExecutionInfo(){
@@ -597,6 +589,7 @@ public class GameSummary{
 
     /**
      * 襲撃概観を文字列化する。
+     *
      * @return 文字列化した襲撃概観
      */
     public CharSequence dumpAssaultInfo(){
@@ -636,23 +629,28 @@ public class GameSummary{
 
     /**
      * まとめサイト用投票Boxを生成する。
+     *
      * @return 投票BoxのWikiテキスト
      */
     public CharSequence dumpVoteBox(){
         StringBuilder wikiText = new StringBuilder();
 
-        for(Player player : getCastingPlayerList()){
-            Avatar avatar = player.getAvatar();
-            if(avatar == Avatar.AVATAR_GERD) continue;
-            GameRole role = player.getRole();
-            CharSequence fullName = avatar.getFullName();
-            CharSequence roleName = role.getRoleName();
-            StringBuilder line = new StringBuilder();
-            line.append("[").append(roleName).append("] ").append(fullName);
-            if(wikiText.length() > 0) wikiText.append(',');
-            wikiText.append(WolfBBS.escapeWikiSyntax(line));
-            wikiText.append("[0]");
-        }
+        getCastingPlayerList().stream()
+                .filter(player ->
+                        ! player.getAvatar().equals(Avatar.AVATAR_GERD)
+                )
+                .forEach(player -> {
+                    Avatar avatar = player.getAvatar();
+                    GameRole role = player.getRole();
+                    CharSequence fullName = avatar.getFullName();
+                    CharSequence roleName = role.getRoleName();
+                    StringBuilder line = new StringBuilder();
+                    line.append("[").append(roleName).append("] ")
+                            .append(fullName);
+                    if(wikiText.length() > 0) wikiText.append(',');
+                    wikiText.append(WolfBBS.escapeWikiSyntax(line));
+                    wikiText.append("[0]");
+                });
 
         wikiText.insert(0, "#vote(").append(")\n");
 
@@ -661,6 +659,7 @@ public class GameSummary{
 
     /**
      * まとめサイト用キャスト表を生成する。
+     *
      * @param iconSet 顔アイコンセット
      * @return キャスト表のWikiテキスト
      */
@@ -809,6 +808,7 @@ public class GameSummary{
 
     /**
      * 村詳細情報を出力する。
+     *
      * @return 村詳細情報
      */
     public CharSequence dumpVillageWiki(){
@@ -911,6 +911,7 @@ public class GameSummary{
 
     /**
      * 最初の発言の時刻を得る。
+     *
      * @return 時刻
      */
     public Date get1stTalkDate(){
@@ -919,6 +920,7 @@ public class GameSummary{
 
     /**
      * 最後の発言の時刻を得る。
+     *
      * @return 時刻
      */
     public Date getLastTalkDate(){
@@ -927,6 +929,7 @@ public class GameSummary{
 
     /**
      * 指定した日の生存者一覧を得る。
+     *
      * @param day 日
      * @return 生存者一覧
      */
@@ -946,32 +949,34 @@ public class GameSummary{
         }
 
         if(period.isEpilogue()){
-            for(Player player : this.playerList){
-                if(player.getDestiny() == Destiny.ALIVE){
-                    result.add(player);
-                }
-            }
+            this.playerList.stream()
+                    .filter(player -> player.getDestiny() == Destiny.ALIVE)
+                    .forEachOrdered(player -> {
+                        result.add(player);
+                    });
             return result;
         }
 
-        for(Topic topic : period.getTopicList()){
-            if( ! (topic instanceof SysEvent) ) continue;
-            SysEvent sysEvent = (SysEvent) topic;
-            if(sysEvent.getSysEventType() == SysEventType.SURVIVOR){
-                List<Avatar> avatarList = sysEvent.getAvatarList();
-                for(Avatar avatar : avatarList){
+        period.getTopicList().stream()
+                .filter(topic -> topic instanceof SysEvent)
+                .map(topic -> (SysEvent) topic)
+                .filter(sysEvent ->
+                        sysEvent.getSysEventType() == SysEventType.SURVIVOR
+                )
+                .flatMap(sysEvent -> sysEvent.getAvatarList().stream())
+                .forEachOrdered(avatar -> {
                     Player player = getPlayer(avatar);
                     result.add(player);
-                }
-            }
-        }
+                });
 
         return result;
     }
 
     /**
      * プレイヤー一覧を得る。
-     * 参加エントリー順
+     *
+     * <p>参加エントリー順
+     *
      * @return プレイヤーのリスト
      */
     public List<Player> getPlayerList(){
@@ -981,6 +986,7 @@ public class GameSummary{
 
     /**
      * キャスティング表用にソートされたプレイヤー一覧を得る。
+     *
      * @return プレイヤーのリスト
      */
     public List<Player> getCastingPlayerList(){
@@ -993,23 +999,20 @@ public class GameSummary{
 
     /**
      * 指定された役職のプレイヤー一覧を得る。
+     *
      * @param role 役職
      * @return 役職に合致するプレイヤーのリスト
      */
     public List<Player> getRoledPlayerList(GameRole role){
-        List<Player> result = new LinkedList<>();
-
-        for(Player player : this.playerList){
-            if(player.getRole() == role){
-                result.add(player);
-            }
-        }
-
+        List<Player> result = this.playerList.stream()
+                .filter(player -> player.getRole() == role)
+                .collect(Collectors.toList());
         return result;
     }
 
     /**
      * 勝利陣営を得る。
+     *
      * @return 勝利した陣営
      */
     public Team getWinnerTeam(){
@@ -1018,18 +1021,21 @@ public class GameSummary{
 
     /**
      * 突然死者数を得る。
+     *
      * @return 突然死者数
      */
     public int countSuddenDeath(){
-        int suddenDeath = 0;
-        for(Player player : this.playerList){
-            if(player.getDestiny() == Destiny.SUDDENDEATH) suddenDeath++;
-        }
-        return suddenDeath;
+        long suddenDeath = this.playerList.stream()
+                .filter(player -> player.getDestiny() == Destiny.SUDDENDEATH)
+                .count();
+
+        int result = (int) suddenDeath;
+        return result;
     }
 
     /**
      * 参加プレイヤー総数を得る。
+     *
      * @return プレイヤー総数
      */
     public int countAvatarNum(){
@@ -1039,7 +1045,9 @@ public class GameSummary{
 
     /**
      * AvatarからPlayerを得る。
-     * 参加していないAvatarならnullを返す。
+     *
+     * <p>参加していないAvatarならnullを返す。
+     *
      * @param avatar Avatar
      * @return Player
      */
@@ -1049,24 +1057,9 @@ public class GameSummary{
     }
 
     /**
-     * AvatarからPlayerを得る。
-     * 無ければ新規に作る。
-     * @param avatar Avatar
-     * @return Player
-     */
-    private Player registPlayer(Avatar avatar){
-        Player player = getPlayer(avatar);
-        if(player == null){
-            player = new Player();
-            player.setAvatar(avatar);
-            this.playerMap.put(avatar, player);
-        }
-        return player;
-    }
-
-    /**
      * プレイヤーのソート仕様の記述。
-     * まとめサイトのキャスト表向け。
+     *
+     * <p>まとめサイトのキャスト表向け。
      */
     private static final class CastingComparator
             implements Comparator<Player> {
@@ -1081,6 +1074,7 @@ public class GameSummary{
 
         /**
          * {@inheritDoc}
+         *
          * @param p1 {@inheritDoc}
          * @param p2 {@inheritDoc}
          * @return {@inheritDoc}
index 17c7014..3c58acc 100644 (file)
@@ -52,6 +52,7 @@ import jp.sfjp.jindolf.dxchg.WebButton;
 import jp.sfjp.jindolf.dxchg.WolfBBS;
 import jp.sfjp.jindolf.util.GUIUtils;
 import jp.sfjp.jindolf.util.Monodizer;
+import jp.sfjp.jindolf.view.AvatarPics;
 import jp.sourceforge.jindolf.corelib.GameRole;
 import jp.sourceforge.jindolf.corelib.Team;
 
@@ -756,7 +757,8 @@ public class VillageDigest
             this.nextPlayer.setEnabled(true);
         }
 
-        Image image = this.village.getAvatarFaceImage(avatar);
+        AvatarPics avatarPics = this.village.getAvatarPics();
+        Image image = avatarPics.getAvatarFaceImage(avatar);
         this.faceIcon.setImage(image);
         this.faceLabel.setIcon(null);          // なぜかこれが必要
         this.faceLabel.setIcon(this.faceIcon);
index 692369f..830fa90 100644 (file)
@@ -7,8 +7,6 @@
 
 package jp.sfjp.jindolf.util;
 
-import java.util.regex.Matcher;
-
 /**
  * 文字列ユーティリティクラス。
  */
@@ -28,55 +26,6 @@ public final class StringUtils{
 
 
     /**
-     * 正規表現にマッチした領域を数値化する。
-     * @param seq 文字列
-     * @param matcher Matcher
-     * @param groupIndex 前方指定グループ番号
-     * @return 数値
-     * @throws IndexOutOfBoundsException 不正なグループ番号
-     */
-    public static int parseInt(CharSequence seq,
-                                Matcher matcher,
-                                int groupIndex     )
-            throws IndexOutOfBoundsException {
-        return parseInt(seq,
-                        matcher.start(groupIndex),
-                        matcher.end(groupIndex)   );
-    }
-
-    /**
-     * 文字列を数値化する。
-     * @param seq 文字列
-     * @return 数値
-     */
-    public static int parseInt(CharSequence seq){
-        return parseInt(seq, 0, seq.length());
-    }
-
-    /**
-     * 部分文字列を数値化する。
-     * @param seq 文字列
-     * @param startPos 範囲開始位置
-     * @param endPos 範囲終了位置
-     * @return パースした数値
-     * @throws IndexOutOfBoundsException 不正な位置指定
-     */
-    public static int parseInt(CharSequence seq, int startPos, int endPos)
-            throws IndexOutOfBoundsException{
-        int result = 0;
-
-        for(int pos = startPos; pos < endPos; pos++){
-            char ch = seq.charAt(pos);
-            int digit = Character.digit(ch, 10);
-            if(digit < 0) break;
-            result *= 10;
-            result += digit;
-        }
-
-        return result;
-    }
-
-    /**
      * 長い文字列を三点リーダで省略する。
      * 「abcdefg」→「abc…efg」
      * @param str 文字列
@@ -94,34 +43,6 @@ public final class StringUtils{
     }
 
     /**
-     * ある文字列の末尾が別の文字列に一致するか判定する。
-     * @param target 判定対象
-     * @param term 末尾文字
-     * @return 一致すればtrue
-     * @throws java.lang.NullPointerException 引数がnull
-     * @see String#endsWith(String)
-     */
-    public static boolean isTerminated(CharSequence target,
-                                         CharSequence term)
-            throws NullPointerException{
-        if(target == null || term == null) throw new NullPointerException();
-
-        int targetLength = target.length();
-        int termLength   = term  .length();
-
-        int offset = targetLength - termLength;
-        if(offset < 0) return false;
-
-        for(int pos = 0; pos < termLength; pos++){
-            char targetch = target.charAt(offset + pos);
-            char termch   = term  .charAt(0      + pos);
-            if(targetch != termch) return false;
-        }
-
-        return true;
-    }
-
-    /**
      * サブシーケンス同士を比較する。
      * @param seq1 サブシーケンス1
      * @param start1 開始インデックス1
diff --git a/src/main/java/jp/sfjp/jindolf/view/AccountPanel.java b/src/main/java/jp/sfjp/jindolf/view/AccountPanel.java
deleted file mode 100644 (file)
index 1c26d24..0000000
+++ /dev/null
@@ -1,499 +0,0 @@
-/*
- * Account panel
- *
- * License : The MIT License
- * Copyright(c) 2008 olyutorskii
- */
-
-package jp.sfjp.jindolf.view;
-
-import java.awt.BorderLayout;
-import java.awt.Container;
-import java.awt.Frame;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import javax.swing.BorderFactory;
-import javax.swing.JButton;
-import javax.swing.JComboBox;
-import javax.swing.JComponent;
-import javax.swing.JDialog;
-import javax.swing.JLabel;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JPasswordField;
-import javax.swing.JSeparator;
-import javax.swing.JTextArea;
-import javax.swing.JTextField;
-import javax.swing.border.Border;
-import jp.sfjp.jindolf.VerInfo;
-import jp.sfjp.jindolf.data.Land;
-import jp.sfjp.jindolf.data.LandsModel;
-import jp.sfjp.jindolf.dxchg.TextPopup;
-import jp.sfjp.jindolf.net.ServerAccess;
-import jp.sfjp.jindolf.util.GUIUtils;
-import jp.sfjp.jindolf.util.Monodizer;
-import jp.sourceforge.jindolf.corelib.LandState;
-
-/**
- * ログインパネル。
- */
-@SuppressWarnings("serial")
-public class AccountPanel
-        extends JDialog
-        implements ActionListener, ItemListener{
-
-    private static final Logger LOGGER = Logger.getAnonymousLogger();
-
-
-    private final Map<Land, String> landUserIDMap =
-            new HashMap<>();
-    private final Map<Land, char[]> landPasswordMap =
-            new HashMap<>();
-
-    private final JComboBox<Land> landBox = new JComboBox<>();
-    private final JTextField idField = new JTextField(15);
-    private final JPasswordField pwField = new JPasswordField(15);
-    private final JButton loginButton = new JButton("ログイン");
-    private final JButton logoutButton = new JButton("ログアウト");
-    private final JButton closeButton = new JButton("閉じる");
-    private final JTextArea status = new JTextArea();
-
-    /**
-     * アカウントパネルを生成。
-     * @param owner フレームオーナー
-     */
-    @SuppressWarnings("LeakingThisInConstructor")
-    public AccountPanel(Frame owner){
-        super(owner);
-        setModal(true);
-
-        GUIUtils.modifyWindowAttributes(this, true, false, true);
-
-        this.landBox.setToolTipText("アカウント管理する国を選ぶ");
-        this.idField.setToolTipText("IDを入力してください");
-        this.pwField.setToolTipText("パスワードを入力してください");
-
-        Monodizer.monodize(this.idField);
-        Monodizer.monodize(this.pwField);
-
-        this.idField.setMargin(new Insets(1, 4, 1, 4));
-        this.pwField.setMargin(new Insets(1, 4, 1, 4));
-
-        this.idField.setComponentPopupMenu(new TextPopup());
-
-        this.landBox.setEditable(false);
-        this.landBox.addItemListener(this);
-
-        this.status.setEditable(false);
-        this.status.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
-        this.status.setRows(2);
-        this.status.setLineWrap(true);
-
-        this.loginButton.addActionListener(this);
-        this.logoutButton.addActionListener(this);
-        this.closeButton.addActionListener(this);
-
-        getRootPane().setDefaultButton(this.loginButton);
-
-        Container content = getContentPane();
-        GridBagLayout layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-        content.setLayout(layout);
-
-        constraints.gridwidth = GridBagConstraints.REMAINDER;
-        constraints.weightx = 1.0;
-        constraints.insets = new Insets(5, 5, 5, 5);
-
-        JComponent accountPanel = createCredential();
-        JComponent buttonPanel = createButtonPanel();
-
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        content.add(accountPanel, constraints);
-
-        Border border = BorderFactory.createTitledBorder("ログインステータス");
-        JPanel panel = new JPanel();
-        panel.setLayout(new BorderLayout());
-        panel.add(this.status, BorderLayout.CENTER);
-        panel.setBorder(border);
-
-        constraints.weighty = 1.0;
-        constraints.fill = GridBagConstraints.BOTH;
-        content.add(panel, constraints);
-
-        constraints.weighty = 0.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        content.add(new JSeparator(), constraints);
-
-        content.add(buttonPanel, constraints);
-
-        return;
-    }
-
-    /**
-     * 認証パネルを生成する。
-     * @return 認証パネル
-     */
-    private JComponent createCredential(){
-        JPanel credential = new JPanel();
-
-        GridBagLayout layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        credential.setLayout(layout);
-
-        constraints.insets = new Insets(5, 5, 5, 5);
-        constraints.fill = GridBagConstraints.NONE;
-
-        constraints.anchor = GridBagConstraints.EAST;
-        credential.add(new JLabel("国名 :"), constraints);
-        constraints.anchor = GridBagConstraints.WEST;
-        credential.add(this.landBox, constraints);
-
-        constraints.gridy = 1;
-        constraints.anchor = GridBagConstraints.EAST;
-        constraints.weightx = 0.0;
-        constraints.fill = GridBagConstraints.NONE;
-        credential.add(new JLabel("ID :"), constraints);
-        constraints.anchor = GridBagConstraints.WEST;
-        constraints.weightx = 1.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        credential.add(this.idField, constraints);
-
-        constraints.gridy = 2;
-        constraints.anchor = GridBagConstraints.EAST;
-        constraints.weightx = 0.0;
-        constraints.fill = GridBagConstraints.NONE;
-        credential.add(new JLabel("パスワード :"), constraints);
-        constraints.anchor = GridBagConstraints.WEST;
-        constraints.weightx = 1.0;
-        constraints.fill = GridBagConstraints.HORIZONTAL;
-        credential.add(this.pwField, constraints);
-
-        return credential;
-    }
-
-    /**
-     * ボタンパネルの作成。
-     * @return ボタンパネル
-     */
-    private JComponent createButtonPanel(){
-        JPanel buttonPanel = new JPanel();
-
-        GridBagLayout layout = new GridBagLayout();
-        GridBagConstraints constraints = new GridBagConstraints();
-
-        buttonPanel.setLayout(layout);
-
-        constraints.fill = GridBagConstraints.NONE;
-        constraints.anchor = GridBagConstraints.WEST;
-        constraints.weightx = 0.0;
-        constraints.weighty = 0.0;
-
-        buttonPanel.add(this.loginButton, constraints);
-
-        constraints.insets = new Insets(0, 5, 0, 0);
-        buttonPanel.add(this.logoutButton, constraints);
-
-        constraints.anchor = GridBagConstraints.EAST;
-        constraints.weightx = 1.0;
-        constraints.insets = new Insets(0, 15, 0, 0);
-        buttonPanel.add(this.closeButton, constraints);
-
-        return buttonPanel;
-    }
-
-    /**
-     * 現在コンボボックスで選択中の国を返す。
-     * @return 現在選択中のLand
-     */
-    private Land getSelectedLand(){
-        Land land = (Land) ( this.landBox.getSelectedItem() );
-        return land;
-    }
-
-    /**
-     * ACTIVEな最初の国がコンボボックスで既に選択されている状態にする。
-     */
-    private void preSelectActiveLand(){
-        for(int index = 0; index < this.landBox.getItemCount(); index++){
-            Object item = this.landBox.getItemAt(index);
-            Land land = (Land) item;
-            LandState state = land.getLandDef().getLandState();
-            if(state == LandState.ACTIVE){
-                this.landBox.setSelectedItem(land);
-                return;
-            }
-        }
-        return;
-    }
-
-    /**
-     * 指定された国のユーザIDを返す。
-     * @param land 国
-     * @return ユーザID
-     */
-    private String getUserID(Land land){
-        return this.landUserIDMap.get(land);
-    }
-
-    /**
-     * 指定された国のパスワードを返す。
-     * @param land 国
-     * @return パスワード
-     */
-    private char[] getPassword(Land land){
-        return this.landPasswordMap.get(land);
-    }
-
-    /**
-     * ネットワークエラーを通知するモーダルダイアログを表示する。
-     * OKボタンを押すまでこのメソッドは戻ってこない。
-     * @param e ネットワークエラー
-     */
-    protected void showNetworkError(IOException e){
-        LOGGER.log(Level.WARNING,
-                "アカウント処理中にネットワークのトラブルが発生しました", e);
-
-        Land land = getSelectedLand();
-        ServerAccess server = land.getServerAccess();
-        String message =
-                land.getLandDef().getLandName()
-                +"を運営するサーバとの間の通信で"
-                +"何らかのトラブルが発生しました。\n"
-                +"相手サーバのURLは [ " + server.getBaseURL() + " ] だよ。\n"
-                +"Webブラウザでも遊べないか確認してみてね!\n";
-
-        JOptionPane pane = new JOptionPane(message,
-                                           JOptionPane.WARNING_MESSAGE,
-                                           JOptionPane.DEFAULT_OPTION );
-
-        String title = VerInfo.getFrameTitle("通信異常発生");
-        JDialog dialog = pane.createDialog(this, title);
-
-        dialog.pack();
-        dialog.setVisible(true);
-        dialog.dispose();
-
-        return;
-    }
-
-    /**
-     * アカウントエラーを通知するモーダルダイアログを表示する。
-     * OKボタンを押すまでこのメソッドは戻ってこない。
-     */
-    protected void showIllegalAccountDialog(){
-        Land land = getSelectedLand();
-        String message =
-                land.getLandDef().getLandName()
-                +"へのログインに失敗しました。\n"
-                +"ユーザ名とパスワードは本当に正しいかな?\n"
-                +"あなたは本当に [ " + getUserID(land) + " ] さんかな?\n"
-                +"WebブラウザによるID登録手続きは本当に完了してるかな?\n"
-                +"Webブラウザでもログインできないか試してみて!\n"
-                +"…ユーザ名やパスワードにある種の特殊文字を使っている人は"
-                +"問題があるかも。";
-
-        JOptionPane pane = new JOptionPane(message,
-                                           JOptionPane.WARNING_MESSAGE,
-                                           JOptionPane.DEFAULT_OPTION );
-
-        String title = VerInfo.getFrameTitle("ログイン認証失敗");
-        JDialog dialog = pane.createDialog(this, title);
-
-        dialog.pack();
-        dialog.setVisible(true);
-        dialog.dispose();
-
-        return;
-    }
-
-    /**
-     * 入力されたアカウント情報を基に現在選択中の国へログインする。
-     * @return ログインに成功すればtrueを返す。
-     */
-    protected boolean login(){
-        Land land = getSelectedLand();
-        ServerAccess server = land.getServerAccess();
-
-        String id = this.idField.getText();
-        char[] password = this.pwField.getPassword();
-        this.landUserIDMap.put(land, id);
-        this.landPasswordMap.put(land, password);
-
-        boolean result = false;
-        try{
-            result = server.login(id, password);
-        }catch(IOException e){
-            showNetworkError(e);
-            return false;
-        }
-
-        if( ! result ){
-            showIllegalAccountDialog();
-        }
-
-        return result;
-    }
-
-    /**
-     * 現在選択中の国からログアウトする。
-     */
-    protected void logout(){
-        try{
-            logoutInternal();
-        }catch(IOException e){
-            showNetworkError(e);
-        }
-        return;
-    }
-
-    /**
-     * 現在選択中の国からログアウトする。
-     * @throws java.io.IOException ネットワークエラー
-     */
-    protected void logoutInternal() throws IOException{
-        Land land = getSelectedLand();
-        ServerAccess server = land.getServerAccess();
-        server.logout();
-        return;
-    }
-
-    /**
-     * 現在選択中の国のログイン状態に合わせてGUIを更新する。
-     */
-    private void updateGUI(){
-        Land land = getSelectedLand();
-        if(land == null) return;
-
-        LandState state = land.getLandDef().getLandState();
-        ServerAccess server = land.getServerAccess();
-        boolean hasLoggedIn = server.hasLoggedIn();
-
-        if(state != LandState.ACTIVE){
-            this.status.setText(
-                     "この国は既に募集を停止しました。\n"
-                    +"ログインは無意味です" );
-            this.idField.setEnabled(false);
-            this.pwField.setEnabled(false);
-            this.loginButton.setEnabled(false);
-            this.logoutButton.setEnabled(false);
-        }else if(hasLoggedIn){
-            this.status.setText("ユーザ [ " + getUserID(land) + " ] として\n"
-                          +"現在ログイン中です");
-            this.idField.setEnabled(false);
-            this.pwField.setEnabled(false);
-            this.loginButton.setEnabled(false);
-            this.logoutButton.setEnabled(true);
-        }else{
-            this.status.setText("現在ログインしていません");
-            this.idField.setEnabled(true);
-            this.pwField.setEnabled(true);
-            this.loginButton.setEnabled(true);
-            this.logoutButton.setEnabled(false);
-        }
-
-        return;
-    }
-
-    /**
-     * 国情報を設定する。
-     * @param model 国情報
-     * @throws NullPointerException 引数がnull
-     */
-    public void setModel(LandsModel model) throws NullPointerException{
-        if(model == null) throw new NullPointerException();
-
-        this.landUserIDMap.clear();
-        this.landPasswordMap.clear();
-        this.landBox.removeAllItems();
-
-        for(Land land : model.getLandList()){
-            String userID = "";
-            char[] password = {};
-            this.landUserIDMap.put(land, userID);
-            this.landPasswordMap.put(land, password);
-            this.landBox.addItem(land);
-        }
-
-        preSelectActiveLand();
-        updateGUI();
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * ボタン操作のリスナ。
-     * @param event イベント {@inheritDoc}
-     */
-    // TODO Return キー押下によるログインもサポートしたい
-    @Override
-    public void actionPerformed(ActionEvent event){
-        Object source = event.getSource();
-
-        if(source == this.closeButton){
-            setVisible(false);
-            dispose();
-            return;
-        }
-
-        if(source == this.loginButton){
-            login();
-        }else if(source == this.logoutButton){
-            logout();
-        }
-
-        updateGUI();
-
-        return;
-    }
-
-    /**
-     * {@inheritDoc}
-     * コンボボックス操作のリスナ。
-     * @param event イベント {@inheritDoc}
-     */
-    @Override
-    public void itemStateChanged(ItemEvent event){
-        Object source = event.getSource();
-        if(source != this.landBox) return;
-
-        Land land = (Land) event.getItem();
-        String id;
-        char[] password;
-
-        switch(event.getStateChange()){
-        case ItemEvent.SELECTED:
-            id = getUserID(land);
-            password = getPassword(land);
-            this.idField.setText(id);
-            this.pwField.setText(new String(password));
-            updateGUI();
-            break;
-        case ItemEvent.DESELECTED:
-            id = this.idField.getText();
-            password = this.pwField.getPassword();
-            this.landUserIDMap.put(land, id);
-            this.landPasswordMap.put(land, password);
-            break;
-        default:
-            assert false;
-            return;
-        }
-
-        return;
-    }
-
-    // TODO IDかパスワードが空の場合はログインボタンを無効にしたい
-}
index 10c5abe..711275f 100644 (file)
@@ -10,10 +10,11 @@ package jp.sfjp.jindolf.view;
 import java.awt.Insets;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
+import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import javax.swing.AbstractButton;
 import javax.swing.ButtonGroup;
 import javax.swing.ButtonModel;
@@ -32,13 +33,17 @@ import jp.sfjp.jindolf.VerInfo;
 import jp.sfjp.jindolf.util.GUIUtils;
 
 /**
- * ã\83¡ã\83\8bã\83¥ã\83¼ã\80\81ã\83\9cã\82¿ã\83³ã\80\81ã\81\9dã\81®ä»\96å\90\84種Actionã\82\92ä¼´ã\81\86ã\82¤ã\83\99ã\83³ã\83\88ã\82\92ç\94\9fæ\88\90する
+ * ã\83¡ã\83\8bã\83¥ã\83¼ã\80\81ã\83\9cã\82¿ã\83³ã\80\81ã\81\9dã\81®ä»\96å\90\84種Actionã\82\92ä¼´ã\81\86ã\82¤ã\83\99ã\83³ã\83\88ã\82\92ç\99ºç\81«する
  * コンポーネントの一括管理。
+ *
+ * <p>メニューバー、ツールバーを提供する。
+ *
+ * <p>アプリの状況に応じてメニューやボタンの一部の操作をマスクする。
  */
 public class ActionManager{
 
     /** アクション{@value}。 */
-    public static final String CMD_ACCOUNT    = "ACCOUNT";
+    public static final String CMD_OPENXML    = "OPENXML";
     /** アクション{@value}。 */
     public static final String CMD_EXIT       = "EXIT";
     /** アクション{@value}。 */
@@ -74,8 +79,6 @@ public class ActionManager{
     /** アクション{@value}。 */
     public static final String CMD_SHOWFILT   = "SHOWFILT";
     /** アクション{@value}。 */
-    public static final String CMD_SHOWEDIT   = "SHOWEDIT";
-    /** アクション{@value}。 */
     public static final String CMD_SHOWLOG    = "SHOWLOG";
     /** アクション{@value}。 */
     public static final String CMD_HELPDOC    = "HELPDOC";
@@ -98,7 +101,7 @@ public class ActionManager{
     public static final String CMD_FONTSIZESEL = "FONTSIZESEL";
 
     /** WWWアイコン。 */
-    public static final Icon ICON_WWW = GUIUtils.getWWWIcon();
+    public static final Icon ICON_WWW;
     /** 検索アイコン。 */
     public static final Icon ICON_FIND;
     /** 前検索アイコン。 */
@@ -112,36 +115,30 @@ public class ActionManager{
     /** 発言エディタアイコン。 */
     public static final Icon ICON_EDITOR;
 
-    private static final KeyStroke KEY_F1 = KeyStroke.getKeyStroke("F1");
-    private static final KeyStroke KEY_F3 = KeyStroke.getKeyStroke("F3");
-    private static final KeyStroke KEY_SHIFT_F3 =
-            KeyStroke.getKeyStroke("shift F3");
-    private static final KeyStroke KEY_F5 = KeyStroke.getKeyStroke("F5");
-    private static final KeyStroke KEY_CTRL_F =
-            KeyStroke.getKeyStroke("ctrl F");
+    private static final KeyStroke KEY_F1;
+    private static final KeyStroke KEY_F3;
+    private static final KeyStroke KEY_SHIFT_F3;
+    private static final KeyStroke KEY_F5;
+    private static final KeyStroke KEY_CTRL_F;
 
     static{
-        ICON_FIND =
-            ResourceManager.getButtonIcon("resources/image/tb_find.png");
-
-        ICON_SEARCH_PREV =
-            ResourceManager.getButtonIcon("resources/image/tb_findprev.png");
-
-        ICON_SEARCH_NEXT =
-            ResourceManager.getButtonIcon("resources/image/tb_findnext.png");
-
-        ICON_RELOAD =
-            ResourceManager.getButtonIcon("resources/image/tb_reload.png");
-
-        ICON_FILTER =
-            ResourceManager.getButtonIcon("resources/image/tb_filter.png");
-
-        ICON_EDITOR =
-            ResourceManager.getButtonIcon("resources/image/tb_editor.png");
+        ICON_WWW         = GUIUtils.getWWWIcon();
+        ICON_FIND        = loadIcon("resources/image/tb_find.png");
+        ICON_SEARCH_PREV = loadIcon("resources/image/tb_findprev.png");
+        ICON_SEARCH_NEXT = loadIcon("resources/image/tb_findnext.png");
+        ICON_RELOAD      = loadIcon("resources/image/tb_reload.png");
+        ICON_FILTER      = loadIcon("resources/image/tb_filter.png");
+        ICON_EDITOR      = loadIcon("resources/image/tb_editor.png");
+
+        KEY_F1       = KeyStroke.getKeyStroke("F1");
+        KEY_F3       = KeyStroke.getKeyStroke("F3");
+        KEY_SHIFT_F3 = KeyStroke.getKeyStroke("shift F3");
+        KEY_F5       = KeyStroke.getKeyStroke("F5");
+        KEY_CTRL_F   = KeyStroke.getKeyStroke("ctrl F");
     }
 
-    private final Set<AbstractButton> actionItems =
-            new HashSet<>();
+
+    private final List<AbstractButton> actionItems = new ArrayList<>(100);
     private final Map<String, JMenuItem> namedMenuItems =
             new HashMap<>();
     private final Map<String, JButton> namedToolButtons =
@@ -160,15 +157,18 @@ public class ActionManager{
     private final JMenu menuLook;
     private final ButtonGroup landfGroup = new ButtonGroup();
     private final Map<ButtonModel, String> landfMap =
-        new HashMap<>();
+        new ConcurrentHashMap<>();
 
     private final JToolBar browseToolBar;
 
+
     /**
      * コンストラクタ。
      */
     public ActionManager(){
-        this.menuFile       = buildMenu("Jindolf",  KeyEvent.VK_F);
+        super();
+
+        this.menuFile       = buildMenu("ファイル", KeyEvent.VK_F);
         this.menuEdit       = buildMenu("編集",     KeyEvent.VK_E);
         this.menuVillage    = buildMenu("村",       KeyEvent.VK_V);
         this.menuDay        = buildMenu("日",       KeyEvent.VK_D);
@@ -178,7 +178,40 @@ public class ActionManager{
 
         this.menuLook = buildLookAndFeelMenu("ルック&フィール", KeyEvent.VK_L);
 
-        buildMenuItem(CMD_ACCOUNT, "アカウント管理", KeyEvent.VK_M);
+        setupMenuItems();
+        setupToolButtons();
+        setupMenuItemIcons();
+
+        registKeyAccelerator();
+
+        this.menuBar       = buildMenuBar();
+        this.browseToolBar = buildBrowseToolBar();
+
+        exposeVillageImpl(false);
+        exposeVillageLocalImpl(false);
+        exposePeriodImpl(false);
+
+        return;
+    }
+
+
+    /**
+     * load icon from resource.
+     *
+     * @param resPath resource path name
+     * @return icon
+     */
+    private static Icon loadIcon(String resPath){
+        Icon result = ResourceManager.getButtonIcon(resPath);
+        return result;
+    }
+
+
+    /**
+     * setup menu items.
+     */
+    private void setupMenuItems(){
+        buildMenuItem(CMD_OPENXML, "アーカイブXMLを開く...", KeyEvent.VK_O);
         buildMenuItem(CMD_EXIT, "終了", KeyEvent.VK_X);
         buildMenuItem(CMD_COPY, "選択範囲をコピー", KeyEvent.VK_C);
         buildMenuItem(CMD_SHOWFIND, "検索...", KeyEvent.VK_F);
@@ -196,19 +229,29 @@ public class ActionManager{
         buildMenuItem(CMD_WEBDAY, "この日をブラウザで表示...", KeyEvent.VK_B);
         buildMenuItem(CMD_OPTION, "オプション...", KeyEvent.VK_O);
         buildMenuItem(CMD_SHOWFILT, "発言フィルタ", KeyEvent.VK_F);
-        buildMenuItem(CMD_SHOWEDIT, "発言エディタ", KeyEvent.VK_E);
         buildMenuItem(CMD_SHOWLOG, "ログ表示", KeyEvent.VK_S);
         buildMenuItem(CMD_HELPDOC, "ヘルプ表示", KeyEvent.VK_H);
         buildMenuItem(CMD_SHOWPORTAL, "ポータルサイト...", KeyEvent.VK_P);
         buildMenuItem(CMD_ABOUT, VerInfo.TITLE + "について...", KeyEvent.VK_A);
+        return;
+    }
 
+    /**
+     * setup toolbuttons.
+     */
+    private void setupToolButtons(){
         buildToolButton(CMD_RELOAD, "選択中の日を強制リロード", ICON_RELOAD);
         buildToolButton(CMD_SHOWFIND,   "検索",     ICON_FIND);
         buildToolButton(CMD_SEARCHPREV, "↑前候補", ICON_SEARCH_PREV);
         buildToolButton(CMD_SEARCHNEXT, "↓次候補", ICON_SEARCH_NEXT);
         buildToolButton(CMD_SHOWFILT, "発言フィルタ", ICON_FILTER);
-        buildToolButton(CMD_SHOWEDIT, "発言エディタ", ICON_EDITOR);
+        return;
+    }
 
+    /**
+     * setup menu item icons.
+     */
+    private void setupMenuItemIcons(){
         getMenuItem(CMD_SHOWPORTAL).setIcon(ICON_WWW);
         getMenuItem(CMD_WEBVILL)   .setIcon(ICON_WWW);
         getMenuItem(CMD_WEBWIKI)   .setIcon(ICON_WWW);
@@ -217,21 +260,12 @@ public class ActionManager{
         getMenuItem(CMD_SEARCHPREV).setIcon(ICON_SEARCH_PREV);
         getMenuItem(CMD_SEARCHNEXT).setIcon(ICON_SEARCH_NEXT);
         getMenuItem(CMD_SHOWFILT)  .setIcon(ICON_FILTER);
-        getMenuItem(CMD_SHOWEDIT)  .setIcon(ICON_EDITOR);
-
-        registKeyAccelerator();
-
-        this.menuBar       = buildMenuBar();
-        this.browseToolBar = buildBrowseToolBar();
-
-        appearVillageImpl(false);
-        appearPeriodImpl(false);
-
         return;
     }
 
     /**
      * メニューを生成する。
+     *
      * @param label メニューラベル
      * @param nemonic ニモニックキー
      * @return メニュー
@@ -249,14 +283,15 @@ public class ActionManager{
 
     /**
      * メニューアイテムを生成する。
+     *
      * @param command アクションコマンド名
      * @param label メニューラベル
      * @param nemonic ニモニックキー
      * @return メニューアイテム
      */
     private JMenuItem buildMenuItem(String command,
-                                      String label,
-                                      int nemonic ){
+                                    String label,
+                                    int nemonic ){
         JMenuItem result = new JMenuItem();
 
         String keyText = label + "(" + KeyEvent.getKeyText(nemonic) + ")";
@@ -273,20 +308,21 @@ public class ActionManager{
 
     /**
      * ツールボタンを生成する。
+     *
      * @param command アクションコマンド名
      * @param tooltip ツールチップ文字列
      * @param icon アイコン画像
      * @return ツールボタン
      */
     private JButton buildToolButton(String command,
-                                      String tooltip,
-                                      Icon icon){
+                                    String tooltip,
+                                    Icon icon){
         JButton result = new JButton();
 
-        result.setIcon(icon);
+        result.setActionCommand(command);
         result.setToolTipText(tooltip);
+        result.setIcon(icon);
         result.setMargin(new Insets(1, 1, 1, 1));
-        result.setActionCommand(command);
 
         this.actionItems.add(result);
         this.namedToolButtons.put(command, result);
@@ -295,10 +331,11 @@ public class ActionManager{
     }
 
     /**
-     * L&F 一覧メニューを作成する。
+     * L&amp;F 一覧メニューを作成する。
+     *
      * @param label メニューラベル
      * @param nemonic ニモニックキー
-     * @return L&F 一覧メニュー
+     * @return L&amp;F 一覧メニュー
      */
     private JMenu buildLookAndFeelMenu(String label, int nemonic){
         JMenu result = buildMenu(label, nemonic);
@@ -344,6 +381,7 @@ public class ActionManager{
 
     /**
      * アクションコマンド名からメニューアイテムを探す。
+     *
      * @param command アクションコマンド名
      * @return メニューアイテム
      */
@@ -354,6 +392,7 @@ public class ActionManager{
 
     /**
      * アクションコマンド名からツールボタンを探す。
+     *
      * @param command アクションコマンド名
      * @return ツールボタン
      */
@@ -363,7 +402,8 @@ public class ActionManager{
     }
 
     /**
-     * 現在メニューで選択中のL&amp;Fのクラス名を返す。
+     * 現在LookAndFeelメニューで選択中のL&amp;Fのクラス名を返す。
+     *
      * @return L&amp;F クラス名
      */
     public String getSelectedLookAndFeel(){
@@ -374,22 +414,24 @@ public class ActionManager{
     }
 
     /**
-     * 全てのボタンにアクションリスナーを登録する。
+     * 管理下の全てのボタンにアクションリスナーを登録する。
+     *
      * @param listener アクションリスナー
      */
     public void addActionListener(ActionListener listener){
-        for(AbstractButton button : this.actionItems){
+        this.actionItems.forEach(button -> {
             button.addActionListener(listener);
-        }
+        });
         return;
     }
 
     /**
      * メニューバーを生成する。
+     *
      * @return メニューバー
      */
     private JMenuBar buildMenuBar(){
-        this.menuFile.add(getMenuItem(CMD_ACCOUNT));
+        this.menuFile.add(getMenuItem(CMD_OPENXML));
         this.menuFile.addSeparator();
         this.menuFile.add(getMenuItem(CMD_EXIT));
 
@@ -416,7 +458,6 @@ public class ActionManager{
         this.menuPreference.add(this.menuLook);
 
         this.menuTool.add(getMenuItem(CMD_SHOWFILT));
-        this.menuTool.add(getMenuItem(CMD_SHOWEDIT));
         this.menuTool.add(getMenuItem(CMD_SHOWLOG));
 
         this.menuHelp.add(getMenuItem(CMD_HELPDOC));
@@ -439,6 +480,7 @@ public class ActionManager{
 
     /**
      * メニューバーを取得する。
+     *
      * @return メニューバー
      */
     public JMenuBar getMenuBar(){
@@ -447,6 +489,7 @@ public class ActionManager{
 
     /**
      * ブラウザ用ツールバーの生成を行う。
+     *
      * @return ツールバー
      */
     private JToolBar buildBrowseToolBar(){
@@ -459,13 +502,13 @@ public class ActionManager{
         toolBar.add(getToolButton(CMD_SEARCHPREV));
         toolBar.addSeparator();
         toolBar.add(getToolButton(CMD_SHOWFILT));
-        toolBar.add(getToolButton(CMD_SHOWEDIT));
 
         return toolBar;
     }
 
     /**
      * ブラウザ用ツールバーを取得する。
+     *
      * @return ツールバー
      */
     public JToolBar getBrowseToolBar(){
@@ -473,12 +516,11 @@ public class ActionManager{
     }
 
     /**
-     * Periodが表示されているか通知を受ける。
-     * @param appear 表示されているときはtrue
+     * Periodの選択表示状況に応じてUIをマスクする。
+     *
+     * @param appear Periodタブが選択されているときはtrue
      */
-    private void appearPeriodImpl(boolean appear){
-        if(appear) appearVillageImpl(appear);
-
+    private void exposePeriodImpl(boolean appear){
         this.menuEdit.setEnabled(appear);
         this.menuDay .setEnabled(appear);
 
@@ -491,32 +533,68 @@ public class ActionManager{
     }
 
     /**
-     * Periodが表示されているか通知を受ける。
-     * @param appear 表示されているときはtrue
+     * Periodの選択表示状況に応じてUIをマスクする。
+     *
+     * @param appear Periodタブが選択されているときはtrue
+     */
+    public void exposePeriod(boolean appear){
+        exposePeriodImpl(appear);
+        return;
+    }
+
+    /**
+     * 村ツリーの選択表示状況に応じてUIをマスクする。
+     *
+     * @param appear 村ノードが選択されているときはtrue
      */
-    public void appearPeriod(boolean appear){
-        appearPeriodImpl(appear);
+    private void exposeVillageImpl(boolean appear){
+        this.menuVillage.setEnabled(appear);
+
+        getMenuItem(CMD_RELOAD)    .setEnabled(appear);
+        getMenuItem(CMD_ALLPERIOD) .setEnabled(appear);
+
+        getToolButton(CMD_RELOAD)  .setEnabled(appear);
+
         return;
     }
 
     /**
-     * 村が表示されているか通知を受ける。
-     * @param appear 表示されているときはtrue
+     * 村ツリーの選択表示状況に応じてUIをマスクする。
+     *
+     * @param appear 村ノードが選択されているときはtrue
      */
-    private void appearVillageImpl(boolean appear){
-        if( ! appear) appearPeriodImpl(appear);
+    public void exposeVillage(boolean appear){
+        exposeVillageImpl(appear);
+        return;
+    }
 
+    /**
+     * ローカルXML村の表示状況に応じてUIをマスクする。
+     *
+     * <p>単一および全日程Periodの強制読み込みが抑止される。
+     *
+     * @param appear ローカルXMLが表示されているときはtrue
+     */
+    private void exposeVillageLocalImpl(boolean appear){
         this.menuVillage.setEnabled(appear);
 
+        getMenuItem(CMD_RELOAD)    .setEnabled( ! appear);
+        getMenuItem(CMD_ALLPERIOD) .setEnabled( ! appear);
+
+        getToolButton(CMD_RELOAD)  .setEnabled( ! appear);
+
         return;
     }
 
     /**
-     * 村が表示されているか通知を受ける。
-     * @param appear 表示されているときはtrue
+     * ローカルXML村の表示状況に応じてUIをマスクする。
+     *
+     * <p>単一および全日程Periodの強制読み込みが抑止される。
+     *
+     * @param appear ローカルXMLが表示されているときはtrue
      */
-    public void appearVillage(boolean appear){
-        appearVillageImpl(appear);
+    public void exposeVillageLocal(boolean appear){
+        exposeVillageLocalImpl(appear);
         return;
     }
 
diff --git a/src/main/java/jp/sfjp/jindolf/view/AvatarPics.java b/src/main/java/jp/sfjp/jindolf/view/AvatarPics.java
new file mode 100644 (file)
index 0000000..00c97d9
--- /dev/null
@@ -0,0 +1,243 @@
+/*
+ * Avatar image loader
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.view;
+
+import java.awt.image.BufferedImage;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import jp.sfjp.jindolf.data.Avatar;
+import jp.sfjp.jindolf.data.Land;
+import jp.sfjp.jindolf.util.GUIUtils;
+import jp.sourceforge.jindolf.corelib.LandDef;
+
+/**
+ * Avatarの顔、全身像、遺影、墓の画像をキャッシュ管理する。
+ */
+public class AvatarPics {
+
+    private final Land land;
+
+    private final Map<Avatar, BufferedImage> faceImageMap;
+    private final Map<Avatar, BufferedImage> bodyImageMap;
+    private final Map<Avatar, BufferedImage> faceMonoImageMap;
+    private final Map<Avatar, BufferedImage> bodyMonoImageMap;
+
+    private BufferedImage graveImage;
+    private BufferedImage graveBodyImage;
+
+
+    /**
+     * Constructor.
+     *
+     * @param land 国
+     */
+    public AvatarPics(Land land){
+        super();
+
+        Objects.nonNull(land);
+        this.land = land;
+
+        this.faceImageMap     = new HashMap<>();
+        this.bodyImageMap     = new HashMap<>();
+        this.faceMonoImageMap = new HashMap<>();
+        this.bodyMonoImageMap = new HashMap<>();
+
+        this.graveImage     = null;
+        this.graveBodyImage = null;
+
+        return;
+    }
+
+
+    /**
+     * Avatarの顔イメージを返す。
+     *
+     * @param avatar Avatar
+     * @return 顔イメージ
+     */
+    public BufferedImage getAvatarFaceImage(Avatar avatar){
+        BufferedImage result;
+        result = this.faceImageMap.get(avatar);
+        if(result != null) return result;
+
+        LandDef landDef = this.land.getLandDef();
+
+        String template = landDef.getFaceURITemplate();
+        int serialNo = avatar.getIdNum();
+        result = loadAvatarImage(template, serialNo);
+
+        this.faceImageMap.put(avatar, result);
+
+        return result;
+    }
+
+    /**
+     * Avatarの顔イメージを設定する。
+     *
+     * @param avatar Avatar
+     * @param image イメージ
+     */
+    public void setAvatarFaceImage(Avatar avatar, BufferedImage image){
+        this.faceImageMap.remove(avatar);
+        this.faceMonoImageMap.remove(avatar);
+        this.faceImageMap.put(avatar, image);
+        return;
+    }
+
+    /**
+     * Avatarの全身像イメージを返す。
+     *
+     * @param avatar Avatar
+     * @return 全身イメージ
+     */
+    public BufferedImage getAvatarBodyImage(Avatar avatar){
+        BufferedImage result;
+        result = this.bodyImageMap.get(avatar);
+        if(result != null) return result;
+
+        LandDef landDef = this.land.getLandDef();
+
+        String template = landDef.getBodyURITemplate();
+        int serialNo = avatar.getIdNum();
+        result = loadAvatarImage(template, serialNo);
+
+        this.bodyImageMap.put(avatar, result);
+
+        return result;
+    }
+
+    /**
+     * Avatarの全身像イメージを設定する。
+     *
+     * @param avatar Avatar
+     * @param image イメージ
+     */
+    public void setAvatarBodyImage(Avatar avatar, BufferedImage image){
+        this.bodyImageMap.remove(avatar);
+        this.bodyMonoImageMap.remove(avatar);
+        this.bodyImageMap.put(avatar, image);
+        return;
+    }
+
+    /**
+     * 各国URLテンプレートと通し番号から
+     * イメージをダウンロードする。
+     *
+     * @param template テンプレート
+     * @param serialNo Avatarの通し番号
+     * @return 顔もしくは全身像イメージ
+     */
+    private BufferedImage loadAvatarImage(String template, int serialNo){
+        String uri = MessageFormat.format(template, serialNo);
+
+        BufferedImage result;
+        result = this.land.downloadImage(uri);
+        if(result == null) result = GUIUtils.getNoImage();
+
+        return result;
+    }
+
+    /**
+     * Avatarのモノクロ顔イメージを返す。
+     *
+     * @param avatar Avatar
+     * @return 顔イメージ
+     */
+    public BufferedImage getAvatarFaceMonoImage(Avatar avatar){
+        BufferedImage result;
+        result = this.faceMonoImageMap.get(avatar);
+        if(result == null){
+            result = getAvatarFaceImage(avatar);
+            result = GUIUtils.createMonoImage(result);
+            this.faceMonoImageMap.put(avatar, result);
+        }
+        return result;
+    }
+
+    /**
+     * Avatarの全身像イメージを返す。
+     *
+     * @param avatar Avatar
+     * @return 全身イメージ
+     */
+    public BufferedImage getAvatarBodyMonoImage(Avatar avatar){
+        BufferedImage result;
+        result = this.bodyMonoImageMap.get(avatar);
+        if(result == null){
+            result = getAvatarBodyImage(avatar);
+            result = GUIUtils.createMonoImage(result);
+            this.bodyMonoImageMap.put(avatar, result);
+        }
+        return result;
+    }
+
+    /**
+     * 国別の墓イメージを返す。
+     *
+     * @return 墓イメージ
+     */
+    public BufferedImage getGraveImage(){
+        if(this.graveImage != null) return this.graveImage;
+
+        BufferedImage result = this.land.getGraveIconImage();
+        this.graveImage = result;
+
+        return result;
+    }
+
+    /**
+     * 墓イメージを設定する。
+     *
+     * @param image イメージ
+     */
+    public void setGraveImage(BufferedImage image){
+        this.graveImage = image;
+        return;
+    }
+
+    /**
+     * 国別の墓イメージ(大)を返す。
+     *
+     * @return 墓イメージ(大)
+     */
+    public BufferedImage getGraveBodyImage(){
+        if(this.graveBodyImage != null) return this.graveBodyImage;
+
+        BufferedImage result = this.land.getGraveBodyImage();
+        this.graveBodyImage = result;
+
+        return result;
+    }
+
+    /**
+     * 墓イメージ(大)を設定する。
+     *
+     * @param image イメージ
+     */
+    public void setGraveBodyImage(BufferedImage image){
+        this.graveBodyImage = image;
+        return;
+    }
+
+    /**
+     * 全画像のキャッシュへの格納を試みる。
+     */
+    public void preload(){
+        for(Avatar avatar : Avatar.getPredefinedAvatarList()){
+            getAvatarFaceImage(avatar);
+            getAvatarBodyImage(avatar);
+            getAvatarFaceMonoImage(avatar);
+            getAvatarBodyMonoImage(avatar);
+            getGraveImage();
+            getGraveBodyImage();
+        }
+    }
+
+}
index 9162f5a..2100db2 100644 (file)
@@ -172,7 +172,7 @@ public class HelpFrame extends JFrame
 
         if(configStore.useStoreFile()){
             info.append("設定格納ディレクトリ : ")
-                .append(configStore.getConfigPath().getPath());
+                .append(configStore.getConfigDir().getPath());
         }else{
             info.append("※ 設定格納ディレクトリは使っていません。");
         }
index e663533..4f91e78 100644 (file)
@@ -28,7 +28,7 @@ import javax.swing.tree.TreePath;
 import javax.swing.tree.TreeSelectionModel;
 import jp.sfjp.jindolf.ResourceManager;
 import jp.sfjp.jindolf.data.Land;
-import jp.sfjp.jindolf.data.LandsModel;
+import jp.sfjp.jindolf.data.LandsTreeModel;
 
 /**
  * 国一覧Tree周辺コンポーネント群。
@@ -151,13 +151,13 @@ public class LandsTree
     }
 
     /**
-     * 管理下のLandsModelを返す。
-     * @return LandsModel
+     * 管理下のLandsTreeModelを返す。
+     * @return LandsTreeModel
      */
-    private LandsModel getLandsModel(){
+    private LandsTreeModel getLandsModel(){
         TreeModel model = this.treeView.getModel();
-        if(model instanceof LandsModel){
-            return (LandsModel) model;
+        if(model instanceof LandsTreeModel){
+            return (LandsTreeModel) model;
         }
         return null;
     }
@@ -179,7 +179,7 @@ public class LandsTree
 
         final TreePath lastPath = this.treeView.getSelectionPath();
 
-        LandsModel model = getLandsModel();
+        LandsTreeModel model = getLandsModel();
         if(model != null){
             model.setAscending(this.ascending);
         }
diff --git a/src/main/java/jp/sfjp/jindolf/view/LocalAvatarImg.java b/src/main/java/jp/sfjp/jindolf/view/LocalAvatarImg.java
new file mode 100644 (file)
index 0000000..481c9dc
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * local avatar images
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+package jp.sfjp.jindolf.view;
+
+import java.awt.image.BufferedImage;
+import java.text.MessageFormat;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import jp.sfjp.jindolf.ResourceManager;
+import jp.sfjp.jindolf.data.Avatar;
+
+/**
+ * 人狼BBSサーバにアクセスできなくなる将来に備えた代替イメージの諸々。
+ *
+ * <p>リソースに格納した代替Avatarイメージへのアクセスを提供する。
+ *
+ * <p>2020-04現在、凪庵氏作の新旧Avatarイメージは
+ * 人狼BBSサーバ群より公衆送信中。
+ *
+ * @see <a href="http://ninjinix.com/">NINJINIX.COM</a>
+ * @see <a href="http://yoroz.jp/">路地裏萬亭</a>
+ */
+public final class LocalAvatarImg {
+
+    private static final String IMGDIR = "resources/image/avatar";
+    private static final String TEMPLATE_FACE =
+            IMGDIR + "/face{0,number,#00}.png";
+    private static final String TEMPLATE_BODY =
+            IMGDIR + "/body{0,number,#00}.png";
+    private static final String RES_GRAVE     = IMGDIR + "/face99.png";
+    private static final String RES_GRAVEBODY = IMGDIR + "/body99.png";
+
+    private static final Map<String, BufferedImage> FACE_MAP;
+    private static final Map<String, BufferedImage> BODY_MAP;
+
+    private static final BufferedImage GRAVE_IMAGE;
+    private static final BufferedImage GRAVEBODY_IMAGE;
+
+    static{
+        FACE_MAP = loadTemplateResImg(TEMPLATE_FACE);
+        BODY_MAP = loadTemplateResImg(TEMPLATE_BODY);
+
+        GRAVE_IMAGE     = ResourceManager.getBufferedImage(RES_GRAVE);
+        GRAVEBODY_IMAGE = ResourceManager.getBufferedImage(RES_GRAVEBODY);
+    }
+
+
+    /**
+     * Hidden constructor.
+     */
+    private LocalAvatarImg(){
+        assert false;
+    }
+
+
+    /**
+     * リソース名テンプレートにAvatarIdNumを適用して得られたリソースから
+     * イメージを読み込む。
+     *
+     * @param resForm リソース名テンプレート
+     * @return AvatarIdとリソースイメージからなるマップ
+     */
+    private static Map<String, BufferedImage> loadTemplateResImg(String resForm){
+        Map<String, BufferedImage> result = new HashMap<>();
+
+        Avatar.getPredefinedAvatarList().forEach(avatar -> {
+            String avatarId = avatar.getIdentifier();
+            int idNum = avatar.getIdNum();
+            String res = MessageFormat.format(resForm, idNum);
+
+            BufferedImage img = ResourceManager.getBufferedImage(res);
+            assert img != null;
+            result.put(avatarId, img);
+        });
+
+        return Collections.unmodifiableMap(result);
+    }
+
+
+    /**
+     * Avatarの代替顔イメージを返す。
+     *
+     * @param avatarId AvatarId
+     * @return 代替顔イメージ
+     */
+    public static BufferedImage getAvatarFaceImage(String avatarId){
+        BufferedImage result = FACE_MAP.get(avatarId);
+        return result;
+    }
+
+    /**
+     * Avatarの代替全身像イメージを返す。
+     *
+     * @param avatarId AvatarId
+     * @return 代替全身像イメージ
+     */
+    public static BufferedImage getAvatarBodyImage(String avatarId){
+        BufferedImage result = BODY_MAP.get(avatarId);
+        return result;
+    }
+
+    /**
+     * 代替墓イメージを返す。
+     *
+     * @return 代替墓イメージ
+     */
+    public static BufferedImage getGraveImage(){
+        return GRAVE_IMAGE;
+    }
+
+    /**
+     * 代替墓イメージ(大)を返す。
+     *
+     * @return 代替墓イメージ(大)
+     */
+    public static BufferedImage getGraveBodyImage(){
+        return GRAVEBODY_IMAGE;
+    }
+
+}
index fd44be2..2a04bf0 100644 (file)
@@ -41,7 +41,17 @@ import jp.sfjp.jindolf.glyph.TalkDraw;
 import jp.sourceforge.jindolf.corelib.TalkType;
 
 /**
- * 発言ブラウザを内包するPeriodビューワ。
+ * 各Periodのビュー。
+ *
+ * <P>Periodの内容が反映される。
+ *
+ * <p>キャプション(村名、Period名)、
+ * リミット(更新時刻)、
+ * プルダウンリストによる会話セレクタ
+ * の管理制御を担当する。
+ *
+ * <p>会話表示{@link Discussion}のスクロール制御
+ * を担当する。
  */
 @SuppressWarnings("serial")
 public class PeriodView extends JPanel implements ItemListener{
@@ -50,6 +60,7 @@ public class PeriodView extends JPanel implements ItemListener{
     private static final Color COLOR_NORMALBG = Color.BLACK;
     private static final Color COLOR_SIMPLEBG = Color.WHITE;
 
+
     private Period period;
 
     private final Discussion discussion;
@@ -62,8 +73,10 @@ public class PeriodView extends JPanel implements ItemListener{
 
     private DialogPref dialogPref = new DialogPref();
 
+
     /**
-     * 発言ブラウザを内包するPeriodビューワを生成する。
+     * コンストラクタ。
+     *
      * @param period 日
      */
     @SuppressWarnings("LeakingThisInConstructor")
@@ -155,8 +168,10 @@ public class PeriodView extends JPanel implements ItemListener{
 
     /**
      * Periodを更新する。
-     * 古いPeriodの表示内容は消える。
+     *
+     * <p>古いPeriodの表示内容は消える。
      * 新しいPeriodの表示内容はまだ反映されない。
+     *
      * @param period 新しいPeriod
      */
     public void setPeriod(Period period){
@@ -171,6 +186,7 @@ public class PeriodView extends JPanel implements ItemListener{
 
     /**
      * 現在のPeriodを返す。
+     *
      * @return 現在のPeriod
      */
     public Period getPeriod(){
@@ -178,7 +194,7 @@ public class PeriodView extends JPanel implements ItemListener{
     }
 
     /**
-     * 上部のGUI(村名、発言一覧)を、Periodの状態に合わせて更新する。
+     * 上部の村名、会話プルダウンリストを、Periodの状態に合わせて更新する。
      */
     private void updateTopPanel(){
         if(this.period == null){
@@ -193,18 +209,8 @@ public class PeriodView extends JPanel implements ItemListener{
 
         String dayCaption   = this.period.getCaption();
         String limitCaption = this.period.getLimit();
-        String account      = this.period.getLoginName();
-
-        String loginout;
-        if(this.period.isFullOpen()){
-            loginout = "";
-        }else if(account != null){
-            loginout = " (ログイン中)";
-        }else{
-            loginout = " (ログアウト中)";
-        }
 
-        String info = villageName + "村 " + dayCaption + loginout;
+        String info = villageName + "村 " + dayCaption;
         this.caption.setText(info);
         this.limit.setText("更新時刻 " + limitCaption);
 
@@ -232,6 +238,7 @@ public class PeriodView extends JPanel implements ItemListener{
 
     /**
      * フォント描画設定を変更する。
+     *
      * @param fontInfo フォント設定
      */
     // TODO スクロール位置の復元
@@ -245,7 +252,8 @@ public class PeriodView extends JPanel implements ItemListener{
     }
 
     /**
-     * 発言表示設定を更新する。
+     * 会話表示設定を変更する。
+     *
      * @param pref 表示設定
      */
     public void setDialogPref(DialogPref pref){
@@ -260,15 +268,17 @@ public class PeriodView extends JPanel implements ItemListener{
     }
 
     /**
-     * ビューポート内の発言ブラウザを返す。
-     * @return 内部ブラウザ
+     * ビューポート内の会話表示{@link Discussion}を返す。
+     *
+     * @return 会話表示
      */
     public Discussion getDiscussion(){
         return this.discussion;
     }
 
     /**
-     * スクロール位置を返す。
+     * 縦スクロール位置を返す。
+     *
      * @return スクロール位置
      */
     public int getVerticalPosition(){
@@ -278,7 +288,8 @@ public class PeriodView extends JPanel implements ItemListener{
     }
 
     /**
-     * スクロール位置を設定する。
+     * 縦スクロール位置を設定する。
+     *
      * @param pos スクロール位置
      */
     public void setVerticalPosition(int pos){
@@ -289,7 +300,9 @@ public class PeriodView extends JPanel implements ItemListener{
 
     /**
      * {@inheritDoc}
-     * コンボボックス操作のリスナ。
+     *
+     * <p>コンボボックス操作(会話プルダウンリスト)のリスナ。
+     *
      * @param event コンボボックス操作イベント {@inheritDoc}
      */
     @Override
@@ -306,7 +319,8 @@ public class PeriodView extends JPanel implements ItemListener{
     }
 
     /**
-     * 任意の発言が表示されるようスクロールする。
+     * 任意の会話が表示域に収まるようスクロールを試みる。
+     *
      * @param talk 発言
      */
     public void scrollToTalk(Talk talk){
@@ -339,7 +353,9 @@ public class PeriodView extends JPanel implements ItemListener{
 
         /**
          * {@inheritDoc}
-         * Talkのアンカー表記と発言者名を描画する。
+         *
+         * <p>Talkのアンカー表記と発言者名を描画する。
+         *
          * @param list {@inheritDoc}
          * @param value {@inheritDoc}
          * @param index {@inheritDoc}
index cb7eafd..04aa1f7 100644 (file)
@@ -9,16 +9,16 @@ package jp.sfjp.jindolf.view;
 
 import java.awt.Component;
 import java.awt.event.ActionListener;
-import java.util.EventListener;
-import java.util.LinkedList;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import javax.swing.BorderFactory;
+import javax.swing.JComponent;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JTabbedPane;
 import javax.swing.SwingConstants;
 import javax.swing.border.Border;
-import javax.swing.event.EventListenerList;
 import jp.sfjp.jindolf.data.DialogPref;
 import jp.sfjp.jindolf.data.Period;
 import jp.sfjp.jindolf.data.Village;
@@ -27,10 +27,19 @@ import jp.sfjp.jindolf.glyph.Discussion;
 import jp.sfjp.jindolf.glyph.FontInfo;
 
 /**
- * タブを用いて村情報と各Periodを閲覧するためのコンポーネント。
+ * タブを用いて村情報と各Periodを切り替え表示するためのコンポーネント。
+ *
+ * <p>村情報タブのビューはVillageInfoPanel、
+ * PeriodタブのビューはPeriodViewが担当する。
+ *
+ * <p>PeriodViewの描画下請Discussionへのアクセス、
+ * およびフォント管理、会話描画設定を提供する。
  */
 @SuppressWarnings("serial")
-public class TabBrowser extends JTabbedPane{
+public final class TabBrowser extends JTabbedPane{
+
+    private static final int PERIODTAB_OFFSET = 1;
+
 
     private Village village;
 
@@ -39,11 +48,11 @@ public class TabBrowser extends JTabbedPane{
     private FontInfo fontInfo;
     private DialogPref dialogPref;
 
-    private final EventListenerList thisListenerList =
-            new EventListenerList();
 
     /**
-     * 村が指定されていない状態のタブパネルを生成する。
+     * コンストラクタ。
+     *
+     * <p>村が指定されていない状態のタブパネルを生成する。
      */
     public TabBrowser(){
         super();
@@ -52,35 +61,88 @@ public class TabBrowser extends JTabbedPane{
         // Mac Aqua L&F ignore WRAP_TAB_LAYOUT
         setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
 
+        JComponent infoPane = decorateVillageInfo();
+        addTab("村情報", infoPane);
+
+        initTab();
+
+        return;
+    }
+
+
+    /**
+     * 村情報表示コンポーネントを装飾する。
+     *
+     * @return 装飾済みコンポーネント
+     */
+    private JComponent decorateVillageInfo(){
         Border border = BorderFactory.createEmptyBorder(5, 5, 5, 5);
         this.villageInfo.setBorder(border);
+        JScrollPane result = new JScrollPane(this.villageInfo);
+        return result;
+    }
+
+    /**
+     * タブ初期化。
+     *
+     * <p>Periodタブは全て消え村情報タブのみになる。
+     */
+    private void initTab(){
+        modifyTabCount(0);
 
-        addTab("村情報", new JScrollPane(this.villageInfo));
+        updateVillageInfo();
+        showVillageInfoTab();
 
-        setVillage(null);
+        repaint();
+        revalidate();
 
         return;
     }
 
     /**
-     * 村情報閲覧用のコンポーネントを更新する。
+     * 指定した数のPeriodが収まるよう必要十分なタブ数を用意する。
+     *
+     * @param periods Periodの数(エピローグを含む)
      */
-    private void updateVillageInfo(){
-        Village target = getVillage();
-        this.villageInfo.updateVillage(target);
+    private void modifyTabCount(int periods){
+        for(;;){   // 短ければタブ追加
+            int periodTabs = getTabCount() - PERIODTAB_OFFSET;
+            if(periods <= periodTabs) break;
+            String tabTitle = "";
+            Component dummy = new JPanel();
+            addTab(tabTitle, dummy);
+        }
+
+        for(;;){   // 長ければ余分なタブ削除
+            int periodTabs = getTabCount() - PERIODTAB_OFFSET;
+            if(periods >= periodTabs) break;
+            int lastTabIndex = getTabCount() - 1;
+            remove(lastTabIndex);
+        }
+
         return;
     }
 
     /**
-     * 村情報表示タブを選択表示する。
+     * Period表示するタブ全てのコンポーネント本体とタイトルを埋める。
      */
-    private void selectVillageInfoTab(){
-        setSelectedIndex(0);
+    private void fillPeriodTab(){
+        this.village.getPeriodList().stream().forEachOrdered(period ->{
+            PeriodView periodView = buildPeriodView(period);
+            String caption = period.getCaption();
+
+            int tabIndex = period.getDay() + PERIODTAB_OFFSET;
+
+            setComponentAt(tabIndex, periodView);
+            setTitleAt(tabIndex, caption);
+        });
+
         return;
     }
 
     /**
      * 設定された村を返す。
+     *
      * @return 設定された村
      */
     public Village getVillage(){
@@ -89,116 +151,88 @@ public class TabBrowser extends JTabbedPane{
 
     /**
      * 新規に村を設定する。
+     *
+     * <p>村のPeriod数に応じてタブの数は変化する。
+     *
      * @param village 新しい村
      */
     public final void setVillage(Village village){
-        if(village == null){
-            if(this.village != null){
-                this.village.unloadPeriods();
-            }
-            this.village = null;
-            selectVillageInfoTab();
-            modifyTabCount(0);
-            updateVillageInfo();
+        Village oldVillage = this.village;
+        if(oldVillage != null && village != oldVillage){
+            oldVillage.unloadPeriods();
+        }
+
+        this.village = village;
+        if(this.village == null){
+            initTab();
             return;
-        }else if(village != this.village){
-            selectVillageInfoTab();
         }
 
-        if(this.village != null){
-            this.village.unloadPeriods();
+        if(this.village != oldVillage){
+            showVillageInfoTab();
         }
-        this.village = village;
 
         updateVillageInfo();
 
         int periodNum = this.village.getPeriodSize();
         modifyTabCount(periodNum);
+        fillPeriodTab();
 
-        for(int periodDays = 0; periodDays < periodNum; periodDays++){
-            Period period = this.village.getPeriod(periodDays);
-            int tabIndex = periodDaysToTabIndex(periodDays);
-            PeriodView periodView = getPeriodView(tabIndex);
-            if(periodView == null){
-                periodView = new PeriodView(period);
-                periodView.setFontInfo(this.fontInfo);
-                periodView.setDialogPref(this.dialogPref);
-                setComponentAt(tabIndex, periodView);
-                Discussion discussion = periodView.getDiscussion();
-                for(ActionListener listener : getActionListeners()){
-                    discussion.addActionListener(listener);
-                }
-                for(AnchorHitListener listener : getAnchorHitListeners()){
-                    discussion.addAnchorHitListener(listener);
-                }
-            }
-            String caption = period.getCaption();
-            setTitleAt(tabIndex, caption);
-            if(period == periodView.getPeriod()) continue;
-            periodView.setPeriod(period);
-        }
+        repaint();
+        revalidate();
 
         return;
     }
 
     /**
-     * 指定した数のPeriodが収まるよう必要十分なタブ数を用意する。
-     * @param periods Periodの数
+     * 村情報閲覧用のコンポーネントを更新する。
      */
-    private void modifyTabCount(int periods){ // TODO 0でも大丈夫?
-        int maxPeriodDays = periods - 1;
-
-        for(;;){   // 短ければタブ追加
-            int maxTabIndex = getTabCount() - 1;
-            if(tabIndexToPeriodDays(maxTabIndex) >= maxPeriodDays) break;
-            String title = "";
-            Component component = new JPanel();
-            addTab(title, component);
-        }
-
-        for(;;){   // 長ければ余分なタブ削除
-            int maxTabIndex = getTabCount() - 1;
-            if(tabIndexToPeriodDays(maxTabIndex) <= maxPeriodDays) break;
-            remove(maxTabIndex);
-        }
-
+    private void updateVillageInfo(){
+        Village target = getVillage();
+        this.villageInfo.updateVillage(target);
         return;
     }
 
     /**
-     * Period日付指定からタブインデックス値への変換。
-     * @param days Period日付指定
-     * @return タブインデックス
+     * PeriodViewインスタンスを生成する。
+     *
+     * <p>フォント設定、会話表示設定、各種リスナの設定が行われる。
+     *
+     * @param period Period
+     * @return PeriodViewインスタンス
      */
-    public int periodDaysToTabIndex(int days){
-        int tabIndex = days+1;
-        if(tabIndex >= getTabCount()) return -1;
-        return tabIndex;
-    }
+    private PeriodView buildPeriodView(Period period){
+        Objects.nonNull(period);
 
-    /**
-     * タブインデックス値からPeriod日付指定への変換。
-     * @param tabIndex タブインデックス
-     * @return Period日付指定
-     */
-    private int tabIndexToPeriodDays(int tabIndex){
-        if(tabIndex >= getTabCount()) return - 1;
-        int days = tabIndex - 1;
-        return days;
+        PeriodView result;
+
+        result = new PeriodView(period);
+        result.setFontInfo(this.fontInfo);
+        result.setDialogPref(this.dialogPref);
+
+        Discussion discussion = result.getDiscussion();
+        for(ActionListener listener : getActionListeners()){
+            discussion.addActionListener(listener);
+        }
+        for(AnchorHitListener listener : getAnchorHitListeners()){
+            discussion.addAnchorHitListener(listener);
+        }
+
+        return result;
     }
 
     /**
      * PeriodView一覧を得る。
+     *
      * @return PeriodView の List
      */
     public List<PeriodView> getPeriodViewList(){
-        List<PeriodView> result = new LinkedList<>();
-
         int tabCount = getTabCount();
-        for(int tabIndex = 0; tabIndex <= tabCount - 1; tabIndex++){
+        int periodCount = tabCount - PERIODTAB_OFFSET;
+        List<PeriodView> result = new ArrayList<>(periodCount);
+
+        for(int tabIndex = PERIODTAB_OFFSET; tabIndex < tabCount; tabIndex++){
             Component component = getComponent(tabIndex);
-            if(component == null) continue;
-            if( ! (component instanceof PeriodView) ) continue;
             PeriodView periodView = (PeriodView) component;
             result.add(periodView);
         }
@@ -207,39 +241,20 @@ public class TabBrowser extends JTabbedPane{
     }
 
     /**
-     * 現在タブ選択中のDiscussionを返す。
-     * Periodに関係ないタブが選択されていたらnullを返す。
-     * @return 現在選択中のDiscussion
-     */
-    public Discussion currentDiscussion(){
-        int tabIndex = getSelectedIndex();
-        Discussion result = getDiscussion(tabIndex);
-        return result;
-    }
-
-    /**
-     * 現在タブ選択中のPeriodViewを返す。
-     * Periodに関係ないタブが選択されていたらnullを返す。
-     * @return 現在選択中のPeriodView
-     */
-    public PeriodView currentPeriodView(){
-        int tabIndex = getSelectedIndex();
-        PeriodView result = getPeriodView(tabIndex);
-        return result;
-    }
-
-    /**
-     * 指定したタブインデックスに関連付けられたPeriodViewを返す。
-     * Periodに関係ないタブが指定されたらnullを返す。
-     * @param tabIndex タブインデックス
+     * 指定したPeriod日付に関連付けられたPeriodViewを返す。
+     *
+     * <p>Periodに関係ないタブが指定されたらnullを返す。
+     *
+     * @param periodIndex Period日付インデックス
      * @return 指定されたPeriodView
      */
-    public PeriodView getPeriodView(int tabIndex){
-        if(tabIndexToPeriodDays(tabIndex) < 0) return null;
-        if(tabIndex >= getTabCount()) return null;
-        Component component = getComponentAt(tabIndex);
-        if(component == null) return null;
+    public PeriodView getPeriodView(int periodIndex){
+        int tabIndex = periodIndex + PERIODTAB_OFFSET;
+        if(tabIndex < PERIODTAB_OFFSET || getTabCount() <= tabIndex){
+            return null;
+        }
 
+        Component component = getComponentAt(tabIndex);
         if( ! (component instanceof PeriodView) ) return null;
         PeriodView periodView = (PeriodView) component;
 
@@ -247,159 +262,162 @@ public class TabBrowser extends JTabbedPane{
     }
 
     /**
-     * 指定したタブインデックスに関連付けられたDiscussionを返す。
-     * Periodに関係ないタブが指定されたらnullを返す。
-     * @param tabIndex タブインデックス
-     * @return 指定されたDiscussion
+     * 現在タブ選択中のPeriodViewを返す。
+     *
+     * <p>Periodに関係ないタブが選択されていたらnullを返す。
+     *
+     * @return 現在選択中のPeriodView
      */
-    private Discussion getDiscussion(int tabIndex){
-        PeriodView periodView = getPeriodView(tabIndex);
-        if(periodView == null) return null;
-
-        Discussion result = periodView.getDiscussion();
+    public PeriodView currentPeriodView(){
+        int tabIndex = getSelectedIndex();
+        int periodIndex = tabIndex - PERIODTAB_OFFSET;
+        PeriodView result = getPeriodView(periodIndex);
         return result;
     }
 
     /**
      * フォント描画設定を変更する。
+     *
+     * <p>設定は各PeriodViewに委譲される。
+     *
      * @param fontInfo フォント
      */
     public void setFontInfo(FontInfo fontInfo){
+        Objects.nonNull(fontInfo);
         this.fontInfo = fontInfo;
 
-        for(int tabIndex = 0; tabIndex <= getTabCount() - 1; tabIndex++){
-            PeriodView periodView = getPeriodView(tabIndex);
-            if(periodView == null) continue;
+        getPeriodViewList().forEach(periodView -> {
             periodView.setFontInfo(this.fontInfo);
-        }
+        });
 
         return;
     }
 
     /**
      * 発言表示設定を変更する。
+     *
+     * <p>設定は各PeriodViewに委譲される。
+     *
      * @param dialogPref 発言表示設定
      */
     public void setDialogPref(DialogPref dialogPref){
+        Objects.nonNull(dialogPref);
         this.dialogPref = dialogPref;
 
-        for(int tabIndex = 0; tabIndex <= getTabCount() - 1; tabIndex++){
-            PeriodView periodView = getPeriodView(tabIndex);
-            if(periodView == null) continue;
+        getPeriodViewList().forEach(periodView -> {
             periodView.setDialogPref(this.dialogPref);
-        }
+        });
 
         return;
     }
 
     /**
+     * 村情報表示タブを選択表示する。
+     */
+    private void showVillageInfoTab(){
+        setSelectedIndex(0);
+        return;
+    }
+
+    /**
+     * 指定した日付インデックスのPeriodのタブを表示する。
+     *
+     * @param periodIndex 日付インデックス
+     */
+    public void showPeriodTab(int periodIndex){
+        int tabIndex = periodIndex + PERIODTAB_OFFSET;
+        setSelectedIndex(tabIndex);
+        return;
+    }
+
+    /**
      * ActionListenerを追加する。
+     *
+     * <p>配下のDiscussionへもリスナは登録される。
+     *
      * @param listener リスナー
      */
     public void addActionListener(ActionListener listener){
-        this.thisListenerList.add(ActionListener.class, listener);
+        this.listenerList.add(ActionListener.class, listener);
 
-        if(this.village == null) return;
-        int periodNum = this.village.getPeriodSize();
-        for(int periodDays = 0; periodDays < periodNum; periodDays++){
-            int tabIndex = periodDaysToTabIndex(periodDays);
-            Discussion discussion = getDiscussion(tabIndex);
-            if(discussion == null) continue;
-            discussion.addActionListener(listener);
-        }
+        getPeriodViewList().stream()
+                .map(PeriodView::getDiscussion)
+                .forEach(discussion ->{
+                    discussion.addActionListener(listener);
+                });
 
         return;
     }
 
     /**
      * ActionListenerを削除する。
+     *
      * @param listener リスナー
      */
     public void removeActionListener(ActionListener listener){
-        this.thisListenerList.remove(ActionListener.class, listener);
+        this.listenerList.remove(ActionListener.class, listener);
 
-        if(this.village == null) return;
-        int periodNum = this.village.getPeriodSize();
-        for(int periodDays = 0; periodDays < periodNum; periodDays++){
-            int tabIndex = periodDaysToTabIndex(periodDays);
-            Discussion discussion = getDiscussion(tabIndex);
-            if(discussion == null) continue;
-            discussion.removeActionListener(listener);
-        }
+        getPeriodViewList().stream()
+                .map(PeriodView::getDiscussion)
+                .forEach(discussion ->{
+                    discussion.removeActionListener(listener);
+                });
 
         return;
     }
 
     /**
      * ActionListenerを列挙する。
+     *
      * @return すべてのActionListener
      */
     public ActionListener[] getActionListeners(){
-        return this.thisListenerList.getListeners(ActionListener.class);
+        return getListeners(ActionListener.class);
     }
 
     /**
      * AnchorHitListenerを追加する。
+     *
+     * <p>配下のDiscussionへもリスナは登録される。
+     *
      * @param listener リスナー
      */
     public void addAnchorHitListener(AnchorHitListener listener){
-        this.thisListenerList.add(AnchorHitListener.class, listener);
+        this.listenerList.add(AnchorHitListener.class, listener);
 
-        if(this.village == null) return;
-        int periodNum = this.village.getPeriodSize();
-        for(int periodDays = 0; periodDays < periodNum; periodDays++){
-            int tabIndex = periodDaysToTabIndex(periodDays);
-            Discussion discussion = getDiscussion(tabIndex);
-            if(discussion == null) continue;
-            discussion.addAnchorHitListener(listener);
-        }
+        getPeriodViewList().stream()
+                .map(PeriodView::getDiscussion)
+                .forEach(discussion -> {
+                    discussion.addAnchorHitListener(listener);
+                });
 
         return;
     }
 
     /**
      * AnchorHitListenerを削除する。
+     *
      * @param listener リスナー
      */
     public void removeAnchorHitListener(AnchorHitListener listener){
-        this.thisListenerList.remove(AnchorHitListener.class, listener);
+        this.listenerList.remove(AnchorHitListener.class, listener);
 
-        if(this.village == null) return;
-        int periodNum = this.village.getPeriodSize();
-        for(int periodDays = 0; periodDays < periodNum; periodDays++){
-            int tabIndex = periodDaysToTabIndex(periodDays);
-            Discussion discussion = getDiscussion(tabIndex);
-            if(discussion == null) continue;
-            discussion.removeAnchorHitListener(listener);
-        }
+        getPeriodViewList().stream()
+                .map(PeriodView::getDiscussion)
+                .forEach(discussion -> {
+                    discussion.removeAnchorHitListener(listener);
+                });
 
         return;
     }
 
     /**
      * AnchorHitListenerを列挙する。
+     *
      * @return すべてのAnchorHitListener
      */
     public AnchorHitListener[] getAnchorHitListeners(){
-        return this.thisListenerList.getListeners(AnchorHitListener.class);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @param <T> {@inheritDoc}
-     * @param listenerType {@inheritDoc}
-     * @return {@inheritDoc}
-     */
-    @Override
-    public <T extends EventListener> T[] getListeners(Class<T> listenerType){
-        T[] result;
-        result = this.thisListenerList.getListeners(listenerType);
-
-        if(result.length <= 0){
-            result = super.getListeners(listenerType);
-        }
-
-        return result;
+        return getListeners(AnchorHitListener.class);
     }
 
 }
index dc0b2aa..6b6d1c2 100644 (file)
@@ -10,6 +10,7 @@ package jp.sfjp.jindolf.view;
 import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Container;
+import java.awt.Cursor;
 import java.awt.LayoutManager;
 import java.awt.event.KeyAdapter;
 import java.awt.event.MouseAdapter;
@@ -18,13 +19,30 @@ import javax.swing.JFrame;
 
 /**
  * メインアプリウィンドウ。
- * {@link TopView}をウィンドウ表示するための皮。
+ *
+ * <p>各種ウィンドウシステムとの接点を管理する。
+ * (ウィンドウ最小化UI、クローズUI、リサイズ操作、
+ * ウィンドウタイトル、タスクバーアイコンなど)
+ *
+ * <p>メニューバーと{@link TopView}を自身のコンテナ上にレイアウトする。
+ * アプリ画面本体の処理は{@link TopView}に委譲される。
+ *
+ * <p>アプリウィンドウ上のカーソル形状を管理する。
+ * ヘビーな処理を行う間は砂時計アイコンになる。
+ *
+ * <p>glass paneの操作により、
+ * ヘビーな処理中の各種アプリ操作(キーボード、マウス)をマスクする。
+ *
+ * <p>アプリによる各ウィンドウの親及び祖先となる。
+ *
+ * <p>各種モーダルダイアログの親となる。
  */
 @SuppressWarnings("serial")
 public class TopFrame extends JFrame{
 
     private final TopView topView = new TopView();
 
+
     /**
      * コンストラクタ。
      */
@@ -39,20 +57,26 @@ public class TopFrame extends JFrame{
         return;
     }
 
+
     /**
      * レイアウトをデザインする。
+     *
      * @param container コンテナ
      */
     private void design(Container container){
         LayoutManager layout = new BorderLayout();
         container.setLayout(layout);
         container.add(this.topView, BorderLayout.CENTER);
-
         return;
     }
 
     /**
      * グラスペインのカスタマイズを行う。
+     *
+     * <p>アプリウィンドウは常に透明なグラスペインに覆い尽くされている。
+     *
+     * <p>このグラスペインは、可視化されている間、
+     * キーボード入力とマウス入力を無視する。
      */
     private void modifyGrassPane(){
         Component glassPane = new JComponent() {};
@@ -66,11 +90,66 @@ public class TopFrame extends JFrame{
     }
 
     /**
-     * トップビューを返す。
+     * 実際のアプリ画面を担当する{@link TopView}を返す。
+     *
      * @return トップビュー
      */
     public TopView getTopView(){
         return this.topView;
     }
 
+    /**
+     * アプリウィンドウ上のマウスカーソルのビジー状態を管理する。
+     *
+     * @param isBusy ビジーならtrue。
+     */
+    private void setCursorBusy(boolean isBusy){
+        Cursor cursor;
+        if(isBusy){
+            cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
+        }else{
+            cursor = Cursor.getDefaultCursor();
+        }
+
+        Component glassPane = getGlassPane();
+        glassPane.setCursor(cursor);
+
+        return;
+    }
+
+    /**
+     * アプリウィンドウ上のマウス及びキー入力のグラブを管理する。
+     *
+     * <p>ビジー状態の場合、
+     * アプリ画面上のマウス及びキー操作は全て事前にグラブされ無視される。
+     *
+     * @param isBusy ビジーならtrue
+     */
+    private void setUiMask(boolean isBusy){
+        Component glassPane = getGlassPane();
+        glassPane.setVisible(isBusy);
+        return;
+    }
+
+    /**
+     * ビジー状態の設定を行う。
+     *
+     * <p>ヘビーなタスク実行をアピールするために、
+     * プログレスバーとカーソルの設定を行う。
+     *
+     * <p>ビジー中のマウス操作、キーボード入力は
+     * 全てグラブされるため無視される。
+     *
+     * <p>プログラスバーの表示操作は{@link TopView}に委譲される。
+     *
+     * @param isBusy trueならプログレスバーのアニメ開始&amp;WAITカーソル。
+     *     falseなら停止&amp;通常カーソル。
+     */
+    public void setBusy(boolean isBusy){
+        setCursorBusy(isBusy);
+        setUiMask(isBusy);
+        this.topView.setBusy(isBusy);
+        return;
+    }
+
 }
index eab665f..8315720 100644 (file)
@@ -29,11 +29,21 @@ import javax.swing.border.CompoundBorder;
 import jp.sfjp.jindolf.VerInfo;
 import jp.sfjp.jindolf.data.Land;
 import jp.sfjp.jindolf.data.Village;
-import jp.sfjp.jindolf.util.GUIUtils;
 
 /**
  * 最上位ビュー。
- * メインアプリウィンドウのコンポーネントの親コンテナ。
+ *
+ * <p>メインアプリウィンドウの各種コンポーネントの祖先コンテナ。
+ *
+ * <p>{@link JSplitPane}の左に国村選択リスト、
+ * 右にカードコンテナがレイアウトされる。
+ *
+ * <p>カードコンテナ上に
+ * 初期画面、国情報パネル{@link LandsTree}とタブブラウザ{@link TabBrowser}
+ * の3コンポーネントが重ねてレイアウトされ、必要に応じて切り替わる。
+ *
+ * <p>ヘビーなタスク実行をアピールするために、
+ * プログレスバーとフッタメッセージの管理を行う。
  */
 @SuppressWarnings("serial")
 public class TopView extends JPanel{
@@ -59,10 +69,12 @@ public class TopView extends JPanel{
 
     private final TabBrowser tabBrowser = new TabBrowser();
 
-    private JComponent browsePanel;
+    // to place toolbar
+    private final JComponent browsePanel = createBrowsePanel();
+
 
     /**
-     * ã\83\88ã\83\83ã\83\97ã\83\93ã\83¥ã\83¼ã\82\92ç\94\9fæ\88\90ã\81\99ã\82\8b
+     * ã\82³ã\83³ã\82¹ã\83\88ã\83©ã\82¯ã\82¿
      */
     public TopView(){
         super();
@@ -81,40 +93,45 @@ public class TopView extends JPanel{
 
     /**
      * カードパネルを生成する。
+     *
      * @return カードパネル
      */
     private JComponent createCards(){
-        this.browsePanel = createBrowsePanel();
+        JComponent initCard = createInitCard();
+        JComponent landInfoCard = createLandInfoCard();
 
-        JPanel panel = new JPanel();
-        panel.setLayout(this.cardLayout);
-        panel.add(INITCARD, createInitCard());
-        panel.add(LANDCARD, createLandInfoCard());
-        panel.add(BROWSECARD, this.browsePanel);
+        JPanel cardContainer = new JPanel();
+        cardContainer.setLayout(this.cardLayout);
 
-        return panel;
+        cardContainer.add(INITCARD, initCard);
+        cardContainer.add(LANDCARD, landInfoCard);
+        cardContainer.add(BROWSECARD, this.browsePanel);
+
+        return cardContainer;
     }
 
     /**
      * 初期パネルを生成。
+     *
      * @return 初期パネル
      */
     private JComponent createInitCard(){
-        JLabel initMessage = new JLabel("← 村を選択してください");
-
-        StringBuilder acct = new StringBuilder();
-        acct.append("※ 参加中の村がある人は<br></br>");
-        acct.append("メニューの「アカウント管理」から<br></br>");
-        acct.append("ログインしてください");
-        acct.insert(0, "<center>").append("</center>");
-        acct.insert(0, "<body>")  .append("</body>");
-        acct.insert(0, "<html>")  .append("</html>");
-        JLabel acctMessage = new JLabel(acct.toString());
+        StringBuilder init = new StringBuilder();
+        init.append("←    人狼BBSサーバから村を選択してください");
+        init.append("<br/><br/>または「ファイル」メニューから");
+        init.append("<br/>JinArchive形式でダウンロードした");
+        init.append("<br/>アーカイブXMLファイルを開いてください");
+        init.insert(0, "<font 'size=+1'>").append("</font>");
+        init.insert(0, "<center>").append("</center>");
+        init.insert(0, "<body>").append("</body>");
+        init.insert(0, "<html>").append("</html>");
+        JLabel initMessage = new JLabel(init.toString());
 
         StringBuilder warn = new StringBuilder();
         warn.append("※ たまにはWebブラウザでアクセスして、");
         warn.append("<br></br>");
         warn.append("運営の動向を確かめようね!");
+        warn.insert(0, "<font 'size=+1'>").append("</font>");
         warn.insert(0, "<center>").append("</center>");
         warn.insert(0, "<body>")  .append("</body>");
         warn.insert(0, "<html>")  .append("</html>");
@@ -130,7 +147,6 @@ public class TopView extends JPanel{
         constraints.anchor = GridBagConstraints.CENTER;
         constraints.gridx = GridBagConstraints.REMAINDER;
         panel.add(initMessage, constraints);
-        panel.add(acctMessage, constraints);
         panel.add(warnMessage, constraints);
 
         JScrollPane scrollPane = new JScrollPane(panel);
@@ -140,17 +156,20 @@ public class TopView extends JPanel{
 
     /**
      * 国別情報を生成。
+     *
      * @return 国別情報
      */
     private JComponent createLandInfoCard(){
-        this.landInfo.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        Border border = BorderFactory.createEmptyBorder(5, 5, 5, 5);
+        this.landInfo.setBorder(border);
         JScrollPane scrollPane = new JScrollPane(this.landInfo);
         return scrollPane;
     }
 
     /**
-     * 内部ブラウザを生成。
-     * @return 内部ブラウザ
+     * タブブラウザにツールバーを併設するためのコンテナを生成。
+     *
+     * @return コンテナ
      */
     private JComponent createBrowsePanel(){
         JPanel panel = new JPanel();
@@ -164,6 +183,7 @@ public class TopView extends JPanel{
 
     /**
      * ブラウザ用ツールバーをセットする。
+     *
      * @param toolbar ツールバー
      */
     public void setBrowseToolBar(JToolBar toolbar){
@@ -173,6 +193,7 @@ public class TopView extends JPanel{
 
     /**
      * SplitPaneを生成。
+     *
      * @param left 左コンポーネント
      * @param right 右コンポーネント
      * @return SplitPane
@@ -185,17 +206,18 @@ public class TopView extends JPanel{
         split.setContinuousLayout(false);
         split.setOneTouchExpandable(true);
         split.setDividerLocation(200);
-
         return split;
     }
 
     /**
      * ステータスバーを生成する。
+     *
      * @return ステータスバー
      */
     private JComponent createStatusBar(){
         this.sysMessage.setText(MSG_THANKS);
         this.sysMessage.setEditable(false);
+
         Border inside  = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
         Border outside = BorderFactory.createEmptyBorder(2, 5, 2, 2);
         Border border = new CompoundBorder(inside, outside);
@@ -226,6 +248,7 @@ public class TopView extends JPanel{
 
     /**
      * 国村選択ツリービューを返す。
+     *
      * @return 国村選択ツリービュー
      */
     public JTree getTreeView(){
@@ -234,6 +257,7 @@ public class TopView extends JPanel{
 
     /**
      * タブビューを返す。
+     *
      * @return タブビュー
      */
     public TabBrowser getTabBrowser(){
@@ -242,6 +266,7 @@ public class TopView extends JPanel{
 
     /**
      * 村一覧ビューを返す。
+     *
      * @return 村一番ビュー
      */
     public LandsTree getLandsTree(){
@@ -249,9 +274,10 @@ public class TopView extends JPanel{
     }
 
     /**
-     * プログレスバーとカーソルの設定を行う。
-     * @param busy trueならプログレスバーのアニメ開始&amp;WAITカーソル。
-     *              falseなら停止&amp;通常カーソル。
+     * プログレスバーの設定を行う。
+     *
+     * @param busy trueならプログレスバーのアニメ開始。
+     *     falseなら停止。
      */
     public void setBusy(boolean busy){
         this.progressBar.setIndeterminate(busy);
@@ -260,18 +286,19 @@ public class TopView extends JPanel{
 
     /**
      * ステータスバーの更新。
+     *
      * @param message 更新文字列
      */
     public void updateSysMessage(String message){
         String text = message;
         if(message == null) text = "";
         this.sysMessage.setText(text);   // Thread safe
-        GUIUtils.dispatchEmptyAWTEvent();
         return;
     }
 
     /**
      * ステータスバー文字列を返す。
+     *
      * @return ステータスバー文字列
      */
     public String getSysMessage(){
@@ -290,19 +317,18 @@ public class TopView extends JPanel{
 
     /**
      * 村情報を表示する。
+     *
      * @param village 村
      */
     public void showVillageInfo(Village village){
         this.tabBrowser.setVillage(village);
         this.cardLayout.show(this.cards, BROWSECARD);
-        this.tabBrowser.repaint();
-        this.tabBrowser.revalidate();
-
         return;
     }
 
     /**
      * 国情報を表示する。
+     *
      * @param land 国
      */
     public void showLandInfo(Land land){
@@ -311,5 +337,4 @@ public class TopView extends JPanel{
         return;
     }
 
-    // TODO setEnabled()を全子フレームにも波及させるべきか
 }
index 7b27168..1949268 100644 (file)
@@ -7,17 +7,15 @@
 
 package jp.sfjp.jindolf.view;
 
-import java.awt.Component;
-import java.awt.Container;
+import java.awt.EventQueue;
 import java.awt.Frame;
 import java.awt.Window;
 import java.util.LinkedList;
 import java.util.List;
-import javax.swing.JComponent;
-import javax.swing.JMenu;
-import javax.swing.JPopupMenu;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
 import jp.sfjp.jindolf.VerInfo;
-import jp.sfjp.jindolf.editor.TalkPreview;
 import jp.sfjp.jindolf.log.LogFrame;
 import jp.sfjp.jindolf.summary.DaySummary;
 import jp.sfjp.jindolf.summary.VillageDigest;
@@ -31,14 +29,10 @@ public class WindowManager {
             getFrameTitle("発言フィルタ");
     private static final String TITLE_LOGGER =
             getFrameTitle("ログ表示");
-    private static final String TITLE_EDITOR =
-            getFrameTitle("発言エディタ");
     private static final String TITLE_OPTION =
             getFrameTitle("オプション設定");
     private static final String TITLE_FIND =
             getFrameTitle("発言検索");
-    private static final String TITLE_ACCOUNT =
-            getFrameTitle("アカウント管理");
     private static final String TITLE_DIGEST =
             getFrameTitle("村のダイジェスト");
     private static final String TITLE_DAYSUMMARY =
@@ -51,10 +45,8 @@ public class WindowManager {
 
     private FilterPanel filterPanel;
     private LogFrame logFrame;
-    private TalkPreview talkPreview;
     private OptionPanel optionPanel;
     private FindPanel findPanel;
-    private AccountPanel accountPanel;
     private VillageDigest villageDigest;
     private DaySummary daySummary;
     private HelpFrame helpFrame;
@@ -74,6 +66,7 @@ public class WindowManager {
 
     /**
      * ウィンドウタイトルに前置詞をつける。
+     *
      * @param text 元タイトル
      * @return タイトル文字列
      */
@@ -85,6 +78,7 @@ public class WindowManager {
 
     /**
      * 発言フィルタウィンドウを生成する。
+     *
      * @return 発言フィルタウィンドウ
      */
     protected FilterPanel createFilterPanel(){
@@ -102,6 +96,7 @@ public class WindowManager {
 
     /**
      * 発言フィルタウィンドウを返す。
+     *
      * @return 発言フィルタウィンドウ
      */
     public FilterPanel getFilterPanel(){
@@ -113,6 +108,7 @@ public class WindowManager {
 
     /**
      * ログウィンドウを生成する。
+     *
      * @return ログウィンドウ
      */
     protected LogFrame createLogFrame(){
@@ -132,6 +128,7 @@ public class WindowManager {
 
     /**
      * ログウィンドウを返す。
+     *
      * @return ログウィンドウ
      */
     public LogFrame getLogFrame(){
@@ -142,36 +139,8 @@ public class WindowManager {
     }
 
     /**
-     * 発言エディタウィンドウを生成する。
-     * @return 発言エディタウィンドウ
-     */
-    protected TalkPreview createTalkPreview(){
-        TalkPreview result;
-
-        result = new TalkPreview();
-        result.setTitle(TITLE_EDITOR);
-        result.pack();
-        result.setSize(700, 500);
-        result.setVisible(false);
-
-        this.windowSet.add(result);
-
-        return result;
-    }
-
-    /**
-     * 発言エディタウィンドウを返す。
-     * @return 発言エディタウィンドウ
-     */
-    public TalkPreview getTalkPreview(){
-        if(this.talkPreview == null){
-            this.talkPreview = createTalkPreview();
-        }
-        return this.talkPreview;
-    }
-
-    /**
      * オプション設定ウィンドウを生成する。
+     *
      * @return オプション設定ウィンドウ
      */
     protected OptionPanel createOptionPanel(){
@@ -190,6 +159,7 @@ public class WindowManager {
 
     /**
      * オプション設定ウィンドウを返す。
+     *
      * @return オプション設定ウィンドウ
      */
     public OptionPanel getOptionPanel(){
@@ -201,6 +171,7 @@ public class WindowManager {
 
     /**
      * 検索ウィンドウを生成する。
+     *
      * @return 検索ウィンドウ
      */
     protected FindPanel createFindPanel(){
@@ -218,6 +189,7 @@ public class WindowManager {
 
     /**
      * 検索ウィンドウを返す。
+     *
      * @return 検索ウィンドウ
      */
     public FindPanel getFindPanel(){
@@ -228,35 +200,8 @@ public class WindowManager {
     }
 
     /**
-     * ログインウィンドウを生成する。
-     * @return ログインウィンドウ
-     */
-    protected AccountPanel createAccountPanel(){
-        AccountPanel result;
-
-        result = new AccountPanel(NULLPARENT);
-        result.setTitle(TITLE_ACCOUNT);
-        result.pack();
-        result.setVisible(false);
-
-        this.windowSet.add(result);
-
-        return result;
-    }
-
-    /**
-     * ログインウィンドウを返す。
-     * @return ログインウィンドウ
-     */
-    public AccountPanel getAccountPanel(){
-        if(this.accountPanel == null){
-            this.accountPanel = createAccountPanel();
-        }
-        return this.accountPanel;
-    }
-
-    /**
      * 村ダイジェストウィンドウを生成する。
+     *
      * @return 村ダイジェストウィンドウ
      */
     protected VillageDigest createVillageDigest(){
@@ -275,6 +220,7 @@ public class WindowManager {
 
     /**
      * 村ダイジェストウィンドウを返す。
+     *
      * @return 村ダイジェストウィンドウ
      */
     public VillageDigest getVillageDigest(){
@@ -286,6 +232,7 @@ public class WindowManager {
 
     /**
      * 発言集計ウィンドウを生成する。
+     *
      * @return 発言集計ウィンドウ
      */
     protected DaySummary createDaySummary(){
@@ -304,6 +251,7 @@ public class WindowManager {
 
     /**
      * 発言集計ウィンドウを返す。
+     *
      * @return 発言集計ウィンドウ
      */
     public DaySummary getDaySummary(){
@@ -315,6 +263,7 @@ public class WindowManager {
 
     /**
      * ヘルプウィンドウを生成する。
+     *
      * @return ヘルプウィンドウ
      */
     protected HelpFrame createHelpFrame(){
@@ -333,6 +282,7 @@ public class WindowManager {
 
     /**
      * ヘルプウィンドウを返す。
+     *
      * @return ヘルプウィンドウ
      */
     public HelpFrame getHelpFrame(){
@@ -344,6 +294,7 @@ public class WindowManager {
 
     /**
      * トップフレームを生成する。
+     *
      * @return トップフレーム
      */
     protected TopFrame createTopFrame(){
@@ -355,6 +306,7 @@ public class WindowManager {
 
     /**
      * トップフレームを返す。
+     *
      * @return トップフレーム
      */
     public TopFrame getTopFrame(){
@@ -366,75 +318,26 @@ public class WindowManager {
 
     /**
      * 管理下にある全ウィンドウのLookAndFeelを更新する。
-     * 必要に応じて再パッキングが行われる。
-     */
-    public void changeAllWindowUI(){
-        for(Window window : this.windowSet){
-            updateTreeUI(window);
-        }
-
-        if(this.filterPanel  != null) this.filterPanel.pack();
-        if(this.findPanel    != null) this.findPanel.pack();
-        if(this.accountPanel != null) this.accountPanel.pack();
-
-        return;
-    }
-
-    /**
-     * 再帰的に下層コンポーネントのLaFを更新する。
      *
-     * <p>{@link javax.swing.SwingUtilities#updateComponentTreeUI(Component)}
-     * がポップアップメニューのLaF更新を正しく行わないSun製JREのバグ
-     * [BugID:6299213]
-     * を回避するために作られた。
+     * <p>必要に応じて再パッキングが行われる。
      *
-     * @param comp 開始コンポーネント
-     * @see <a href="http://bugs.sun.com/view_bug.do?bug_id=6299213">
-     *     BugID:6299213
-     * </a>
+     * @param className Look and Feel
+     * @throws java.lang.ReflectiveOperationException reflection error
+     * @throws javax.swing.UnsupportedLookAndFeelException Unsupported LAF
      */
-    public static void updateTreeUI(Component comp) {
-        updateTreeUI(comp, true);
-        return;
-    }
+    public void changeAllWindowUI(String className)
+            throws ReflectiveOperationException,
+                   UnsupportedLookAndFeelException {
+        assert EventQueue.isDispatchThread();
 
-    /**
-     * 再帰的に下層コンポーネントのLaFを更新する。
-     * @param comp 開始コンポーネント
-     * @param isRoot このコンポーネントが最上位か否か指定する。
-     *     trueが指定された場合、LaF更新作業の後に再レイアウトを促す。
-     *     パフォーマンスの観点から、
-     *     ポップアップ以外の下層コンポーネントには
-     *     必要のない限りfalse指定を推奨。
-     */
-    public static void updateTreeUI(Component comp, boolean isRoot) {
-        if(comp instanceof JComponent){
-            JComponent jcomp = (JComponent) comp;
-            jcomp.updateUI();
-
-            JPopupMenu popup = jcomp.getComponentPopupMenu();
-            if(popup != null){
-                updateTreeUI(popup, true);
-            }
-        }
+        UIManager.setLookAndFeel(className);
 
-        if(comp instanceof JMenu){
-            JMenu menu = (JMenu) comp;
-            for(Component child : menu.getMenuComponents()){
-                updateTreeUI(child, false);
-            }
-        }else if(comp instanceof Container){
-            Container cont = (Container) comp;
-            for(Component child : cont.getComponents()){
-                updateTreeUI(child, false);
-            }
-        }
+        this.windowSet.forEach((window) -> {
+            SwingUtilities.updateComponentTreeUI(window);
+        });
 
-        if(isRoot){
-            comp.invalidate();
-            comp.validate();
-            comp.repaint();
-        }
+        if(this.filterPanel  != null) this.filterPanel.pack();
+        if(this.findPanel    != null) this.findPanel.pack();
 
         return;
     }
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body01.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body01.png
new file mode 100644 (file)
index 0000000..c0f81e9
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body01.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body02.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body02.png
new file mode 100644 (file)
index 0000000..65f6795
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body02.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body03.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body03.png
new file mode 100644 (file)
index 0000000..059da6d
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body03.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body04.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body04.png
new file mode 100644 (file)
index 0000000..322ba38
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body04.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body05.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body05.png
new file mode 100644 (file)
index 0000000..01fd8a4
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body05.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body06.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body06.png
new file mode 100644 (file)
index 0000000..dc5f7eb
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body06.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body07.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body07.png
new file mode 100644 (file)
index 0000000..a1b71bf
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body07.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body08.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body08.png
new file mode 100644 (file)
index 0000000..1a507e2
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body08.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body09.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body09.png
new file mode 100644 (file)
index 0000000..6d5bc0b
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body09.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body10.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body10.png
new file mode 100644 (file)
index 0000000..6e53162
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body10.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body11.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body11.png
new file mode 100644 (file)
index 0000000..8f05cc6
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body11.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body12.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body12.png
new file mode 100644 (file)
index 0000000..cb98452
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body12.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body13.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body13.png
new file mode 100644 (file)
index 0000000..a855dd7
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body13.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body14.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body14.png
new file mode 100644 (file)
index 0000000..21c60bc
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body14.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body15.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body15.png
new file mode 100644 (file)
index 0000000..65b272d
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body15.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body16.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body16.png
new file mode 100644 (file)
index 0000000..f2ca213
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body16.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body17.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body17.png
new file mode 100644 (file)
index 0000000..62a4423
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body17.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body18.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body18.png
new file mode 100644 (file)
index 0000000..6c7d02f
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body18.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body19.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body19.png
new file mode 100644 (file)
index 0000000..88caf59
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body19.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body20.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body20.png
new file mode 100644 (file)
index 0000000..9fd2f91
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body20.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body99.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body99.png
new file mode 100644 (file)
index 0000000..159d087
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/body99.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face01.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face01.png
new file mode 100644 (file)
index 0000000..d18edc1
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face01.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face02.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face02.png
new file mode 100644 (file)
index 0000000..1f743bb
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face02.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face03.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face03.png
new file mode 100644 (file)
index 0000000..dc1c1fc
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face03.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face04.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face04.png
new file mode 100644 (file)
index 0000000..0bd3e27
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face04.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face05.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face05.png
new file mode 100644 (file)
index 0000000..4963724
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face05.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face06.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face06.png
new file mode 100644 (file)
index 0000000..ea42bc0
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face06.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face07.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face07.png
new file mode 100644 (file)
index 0000000..e8daf7c
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face07.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face08.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face08.png
new file mode 100644 (file)
index 0000000..555927a
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face08.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face09.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face09.png
new file mode 100644 (file)
index 0000000..bddc323
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face09.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face10.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face10.png
new file mode 100644 (file)
index 0000000..76156cd
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face10.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face11.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face11.png
new file mode 100644 (file)
index 0000000..0de4b11
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face11.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face12.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face12.png
new file mode 100644 (file)
index 0000000..bfe4454
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face12.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face13.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face13.png
new file mode 100644 (file)
index 0000000..83d3762
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face13.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face14.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face14.png
new file mode 100644 (file)
index 0000000..75ea6b9
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face14.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face15.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face15.png
new file mode 100644 (file)
index 0000000..bb6258d
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face15.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face16.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face16.png
new file mode 100644 (file)
index 0000000..3342b68
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face16.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face17.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face17.png
new file mode 100644 (file)
index 0000000..404ae12
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face17.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face18.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face18.png
new file mode 100644 (file)
index 0000000..56304b4
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face18.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face19.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face19.png
new file mode 100644 (file)
index 0000000..ae10113
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face19.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face20.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face20.png
new file mode 100644 (file)
index 0000000..39cfbf8
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face20.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face99.png b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face99.png
new file mode 100644 (file)
index 0000000..b7920d7
Binary files /dev/null and b/src/main/resources/jp/sfjp/jindolf/resources/image/avatar/face99.png differ
diff --git a/src/main/resources/jp/sfjp/jindolf/resources/image/avatarCache.json b/src/main/resources/jp/sfjp/jindolf/resources/image/avatarCache.json
new file mode 100644 (file)
index 0000000..e1a13ab
--- /dev/null
@@ -0,0 +1,51 @@
+{
+
+  "avatarFace" : {
+    "gerd"      : "face01.jpg" ,
+    "walter"    : "face02.jpg" ,
+    "moritz"    : "face03.jpg" ,
+    "simson"    : "face04.jpg" ,
+    "thomas"    : "face05.jpg" ,
+    "nicolas"   : "face06.jpg" ,
+    "dieter"    : "face07.jpg" ,
+    "peter"     : "face08.jpg" ,
+    "liesa"     : "face09.jpg" ,
+    "albin"     : "face10.jpg" ,
+    "katharina" : "face11.jpg" ,
+    "otto"      : "face12.jpg" ,
+    "joachim"   : "face13.jpg" ,
+    "pamela"    : "face14.jpg" ,
+    "jacob"     : "face15.jpg" ,
+    "regina"    : "face16.jpg" ,
+    "fridel"    : "face17.jpg" ,
+    "erna"      : "face18.jpg" ,
+    "clara"     : "face19.jpg" ,
+    "simon"     : "face20.jpg" ,
+    "tomb"      : "face99.jpg"
+  } ,
+
+  "avatarBody" : {
+    "gerd"      : "body01.jpg" ,
+    "walter"    : "body02.jpg" ,
+    "moritz"    : "body03.jpg" ,
+    "simson"    : "body04.jpg" ,
+    "thomas"    : "body05.jpg" ,
+    "nicolas"   : "body06.jpg" ,
+    "dieter"    : "body07.jpg" ,
+    "peter"     : "body08.jpg" ,
+    "liesa"     : "body09.jpg" ,
+    "albin"     : "body10.jpg" ,
+    "katharina" : "body11.jpg" ,
+    "otto"      : "body12.jpg" ,
+    "joachim"   : "body13.jpg" ,
+    "pamela"    : "body14.jpg" ,
+    "jacob"     : "body15.jpg" ,
+    "regina"    : "body16.jpg" ,
+    "fridel"    : "body17.jpg" ,
+    "erna"      : "body18.jpg" ,
+    "clara"     : "body19.jpg" ,
+    "simon"     : "body20.jpg" ,
+    "tomb"      : "body99.jpg"
+  }
+
+}
index bdfeda7..3917c84 100644 (file)
@@ -1,6 +1,6 @@
 #
 # \u307e\u3068\u3081\u30b5\u30a4\u30c8\u5411\u3051\u9854\u30a2\u30a4\u30b3\u30f3\u30bb\u30c3\u30c8\u5b9a\u7fa9
-# \u307e\u3061\u3085\u6c0f\u306e\u904b\u55b6\u3059\u308b\u307e\u3068\u3081\u30b5\u30a4\u30c8\u306f => http://wolfbbs.jp/
+# \u307e\u3061\u3085\u6c0f\u306e\u904b\u55b6\u3059\u308b\u307e\u3068\u3081\u30b5\u30a4\u30c8\u306f => https://wolfbbs.jp/
 #
 # \u203b \u8457\u4f5c\u8ca1\u7523\u6a29\u4fdd\u6301\u8005\u304a\u3088\u3073\u753b\u50cf\u30b5\u30fc\u30d0\u904b\u55b6\u8005\u304b\u3089\u65b0\u3057\u3044\u610f\u5411\u304c\u793a\u3055\u308c\u305f\u5834\u5408\u3001
 # \u3000 \u305d\u3061\u3089\u3092\u6700\u512a\u5148\u3067\u5c0a\u91cd\u3057\u3066\u304f\u3060\u3055\u3044\u3002
@@ -13,7 +13,7 @@
 
 # --- start
 
-update = 2016-06-25T05:00:00+09:00
+update = 2020-03-22T16:04:00+09:00
 codeCheck = \u72fc
 
 iconset.order.100 = nagianPuki
@@ -23,7 +23,7 @@ iconset.order.300 = hagiosChimako
 #iconset.order.500 = kuroink
 iconset.order.600 = kuroink2
 iconset.order.700 = manabio
-iconset.order.800 = katainu
+#iconset.order.800 = katainu
 
 nagianPuki.author = \u51ea\u5eb5
 nagianPuki.caption = \u51ea\u5eb5\u6c0f\u4f5c \u65b0\u9854\u30a2\u30a4\u30b3\u30f3
@@ -51,7 +51,7 @@ nagianPuki.iconWiki.simon     = &ref(http://ninjinix.x0.com/wolfc/plugin_wolf/im
 
 hagiosHandsom.author = hagios
 hagiosHandsom.caption = hagios\u6c0f\u4f5c \u7f8e\u5f62\u9854\u30a2\u30a4\u30b3\u30f3
-hagiosHandsom.url = http://wolfbbs.jp/hagios.html
+hagiosHandsom.url = https://wolfbbs.jp/hagios.html
 hagiosHandsom.iconWiki.gerd      = &ref(http://abendgebet.jp/wolves/face01.jpg,nolink);
 hagiosHandsom.iconWiki.walter    = &ref(http://abendgebet.jp/wolves/face02.jpg,nolink);
 hagiosHandsom.iconWiki.moritz    = &ref(http://abendgebet.jp/wolves/face03.jpg,nolink);
@@ -75,7 +75,7 @@ hagiosHandsom.iconWiki.simon     = &ref(http://abendgebet.jp/wolves/face20.jpg,n
 
 hagiosChimako.author = hagios
 hagiosChimako.caption = hagios\u6c0f\u4f5c \u3061\u307e\u5b50\u9854\u30a2\u30a4\u30b3\u30f3
-hagiosChimako.url = http://wolfbbs.jp/hagios.html
+hagiosChimako.url = https://wolfbbs.jp/hagios.html
 hagiosChimako.iconWiki.gerd      = &ref(http://abendgebet.jp/wolves/mini01.jpg,nolink);
 hagiosChimako.iconWiki.walter    = &ref(http://abendgebet.jp/wolves/mini02.jpg,nolink);
 hagiosChimako.iconWiki.moritz    = &ref(http://abendgebet.jp/wolves/mini03.jpg,nolink);
@@ -99,7 +99,7 @@ hagiosChimako.iconWiki.simon     = &ref(http://abendgebet.jp/wolves/mini20.jpg,n
 
 fernery.author = fernery
 fernery.caption = \u30d1\u30b9\u30c6\u30eb\u30ab\u30e9\u30fc\u30a2\u30a4\u30b3\u30f3
-fernery.url = http://wolfbbs.jp/fernery.html
+fernery.url = https://wolfbbs.jp/fernery.html
 fernery.iconWiki.gerd      = &ref(http://www2.atpaint.jp/limedoline/src/1226856733860.png,nolink);
 fernery.iconWiki.walter    = &ref(http://www2.atpaint.jp/limedoline/src/1226856755265.png,nolink);
 fernery.iconWiki.moritz    = &ref(http://www2.atpaint.jp/limedoline/src/1226856785428.png,nolink);
@@ -123,7 +123,7 @@ fernery.iconWiki.simon     = &ref(http://www2.atpaint.jp/limedoline/src/12268572
 
 kuroink.author = kuroink
 kuroink.caption = \u30ad\u30e3\u30e9\u30a2\u30a4\u30b3\u30f3
-kuroink.url = http://wolfbbs.jp/kuroink.html
+kuroink.url = https://wolfbbs.jp/kuroink.html
 kuroink.iconWiki.gerd      = &ref(http://www3.atpaint.jp/happato/src/1354520519017.png,nolink);
 kuroink.iconWiki.walter    = &ref(http://www3.atpaint.jp/happato/src/1354531673515.png,nolink);
 kuroink.iconWiki.moritz    = &ref(http://www3.atpaint.jp/happato/src/1354532069882.png,nolink);
@@ -147,7 +147,7 @@ kuroink.iconWiki.simon     = &ref(http://www3.atpaint.jp/happato/src/13545350947
 
 kuroink2.author = kuroink
 kuroink2.caption = kuroink\u6c0f\u4f5c \u30ad\u30e3\u30e9\u30a2\u30a4\u30b3\u30f3
-kuroink2.url = http://wolfbbs.jp/kuroink.html
+kuroink2.url = https://wolfbbs.jp/kuroink.html
 kuroink2.iconWiki.gerd      = &ref(http://blog-imgs-89.fc2.com/h/e/r/herbsfolles/40ger.png,nolink);
 kuroink2.iconWiki.walter    = &ref(http://blog-imgs-89.fc2.com/h/e/r/herbsfolles/40wal.png,nolink);
 kuroink2.iconWiki.moritz    = &ref(http://blog-imgs-89.fc2.com/h/e/r/herbsfolles/40mor.png,nolink);
@@ -171,7 +171,7 @@ kuroink2.iconWiki.simon     = &ref(http://blog-imgs-89.fc2.com/h/e/r/herbsfolles
 
 manabio.author = manabio
 manabio.caption = manabio\u6c0f\u4f5c \u30c9\u30c3\u30c8\u7d75\u30a2\u30a4\u30b3\u30f3
-manabio.url = http://wolfbbs.jp/manabio.html
+manabio.url = https://wolfbbs.jp/manabio.html
 manabio.iconWiki.gerd      = &ref(http://bbs.mottoki.com/img/haruka1201/1432981752-yj641l.gif,nolink);
 manabio.iconWiki.walter    = &ref(http://bbs.mottoki.com/img/haruka1201/1432981988-oci17k.gif,nolink);
 manabio.iconWiki.moritz    = &ref(http://bbs.mottoki.com/img/haruka1201/1432981960-8mbzqq.gif,nolink);
@@ -195,7 +195,7 @@ manabio.iconWiki.simon     = &ref(http://bbs.mottoki.com/img/haruka1201/14329821
 
 katainu.author = katainu
 katainu.caption = katainu\u6c0f\u4f5c \u30ad\u30e3\u30e9\u30a2\u30a4\u30b3\u30f3
-katainu.url = http://wolfbbs.jp/katainu.html
+katainu.url = https://wolfbbs.jp/katainu.html
 katainu.iconWiki.gerd      = &ref(http://www.netriver.jp/rbs/usr/kujiro/data/img/IM00001.png,nolink);
 katainu.iconWiki.walter    = &ref(http://www.netriver.jp/rbs/usr/kujiro/data/img/IM00002.png,nolink);
 katainu.iconWiki.moritz    = &ref(http://www.netriver.jp/rbs/usr/kujiro/data/img/IM00005.png,nolink);
index e886be4..7cd2fc4 100644 (file)
@@ -58,7 +58,7 @@ public class JreCheckerTest {
     @Test
     public void testHas11Runtime() {
         System.out.println("has11Runtime");
-        assertTrue(JreChecker.has11Runtime());
+        assertTrue(JreChecker.has1_1Runtime());
         return;
     }
 
@@ -68,7 +68,7 @@ public class JreCheckerTest {
     @Test
     public void testHas12Runtime() {
         System.out.println("has12Runtime");
-        assertTrue(JreChecker.has12Runtime());
+        assertTrue(JreChecker.has1_2Runtime());
         return;
     }
 
@@ -78,7 +78,7 @@ public class JreCheckerTest {
     @Test
     public void testHas13Runtime() {
         System.out.println("has13Runtime");
-        assertTrue(JreChecker.has13Runtime());
+        assertTrue(JreChecker.has1_3Runtime());
         return;
     }
 
@@ -88,7 +88,7 @@ public class JreCheckerTest {
     @Test
     public void testHas14Runtime() {
         System.out.println("has14Runtime");
-        assertTrue(JreChecker.has14Runtime());
+        assertTrue(JreChecker.has1_4Runtime());
         return;
     }
 
@@ -98,7 +98,7 @@ public class JreCheckerTest {
     @Test
     public void testHas15Runtime() {
         System.out.println("has15Runtime");
-        assertTrue(JreChecker.has15Runtime());
+        assertTrue(JreChecker.has1_5Runtime());
         return;
     }
 
@@ -108,7 +108,7 @@ public class JreCheckerTest {
     @Test
     public void testHas16Runtime() {
         System.out.println("has16Runtime");
-        assertTrue(JreChecker.has16Runtime());
+        assertTrue(JreChecker.has1_6Runtime());
         return;
     }
 
@@ -118,7 +118,17 @@ public class JreCheckerTest {
     @Test
     public void testHas17Runtime() {
         System.out.println("has17Runtime");
-        assertTrue(JreChecker.has17Runtime());
+        assertTrue(JreChecker.has1_7Runtime());
+        return;
+    }
+
+    /**
+     * Test of has18Runtime method, of class JreChecker.
+     */
+    @Test
+    public void testHas18Runtime() {
+        System.out.println("has18Runtime");
+        assertTrue(JreChecker.has1_8Runtime());
         return;
     }
 
diff --git a/src/test/java/jp/sfjp/jindolf/data/AnchorTest.java b/src/test/java/jp/sfjp/jindolf/data/AnchorTest.java
new file mode 100644 (file)
index 0000000..fc2a22d
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ */
+
+package jp.sfjp.jindolf.data;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ *
+ */
+public class AnchorTest {
+
+    public AnchorTest() {
+    }
+
+    @BeforeClass
+    public static void setUpClass() {
+    }
+
+    @AfterClass
+    public static void tearDownClass() {
+    }
+
+    @Before
+    public void setUp() {
+    }
+
+    @After
+    public void tearDown() {
+    }
+
+    /**
+     * Test of parseInt method, of class Anchor.
+     */
+    @Test
+    public void testParseInt_3args_1() {
+        System.out.println("parseInt");
+
+        int result;
+        Matcher matcher;
+        Pattern pattern;
+        String input = "ABC123PQR456XYZ";
+
+        pattern = Pattern.compile("([0-9]+)[A-Z]*([0-9]+)");
+        matcher = pattern.matcher(input);
+
+        assertTrue(matcher.find());
+
+        result = Anchor.parseInt(input, matcher, 1);
+        assertEquals(123, result);
+
+        result = Anchor.parseInt(input, matcher, 2);
+        assertEquals(456, result);
+
+        try{
+            Anchor.parseInt(null, matcher, 1);
+            fail();
+        }catch(NullPointerException e){
+        }
+
+        try{
+            Anchor.parseInt(input, null, 1);
+            fail();
+        }catch(NullPointerException e){
+        }
+
+        return;
+    }
+
+    /**
+     * Test of parseInt method, of class Anchor.
+     */
+    @Test
+    public void testParseInt_3args_2() {
+        System.out.println("parseInt");
+
+        int result;
+
+        try{
+            Anchor.parseInt(null, 1, 3);
+            fail();
+        }catch(NullPointerException e){
+        }
+
+        result = Anchor.parseInt("1234567", 2, 5);
+        assertEquals(345, result);
+
+        result = Anchor.parseInt("1234567", 2, 3);
+        assertEquals(3, result);
+
+        result = Anchor.parseInt("1234567", 2, 2);
+        assertEquals(0, result);
+
+        result = Anchor.parseInt("1234567", 2, 1);
+        assertEquals(0, result);
+
+        result = Anchor.parseInt("1234567", 0, 0);
+        assertEquals(0, result);
+
+        try{
+            Anchor.parseInt("1234567", 2, 999);
+            fail();
+        }catch(StringIndexOutOfBoundsException e){
+        }
+
+        try{
+            Anchor.parseInt("1234567", -1, 5);
+            fail();
+        }catch(StringIndexOutOfBoundsException e){
+        }
+
+        return;
+    }
+
+}
index e852a6b..352e3a1 100644 (file)
@@ -7,8 +7,6 @@
 package jp.sfjp.jindolf.data;
 
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -54,59 +52,15 @@ public class AvatarTest {
     }
 
     /**
-     * Test of getPredefinedAvatar method, of class Avatar.
+     * Test of getAvatarByFullname method, of class Avatar.
      */
     @Test
-    public void testGetPredefinedAvatar(){
+    public void testGetAvatarByFullname(){
         System.out.println("getPredefinedAvatar");
         Avatar result;
-        result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+        result = Avatar.getAvatarByFullname("農夫 ヤコブ");
         assertNotNull(result);
         assertTrue(result.equals(result));
-        result = Avatar.getPredefinedAvatar((CharSequence)"農夫 ヤコブ");
-        assertNotNull(result);
-        assertTrue(result.equals(result));
-        return;
-    }
-
-    /**
-     * Test of lookingAtAvatar method, of class Avatar.
-     */
-    @Test
-    public void testMatchAvatar(){
-        System.out.println("matchAvatar");
-        Matcher matcher;
-        Avatar avatar;
-
-        Pattern pattern = Pattern.compile(".+");
-
-        matcher = pattern.matcher("農夫 ヤコブ");
-        avatar = Avatar.lookingAtAvatar(matcher);
-        assertNotNull(avatar);
-        assertEquals("農夫 ヤコブ", avatar.getFullName());
-
-        matcher = pattern.matcher("農夫 ヤコブXYZ");
-        avatar = Avatar.lookingAtAvatar(matcher);
-        assertNotNull(avatar);
-        assertEquals("農夫 ヤコブ", avatar.getFullName());
-
-        matcher = pattern.matcher("ABC農夫 ヤコブ");
-        avatar = Avatar.lookingAtAvatar(matcher);
-        assertNull(avatar);
-
-        matcher = pattern.matcher("農夫 ヤコブならず者 ディーター");
-        avatar = Avatar.lookingAtAvatar(matcher);
-        assertNotNull(avatar);
-        assertEquals("農夫 ヤコブ", avatar.getFullName());
-        int regionStart;
-        int regionEnd;
-        regionStart = matcher.end();
-        regionEnd = matcher.regionEnd();
-        matcher.region(regionStart, regionEnd);
-        avatar = Avatar.lookingAtAvatar(matcher);
-        assertNotNull(avatar);
-        assertEquals("ならず者 ディーター", avatar.getFullName());
-
         return;
     }
 
@@ -116,7 +70,7 @@ public class AvatarTest {
     @Test
     public void testGetFullName(){
         System.out.println("getFullName");
-        Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+        Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
         assertNotNull(result);
         assertEquals("農夫 ヤコブ", result.getFullName());
         return;
@@ -128,7 +82,7 @@ public class AvatarTest {
     @Test
     public void testGetJobTitle(){
         System.out.println("getJobTitle");
-        Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+        Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
         assertNotNull(result);
         assertEquals("農夫", result.getJobTitle());
         return;
@@ -140,7 +94,7 @@ public class AvatarTest {
     @Test
     public void testGetName(){
         System.out.println("getName");
-        Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+        Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
         assertNotNull(result);
         assertEquals("ヤコブ", result.getName());
         return;
@@ -152,7 +106,7 @@ public class AvatarTest {
     @Test
     public void testGetIdNum(){
         System.out.println("getIdNum");
-        Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+        Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
         assertNotNull(result);
         assertEquals(15, result.getIdNum());
         return;
@@ -164,7 +118,7 @@ public class AvatarTest {
     @Test
     public void testEquals(){
         System.out.println("equals");
-        Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+        Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
         assertTrue(result.equals(result));
         return;
     }
@@ -175,9 +129,9 @@ public class AvatarTest {
     @Test
     public void testCompareTo(){
         System.out.println("compareTo");
-        Avatar avatar1 = Avatar.getPredefinedAvatar("農夫 ヤコブ");
-        Avatar avatar2 = Avatar.getPredefinedAvatar("シスター フリーデル");
-        Avatar avatar3 = Avatar.getPredefinedAvatar("羊飼い カタリナ");
+        Avatar avatar1 = Avatar.getAvatarByFullname("農夫 ヤコブ");
+        Avatar avatar2 = Avatar.getAvatarByFullname("シスター フリーデル");
+        Avatar avatar3 = Avatar.getAvatarByFullname("羊飼い カタリナ");
         assertTrue(avatar1.compareTo(avatar2) < 0);
         assertTrue(avatar2.compareTo(avatar3) > 0);
         assertTrue(avatar2.compareTo(avatar2) == 0);
diff --git a/src/test/java/jp/sfjp/jindolf/data/TalkTest.java b/src/test/java/jp/sfjp/jindolf/data/TalkTest.java
new file mode 100644 (file)
index 0000000..103fb99
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ */
+
+package jp.sfjp.jindolf.data;
+
+import jp.sourceforge.jindolf.corelib.TalkType;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ *
+ */
+public class TalkTest {
+
+    public TalkTest() {
+    }
+
+    @BeforeClass
+    public static void setUpClass() {
+    }
+
+    @AfterClass
+    public static void tearDownClass() {
+    }
+
+    @Before
+    public void setUp() {
+    }
+
+    @After
+    public void tearDown() {
+    }
+
+    /**
+     * Test of encodeColorName method, of class Talk.
+     */
+    @Test
+    public void testEncodeColorName() {
+        System.out.println("encodeColorName");
+
+        assertEquals("白", Talk.encodeColorName(TalkType.PUBLIC));
+        assertEquals("灰", Talk.encodeColorName(TalkType.PRIVATE));
+        assertEquals("赤", Talk.encodeColorName(TalkType.WOLFONLY));
+        assertEquals("青", Talk.encodeColorName(TalkType.GRAVE));
+
+        try{
+            Talk.encodeColorName(null);
+            fail();
+        }catch(NullPointerException e){
+            assert true;
+        }
+
+        return;
+    }
+
+    /**
+     * Test of isTerminated method, of class Talk.
+     */
+    @Test
+    public void testIsTerminated() {
+        System.out.println("isTerminated");
+
+        try{
+            Talk.isTerminated(null, null);
+            fail();
+        }catch(NullPointerException e){
+            assert true;
+        }
+
+        try{
+            Talk.isTerminated("A", null);
+            fail();
+        }catch(NullPointerException e){
+            assert true;
+        }
+
+        try{
+            Talk.isTerminated(null, "X");
+            fail();
+        }catch(NullPointerException e){
+            assert true;
+        }
+
+        assertTrue(Talk.isTerminated("ABCXYZ", "XYZ"));
+        assertTrue(Talk.isTerminated("ABCXYZ", "ABCXYZ"));
+        assertTrue(Talk.isTerminated("ABCXYZ", ""));
+        assertTrue(Talk.isTerminated("", ""));
+
+        assertFalse(Talk.isTerminated("ABCXYZ", "PQR"));
+        assertFalse(Talk.isTerminated("ABCXYZ", "1ABCXYZ"));
+        assertFalse(Talk.isTerminated("ABC", "ABCXYZ"));
+        assertFalse(Talk.isTerminated("", "XYZ"));
+
+        return;
+    }
+
+}
index aba3523..280fd5c 100644 (file)
@@ -73,6 +73,11 @@ public class HttpUtilsTest {
         result = HttpUtils.escapeHttpComment(comment);
         assertEquals(expResult, result);
 
+        comment = "a🐑b";
+        expResult = "(a?b)";
+        result = HttpUtils.escapeHttpComment(comment);
+        assertEquals(expResult, result);
+
         return;
     }
 
@@ -113,6 +118,7 @@ public class HttpUtilsTest {
 
     /**
      * Test of formatHttpStat method, of class HttpUtils.
+     * @throws java.lang.Exception
      */
     @Test
     public void testFormatHttpStat() throws Exception{
@@ -150,6 +156,7 @@ public class HttpUtilsTest {
 
     /**
      * Test of getHTMLCharset method, of class HttpUtils.
+     * @throws java.lang.Exception
      */
     @Test
     public void testGetHTMLCharset_URLConnection() throws Exception{
index c8b70fe..7610998 100644 (file)
@@ -6,8 +6,6 @@
 
 package jp.sfjp.jindolf.util;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -41,122 +39,6 @@ public class StringUtilsTest {
     }
 
     /**
-     * Test of parseInt method, of class StringUtils.
-     */
-    @Test
-    public void testParseInt_3args_1(){
-        System.out.println("parseInt");
-
-        int result;
-        Matcher matcher;
-        Pattern pattern;
-        String input = "ABC123PQR456XYZ";
-
-        pattern = Pattern.compile("([0-9]+)[A-Z]*([0-9]+)");
-        matcher = pattern.matcher(input);
-
-        assertTrue(matcher.find());
-
-        result = StringUtils.parseInt(input, matcher, 1);
-        assertEquals(123, result);
-
-        result = StringUtils.parseInt(input, matcher, 2);
-        assertEquals(456, result);
-
-        try{
-            StringUtils.parseInt(null, matcher, 1);
-            fail();
-        }catch(NullPointerException e){
-        }
-
-        try{
-            StringUtils.parseInt(input, null, 1);
-            fail();
-        }catch(NullPointerException e){
-        }
-
-        return;
-    }
-
-    /**
-     * Test of parseInt method, of class StringUtils.
-     */
-    @Test
-    public void testParseInt_CharSequence(){
-        System.out.println("parseInt");
-
-        int result;
-
-        try{
-            StringUtils.parseInt(null);
-            fail();
-        }catch(NullPointerException e){
-        }
-
-        result = StringUtils.parseInt("");
-        assertEquals(0, result);
-
-        result = StringUtils.parseInt("0");
-        assertEquals(0, result);
-
-        result = StringUtils.parseInt("999");
-        assertEquals(999, result);
-
-        result = StringUtils.parseInt("X");
-        assertEquals(0, result);
-
-        result = StringUtils.parseInt("-1");
-        assertEquals(0, result);
-
-        return;
-    }
-
-    /**
-     * Test of parseInt method, of class StringUtils.
-     */
-    @Test
-    public void testParseInt_3args_2(){
-        System.out.println("parseInt");
-
-        int result;
-
-        try{
-            StringUtils.parseInt(null, 1, 3);
-            fail();
-        }catch(NullPointerException e){
-        }
-
-        result = StringUtils.parseInt("1234567", 2, 5);
-        assertEquals(345, result);
-
-        result = StringUtils.parseInt("1234567", 2, 3);
-        assertEquals(3, result);
-
-        result = StringUtils.parseInt("1234567", 2, 2);
-        assertEquals(0, result);
-
-        result = StringUtils.parseInt("1234567", 2, 1);
-        assertEquals(0, result);
-
-        result = StringUtils.parseInt("1234567", 0, 0);
-        assertEquals(0, result);
-
-        try{
-            StringUtils.parseInt("1234567", 2, 999);
-            fail();
-        }catch(StringIndexOutOfBoundsException e){
-        }
-
-        try{
-            StringUtils.parseInt("1234567", -1, 5);
-            fail();
-        }catch(StringIndexOutOfBoundsException e){
-        }
-
-        return;
-    }
-
-    /**
      * Test of suppressString method, of class StringUtils.
      */
     @Test
@@ -187,42 +69,6 @@ public class StringUtilsTest {
     }
 
     /**
-     * Test of isTerminated method, of class StringUtils.
-     */
-    @Test
-    public void testIsTerminated(){
-        System.out.println("isTerminated");
-
-        try{
-            StringUtils.isTerminated(null, null);
-            fail();
-        }catch(NullPointerException e){
-        }
-
-        try{
-            StringUtils.isTerminated("A", null);
-            fail();
-        }catch(NullPointerException e){
-        }
-
-        try{
-            StringUtils.isTerminated(null, "X");
-            fail();
-        }catch(NullPointerException e){
-        }
-
-        assertTrue(StringUtils.isTerminated("ABCXYZ", "XYZ"));
-        assertTrue(StringUtils.isTerminated("ABCXYZ", ""));
-        assertTrue(StringUtils.isTerminated("", ""));
-
-        assertFalse(StringUtils.isTerminated("ABCXYZ", "PQR"));
-        assertFalse(StringUtils.isTerminated("ABC", "ABCXYZ"));
-        assertFalse(StringUtils.isTerminated("", "XYZ"));
-
-        return;
-    }
-
-    /**
      * Test of compareSubSequence method, of class StringUtils.
      */
     @Test