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を移行。
- 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の実行には日本語環境が
必要です。
<!--
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" />
<suppress files="" checks="FinalParameters" />
<suppress files="" checks="TrailingComment" />
- <!-- Modifiers -->
+ <!-- Modifier -->
<suppress files="" checks="InterfaceMemberImpliedModifier" />
+ <suppress files="" checks="RedundantModifier" />
<!-- Whitespace -->
<suppress files="" checks="SingleSpaceSeparator" />
<!--
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="<?xml version="1.0" encoding="UTF-8" ?>" />
<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" />
<!--
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
-->
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"
>
<exclude name="OnlyOneReturn" />
<exclude name="ShortVariable" />
<exclude name="UnnecessaryLocalBeforeReturn" />
+ <exclude name="UnnecessaryModifier" />
<exclude name="UnnecessaryReturn" />
</rule>
<rule ref="category/java/codestyle.xml/ControlStatementBraces" >
<groupId>jp.sourceforge.jindolf</groupId>
<artifactId>jindolf</artifactId>
- <version>3.303.106</version>
+ <version>4.101.2</version>
<packaging>jar</packaging>
<name>Jindolf</name>
<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>
>
<!--
- OSDN.net用リリースファイル構成定義ファイル
- Maven3 assembly用
+ for maven-assembly-plugin
-->
<id>src</id>
<formats>
+ <format>tar.gz</format>
<format>zip</format>
</formats>
package jp.sfjp.jindolf;
-import java.awt.Component;
-import java.awt.Cursor;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.Window;
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;
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;
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;
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;
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;
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;
* @param setting アプリ設定
*/
@SuppressWarnings("LeakingThisInConstructor")
- public Controller(LandsModel model,
+ public Controller(LandsTreeModel model,
WindowManager windowManager,
ActionManager actionManager,
AppSetting setting){
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()
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());
}
});
- 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);
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 ウィンドウマネジャ
}
/**
+ * トップフレームのタイトルを設定する。
+ * タイトルは指定された国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ならプログレスバーのアニメ開始&WAITカーソル。
+ * falseなら停止&通常カーソル。
+ * @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移行に遅延される。
* @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;
}
}
/**
- * 軽量タスクを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>タスク実行中はビジー状態となる。
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;
}
* 村をWebブラウザで表示する。
*/
private void actionShowWebVillage(){
- TabBrowser browser = this.topView.getTabBrowser();
- Village village = browser.getVillage();
+ Village village = getVillage();
if(village == null) return;
Land land = village.getParentLand();
* 村に対応するまとめサイトを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);
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();
URL url = server.getPeriodURL(period);
String urlText = url.toString();
- if(period.isHot()) urlText += "#bottom";
WebIPCDialog.showDialog(getTopFrame(), urlText);
* 個別の発言を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();
}
/**
- * L&Fの変更を行う。
+ * L&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;
}
}
/**
- * アカウント管理画面を表示する。
- */
- private void actionShowAccount(){
- AccountPanel accountPanel = this.windowManager.getAccountPanel();
- toggleWindow(accountPanel);
- return;
- }
-
- /**
* ログ表示画面を表示する。
*/
private void actionShowLog(){
}
/**
- * 発言エディタを表示する。
- */
- private void actionTalkPreview(){
- TalkPreview talkPreview = this.windowManager.getTalkPreview();
- toggleWindow(talkPreview);
- return;
- }
-
- /**
* オプション設定画面を表示する。
*/
private void actionOption(){
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;
* 村ダイジェスト画面を表示する。
*/
private void actionShowDigest(){
- TabBrowser browser = this.topView.getTabBrowser();
- final Village village = browser.getVillage();
+ Village village = getVillage();
if(village == null) return;
VillageState villageState = village.getState();
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;
}
*/
// 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;
}
* 一括検索処理。
*/
private void bulkSearch(){
- Executor executor = Executors.newCachedThreadPool();
- executor.execute(new Runnable(){
- @Override
- public void run(){
- taskBulkSearch();
- return;
- }
- });
+ submitHeavyBusyTask(
+ () -> {taskBulkSearch();},
+ null, null
+ );
+ return;
}
/**
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;
* 全日程の一括ロード。
*/
private void actionLoadAllPeriod(){
- Executor executor = Executors.newCachedThreadPool();
- executor.execute(new Runnable(){
- @Override
- public void run(){
- taskLoadAllPeriod();
- return;
- }
- });
+ submitHeavyBusyTask(
+ () -> {taskLoadAllPeriod();},
+ "一括読み込み開始",
+ "一括読み込み完了"
+ );
return;
}
* 全日程の一括ロード。ヘビータスク版。
*/
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;
}
}
/**
+ * アンカー先を含む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(){
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;
}
* @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;
}
* @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;
* @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;
}
}
/**
- * 現在選択中の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 村
}
/**
- * {@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;
}
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;
}
}
* Jindolf のスタートアップエントリ。
*
* <p>互換性検査が行われた後、
- * JRE1.7解除版エントリ{@link JindolfJre17}
+ * JRE1.8解除版エントリ{@link JindolfJre18}
* に制御を渡す。
*
* @param args コマンドライン引数
exitCode = JreChecker.checkJre();
if(exitCode != 0) System.exit(exitCode);
- exitCode = JindolfJre17.main(args);
+ exitCode = JindolfJre18.main(args);
if(exitCode != 0) System.exit(exitCode);
// デーモンスレッドがいなければ、(アプリ画面が出ていなければ)
import javax.swing.JOptionPane;
/**
- * JRE1.7の利用が解禁されたJindolfエントリ。
+ * JRE1.8の利用が解禁されたJindolfエントリ。
*
* <p>起動クラスJindolfの下請けとしての機能が想定される。
*
*
* <p>各種診断を通過した後、JindolfMainに制御を渡す。
*/
-public final class JindolfJre17 {
+public final class JindolfJre18 {
/** GUI環境に接続できないときの終了コード。 */
public static final int EXIT_CODE_HEADLESS = 1;
/**
* 隠しコンストラクタ。
*/
- private JindolfJre17(){
+ private JindolfJre18(){
assert false;
}
/**
* Jindolf のスタートアップエントリ。
*
- * <p>ここからJRE1.7の利用が解禁される。
+ * <p>ここからJRE1.8の利用が解禁される。
*
* <p>最終的に{@link JindolfMain}へ制御が渡される。
*
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;
/**
* Jindolf スタートアップクラス。
*
- * <p>{@link JindolfJre17}の下請けとして本格的なJindolf起動処理に入る。
+ * <p>{@link JindolfJre18}の下請けとして本格的なJindolf起動処理に入る。
*/
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);
}
* @return アプリケーションのトップフレーム
*/
private static JFrame buildMVC(AppSetting appSetting){
- LandsModel model = new LandsModel();
+ LandsTreeModel model = new LandsTreeModel();
WindowManager windowManager = new WindowManager();
ActionManager actionManager = new ActionManager();
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;
/**
* クラス名に相当するクラスがロードできるか判定する。
+ *
* @param klassName FQDNなクラス名
* @return ロードできたらtrue
*/
/**
* 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(){
/**
* JREのインストール情報を返す。
- * システムプロパティ<code>java.home</code>の取得が試みられる。
+ *
+ * <p>システムプロパティ<code>java.home</code>の取得が試みられる。
+ *
* @return インストール情報。不明ならnull
*/
public static String getJreHome(){
/**
* 非互換エラーメッセージを組み立てる。
+ *
* @return エラーメッセージ
*/
public static String buildErrMessage(){
}
/**
- * 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);
}
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;
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;
* リカバリの対象外とする。(ビルド工程の不手際扱い。)
*
* @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();
}
/**
* リソース名が絶対パスか否か判定する。
*
- * <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;
}
* {@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;
}
* リソース名を用いて、
* デフォルトのクラスローダとデフォルトのルートパッケージから
* リソースのURLを取得する。
+ *
* @param resPath リソース名
* @return リソースのURL。リソースが見つからなければnull。
*/
/**
* 任意のルートパッケージと相対リソース名を用いて、
* デフォルトのクラスローダからリソースの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;
* リソース名を用いて、
* デフォルトのクラスローダとデフォルトのルートパッケージから
* リソースの入力ストリームを取得する。
+ *
* @param resPath リソース名
* @return リソースの入力ストリーム。リソースが見つからなければnull。
*/
/**
* 任意のルートパッケージと相対リソース名を用いて、
* デフォルトのクラスローダからリソースの入力ストリームを取得する。
+ *
* @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;
/**
* リソース名を用いてイメージ画像を取得する。
+ *
* @param resPath 画像リソース名
* @return イメージ画像。リソースが見つからなければnull。
*/
/**
* リソース名を用いてアイコン画像を取得する。
+ *
* @param resPath アイコン画像リソース名
* @return アイコン画像。リソースが見つからなければnull。
*/
/**
* リソース名を用いてプロパティを取得する。
+ *
* @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;
}
* @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;
}
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;
/**
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;
private JsValue loadedNetConfig;
private JsValue loadedTalkConfig;
+ private final Map<String, BufferedImage> avatarFaceMap = new HashMap<>();
+ private final Map<String, BufferedImage> avatarBodyMap = new HashMap<>();
+
+
/**
* コンストラクタ。
* @param info コマンドライン引数
}
/**
+ * 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(){
public void loadConfig(){
loadNetConfig();
loadTalkConfig();
+ loadLocalImageConfig();
return;
}
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;
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>"
* 隠れコンストラクタ。
*/
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(){
/**
* まだ存在しない設定格納ディレクトリを新規に作成する。
- * エラーがあればダイアログ提示とともに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();
}
/**
+ * ローカル画像キャッシュディレクトリを作る。
+ *
+ * <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){
/**
* 設定ディレクトリ操作の
* 共通エラーメッセージ確認ダイアログを表示する。
- * 閉じるまで待つ。
+ *
+ * <p>閉じるまで待つ。
+ *
* @param seq メッセージ
*/
private static void showWarnMessage(CharSequence seq){
/**
* 設定ディレクトリ操作の
* 情報提示メッセージ確認ダイアログを表示する。
- * 閉じるまで待つ。
+ *
+ * <p>閉じるまで待つ。
+ *
* @param seq メッセージ
*/
private static void showInfoMessage(CharSequence seq){
/**
* ダイアログを表示し、閉じられるまで待つ。
+ *
* @param pane ダイアログの元となるペイン
*/
private static void showDialog(JOptionPane pane){
/**
* 設定ディレクトリのルートファイルシステムもしくはドライブレターに
* アクセスできないエラーをダイアログに提示し、VM終了する。
+ *
* @param path 設定ディレクトリ
* @param preMessage メッセージ前半
*/
/**
* 設定ディレクトリの祖先に書き込めないエラーをダイアログで提示し、
* VM終了する。
+ *
* @param existsAncestor 存在するもっとも近い祖先
* @param preMessage メッセージ前半
*/
/**
* 設定ディレクトリを新規に生成してよいかダイアログで問い合わせる。
+ *
* @param existsAncestor 存在するもっとも近い祖先
* @param preMessage メッセージ前半
* @return 生成してよいと指示があればtrue
/**
* 設定ディレクトリが生成できないエラーをダイアログで提示し、
* VM終了する。
+ *
* @param path 生成できなかったディレクトリ
*/
private static void abortCantBuildConfigDir(File path){
/**
* 設定ディレクトリへアクセスできないエラーをダイアログで提示し、
* VM終了する。
+ *
* @param path アクセスできないディレクトリ
*/
private static void abortCantAccessConfigDir(File path){
/**
* ファイルに書き込めないエラーをダイアログで提示し、VM終了する。
+ *
* @param file 書き込めなかったファイル
*/
private static void abortCantWrite(File file){
/**
* 指定されたディレクトリにREADMEファイルを生成する。
- * 生成できなければダイアログ表示とともにVM終了する。
+ *
+ * <p>生成できなければダイアログ表示とともにVM終了する。
+ *
* @param path READMEの格納ディレクトリ
*/
private static void touchReadme(File path){
/**
* 設定ディレクトリがアクセス可能でなければ
* エラーダイアログを出してVM終了する。
+ *
* @param confDir 設定ディレクトリ
*/
public static void checkAccessibility(File confDir){
/**
* センタリングされたファイル名表示のHTML表記を出力する。
+ *
* @param path ファイル
* @return HTML表記
*/
/**
* ロックエラーダイアログの表示。
- * 呼び出しから戻ってもまだロックオブジェクトが
+ *
+ * <p>呼び出しから戻ってもまだロックオブジェクトが
* ロックファイルのオーナーでない場合、
* 今後設定ディレクトリは一切使わずに起動を続行するものとする。
- * ロックファイルの強制解除に失敗した場合はVM終了する。
+ *
+ * <p>ロックファイルの強制解除に失敗した場合はVM終了する。
+ *
* @param lock エラーを起こしたロック
*/
public static void confirmLockError(InterVMLock lock){
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;
/** 検索履歴ファイル。 */
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;
/**
* 設定ディレクトリを使うか否か判定する。
+ *
* @return 設定ディレクトリを使うならtrue。
*/
public boolean useStoreFile(){
/**
* 設定ディレクトリを返す。
+ *
* @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;
}
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;
/**
* ロックファイルの取得を試みる。
+ *
+ * <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();
ConfigFile.confirmLockError(lock);
if( ! lock.isFileOwner() ){
this.useStoreFile = false;
- this.configPath = null;
+ this.isImplicitPath = true;
+ this.configDir = null;
}
}
/**
* 設定ディレクトリ上のOBJECT型JSONファイルを読み込む。
+ *
* @param file JSONファイルの相対パス。
* @return JSON object。
* 設定ディレクトリを使わない設定、
/**
* 設定ディレクトリ上のJSONファイルを読み込む。
+ *
* @param file JSONファイルの相対パス
* @return JSON objectまたはarray。
* 設定ディレクトリを使わない設定、
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;
}
/**
* 文字ストリーム上のJSONデータを読み込む。
+ *
* @param reader 文字ストリーム
* @return JSON objectまたはarray。
* @throws IOException 入力エラー
/**
* 設定ディレクトリ上のJSONファイルに書き込む。
+ *
* @param file JSONファイルの相対パス
* @param root JSON objectまたはarray
* @return 正しくセーブが行われればtrue。
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();
/**
* 文字ストリームにJSONデータを書き込む。
+ *
* @param writer 文字ストリーム出力
* @param root JSON objectまたはarray
* @throws IOException 出力エラー
/**
* 検索履歴ファイルを読み込む。
+ *
* @return 履歴データ。履歴を読まないもしくは読めない場合はnull
*/
public JsObject loadHistoryConfig(){
}
/**
- * 原稿ファイルを読み込む。
- * @return 原稿データ。原稿を読まないもしくは読めない場合はnull
- */
- public JsObject loadDraftConfig(){
- JsObject result = loadJsObject(DRAFT_FILE);
- return result;
- }
-
- /**
* ネットワーク設定ファイルを読み込む。
+ *
* @return ネットワーク設定データ。
* 設定を読まないもしくは読めない場合はnull
*/
/**
* 台詞表示設定ファイルを読み込む。
+ *
* @return 台詞表示設定データ。
* 設定を読まないもしくは読めない場合はnull
*/
}
/**
- * 検索履歴ファイルに書き込む。
- * @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
*/
/**
* 台詞表示設定ファイルに書き込む。
+ *
* @param root 台詞表示設定
* @return 書き込まなかったもしくは書き込めなかった場合はfalse
*/
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import jp.sfjp.jindolf.util.StringUtils;
/**
* 発言アンカー。
/* 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;
}
}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;
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;
}
/**
+ * 正規表現にマッチした領域を数値化する。
+ *
+ * @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 アンカーの含まれる文字列
*/
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;
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;
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(){
}
/**
- * 定義済み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(){
/**
* 職業名を取得する。
+ *
* @return 職業名
*/
public String getJobTitle(){
/**
* 通常名を取得する。
+ *
* @return 通常名
*/
public String getName(){
/**
* 通し番号を返す。
+ *
* @return 通し番号
*/
public int getIdNum(){
}
/**
- * 識別文字列を返す。
+ * AvatarID識別文字列を返す。
+ *
* @return 識別文字列
*/
public String getIdentifier(){
/**
* {@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
/**
* {@inheritDoc}
+ *
* 通し番号順に順序づける。
* @param avatar {@inheritDoc}
* @return {@inheritDoc}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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<>();
}
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 &}」は「{@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("&", "&");
-
- 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 国定義
*/
}
/**
- * 村一覧情報をダウンロードする。
- * リスト元情報は国のトップページと村一覧ページ。
- * 古国の場合は村一覧にアクセスせずトップページのみ。
- * 古国以外で村建てをやめた国はトップページにアクセスしない。
- * 村リストは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);
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;
- }
-
- }
-
}
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;
* コンストラクタ。
* この時点ではまだ国一覧が読み込まれない。
*/
- public LandsModel(){
+ public LandsTreeModel(){
super();
return;
}
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;
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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;
/**
* いわゆる「日」。
* 人気のないプロローグなどで、
* 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 =
/**
- * この 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;
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(){
/**
* Period種別を返す。
+ *
* @return 種別
*/
public PeriodType getType(){
/**
* Period通番を返す。
- * プロローグは常に0番。
- * n日目のゲーム進行日はn番
- * エピローグは最後のゲーム進行日+1番
+ *
+ * <p>プロローグは常に0番。
+ * n日目のゲーム進行日はn番。
+ * エピローグは最後のゲーム進行日+1番。
+ *
* @return Period通番
*/
public int getDay(){
}
/**
+ * 更新時刻を設定する。
+ *
+ * @param hour 時
+ * @param minute 分
+ */
+ public void setLimit(int hour, int minute){
+ this.limitHour = hour;
+ this.limitMinute = minute;
+ return;
+ }
+
+ /**
* 更新時刻の文字表記を返す。
+ *
* @return 更新時刻の文字表記
*/
public String getLimit(){
}
/**
- * 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(){
/**
* エピローグか否か判定する。
+ *
* @return エピローグならtrue
*/
public boolean isEpilogue(){
/**
* 進行日か否か判定する。
+ *
* @return 進行日ならtrue
*/
public boolean isProgress(){
/**
* このPeriodにアクセスするためのクエリーを生成する。
+ *
* @return CGIに渡すクエリー
*/
public String getCGIQuery(){
Village village = getVillage();
result.append(village.getCGIQuery());
- if(isHot()){
- result.append("&mes=all"); // 全表示指定
- return result.toString();
- }
-
Land land = village.getParentLand();
LandDef ldef = land.getLandDef();
/**
* Periodに含まれるTopicのリストを返す。
- * このリストは上書き操作不能。
+ *
+ * <p>このリストは上書き操作不能。
+ *
* @return Topicのリスト
*/
public List<Topic> getTopicList(){
}
/**
+ * Topicのリスト内容を消す。
+ */
+ public void clearTopicList(){
+ this.topicList.clear();
+ return;
+ }
+
+ /**
* Periodに含まれるTopicの総数を返す。
+ *
* @return Topic総数
*/
public int getTopics(){
/**
* 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;
/**
* Periodのキャプション文字列を返す。
- * 主な用途はタブ画面の耳のラベルなど。
+ *
+ * <p>主な用途はタブ画面の耳のラベルなど。
+ *
* @return キャプション文字列
*/
public String getCaption(){
}
/**
- * このPeriodをダウンロードしたときのログイン名を返す。
- * @return ログイン名。ログアウト中はnull。
- */
- public String getLoginName(){
- return this.loginName;
- }
-
- /**
* 公開発言番号にマッチする発言を返す。
+ *
* @param talkNo 公開発言番号
* @return 発言。見つからなければnull
*/
}
/**
- * このPeriodの内容にゲーム進行上隠された部分がある可能性を判定する。
- * @return 隠れた要素がありうるならfalse
- */
- public boolean isFullOpen(){
- return this.isFullOpen;
- }
-
- /**
* ロード済みか否かチェックする。
+ *
* @return ロード済みならtrue
*/
public boolean hasLoaded(){
- return getTopics() > 0;
+ return ! this.topicList.isEmpty();
}
/**
public void unload(){
this.limitHour = 0;
this.limitMinute = 0;
- this.loginName = null;
- this.isFullOpen = false;
-
- this.isHot = false;
this.topicList.clear();
/**
* 襲撃メッセージの有無を判定する。
- * 決着が付くまで非狼陣営には見えない。
+ *
+ * <p>決着が付くまで非狼陣営には見えない。
* 偽装GJでは狼にも見えない。
+ *
* @return 襲撃メッセージがあればtrue
*/
public boolean hasAssaultTried(){
/**
* 処刑されたAvatarを返す。
+ *
* @return 処刑されたAvatar。突然死などなんらかの理由でいない場合はnull
*/
public Avatar getExecutedAvatar(){
/**
* 投票に参加したAvatarの集合を返す。
+ *
* @return 投票に参加したAvatarのSet
*/
public Set<Avatar> getVoterSet(){
/**
* 任意のタイプのシステムイベントを返す。
- * 複数存在する場合、返すのは最初の一つだけ。
+ *
+ * <p>複数存在する場合、返すのは最初の一つだけ。
+ *
* @param type イベントタイプ
* @return システムイベント
*/
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 村名のチェックは不要か?
- }
-
}
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;
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<>();
+
/**
* コンストラクタ。
}
/**
+ * 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一覧
*/
}
/**
+ * 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
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:
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;
}
package jp.sfjp.jindolf.data;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Objects;
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
/**
* 会話種別から色名への変換を行う。
+ *
* @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(){
/**
* 発言種別を得る。
+ *
* @return 種別
*/
public TalkType getTalkType(){
/**
* 墓下発言か否か判定する。
+ *
* @return 墓下発言ならtrue
*/
public boolean isGrave(){
}
/**
- * 発言種別ごとにその日(Period)の累積発言回数を返す。
- * 1から始まる。
+ * 各Avatarの発言種別ごとにその日(Period)の累積発言回数を返す。
+ *
+ * <p>システム生成の襲撃予告の場合は負の値となる。
+ *
* @return 累積発言回数。
*/
public int getTalkCount(){
/**
* 発言文字数を返す。
- * 改行(\n)は1文字。
+ *
+ * <p>改行(\n)は1文字。
+ *
* @return 文字数
*/
public int getTotalChars(){
/**
* 発言元Avatarを得る。
+ *
* @return 発言元Avatar
*/
public Avatar getAvatar(){
/**
* 公開発言番号を取得する。
- * 公開発言番号が割り振られてなければ0以下の値を返す。
+ *
+ * <p>公開発言番号が割り振られてなければ0以下の値を返す。
+ *
* @return 公開発言番号
*/
public int getTalkNo(){
}
/**
+ * 公開発言番号を設定する。
+ *
+ * <p>0以下の値は公開発言番号を持たないと判断される。
+ *
+ * @param talkNo 公開発言番号
+ */
+ public void setTalkNo(int talkNo){
+ this.talkNo = talkNo;
+ return;
+ }
+
+ /**
* 公開発言番号の有無を返す。
+ *
* @return 公開発言番号が割り当てられているならtrueを返す。
*/
public boolean hasTalkNo(){
/**
* メッセージIDを取得する。
+ *
* @return メッセージID
*/
public String getMessageID(){
/**
* メッセージIDからエポック秒(ms)に変換する。
+ *
* @return GMT 1970-01-01 00:00:00 からのエポック秒(ms)
*/
public long getTimeFromID(){
/**
* 発言時を取得する。
+ *
* @return 発言時
*/
public int getHour(){
/**
* 発言分を取得する。
+ *
* @return 発言分
*/
public int getMinute(){
/**
* 会話データを取得する。
+ *
* @return 会話データ
*/
public CharSequence getDialog(){
}
/**
+ * 会話データを設定する。
+ *
+ * @param seq 会話データ
+ */
+ public void setDialog(CharSequence seq){
+ this.dialog = seq;
+ this.charNum = this.dialog.length();
+ return;
+ }
+
+ /**
* 発言種別ごとの発言回数を設定する。
+ *
+ * <p>システム生成の襲撃予告では負の値を入れれば良い。
+ *
* @param count 発言回数
*/
public void setCount(int count){
/**
* この会話を識別するためのアンカー文字列を生成する。
- * 例えば「3d09:56」など。
+ *
+ * <p>例えば「3d09:56」など。
+ *
* @return アンカー文字列
*/
public String getAnchorNotation(){
/**
* この会話を識別するためのG国用アンカー文字列を発言番号から生成する。
- * 例えば「{@literal >>172}」など。
+ *
+ * <p>例えば「{@literal >>172}」など。
+ *
* @return アンカー文字列。発言番号がなければ空文字列。
*/
public String getAnchorNotation_G(){
}
/**
+ * 会話テキスト本文が襲撃予告たりうるか判定する。
+ *
+ * <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();
}
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;
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 村の名前
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(){
/**
* 村のID文字列を返す。
+ *
* @return 村ID
*/
public String getVillageID(){
/**
* 村のID数値を返す。
+ *
* @return 村ID
*/
public int getVillageIDNum(){
/**
* 村の名前を返す。
+ *
* @return 村の名前
*/
public String getVillageName(){
/**
* 村の長い名前を返す。
+ *
* @return 村の長い名前
*/
public String getVillageFullName(){
/**
* 村の状態を返す。
+ *
* @return 村の状態
*/
public VillageState getState(){
/**
* 村の状態を設定する。
+ *
* @param state 村の状態
*/
public void setState(VillageState state){
}
/**
+ * 日程及び更新時刻を持っているか判定する。
+ *
+ * @return 日程が不明ならtrue
+ */
+ public boolean hasSchedule(){
+ boolean result = ! this.periodList.isEmpty();
+ return result;
+ }
+
+ /**
* プロローグを返す。
+ *
* @return プロローグ
*/
public Period getPrologue(){
/**
* エピローグを返す。
+ *
* @return エピローグ
*/
public Period getEpilogue(){
/**
* 指定された日付の進行日を返す。
+ *
* @param day 日付
* @return Period
*/
/**
* PROGRESS状態のPeriodの総数を返す。
+ *
* @return PROGRESS状態のPeriod総数
*/
public int getProgressDays(){
/**
* 指定されたPeriodインデックスのPeriodを返す。
- * プロローグやエピローグへのアクセスも可能。
+ *
+ * <p>プロローグやエピローグへのアクセスも可能。
+ *
* @param day Periodインデックス
* @return Period
*/
/**
* 指定されたアンカーの対象のPeriodを返す。
+ *
* @param anchor アンカー
* @return Period
*/
/**
* Period総数を返す。
+ *
* @return Period総数
*/
public int getPeriodSize(){
/**
* Periodへのリストを返す。
+ *
* @return Periodのリスト。
*/
public List<Period> getPeriodList(){
}
/**
- * 指定した名前で村に登録されている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(){
/**
* 次回更新日を返す。
+ *
* @return 更新日(1-31)
*/
public int getLimitDay(){
/**
* 次回更新時を返す。
+ *
* @return 更新時(0-23)
*/
public int getLimitHour(){
/**
* 次回更新分を返す。
+ *
* @return 更新分(0-59)
*/
public int getLimitMinute(){
/**
* 有効な村か否か判定する。
+ *
* @return 無効な村ならfalse
*/
public boolean isValid(){
/**
* 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){
/**
* アンカーに一致する会話(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;
Period anchorPeriod = getPeriod(anchor);
if(anchorPeriod == null) return result;
- Period.parsePeriod(anchorPeriod, false);
+ // 事前にアンカー対象Periodの全会話がロードされているのが前提
for(Topic topic : anchorPeriod.getTopicList()){
if( ! (topic instanceof Talk) ) continue;
}
/**
- * {@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
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;
- }
-
- }
-
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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&\\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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * package info
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+/**
+ * 人狼BBSサーバから受信したHTMLデータから、
+ * JinParserなどを用いて各種データモデルを生成するクラス群。
+ */
+
+package jp.sfjp.jindolf.data.html;
+
+/* EOF */
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * package info
+ *
+ * License : The MIT License
+ * Copyright(c) 2020 olyutorskii
+ */
+
+/**
+ * JinArchiverなどでXMLファイルにアーカイブされた人狼BBSの村プレイ記録から、
+ * 各種データモデルを生成するクラス群。
+ */
+
+package jp.sfjp.jindolf.data.xml;
+
+/* EOF */
}
/**
- * D&Dに成功したらダイアログを閉じる。
+ * D&Dに成功したらダイアログを閉じる。
*
* <p>{@inheritDoc}
*
/**
* まちゅ氏運営のまとめサイト(wolfbbs)に関する諸々。
*
- * PukiWikiベース。
+ * <p>PukiWikiベース。
*
* @see <a href="https://wolfbbs.jp/">まとめサイト</a>
* @see <a href="https://pukiwiki.osdn.jp/">PukiWiki</a>
/**
* 数値参照文字に変換された文字を追加する。
*
- * 例){@literal 'D' => "D}"
+ * <p>例){@literal 'D' => "D}"
*
* @param app 追加対象
* @param ch 1文字
/**
* 任意の文字を数値参照文字列に変換する。
*
- * 例){@literal 'D' => "D"}
+ * <p>例){@literal 'D' => "D"}
*
* @param ch 文字
* @return 変換後の文字列
/**
* ColorのRGB各成分をWikiカラー表記に変換する。
*
- * α成分は無視される。
+ * <p>α成分は無視される。
*
* @param color 色
* @return Wikiカラー表記
+++ /dev/null
-/*
- * 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;
- }
- }
-
-}
+++ /dev/null
-/*
- * エディタ集合の操作
- *
- * 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;
- }
- }
-
-}
+++ /dev/null
-/*
- * 原稿作成支援エディタ
- *
- * 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;
- }
-
-}
+++ /dev/null
-/*
- * 発言エディットパネル
- *
- * 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 アンカーの表記揺れの指摘
-}
+++ /dev/null
-/*
- * 原稿作成支援用テキストコンポーネント
- *
- * 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 改発言記号の導入
-}
+++ /dev/null
-/*
- * パッケージ情報
- *
- * License : The MIT License
- * Copyright(c) 2011 olyutorskii
- */
-
-/**
- * 発言エディタ関連の一連のクラス。
- */
-
-package jp.sfjp.jindolf.editor;
-
-/* EOF */
import jp.sfjp.jindolf.data.Period;
import jp.sfjp.jindolf.data.Talk;
import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.view.AvatarPics;
/**
* アンカー描画。
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;
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;
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;
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;
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;
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<>();
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(){
setComponentPopupMenu(this.popup);
- updateInputMap();
- ActionMap actionMap = getActionMap();
- actionMap.put(DefaultEditorKit.copyAction, this.copySelectedAction);
+ modifyInputMap();
+ modifyActionMap();
setColorDesign();
/**
* 描画設定の更新。
- * FontRenderContextが更新された後は必ず呼び出す必要がある。
+ *
+ * <p>FontRenderContextが更新された後は必ず呼び出す必要がある。
*/
private void updateRenderingHints(){
Object textAliaseValue;
/**
* フォント描画設定を変更する。
+ *
* @param newFontInfo フォント設定
*/
public void setFontInfo(FontInfo newFontInfo){
}
/**
- * 発言表示設定を変更する。
- * @param newPref 発言表示設定
+ * 会話表示設定を変更する。
+ *
+ * @param newPref 会話表示設定
*/
public void setDialogPref(DialogPref newPref){
this.dialogPref = newPref;
/**
* 現在のPeriodを返す。
+ *
* @return 現在のPeriod
*/
public Period getPeriod(){
/**
* Periodを更新する。
- * 新しいPeriodの表示内容はまだ反映されない。
+ *
+ * <p>新しいPeriodの表示内容はまだ反映されない。
+ *
* @param period 新しいPeriod
*/
public final void setPeriod(Period period){
}
/**
- * 発言フィルタを設定する。
- * @param filter 発言フィルタ
+ * 会話フィルタを設定する。
+ *
+ * @param filter 会話フィルタ
*/
public void setTopicFilter(TopicFilter filter){
this.topicFilter = filter;
}
/**
- * 発言フィルタを適用する。
+ * 会話フィルタを適用する。
*/
public void filtering(){
if( this.topicFilter != null
/**
* 検索パターンを取得する。
+ *
* @return 検索パターン
*/
public RegexPattern getRegexPattern(){
/**
* 与えられた正規表現にマッチする文字列をハイライト描画する。
+ *
* @param newPattern 検索パターン
* @return ヒット件数
*/
/**
* 指定した領域に若干の上下マージンを付けて
* スクロールウィンドウに表示させる。
+ *
* @param rectangle 指定領域
*/
private void scrollRectWithMargin(Rectangle rectangle){
/**
* 指定した矩形がフィルタリング対象か判定する。
+ *
* @param row 矩形
* @return フィルタリング対象ならtrue
*/
/**
* 幅を設定する。
- * 全子TextRowがリサイズされる。
+ *
+ * <p>全子TextRowがリサイズされる。
+ *
* @param width コンポーネント幅
*/
private void setWidth(int width){
/**
* 子TextRowの縦位置レイアウトを行う。
- * フィルタリングが反映される。
- * TextRowは必要に応じて移動させられるがリサイズされることはない。
+ *
+ * <p>フィルタリングが反映される。
+ *
+ * <p>TextRowは必要に応じて移動させられるがリサイズされることはない。
*/
private void layoutVertical(){
Rectangle unionRect = null;
/**
* {@inheritDoc}
+ *
+ * <p>描画処理
+ *
* @param g {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @return {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @return {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @return {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @param visibleRect {@inheritDoc}
* @param orientation {@inheritDoc}
* @param direction {@inheritDoc}
/**
* {@inheritDoc}
+ *
* @param visibleRect {@inheritDoc}
* @param orientation {@inheritDoc}
* @param direction {@inheritDoc}
}
/**
- * 任意の発言の表示が占める画面領域を返す。
- * 発言がフィルタリング対象の時はnullを返す。
- * @param talk 発言
+ * 任意の会話の表示が占める画面領域を返す。
+ *
+ * <p>会話がフィルタリング対象の時はnullを返す。
+ *
+ * @param talk 会話
* @return 領域
*/
public Rectangle getTalkBounds(Talk talk){
/**
* ドラッグ処理を行う。
+ *
* @param from ドラッグ開始位置
* @param to 現在のドラッグ位置
*/
}
/**
- * 与えられた点座標を包含する発言を返す。
+ * 与えられた点座標を包含する会話を返す。
+ *
* @param pt 点座標(JComponent基準)
- * @return 点座標を含む発言。含む発言がなければnullを返す。
+ * @return 点座標を含む会話。含む会話がなければnullを返す。
*/
// TODO 二分探索とかしたい。
private TalkDraw getHittedTalkDraw(Point pt){
/**
* アンカークリック動作の処理。
+ *
* @param pt クリックポイント
*/
private void hitAnchor(Point pt){
/**
* 検索マッチ文字列クリック動作の処理。
+ *
* @param pt クリックポイント
*/
private void hitRegex(Point pt){
/**
* {@inheritDoc}
- * アンカーヒット処理を行う。
- * MouseInputListenerを参照せよ。
+ *
+ * <p>アンカーヒット処理を行う。
+ *
+ * <p>MouseInputListenerを参照せよ。
+ *
* @param event {@inheritDoc}
*/
// TODO 距離判定がシビアすぎ
/**
* {@inheritDoc}
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
- * ドラッグ開始処理を行う。
+ *
+ * <p>ドラッグ開始処理を行う。
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
- * ドラッグ終了処理を行う。
+ *
+ * <p>ドラッグ終了処理を行う。
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
- * ドラッグ処理を行う。
+ *
+ * <p>ドラッグ処理を行う。
+ *
* @param event {@inheritDoc}
*/
// TODO ドラッグ範囲がビューポートを超えたら自動的にスクロールしてほしい。
/**
* {@inheritDoc}
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* {@inheritDoc}
+ *
* @param event {@inheritDoc}
*/
@Override
/**
* 選択文字列を返す。
+ *
* @return 選択文字列
*/
public CharSequence getSelected(){
/**
* 選択文字列をクリップボードにコピーする。
- * @return 選択文字列
+ *
+ * @return 選択文字列。なければnull
*/
public CharSequence copySelected(){
CharSequence selected = getSelected();
}
/**
- * 矩形の示す一発言をクリップボードにコピーする。
+ * 一会話全体の文字列内容をクリップボードにコピーする。
+ *
* @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();
}
/**
- * ポップアップメニュートリガ座標に発言があればそれを返す。
- * @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("選択範囲をコピー");
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();
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変更を追跡するリスナ化
}
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;
/**
*/
private BufferedImage getFaceImage(){
Village village = this.talk.getPeriod().getVillage();
+ AvatarPics avatarPics = village.getAvatarPics();
Avatar avatar = this.talk.getAvatar();
boolean useBodyImage = this.dialogPref.useBodyImage();
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);
}
}
int total = 0;
total += this.dialog.setRegex(searchRegex);
-/*
+ /*
for(AnchorDraw anchorDraw : this.anchorTalks){
total += anchorDraw.setRegex(searchRegex);
}
-*/ // TODO よくわからんので保留
+ */
+ // TODO よくわからんので保留
return total;
}
+++ /dev/null
-/*
- * 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;
- }
-
-}
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;
/**
* ネットワークのスループット報告用文字列を生成する。
+ *
* @param size 転送サイズ(バイト数)
* @param nano 所要時間(ナノ秒)
* @return スループット文字列。引数のいずれかが0以下なら空文字列
/**
* HTTPセッションの各種結果を文字列化する。
+ *
* @param conn HTTPコネクション
* @param size 転送サイズ
* @param nano 転送に要したナノ秒
}
/**
- * ユーザエージェント名を返す。
- * @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;
/**
* HTTP応答からCharsetを取得する。
+ *
* @param connection HTTP接続
* @return Charset文字列
*/
/**
* ContentTypeからCharsetを取得する。
+ *
* @param contentType ContentType
* @return Charset文字列
*/
/**
* プロクシサーバ選択画面。
+ *
* @see <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>
* @see <a href="http://www.ietf.org/rfc/rfc1928.txt">RFC1928</a>
*/
/**
* コンストラクタ。
+ *
* @param proxyInfo プロクシ設定
*/
public ProxyChooser(ProxyInfo proxyInfo){
/**
* ポート番号選択肢を生成する。
+ *
* @return ポート番号選択肢
*/
private static ComboBoxModel<Integer> buildPortRecommender(){
/**
* レイアウトを行う。
+ *
* @param content コンテナ
*/
private void design(Container content){
/**
* サーバ情報パネルを生成する。
+ *
* @return サーバ情報パネル
*/
private JComponent buildServerPanel(){
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);
/**
* プロクシの種別を返す。
+ *
* @return プロクシ種別
*/
protected Proxy.Type getType(){
/**
* サーバ名を返す。
- * プロクシのホスト名として妥当なものか否かは検証されない。
+ *
+ * <p>プロクシのホスト名として妥当なものか否かは検証されない。
+ *
* @return サーバ名
*/
protected String getHostName(){
/**
* ポート番号を返す。
- * 番号の体をなしていなければゼロを返す。
+ *
+ * <p>番号の体をなしていなければゼロを返す。
+ *
* @return ポート番号
*/
protected int getPort(){
/**
* サーバへのソケットアドレスを生成する。
+ *
* @return ソケットアドレス
*/
protected InetSocketAddress getInetSocketAddress(){
/**
* プロクシ設定を返す。
+ *
* @return プロクシ設定
*/
public ProxyInfo getProxyInfo(){
/**
* プロクシ設定を設定する。
- * UIに反映される。
+ *
+ * <p>UIに反映される。
+ *
* @param proxyInfo プロクシ設定。nullなら直接接続と解釈される。
*/
public final void setProxyInfo(ProxyInfo proxyInfo){
/**
* プロクシ種別ボタン操作の受信。
+ *
* @param event ボタン操作イベント
*/
@Override
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;
/**
* 国ごとの人狼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;
/**
* 人狼BBSサーバとの接続管理を生成する。
- * この時点ではまだ通信は行われない。
+ *
+ * <p>この時点ではまだ通信は行われない。
+ *
* @param baseURL 国別のベースURL
* @param charset 国のCharset
* @throws IllegalArgumentException 不正なURL
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。
*/
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);
return;
}
+
/**
- * HTTP-Proxyを返す。
+ * HTTP通信に使われるProxyを返す。
+ *
* @return HTTP-Proxy
*/
public Proxy getProxy(){
}
/**
- * HTTP-Proxyを設定する。
+ * HTTP通信に使われるProxyを設定する。
+ *
* @param proxy HTTP-Proxy。nullならProxyなしと解釈される。
*/
public void setProxy(Proxy proxy){
/**
* 国のベースURLを返す。
+ *
* @return ベースURL
*/
public URL getBaseURL(){
/**
* 与えられたクエリーとCGIのURLから新たにURLを合成する。
- * @param query クエリー
+ *
+ * @param query ?から始まるクエリー
* @return 新たなURL
*/
protected URL getQueryURL(String query){
}
/**
- * エンコーディングされた入力ストリームから文字列を生成する。
+ * 指定された村の最新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 入出力エラー(おそらくネットワーク関連)
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;
}
/**
- * 与えられたクエリーを用いて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 ネットワークエラー
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);
}
/**
- * 絶対または相対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 ネットワークエラー
*/
/**
* 国に含まれる村一覧HTMLデータを取得する。
+ *
+ * <p>wolf国には存在しない。
+ *
* @return HTMLデータ
* @throws java.io.IOException ネットワークエラー
*/
/**
* 指定された村の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 ネットワークエラー
}
/**
- * 指定された村の最新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 ネットワークエラー
}
/**
- * 指定された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;
}
}
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;
/**
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);
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;
/**
* プレイヤーのリストから役職バランス文字列を得る。
- * 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();
* 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;
}
}
/**
- * 参加者集計。
- */
- 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;
}
* 会話時刻のサマライズ。
*/
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;
}
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;
/**
* 占い師の活動を文字列化する。
+ *
* @return 占い師の活動
*/
public CharSequence dumpSeerActivity(){
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;
}
}
- for(Period period : this.village.getPeriodList()){
+ this.village.getPeriodList().forEach(period -> {
summarizeGjPeriod(period);
- }
+ });
return;
}
/**
* 狩人GJの日ごとの集計。
+ *
* @param period 日
*/
private void summarizeGjPeriod(Period period){
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){
/**
* 狩人の活動を文字列化する。
+ *
* @return 狩人の活動
*/
public CharSequence dumpHunterActivity(){
/**
* 処刑概観を文字列化する。
+ *
* @return 文字列化した処刑概観
*/
public CharSequence dumpExecutionInfo(){
/**
* 襲撃概観を文字列化する。
+ *
* @return 文字列化した襲撃概観
*/
public CharSequence dumpAssaultInfo(){
/**
* まとめサイト用投票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");
/**
* まとめサイト用キャスト表を生成する。
+ *
* @param iconSet 顔アイコンセット
* @return キャスト表のWikiテキスト
*/
/**
* 村詳細情報を出力する。
+ *
* @return 村詳細情報
*/
public CharSequence dumpVillageWiki(){
/**
* 最初の発言の時刻を得る。
+ *
* @return 時刻
*/
public Date get1stTalkDate(){
/**
* 最後の発言の時刻を得る。
+ *
* @return 時刻
*/
public Date getLastTalkDate(){
/**
* 指定した日の生存者一覧を得る。
+ *
* @param day 日
* @return 生存者一覧
*/
}
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(){
/**
* キャスティング表用にソートされたプレイヤー一覧を得る。
+ *
* @return プレイヤーのリスト
*/
public List<Player> getCastingPlayerList(){
/**
* 指定された役職のプレイヤー一覧を得る。
+ *
* @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(){
/**
* 突然死者数を得る。
+ *
* @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(){
/**
* AvatarからPlayerを得る。
- * 参加していないAvatarならnullを返す。
+ *
+ * <p>参加していないAvatarならnullを返す。
+ *
* @param avatar Avatar
* @return Player
*/
}
/**
- * 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> {
/**
* {@inheritDoc}
+ *
* @param p1 {@inheritDoc}
* @param p2 {@inheritDoc}
* @return {@inheritDoc}
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;
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);
package jp.sfjp.jindolf.util;
-import java.util.regex.Matcher;
-
/**
* 文字列ユーティリティクラス。
*/
/**
- * 正規表現にマッチした領域を数値化する。
- * @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 文字列
}
/**
- * ある文字列の末尾が別の文字列に一致するか判定する。
- * @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
+++ /dev/null
-/*
- * 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かパスワードが空の場合はログインボタンを無効にしたい
-}
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;
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}。 */
/** アクション{@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";
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;
/** 前検索アイコン。 */
/** 発言エディタアイコン。 */
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 =
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);
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);
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);
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 メニュー
/**
* メニューアイテムを生成する。
+ *
* @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) + ")";
/**
* ツールボタンを生成する。
+ *
* @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);
}
/**
- * L&F 一覧メニューを作成する。
+ * L&F 一覧メニューを作成する。
+ *
* @param label メニューラベル
* @param nemonic ニモニックキー
- * @return L&F 一覧メニュー
+ * @return L&F 一覧メニュー
*/
private JMenu buildLookAndFeelMenu(String label, int nemonic){
JMenu result = buildMenu(label, nemonic);
/**
* アクションコマンド名からメニューアイテムを探す。
+ *
* @param command アクションコマンド名
* @return メニューアイテム
*/
/**
* アクションコマンド名からツールボタンを探す。
+ *
* @param command アクションコマンド名
* @return ツールボタン
*/
}
/**
- * 現在メニューで選択中のL&Fのクラス名を返す。
+ * 現在LookAndFeelメニューで選択中のL&Fのクラス名を返す。
+ *
* @return L&F クラス名
*/
public String getSelectedLookAndFeel(){
}
/**
- * 全てのボタンにアクションリスナーを登録する。
+ * 管理下の全てのボタンにアクションリスナーを登録する。
+ *
* @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));
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));
/**
* メニューバーを取得する。
+ *
* @return メニューバー
*/
public JMenuBar getMenuBar(){
/**
* ブラウザ用ツールバーの生成を行う。
+ *
* @return ツールバー
*/
private JToolBar buildBrowseToolBar(){
toolBar.add(getToolButton(CMD_SEARCHPREV));
toolBar.addSeparator();
toolBar.add(getToolButton(CMD_SHOWFILT));
- toolBar.add(getToolButton(CMD_SHOWEDIT));
return toolBar;
}
/**
* ブラウザ用ツールバーを取得する。
+ *
* @return ツールバー
*/
public JToolBar getBrowseToolBar(){
}
/**
- * 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);
}
/**
- * 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;
}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+
+}
if(configStore.useStoreFile()){
info.append("設定格納ディレクトリ : ")
- .append(configStore.getConfigPath().getPath());
+ .append(configStore.getConfigDir().getPath());
}else{
info.append("※ 設定格納ディレクトリは使っていません。");
}
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周辺コンポーネント群。
}
/**
- * 管理下の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;
}
final TreePath lastPath = this.treeView.getSelectionPath();
- LandsModel model = getLandsModel();
+ LandsTreeModel model = getLandsModel();
if(model != null){
model.setAscending(this.ascending);
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
import jp.sourceforge.jindolf.corelib.TalkType;
/**
- * 発言ブラウザを内包するPeriodビューワ。
+ * 各Periodのビュー。
+ *
+ * <P>Periodの内容が反映される。
+ *
+ * <p>キャプション(村名、Period名)、
+ * リミット(更新時刻)、
+ * プルダウンリストによる会話セレクタ
+ * の管理制御を担当する。
+ *
+ * <p>会話表示{@link Discussion}のスクロール制御
+ * を担当する。
*/
@SuppressWarnings("serial")
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;
private DialogPref dialogPref = new DialogPref();
+
/**
- * 発言ブラウザを内包するPeriodビューワを生成する。
+ * コンストラクタ。
+ *
* @param period 日
*/
@SuppressWarnings("LeakingThisInConstructor")
/**
* Periodを更新する。
- * 古いPeriodの表示内容は消える。
+ *
+ * <p>古いPeriodの表示内容は消える。
* 新しいPeriodの表示内容はまだ反映されない。
+ *
* @param period 新しいPeriod
*/
public void setPeriod(Period period){
/**
* 現在のPeriodを返す。
+ *
* @return 現在のPeriod
*/
public Period getPeriod(){
}
/**
- * 上部のGUI(村名、発言一覧)を、Periodの状態に合わせて更新する。
+ * 上部の村名、会話プルダウンリストを、Periodの状態に合わせて更新する。
*/
private void updateTopPanel(){
if(this.period == null){
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);
/**
* フォント描画設定を変更する。
+ *
* @param fontInfo フォント設定
*/
// TODO スクロール位置の復元
}
/**
- * 発言表示設定を更新する。
+ * 会話表示設定を変更する。
+ *
* @param pref 表示設定
*/
public void setDialogPref(DialogPref pref){
}
/**
- * ビューポート内の発言ブラウザを返す。
- * @return 内部ブラウザ
+ * ビューポート内の会話表示{@link Discussion}を返す。
+ *
+ * @return 会話表示
*/
public Discussion getDiscussion(){
return this.discussion;
}
/**
- * スクロール位置を返す。
+ * 縦スクロール位置を返す。
+ *
* @return スクロール位置
*/
public int getVerticalPosition(){
}
/**
- * スクロール位置を設定する。
+ * 縦スクロール位置を設定する。
+ *
* @param pos スクロール位置
*/
public void setVerticalPosition(int pos){
/**
* {@inheritDoc}
- * コンボボックス操作のリスナ。
+ *
+ * <p>コンボボックス操作(会話プルダウンリスト)のリスナ。
+ *
* @param event コンボボックス操作イベント {@inheritDoc}
*/
@Override
}
/**
- * 任意の発言が表示されるようスクロールする。
+ * 任意の会話が表示域に収まるようスクロールを試みる。
+ *
* @param talk 発言
*/
public void scrollToTalk(Talk talk){
/**
* {@inheritDoc}
- * Talkのアンカー表記と発言者名を描画する。
+ *
+ * <p>Talkのアンカー表記と発言者名を描画する。
+ *
* @param list {@inheritDoc}
* @param value {@inheritDoc}
* @param index {@inheritDoc}
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;
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;
private FontInfo fontInfo;
private DialogPref dialogPref;
- private final EventListenerList thisListenerList =
- new EventListenerList();
/**
- * 村が指定されていない状態のタブパネルを生成する。
+ * コンストラクタ。
+ *
+ * <p>村が指定されていない状態のタブパネルを生成する。
*/
public TabBrowser(){
super();
// 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(){
/**
* 新規に村を設定する。
+ *
+ * <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);
}
}
/**
- * 現在タブ選択中の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;
}
/**
- * 指定したタブインデックスに関連付けられた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);
}
}
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;
/**
* メインアプリウィンドウ。
- * {@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();
+
/**
* コンストラクタ。
*/
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() {};
}
/**
- * トップビューを返す。
+ * 実際のアプリ画面を担当する{@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ならプログレスバーのアニメ開始&WAITカーソル。
+ * falseなら停止&通常カーソル。
+ */
+ public void setBusy(boolean isBusy){
+ setCursorBusy(isBusy);
+ setUiMask(isBusy);
+ this.topView.setBusy(isBusy);
+ return;
+ }
+
}
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{
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();
/**
* カードパネルを生成する。
+ *
* @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>");
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);
/**
* 国別情報を生成。
+ *
* @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();
/**
* ブラウザ用ツールバーをセットする。
+ *
* @param toolbar ツールバー
*/
public void setBrowseToolBar(JToolBar toolbar){
/**
* SplitPaneを生成。
+ *
* @param left 左コンポーネント
* @param right 右コンポーネント
* @return SplitPane
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);
/**
* 国村選択ツリービューを返す。
+ *
* @return 国村選択ツリービュー
*/
public JTree getTreeView(){
/**
* タブビューを返す。
+ *
* @return タブビュー
*/
public TabBrowser getTabBrowser(){
/**
* 村一覧ビューを返す。
+ *
* @return 村一番ビュー
*/
public LandsTree getLandsTree(){
}
/**
- * プログレスバーとカーソルの設定を行う。
- * @param busy trueならプログレスバーのアニメ開始&WAITカーソル。
- * falseなら停止&通常カーソル。
+ * プログレスバーの設定を行う。
+ *
+ * @param busy trueならプログレスバーのアニメ開始。
+ * falseなら停止。
*/
public void setBusy(boolean busy){
this.progressBar.setIndeterminate(busy);
/**
* ステータスバーの更新。
+ *
* @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(){
/**
* 村情報を表示する。
+ *
* @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){
return;
}
- // TODO setEnabled()を全子フレームにも波及させるべきか
}
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;
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 =
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;
/**
* ウィンドウタイトルに前置詞をつける。
+ *
* @param text 元タイトル
* @return タイトル文字列
*/
/**
* 発言フィルタウィンドウを生成する。
+ *
* @return 発言フィルタウィンドウ
*/
protected FilterPanel createFilterPanel(){
/**
* 発言フィルタウィンドウを返す。
+ *
* @return 発言フィルタウィンドウ
*/
public FilterPanel getFilterPanel(){
/**
* ログウィンドウを生成する。
+ *
* @return ログウィンドウ
*/
protected LogFrame createLogFrame(){
/**
* ログウィンドウを返す。
+ *
* @return ログウィンドウ
*/
public LogFrame getLogFrame(){
}
/**
- * 発言エディタウィンドウを生成する。
- * @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(){
/**
* オプション設定ウィンドウを返す。
+ *
* @return オプション設定ウィンドウ
*/
public OptionPanel getOptionPanel(){
/**
* 検索ウィンドウを生成する。
+ *
* @return 検索ウィンドウ
*/
protected FindPanel createFindPanel(){
/**
* 検索ウィンドウを返す。
+ *
* @return 検索ウィンドウ
*/
public FindPanel getFindPanel(){
}
/**
- * ログインウィンドウを生成する。
- * @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(){
/**
* 村ダイジェストウィンドウを返す。
+ *
* @return 村ダイジェストウィンドウ
*/
public VillageDigest getVillageDigest(){
/**
* 発言集計ウィンドウを生成する。
+ *
* @return 発言集計ウィンドウ
*/
protected DaySummary createDaySummary(){
/**
* 発言集計ウィンドウを返す。
+ *
* @return 発言集計ウィンドウ
*/
public DaySummary getDaySummary(){
/**
* ヘルプウィンドウを生成する。
+ *
* @return ヘルプウィンドウ
*/
protected HelpFrame createHelpFrame(){
/**
* ヘルプウィンドウを返す。
+ *
* @return ヘルプウィンドウ
*/
public HelpFrame getHelpFrame(){
/**
* トップフレームを生成する。
+ *
* @return トップフレーム
*/
protected TopFrame createTopFrame(){
/**
* トップフレームを返す。
+ *
* @return トップフレーム
*/
public TopFrame getTopFrame(){
/**
* 管理下にある全ウィンドウの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;
}
--- /dev/null
+{
+
+ "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"
+ }
+
+}
#
# \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
# --- start
-update = 2016-06-25T05:00:00+09:00
+update = 2020-03-22T16:04:00+09:00
codeCheck = \u72fc
iconset.order.100 = nagianPuki
#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
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);
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);
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);
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);
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);
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);
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);
@Test
public void testHas11Runtime() {
System.out.println("has11Runtime");
- assertTrue(JreChecker.has11Runtime());
+ assertTrue(JreChecker.has1_1Runtime());
return;
}
@Test
public void testHas12Runtime() {
System.out.println("has12Runtime");
- assertTrue(JreChecker.has12Runtime());
+ assertTrue(JreChecker.has1_2Runtime());
return;
}
@Test
public void testHas13Runtime() {
System.out.println("has13Runtime");
- assertTrue(JreChecker.has13Runtime());
+ assertTrue(JreChecker.has1_3Runtime());
return;
}
@Test
public void testHas14Runtime() {
System.out.println("has14Runtime");
- assertTrue(JreChecker.has14Runtime());
+ assertTrue(JreChecker.has1_4Runtime());
return;
}
@Test
public void testHas15Runtime() {
System.out.println("has15Runtime");
- assertTrue(JreChecker.has15Runtime());
+ assertTrue(JreChecker.has1_5Runtime());
return;
}
@Test
public void testHas16Runtime() {
System.out.println("has16Runtime");
- assertTrue(JreChecker.has16Runtime());
+ assertTrue(JreChecker.has1_6Runtime());
return;
}
@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;
}
--- /dev/null
+/*
+ */
+
+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;
+ }
+
+}
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;
}
/**
- * 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;
}
@Test
public void testGetFullName(){
System.out.println("getFullName");
- Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+ Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
assertNotNull(result);
assertEquals("農夫 ヤコブ", result.getFullName());
return;
@Test
public void testGetJobTitle(){
System.out.println("getJobTitle");
- Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+ Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
assertNotNull(result);
assertEquals("農夫", result.getJobTitle());
return;
@Test
public void testGetName(){
System.out.println("getName");
- Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+ Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
assertNotNull(result);
assertEquals("ヤコブ", result.getName());
return;
@Test
public void testGetIdNum(){
System.out.println("getIdNum");
- Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+ Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
assertNotNull(result);
assertEquals(15, result.getIdNum());
return;
@Test
public void testEquals(){
System.out.println("equals");
- Avatar result = Avatar.getPredefinedAvatar("農夫 ヤコブ");
+ Avatar result = Avatar.getAvatarByFullname("農夫 ヤコブ");
assertTrue(result.equals(result));
return;
}
@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);
--- /dev/null
+/*
+ */
+
+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;
+ }
+
+}
result = HttpUtils.escapeHttpComment(comment);
assertEquals(expResult, result);
+ comment = "a🐑b";
+ expResult = "(a?b)";
+ result = HttpUtils.escapeHttpComment(comment);
+ assertEquals(expResult, result);
+
return;
}
/**
* Test of formatHttpStat method, of class HttpUtils.
+ * @throws java.lang.Exception
*/
@Test
public void testFormatHttpStat() throws Exception{
/**
* Test of getHTMLCharset method, of class HttpUtils.
+ * @throws java.lang.Exception
*/
@Test
public void testGetHTMLCharset_URLConnection() throws Exception{
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;
}
/**
- * 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
}
/**
- * 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