OSDN Git Service

new repository
authorargius <argius.net@gmail.com>
Sun, 28 Apr 2013 01:55:56 +0000 (10:55 +0900)
committerargius <argius.net@gmail.com>
Sun, 28 Apr 2013 01:55:56 +0000 (10:55 +0900)
123 files changed:
.classpath [new file with mode: 0644]
.exclude [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.project [new file with mode: 0644]
.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
.settings/org.eclipse.core.runtime.prefs [new file with mode: 0644]
.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
.settings/org.eclipse.jdt.ui.prefs [new file with mode: 0644]
FEATURE_ja.md [new file with mode: 0644]
LICENSE [new file with mode: 0644]
MANUAL_ja.html [new file with mode: 0644]
MANUAL_ja.md [new file with mode: 0644]
README_ja.md [new file with mode: 0644]
build.xml [new file with mode: 0644]
logging.properties [new file with mode: 0644]
markdown.css [new file with mode: 0644]
src/MANIFEST.MF [new file with mode: 0644]
src/net/argius/Stew.java [new file with mode: 0644]
src/net/argius/logging/BasicFormatter.java [new file with mode: 0644]
src/net/argius/stew/Alias.java [new file with mode: 0644]
src/net/argius/stew/AnonymousConnector.java [new file with mode: 0644]
src/net/argius/stew/Bootstrap.java [new file with mode: 0644]
src/net/argius/stew/CipherPassword.java [new file with mode: 0644]
src/net/argius/stew/ColumnOrder.java [new file with mode: 0644]
src/net/argius/stew/Command.java [new file with mode: 0644]
src/net/argius/stew/Command.u8p [new file with mode: 0644]
src/net/argius/stew/CommandException.java [new file with mode: 0644]
src/net/argius/stew/CommandProcessor.java [new file with mode: 0644]
src/net/argius/stew/Connector.java [new file with mode: 0644]
src/net/argius/stew/ConnectorConfiguration.java [new file with mode: 0644]
src/net/argius/stew/ConnectorDriverManager.java [new file with mode: 0644]
src/net/argius/stew/ConnectorMap.java [new file with mode: 0644]
src/net/argius/stew/DaemonThreadFactory.java [new file with mode: 0644]
src/net/argius/stew/DynamicLoader.java [new file with mode: 0644]
src/net/argius/stew/DynamicLoadingException.java [new file with mode: 0644]
src/net/argius/stew/Environment.java [new file with mode: 0644]
src/net/argius/stew/Logger.java [new file with mode: 0644]
src/net/argius/stew/Parameter.java [new file with mode: 0644]
src/net/argius/stew/Password.java [new file with mode: 0644]
src/net/argius/stew/PbePassword.java [new file with mode: 0644]
src/net/argius/stew/PlainTextPassword.java [new file with mode: 0644]
src/net/argius/stew/ResourceManager.java [new file with mode: 0644]
src/net/argius/stew/ResultSetReference.java [new file with mode: 0644]
src/net/argius/stew/UsageException.java [new file with mode: 0644]
src/net/argius/stew/command/Download.java [new file with mode: 0644]
src/net/argius/stew/command/Export.java [new file with mode: 0644]
src/net/argius/stew/command/Find.java [new file with mode: 0644]
src/net/argius/stew/command/Import.java [new file with mode: 0644]
src/net/argius/stew/command/Load.java [new file with mode: 0644]
src/net/argius/stew/command/Report.java [new file with mode: 0644]
src/net/argius/stew/command/Time.java [new file with mode: 0644]
src/net/argius/stew/command/Upload.java [new file with mode: 0644]
src/net/argius/stew/command/Wait.java [new file with mode: 0644]
src/net/argius/stew/io/CsvFormatter.java [new file with mode: 0644]
src/net/argius/stew/io/Exporter.java [new file with mode: 0644]
src/net/argius/stew/io/ExporterFactory.java [new file with mode: 0644]
src/net/argius/stew/io/HtmlExporter.java [new file with mode: 0644]
src/net/argius/stew/io/Importer.java [new file with mode: 0644]
src/net/argius/stew/io/ImporterFactory.java [new file with mode: 0644]
src/net/argius/stew/io/Path.java [new file with mode: 0644]
src/net/argius/stew/io/SimpleExporter.java [new file with mode: 0644]
src/net/argius/stew/io/SmartImporter.java [new file with mode: 0644]
src/net/argius/stew/io/StringBasedSerializer.java [new file with mode: 0644]
src/net/argius/stew/io/XmlExporter.java [new file with mode: 0644]
src/net/argius/stew/io/XmlImporter.java [new file with mode: 0644]
src/net/argius/stew/io/stew-table.dtd [new file with mode: 0644]
src/net/argius/stew/messages.u8p [new file with mode: 0644]
src/net/argius/stew/messages_ja.u8p [new file with mode: 0644]
src/net/argius/stew/text/PrintFormat.java [new file with mode: 0644]
src/net/argius/stew/text/TextUtilities.java [new file with mode: 0644]
src/net/argius/stew/ui/Launcher.java [new file with mode: 0644]
src/net/argius/stew/ui/OutputProcessor.java [new file with mode: 0644]
src/net/argius/stew/ui/Prompt.java [new file with mode: 0644]
src/net/argius/stew/ui/console/ConnectorMapEditor.java [new file with mode: 0644]
src/net/argius/stew/ui/console/ConnectorMapEditor.u8p [new file with mode: 0644]
src/net/argius/stew/ui/console/ConsoleLauncher.java [new file with mode: 0644]
src/net/argius/stew/ui/console/ConsoleOutputProcessor.java [new file with mode: 0644]
src/net/argius/stew/ui/window/AnyAction.java [new file with mode: 0644]
src/net/argius/stew/ui/window/AnyActionEvent.java [new file with mode: 0644]
src/net/argius/stew/ui/window/AnyActionKey.java [new file with mode: 0644]
src/net/argius/stew/ui/window/AnyActionListener.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ClipboardHelper.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ConnectorEditDialog.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ConnectorEditDialog.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/ConnectorEntry.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ConnectorMapEditDialog.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ConnectorMapEditDialog.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/ConsoleTextArea.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ContextMenu.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ContextMenu.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/ContextMenu_ja.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/DatabaseInfoTree.java [new file with mode: 0644]
src/net/argius/stew/ui/window/FlexiblePanel.java [new file with mode: 0644]
src/net/argius/stew/ui/window/FontControlLookAndFeel.java [new file with mode: 0644]
src/net/argius/stew/ui/window/Menu.java [new file with mode: 0644]
src/net/argius/stew/ui/window/Menu.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/Menu_ja.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/ResultSetTable.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ResultSetTableModel.java [new file with mode: 0644]
src/net/argius/stew/ui/window/TextSearch.java [new file with mode: 0644]
src/net/argius/stew/ui/window/TextSearchPanel.java [new file with mode: 0644]
src/net/argius/stew/ui/window/TextSearchPanel.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/Utilities.java [new file with mode: 0644]
src/net/argius/stew/ui/window/ValueTransporter.java [new file with mode: 0644]
src/net/argius/stew/ui/window/WindowLauncher.java [new file with mode: 0644]
src/net/argius/stew/ui/window/WindowOutputProcessor.java [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/close.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/linkable-false.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/linkable-true.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-catalog.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-column.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-connector.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-schema.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-table.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-tabletype-.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-tabletype-INDEX.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-tabletype-SEQUENCE.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-tabletype-TABLE.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/node-tabletype-VIEW.png [new file with mode: 0644]
src/net/argius/stew/ui/window/icon/stew.png [new file with mode: 0644]
src/net/argius/stew/ui/window/messages.u8p [new file with mode: 0644]
src/net/argius/stew/ui/window/messages_ja.u8p [new file with mode: 0644]
src/net/argius/stew/version [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..176ce53
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry excluding="net/argius/stew/ui/window/ActionUtility.java|net/argius/stew/ui/window/WindowLaunchHelper1.java|net/argius/stew/ui/window/WindowLaunchHelper2.java|net/argius/stew/ui/window/WindowLaunchHelper3.java|net/argius/stew/ui/window/Resource.java" kind="src" path="src"/>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+       <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.exclude b/.exclude
new file mode 100644 (file)
index 0000000..595dc67
--- /dev/null
+++ b/.exclude
@@ -0,0 +1,5 @@
+.*
+BLD
+src.test
+MANUAL_ja.txt
+checkstyle.xml
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b84dee3
--- /dev/null
@@ -0,0 +1,5 @@
+/bin
+/BLD
+/*.log*
+/*.jar
+/tmp
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..a8b9821
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<projectDescription>\r
+       <name>Stew4</name>\r
+       <comment></comment>\r
+       <projects>\r
+       </projects>\r
+       <buildSpec>\r
+               <buildCommand>\r
+                       <name>org.eclipse.jdt.core.javabuilder</name>\r
+                       <arguments>\r
+                       </arguments>\r
+               </buildCommand>\r
+       </buildSpec>\r
+       <natures>\r
+               <nature>org.eclipse.jdt.core.javanature</nature>\r
+       </natures>\r
+</projectDescription>\r
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..5dd04f0
--- /dev/null
@@ -0,0 +1,3 @@
+#Sat Dec 08 22:50:36 JST 2012\r
+eclipse.preferences.version=1\r
+encoding/<project>=UTF-8\r
diff --git a/.settings/org.eclipse.core.runtime.prefs b/.settings/org.eclipse.core.runtime.prefs
new file mode 100644 (file)
index 0000000..4269b05
--- /dev/null
@@ -0,0 +1,3 @@
+#Sat Dec 08 22:50:36 JST 2012\r
+eclipse.preferences.version=1\r
+line.separator=\n\r
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..5c3b53d
--- /dev/null
@@ -0,0 +1,95 @@
+eclipse.preferences.version=1\r
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled\r
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6\r
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve\r
+org.eclipse.jdt.core.compiler.compliance=1.6\r
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate\r
+org.eclipse.jdt.core.compiler.debug.localVariable=generate\r
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate\r
+org.eclipse.jdt.core.compiler.doc.comment.support=disabled\r
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning\r
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error\r
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore\r
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning\r
+org.eclipse.jdt.core.compiler.problem.deadCode=warning\r
+org.eclipse.jdt.core.compiler.problem.deprecation=warning\r
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled\r
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled\r
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning\r
+org.eclipse.jdt.core.compiler.problem.emptyStatement=warning\r
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error\r
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning\r
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled\r
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning\r
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning\r
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning\r
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error\r
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning\r
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled\r
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning\r
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning\r
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=warning\r
+org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning\r
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=disabled\r
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled\r
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=disabled\r
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=public\r
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore\r
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning\r
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning\r
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocComments=warning\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled\r
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=default\r
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning\r
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled\r
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore\r
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=warning\r
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning\r
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning\r
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore\r
+org.eclipse.jdt.core.compiler.problem.nullReference=warning\r
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning\r
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=warning\r
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning\r
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning\r
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning\r
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning\r
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning\r
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning\r
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=warning\r
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore\r
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=enabled\r
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning\r
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled\r
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled\r
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore\r
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning\r
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled\r
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning\r
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning\r
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning\r
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning\r
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning\r
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore\r
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore\r
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled\r
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled\r
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled\r
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning\r
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning\r
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning\r
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning\r
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore\r
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled\r
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=enabled\r
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=enabled\r
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning\r
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning\r
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning\r
+org.eclipse.jdt.core.compiler.source=1.6\r
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644 (file)
index 0000000..ecde529
--- /dev/null
@@ -0,0 +1,59 @@
+#Sat Dec 15 16:40:04 JST 2012\r
+eclipse.preferences.version=1\r
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true\r
+org.eclipse.jdt.ui.ignorelowercasenames=true\r
+org.eclipse.jdt.ui.importorder=java;javax;org;com;\r
+org.eclipse.jdt.ui.ondemandthreshold=1\r
+org.eclipse.jdt.ui.staticondemandthreshold=3\r
+sp_cleanup.add_default_serial_version_id=true\r
+sp_cleanup.add_generated_serial_version_id=false\r
+sp_cleanup.add_missing_annotations=true\r
+sp_cleanup.add_missing_deprecated_annotations=true\r
+sp_cleanup.add_missing_methods=false\r
+sp_cleanup.add_missing_nls_tags=false\r
+sp_cleanup.add_missing_override_annotations=true\r
+sp_cleanup.add_missing_override_annotations_interface_methods=false\r
+sp_cleanup.add_serial_version_id=false\r
+sp_cleanup.always_use_blocks=true\r
+sp_cleanup.always_use_parentheses_in_expressions=false\r
+sp_cleanup.always_use_this_for_non_static_field_access=false\r
+sp_cleanup.always_use_this_for_non_static_method_access=false\r
+sp_cleanup.convert_to_enhanced_for_loop=false\r
+sp_cleanup.correct_indentation=false\r
+sp_cleanup.format_source_code=true\r
+sp_cleanup.format_source_code_changes_only=true\r
+sp_cleanup.make_local_variable_final=false\r
+sp_cleanup.make_parameters_final=false\r
+sp_cleanup.make_private_fields_final=true\r
+sp_cleanup.make_type_abstract_if_missing_method=false\r
+sp_cleanup.make_variable_declarations_final=true\r
+sp_cleanup.never_use_blocks=false\r
+sp_cleanup.never_use_parentheses_in_expressions=true\r
+sp_cleanup.on_save_use_additional_actions=false\r
+sp_cleanup.organize_imports=true\r
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false\r
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true\r
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true\r
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false\r
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false\r
+sp_cleanup.remove_private_constructors=true\r
+sp_cleanup.remove_trailing_whitespaces=false\r
+sp_cleanup.remove_trailing_whitespaces_all=true\r
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false\r
+sp_cleanup.remove_unnecessary_casts=true\r
+sp_cleanup.remove_unnecessary_nls_tags=false\r
+sp_cleanup.remove_unused_imports=false\r
+sp_cleanup.remove_unused_local_variables=false\r
+sp_cleanup.remove_unused_private_fields=true\r
+sp_cleanup.remove_unused_private_members=false\r
+sp_cleanup.remove_unused_private_methods=true\r
+sp_cleanup.remove_unused_private_types=true\r
+sp_cleanup.sort_members=false\r
+sp_cleanup.sort_members_all=false\r
+sp_cleanup.use_blocks=false\r
+sp_cleanup.use_blocks_only_for_return_and_throw=false\r
+sp_cleanup.use_parentheses_in_expressions=false\r
+sp_cleanup.use_this_for_non_static_field_access=false\r
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true\r
+sp_cleanup.use_this_for_non_static_method_access=false\r
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true\r
diff --git a/FEATURE_ja.md b/FEATURE_ja.md
new file mode 100644 (file)
index 0000000..3f850fb
--- /dev/null
@@ -0,0 +1,39 @@
+% Stew4の新機能と変更点
+
+
+## 新機能
+
+バージョン4の主な新機能は以下のとおりです。
+各機能の詳細については、マニュアル(MANUAL_ja.html)をご覧ください。
+
+特殊コマンド "?" - 実行環境情報の表示
+:   システムプロパティを表示します。(System.getProperty)
+    引数を指定しない場合は、JRE,OS,Localeの情報を表示します。
+
+
+xxx
+:    xxx
+
+
+## 変更点
+
+バージョン3から4への主な変更点は以下のとおりです。
+
+Java6以降のみサポート
+:   Java5.0が対象外となりました。
+    より効率の良い実装を行うため、
+    Java6の新しい言語機能とAPIを導入しています。
+
+実装の整理
+:   Java6への移行に伴い、実装の整理を行いました。
+    具体的には、不要クラスの削除と、より適切なAPIを使用するようにしました。
+
+
+## その他
+
+接続設定ファイル(connector.properties)は、バージョン3と互換性があります。
+
+---TODO---
+-s
+?
+DBInfo自動展開
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..f820d4b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,203 @@
+/*
+ *                                 Apache License
+ *                           Version 2.0, January 2004
+ *                        http://www.apache.org/licenses/
+ *
+ *   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+ *
+ *   1. Definitions.
+ *
+ *      "License" shall mean the terms and conditions for use, reproduction,
+ *      and distribution as defined by Sections 1 through 9 of this document.
+ *
+ *      "Licensor" shall mean the copyright owner or entity authorized by
+ *      the copyright owner that is granting the License.
+ *
+ *      "Legal Entity" shall mean the union of the acting entity and all
+ *      other entities that control, are controlled by, or are under common
+ *      control with that entity. For the purposes of this definition,
+ *      "control" means (i) the power, direct or indirect, to cause the
+ *      direction or management of such entity, whether by contract or
+ *      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ *      outstanding shares, or (iii) beneficial ownership of such entity.
+ *
+ *      "You" (or "Your") shall mean an individual or Legal Entity
+ *      exercising permissions granted by this License.
+ *
+ *      "Source" form shall mean the preferred form for making modifications,
+ *      including but not limited to software source code, documentation
+ *      source, and configuration files.
+ *
+ *      "Object" form shall mean any form resulting from mechanical
+ *      transformation or translation of a Source form, including but
+ *      not limited to compiled object code, generated documentation,
+ *      and conversions to other media types.
+ *
+ *      "Work" shall mean the work of authorship, whether in Source or
+ *      Object form, made available under the License, as indicated by a
+ *      copyright notice that is included in or attached to the work
+ *      (an example is provided in the Appendix below).
+ *
+ *      "Derivative Works" shall mean any work, whether in Source or Object
+ *      form, that is based on (or derived from) the Work and for which the
+ *      editorial revisions, annotations, elaborations, or other modifications
+ *      represent, as a whole, an original work of authorship. For the purposes
+ *      of this License, Derivative Works shall not include works that remain
+ *      separable from, or merely link (or bind by name) to the interfaces of,
+ *      the Work and Derivative Works thereof.
+ *
+ *      "Contribution" shall mean any work of authorship, including
+ *      the original version of the Work and any modifications or additions
+ *      to that Work or Derivative Works thereof, that is intentionally
+ *      submitted to Licensor for inclusion in the Work by the copyright owner
+ *      or by an individual or Legal Entity authorized to submit on behalf of
+ *      the copyright owner. For the purposes of this definition, "submitted"
+ *      means any form of electronic, verbal, or written communication sent
+ *      to the Licensor or its representatives, including but not limited to
+ *      communication on electronic mailing lists, source code control systems,
+ *      and issue tracking systems that are managed by, or on behalf of, the
+ *      Licensor for the purpose of discussing and improving the Work, but
+ *      excluding communication that is conspicuously marked or otherwise
+ *      designated in writing by the copyright owner as "Not a Contribution."
+ *
+ *      "Contributor" shall mean Licensor and any individual or Legal Entity
+ *      on behalf of whom a Contribution has been received by Licensor and
+ *      subsequently incorporated within the Work.
+ *
+ *   2. Grant of Copyright License. Subject to the terms and conditions of
+ *      this License, each Contributor hereby grants to You a perpetual,
+ *      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ *      copyright license to reproduce, prepare Derivative Works of,
+ *      publicly display, publicly perform, sublicense, and distribute the
+ *      Work and such Derivative Works in Source or Object form.
+ *
+ *   3. Grant of Patent License. Subject to the terms and conditions of
+ *      this License, each Contributor hereby grants to You a perpetual,
+ *      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ *      (except as stated in this section) patent license to make, have made,
+ *      use, offer to sell, sell, import, and otherwise transfer the Work,
+ *      where such license applies only to those patent claims licensable
+ *      by such Contributor that are necessarily infringed by their
+ *      Contribution(s) alone or by combination of their Contribution(s)
+ *      with the Work to which such Contribution(s) was submitted. If You
+ *      institute patent litigation against any entity (including a
+ *      cross-claim or counterclaim in a lawsuit) alleging that the Work
+ *      or a Contribution incorporated within the Work constitutes direct
+ *      or contributory patent infringement, then any patent licenses
+ *      granted to You under this License for that Work shall terminate
+ *      as of the date such litigation is filed.
+ *
+ *   4. Redistribution. You may reproduce and distribute copies of the
+ *      Work or Derivative Works thereof in any medium, with or without
+ *      modifications, and in Source or Object form, provided that You
+ *      meet the following conditions:
+ *
+ *      (a) You must give any other recipients of the Work or
+ *          Derivative Works a copy of this License; and
+ *
+ *      (b) You must cause any modified files to carry prominent notices
+ *          stating that You changed the files; and
+ *
+ *      (c) You must retain, in the Source form of any Derivative Works
+ *          that You distribute, all copyright, patent, trademark, and
+ *          attribution notices from the Source form of the Work,
+ *          excluding those notices that do not pertain to any part of
+ *          the Derivative Works; and
+ *
+ *      (d) If the Work includes a "NOTICE" text file as part of its
+ *          distribution, then any Derivative Works that You distribute must
+ *          include a readable copy of the attribution notices contained
+ *          within such NOTICE file, excluding those notices that do not
+ *          pertain to any part of the Derivative Works, in at least one
+ *          of the following places: within a NOTICE text file distributed
+ *          as part of the Derivative Works; within the Source form or
+ *          documentation, if provided along with the Derivative Works; or,
+ *          within a display generated by the Derivative Works, if and
+ *          wherever such third-party notices normally appear. The contents
+ *          of the NOTICE file are for informational purposes only and
+ *          do not modify the License. You may add Your own attribution
+ *          notices within Derivative Works that You distribute, alongside
+ *          or as an addendum to the NOTICE text from the Work, provided
+ *          that such additional attribution notices cannot be construed
+ *          as modifying the License.
+ *
+ *      You may add Your own copyright statement to Your modifications and
+ *      may provide additional or different license terms and conditions
+ *      for use, reproduction, or distribution of Your modifications, or
+ *      for any such Derivative Works as a whole, provided Your use,
+ *      reproduction, and distribution of the Work otherwise complies with
+ *      the conditions stated in this License.
+ *
+ *   5. Submission of Contributions. Unless You explicitly state otherwise,
+ *      any Contribution intentionally submitted for inclusion in the Work
+ *      by You to the Licensor shall be under the terms and conditions of
+ *      this License, without any additional terms or conditions.
+ *      Notwithstanding the above, nothing herein shall supersede or modify
+ *      the terms of any separate license agreement you may have executed
+ *      with Licensor regarding such Contributions.
+ *
+ *   6. Trademarks. This License does not grant permission to use the trade
+ *      names, trademarks, service marks, or product names of the Licensor,
+ *      except as required for reasonable and customary use in describing the
+ *      origin of the Work and reproducing the content of the NOTICE file.
+ *
+ *   7. Disclaimer of Warranty. Unless required by applicable law or
+ *      agreed to in writing, Licensor provides the Work (and each
+ *      Contributor provides its Contributions) on an "AS IS" BASIS,
+ *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ *      implied, including, without limitation, any warranties or conditions
+ *      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ *      PARTICULAR PURPOSE. You are solely responsible for determining the
+ *      appropriateness of using or redistributing the Work and assume any
+ *      risks associated with Your exercise of permissions under this License.
+ *
+ *   8. Limitation of Liability. In no event and under no legal theory,
+ *      whether in tort (including negligence), contract, or otherwise,
+ *      unless required by applicable law (such as deliberate and grossly
+ *      negligent acts) or agreed to in writing, shall any Contributor be
+ *      liable to You for damages, including any direct, indirect, special,
+ *      incidental, or consequential damages of any character arising as a
+ *      result of this License or out of the use or inability to use the
+ *      Work (including but not limited to damages for loss of goodwill,
+ *      work stoppage, computer failure or malfunction, or any and all
+ *      other commercial damages or losses), even if such Contributor
+ *      has been advised of the possibility of such damages.
+ *
+ *   9. Accepting Warranty or Additional Liability. While redistributing
+ *      the Work or Derivative Works thereof, You may choose to offer,
+ *      and charge a fee for, acceptance of support, warranty, indemnity,
+ *      or other liability obligations and/or rights consistent with this
+ *      License. However, in accepting such obligations, You may act only
+ *      on Your own behalf and on Your sole responsibility, not on behalf
+ *      of any other Contributor, and only if You agree to indemnify,
+ *      defend, and hold each Contributor harmless for any liability
+ *      incurred by, or claims asserted against, such Contributor by reason
+ *      of your accepting any such warranty or additional liability.
+ *
+ *   END OF TERMS AND CONDITIONS
+ *
+ *   APPENDIX: How to apply the Apache License to your work.
+ *
+ *      To apply the Apache License to your work, attach the following
+ *      boilerplate notice, with the fields enclosed by brackets "[]"
+ *      replaced with your own identifying information. (Don't include
+ *      the brackets!)  The text should be enclosed in the appropriate
+ *      comment syntax for the file format. We also recommend that a
+ *      file or class name and description of purpose be included on the
+ *      same "printed page" as the copyright notice for easier
+ *      identification within third-party archives.
+ *
+ *   Copyright [yyyy] [name of copyright owner]
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
diff --git a/MANUAL_ja.html b/MANUAL_ja.html
new file mode 100644 (file)
index 0000000..dd8f577
--- /dev/null
@@ -0,0 +1,590 @@
+<!DOCTYPE html>\r
+<html>\r
+<head>\r
+  <meta charset="utf-8">\r
+  <meta name="generator" content="pandoc">\r
+  <title>Stew マニュアル</title>\r
+  <!--[if lt IE 9]>\r
+    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>\r
+  <![endif]-->\r
+  <link rel="stylesheet" href="markdown.css">\r
+</head>\r
+<body>\r
+<header>\r
+<h1 class="title">Stew マニュアル</h1>\r
+<h3 class="date">version 4.0</h3>\r
+</header>\r
+<nav id="TOC">\r
+<ul>\r
+<li><a href="#stewとは何ですか">Stewとは何ですか?</a></li>\r
+<li><a href="#使用上の注意">使用上の注意</a><ul>\r
+<li><a href="#パスワードの保存方式">パスワードの保存方式</a></li>\r
+<li><a href="#コネクション切断時はrollbackしない">コネクション切断時はrollbackしない</a></li>\r
+<li><a href="#その他">その他</a></li>\r
+</ul></li>\r
+<li><a href="#インストール">インストール</a></li>\r
+<li><a href="#起動方法">起動方法</a></li>\r
+<li><a href="#アンインストール">アンインストール</a></li>\r
+<li><a href="#使い方">使い方</a><ul>\r
+<li><a href="#接続設定">接続設定</a></li>\r
+<li><a href="#パスワードの保存について">パスワードの保存について</a></li>\r
+<li><a href="#無名接続">無名接続</a></li>\r
+<li><a href="#対話モード">対話モード</a></li>\r
+<li><a href="#一行完結モードone-liner">一行完結モード(One-Liner)</a></li>\r
+<li><a href="#エイリアスalias">エイリアス(alias)</a></li>\r
+</ul></li>\r
+<li><a href="#コマンドstew内部コマンド">コマンド(Stew内部コマンド)</a><ul>\r
+<li><a href="#connect---データベースに接続する-組み込みコマンド">connect - データベースに接続する (組み込みコマンド)</a></li>\r
+<li><a href="#disconnect---データベースとの接続を切断する-組み込みコマンド">disconnect - データベースとの接続を切断する (組み込みコマンド)</a></li>\r
+<li><a href="#commit---トランザクションのコミット-組み込みコマンド">commit - トランザクションのコミット (組み込みコマンド)</a></li>\r
+<li><a href="#rollback---トランザクションのロールバック-組み込みコマンド">rollback - トランザクションのロールバック (組み込みコマンド)</a></li>\r
+<li><a href="#e---複数コマンドの評価-組み込みコマンド">-e - 複数コマンドの評価 (組み込みコマンド)</a></li>\r
+<li><a href="#f---ファイル内容をコマンドとして実行-組み込みコマンド">-f - ファイル内容をコマンドとして実行 (組み込みコマンド)</a></li>\r
+<li><a href="#s---ファイル内容をスクリプトとして実行-組み込みコマンド">-s - ファイル内容をスクリプトとして実行 (組み込みコマンド)</a></li>\r
+<li><a href="#cd---カレントディレクトリの移動-組み込みコマンド">cd - カレントディレクトリの移動 (組み込みコマンド)</a></li>\r
+<li><a href="#場所の表示-組み込みコマンド">@ - 場所の表示 (組み込みコマンド)</a></li>\r
+<li><a href="#alias---エイリアスコマンド別名の登録-組み込みコマンド">alias - エイリアス(コマンド別名)の登録 (組み込みコマンド)</a></li>\r
+<li><a href="#unalias---エイリアスコマンド別名の解除-組み込みコマンド">unalias - エイリアス(コマンド別名)の解除 (組み込みコマンド)</a></li>\r
+<li><a href="#exit---終了-組み込みコマンド">exit - 終了 (組み込みコマンド)</a></li>\r
+<li><a href="#load---ファイルから実行">load - ファイルから実行</a></li>\r
+<li><a href="#import---ファイルのインポート">import - ファイルのインポート</a></li>\r
+<li><a href="#export---検索結果のエクスポート">export - 検索結果のエクスポート</a></li>\r
+<li><a href="#time---実行時間計測">time - 実行時間計測</a></li>\r
+<li><a href="#find---テーブル名検索">find - テーブル名検索</a></li>\r
+<li><a href="#report---データベース情報表示">report - データベース情報表示</a></li>\r
+<li><a href="#download---1データごとにダウンロード">download - 1データごとにダウンロード</a></li>\r
+<li><a href="#upload---ファイル内容を1データとして登録">upload - ファイル内容を1データとして登録</a></li>\r
+<li><a href="#wait---待機">wait - 待機</a></li>\r
+</ul></li>\r
+<li><a href="#guiモードメニュー">GUIモード・メニュー</a><ul>\r
+<li><a href="#ファイルf---新しいウィンドウn-ctrl-n">ファイル(F) - 新しいウィンドウ(N) Ctrl-N</a></li>\r
+<li><a href="#ファイルf---閉じるc-ctrl-w">ファイル(F) - 閉じる(C) Ctrl-W</a></li>\r
+<li><a href="#ファイルf---終了x-ctrl-q">ファイル(F) - 終了(X) Ctrl-Q</a></li>\r
+<li><a href="#編集e---切り取りt-ctrl-x">編集(E) - 切り取り(T) Ctrl-X</a></li>\r
+<li><a href="#編集e---コピーc-ctrl-c">編集(E) - コピー(C) Ctrl-C</a></li>\r
+<li><a href="#編集e---貼り付けp-ctrl-v">編集(E) - 貼り付け(P) Ctrl-V</a></li>\r
+<li><a href="#編集e---すべて選択a-ctrl-a">編集(E) - すべて選択(A) Ctrl-A</a></li>\r
+<li><a href="#編集e---検索f-ctrl-f">編集(E) - 検索(F) Ctrl-F</a></li>\r
+<li><a href="#編集e---フォーカス切替g-ctrl-g">編集(E) - フォーカス切替(G) Ctrl-G</a></li>\r
+<li><a href="#編集e---メッセージのクリア">編集(E) - メッセージのクリア</a></li>\r
+<li><a href="#表示v---ステータス-バーb">表示(V) - ステータス バー(B)</a></li>\r
+<li><a href="#表示v---列番号を表示c">表示(V) - 列番号を表示(C)</a></li>\r
+<li><a href="#表示v---情報ツリーペインを表示i">表示(V) - 情報ツリーペインを表示(I)</a></li>\r
+<li><a href="#表示v---常に手前に表示t">表示(V) - 常に手前に表示(T)</a></li>\r
+<li><a href="#表示v---最新状態に更新r-f5">表示(V) - 最新状態に更新(R) F5</a></li>\r
+<li><a href="#表示v---列幅を拡大w-ctrl-.period">表示(V) - 列幅を拡大(W) Ctrl-.(period)</a></li>\r
+<li><a href="#表示v---列幅を縮小n-ctrl-comma">表示(V) - 列幅を縮小(N) Ctrl-,(comma)</a></li>\r
+<li><a href="#表示v---列幅を調整a-ctrl-slash">表示(V) - 列幅を調整(A) Ctrl-/(slash)</a></li>\r
+<li><a href="#表示v---列幅自動調整m">表示(V) - 列幅自動調整(M)</a></li>\r
+<li><a href="#コマンドc---実行x-ctrl-m">コマンド(C) - 実行(X) Ctrl-M</a></li>\r
+<li><a href="#コマンドc---中断b-ctrl-pausebreak">コマンド(C) - 中断(B) Ctrl-Pause(Break)</a></li>\r
+<li><a href="#コマンドc---前のコマンド履歴p-ctrl-">コマンド(C) - 前のコマンド履歴(P) Ctrl-↑</a></li>\r
+<li><a href="#コマンドc---次のコマンド履歴n-ctrl-">コマンド(C) - 次のコマンド履歴(N) Ctrl-↓</a></li>\r
+<li><a href="#コマンドc---ロールバックr">コマンド(C) - ロールバック(R)</a></li>\r
+<li><a href="#コマンドc---コミットm">コマンド(C) - コミット(M)</a></li>\r
+<li><a href="#コマンドc---接続c-ctrl-e">コマンド(C) - 接続(C) Ctrl-E</a></li>\r
+<li><a href="#コマンドc---切断d-ctrl-d">コマンド(C) - 切断(D) Ctrl-D</a></li>\r
+<li><a href="#コマンドc---終了処理0">コマンド(C) - 終了処理(0)</a></li>\r
+<li><a href="#コマンドc---暗号鍵の入力k">コマンド(C) - 暗号鍵の入力(K)</a></li>\r
+<li><a href="#コマンドc---接続設定e">コマンド(C) - 接続設定(E)</a></li>\r
+<li><a href="#データd---並び替えs-alt-s">データ(D) - 並び替え(S) Alt-S</a></li>\r
+<li><a href="#データd---インポートi">データ(D) - インポート(I)</a></li>\r
+<li><a href="#データd---エクスポートe-ctrl-shift-s">データ(D) - エクスポート(E) Ctrl-Shift-S</a></li>\r
+<li><a href="#ヘルプh---ヘルプを表示h">ヘルプ(H) - ヘルプを表示(H)</a></li>\r
+<li><a href="#ヘルプh---stew-についてa">ヘルプ(H) - Stew について(A)</a></li>\r
+</ul></li>\r
+<li><a href="#guiモードその他">GUIモード・その他</a><ul>\r
+<li><a href="#全体">全体</a></li>\r
+<li><a href="#結果テーブル">結果テーブル</a></li>\r
+<li><a href="#入出力欄">入出力欄</a></li>\r
+<li><a href="#ステータスバー">ステータスバー</a></li>\r
+<li><a href="#コンテキストメニュー">コンテキストメニュー</a></li>\r
+<li><a href="#データベース情報ツリー">データベース情報ツリー</a></li>\r
+<li><a href="#設定保存">設定保存</a></li>\r
+<li><a href="#メニューのキー割り当て">メニューのキー割り当て</a></li>\r
+</ul></li>\r
+<li><a href="#プロパティ">プロパティ</a><ul>\r
+<li><a href="#net.argius.stew.properties---プロパティファイルの場所">net.argius.stew.properties - プロパティファイルの場所</a></li>\r
+<li><a href="#net.argius.stew.directory---作業ディレクトリ">net.argius.stew.directory - 作業ディレクトリ</a></li>\r
+<li><a href="#net.argius.stew.query.timeout---クエリのタイムアウト値">net.argius.stew.query.timeout - クエリのタイムアウト値</a></li>\r
+<li><a href="#net.argius.stew.rowcount.limit---出力件数の上限">net.argius.stew.rowcount.limit - 出力件数の上限</a></li>\r
+<li><a href="#net.argius.stew.command.import.batch.limit---importのバッチ数の上限値">net.argius.stew.command.Import.batch.limit - Importのバッチ数の上限値</a></li>\r
+<li><a href="#net.argius.stew.ui.window.resident---windowの常駐">net.argius.stew.ui.window.resident - Windowの常駐</a></li>\r
+<li><a href="#loggerの設定">Loggerの設定</a></li>\r
+</ul></li>\r
+</ul>\r
+</nav>\r
+<h2 id="stewとは何ですか"><a href="#TOC">Stewとは何ですか?</a></h2>\r
+<p>Stewは、JDBCを使った小規模なデータベースフロントエンドです。 コマンドラインのようなインターフェイスを持っていて、SQLを入力して実行したりできます。 ちょっとした処理であれば、バッチのように使用することもできます。</p>\r
+<p>概要については、README_ja.mdをご覧ください。</p>\r
+<h2 id="使用上の注意"><a href="#TOC">使用上の注意</a></h2>\r
+<h3 id="パスワードの保存方式"><a href="#TOC">パスワードの保存方式</a></h3>\r
+<p>パスワードは、デフォルトではそのまま保存します。 生のパスワードを保存したくない場合は、暗号化を利用できます。</p>\r
+<p>詳しくは、<a href="#接続設定">使い方-接続設定</a>を参照してください。</p>\r
+<h3 id="コネクション切断時はrollbackしない"><a href="#TOC">コネクション切断時はrollbackしない</a></h3>\r
+<p>デフォルトでは、disconnectコマンドによりコネクションを切断するとき、rollbackを発行しません。 DBMSによっては、トランザクションが自動的にコミットされてしまうことがありますので注意が必要です。</p>\r
+<p>「切断時に自動ロールバック」を設定すると、disconnectの際に自動的にrollbackを発行します。 詳細は、<a href="#接続設定">使い方-接続設定</a>を参照してください。</p>\r
+<h3 id="その他"><a href="#TOC">その他</a></h3>\r
+<p>プロジェクトサイトにて追加説明を行っていますので、あわせてご利用ください。</p>\r
+<p><a href="http://stew.sourceforge.jp/"><code class="url">http://stew.sourceforge.jp/</code></a><br><a href="http://argius.net/wiki/index.php?Stew%20tutorial"><code class="url">http://argius.net/wiki/index.php?Stew%20tutorial</code></a> (argius.net)</p>\r
+<h2 id="インストール"><a href="#TOC">インストール</a></h2>\r
+<p>Java実行環境バージョン6(JRE6)以上がインストールされている必要があります。 また、利用するデータベースのJDBCドライバが必要です。</p>\r
+<p>リリースパッケージ(通常はzipファイル)を任意のディレクトリに展開します。</p>\r
+<p>インストールサイズを最小限にしたい場合は、 リリースパッケージに含まれている&quot;stew.jar&quot;だけを展開してください。</p>\r
+<p>起動コマンドの設定は、次の起動方法を参照してください。</p>\r
+<h2 id="起動方法"><a href="#TOC">起動方法</a></h2>\r
+<p>GUIモードで起動する場合は、以下のコマンドを実行します。</p>\r
+<pre><code>&gt; java -jar stew.jar --gui</code></pre>\r
+<p>CUIモードで起動する場合は、以下のコマンドを実行します。</p>\r
+<pre><code>&gt; java -jar stew.jar --cui</code></pre>\r
+<p>起動スクリプトもしくはショートカットやエイリアスを作成しておくと便利です。 UNIX系OSの場合は&quot;stew.sh&quot;を、Windowsの場合は&quot;stew.bat&quot;を参照してください。</p>\r
+<p>Stewを実行すると、システムディレクトリ&quot;.stew&quot;が作成され、設定の保存に使用されます。 &quot;.stew&quot;ディレクトリは、デフォルトではカレントディレクトリに作成されます。</p>\r
+<h2 id="アンインストール"><a href="#TOC">アンインストール</a></h2>\r
+<p>インストールしたファイルと&quot;.stew&quot;ディレクトリを削除してください。</p>\r
+<hr>\r
+<h2 id="使い方"><a href="#TOC">使い方</a></h2>\r
+<p>Stewを使用するには、JDBC接続が可能なデータベースと、JDBCドライバが必要です。 JDBCドライバ自体の詳細については、各データベースの説明書などを参照してください。</p>\r
+<h3 id="接続設定"><a href="#TOC">接続設定</a></h3>\r
+<p>CUIの場合は、起動時に--editオプションを指定、またはコマンドとして--editを実行すると、 編集プログラムが起動します。</p>\r
+<p>GUIの場合は、メニューの&quot;接続設定&quot;を実行すると、編集ダイアログが開きます。</p>\r
+<p>それぞれの設定項目の説明は次のとおりです。</p>\r
+<dl>\r
+<dt>コネクタID</dt>\r
+<dd><p>connectコマンドなどに渡すIDです。英数字のみ指定できます。</p>\r
+</dd>\r
+<dt>コネクタ名</dt>\r
+<dd><p>プロンプトに表示される接続名です。文字制限は特にありません。</p>\r
+</dd>\r
+<dt>クラスパス</dt>\r
+<dd><p>JDBCドライバのクラスパスを指定します。Javaの-CLASSPATHオプションと同じ形式で指定します。</p>\r
+</dd>\r
+<dt>ドライバ</dt>\r
+<dd><p>JDBCドライバのDriver実装クラスを指定します。GUIの場合、クラスパスが指定されていれば、 &quot;ドライバの検索&quot;から選択できます。</p>\r
+</dd>\r
+<dt>接続先URL</dt>\r
+<dd><p>JDBCのURL(DriverManager.getConnection(url)に指定するurlと同じもの)を指定します。</p>\r
+</dd>\r
+<dt>ユーザ</dt>\r
+<dd><p>JDBCのURLと同時に指定するユーザIDを指定します。</p>\r
+</dd>\r
+<dt>パスワード</dt>\r
+<dd><p>ユーザIDのパスワードを指定します。</p>\r
+</dd>\r
+<dt>暗号化処理</dt>\r
+<dd><p>パスワードの保存方法を選択します。次項&quot;パスワードの保存について&quot;を参照してください。</p>\r
+</dd>\r
+<dt>読取専用</dt>\r
+<dd><p>コネクションをREADONLYに設定し、更新の有るコマンドを実行できないようにします。 (コマンド側のReadOnlyに拠る。)</p>\r
+</dd>\r
+<dt>自動ロールバック</dt>\r
+<dd><p>設定すると、切断時に自動でロールバックします。</p>\r
+</dd>\r
+</dl>\r
+<p>これらの情報は、&quot;.stew/connectors.properties&quot;に保存されます。 手動で編集することもできますが、その際は注意して行ってください。</p>\r
+<h3 id="パスワードの保存について"><a href="#TOC">パスワードの保存について</a></h3>\r
+<p>パスワードは設定ファイルに保存されるため、そのまま保存すると都合が悪い場合があります。</p>\r
+<p>パスワードの保存方法は、暗号化処理を選択することができます。</p>\r
+<dl>\r
+<dt>PlainTextPassword</dt>\r
+<dd><p>(デフォルト)パスワードをそのまま保存します。</p>\r
+</dd>\r
+<dt>PbePassword</dt>\r
+<dd><p>PBE暗号を使用してパスワードを保存します。 <a href="#コマンドc---暗号鍵の入力k">メニュー-暗号鍵の入力</a>で暗号鍵を入力した後、 接続設定でパスワードを入力して保存すると、暗号化された状態で保存します。 次回起動時、再度暗号鍵を入力するまで、パスワードが復号できなくなります。</p>\r
+</dd>\r
+</dl>\r
+<p>独自のパスワード暗号化を追加することもできます。 (実装詳細:Passwordインタフェースの実装クラスを追加します。)</p>\r
+<h3 id="無名接続"><a href="#TOC">無名接続</a></h3>\r
+<p>接続設定なしで接続を行うことができます。 ドライバが特定できるようになっている必要があります。(次の段落で説明)</p>\r
+<pre><code>&gt; connect &lt;user&gt;/&lt;password&gt;@&lt;URL&gt;\r
+\r
+(例)\r
+&gt; connect user/password1@jdbc:firebirdsql://127.0.0.1//home/argius/test.fdb</code></pre>\r
+<p>最も簡単な方法は、ドライバファイルを起動ディレクトリに置くだけです。 ドライバ名が指定されていない場合は、このファイルの中を探索してドライバ名を特定します。 但し、この探索は時間がかかるので、時間を節約するには&quot;jdbc.drivers&quot;システムプロパティを 指定してください。</p>\r
+<h3 id="対話モード"><a href="#TOC">対話モード</a></h3>\r
+<p>通常の起動では、対話モードでの操作となります。 対話モードでは、コマンド入力待ち状態になった時にコマンドを入力し、 処理が終わると再びコマンド入力待ちになります。</p>\r
+<h3 id="一行完結モードone-liner"><a href="#TOC">一行完結モード(One-Liner)</a></h3>\r
+<p>Stewの処理が一行の入力で完結します。 &quot;stew&quot;コマンドを設定していると仮定すると、次のような形式で実行します。</p>\r
+<pre><code>&gt; stew &lt;connector-id&gt; &lt;command&gt;</code></pre>\r
+<p>実行すると、<connector-id>の接続設定で接続を開始し、その接続を使用してコマンドを実行します。 コマンド実行後、接続を切断して終了します。</p>\r
+<p>このモードでは、ご利用のshell環境の制約(例えば、ワイルドカード,リダイレクト など)を 受けますのでご注意ください。</p>\r
+<h3 id="エイリアスalias"><a href="#TOC">エイリアス(alias)</a></h3>\r
+<p>長いコマンドに別名(alias)をつけて、短縮コマンドとして利用できます。 詳しくは、コマンド<a href="#alias%20-%20エイリアス(コマンド別名)の登録%20(組み込みコマンド)">alias</a>, <a href="#unalias%20-%20エイリアス(コマンド別名)の解除%20(組み込みコマンド)">unalias</a>を参照してください。</p>\r
+<hr>\r
+<h2 id="コマンドstew内部コマンド"><a href="#TOC">コマンド(Stew内部コマンド)</a></h2>\r
+<p>組み込みコマンドは、Stewから切り離すことができないコマンドです。 それ以外は、追加コマンドとして実装されています。 (実装詳細:追加コマンドはCommandクラスのサブクラスによる実装ですが、 組み込みコマンドはCommand.invokeメソッドに直接書かれている処理です。)</p>\r
+<p>コマンドでない文が指定された場合は、SQLとして実行されます。 バインド変数のあるSQL文を使う場合は、最後に&quot;;&quot;(セミコロン)をつけ、 それ以降にカンマ区切りでパラメータを指定できます。 この機能は、一部のコマンドでも利用できます。</p>\r
+<p>組み込みコマンドのうち、connect/-e/-f/alias/unalias/exitは接続時以外でも使用できます。</p>\r
+<h3 id="connect---データベースに接続する-組み込みコマンド"><a href="#TOC">connect - データベースに接続する (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; connect &lt;connector-id&gt;\r
+&gt; -c &lt;connector-id&gt;</code></pre>\r
+<p>予め用意した接続設定を使用して、データベースに接続します。</p>\r
+<p>対話モードでは、disconnectまたは強制的に切断されるまで、接続が維持されます。 すでに接続中の場合は、その接続を切断してから接続を行います。</p>\r
+<h3 id="disconnect---データベースとの接続を切断する-組み込みコマンド"><a href="#TOC">disconnect - データベースとの接続を切断する (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; disconnect\r
+&gt; -d</code></pre>\r
+<p>データベースとの接続を切断します。</p>\r
+<p>接続設定でrollbackを指定している場合は、rollbackを試みます。</p>\r
+<h3 id="commit---トランザクションのコミット-組み込みコマンド"><a href="#TOC">commit - トランザクションのコミット (組み込みコマンド)</a></h3>\r
+<p>現在のトランザクションでの変更をコミットします。</p>\r
+<p><strong>コミットする際は、誤って必要なデータを削除しないよう注意してください。</strong></p>\r
+<h3 id="rollback---トランザクションのロールバック-組み込みコマンド"><a href="#TOC">rollback - トランザクションのロールバック (組み込みコマンド)</a></h3>\r
+<p>現在のトランザクションでの変更をロールバックします。</p>\r
+<h3 id="e---複数コマンドの評価-組み込みコマンド"><a href="#TOC">-e - 複数コマンドの評価 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; -e &lt;コマンド&gt; -e &lt;コマンド&gt; ...</code></pre>\r
+<p>コマンドを連続して実行させます。 exportなどを同時に実行する場合や、コマンドラインからの実行の場合に使います。</p>\r
+<h3 id="f---ファイル内容をコマンドとして実行-組み込みコマンド"><a href="#TOC">-f - ファイル内容をコマンドとして実行 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; -f &lt;ファイル&gt;</code></pre>\r
+<p>ファイルの内容をコマンドとして実行します。 再帰的に指定できますが、無限ループは検知できないので注意してください。</p>\r
+<h3 id="s---ファイル内容をスクリプトとして実行-組み込みコマンド"><a href="#TOC">-s - ファイル内容をスクリプトとして実行 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; -s &lt;ファイル&gt;</code></pre>\r
+<p>ファイルの内容をスクリプト(JavaScript)として実行します。</p>\r
+<p>スクリプト内では、以下の変数が定義済みになります。</p>\r
+<ul>\r
+<li>接続中のコネクション: connection, conn</li>\r
+<li>パラメーター: parameter, p</li>\r
+<li>出力制御: outputProcessor, op</li>\r
+</ul>\r
+<h3 id="cd---カレントディレクトリの移動-組み込みコマンド"><a href="#TOC">cd - カレントディレクトリの移動 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; cd &lt;ディレクトリ&gt;</code></pre>\r
+<p>カレントディレクトリを指定したディレクトリに移動します。 (このカレントディレクトリはStew内部で管理するものです。)</p>\r
+<h3 id="場所の表示-組み込みコマンド"><a href="#TOC">@ - 場所の表示 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; @</code></pre>\r
+<p>カレントディレクトリとシステムディレクトリの場所を表示します。</p>\r
+<h3 id="alias---エイリアスコマンド別名の登録-組み込みコマンド"><a href="#TOC">alias - エイリアス(コマンド別名)の登録 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; alias [&lt;エイリアス&gt; [&lt;コマンド&gt;]]</code></pre>\r
+<p>コマンド別名を登録します。 引数が1つで実行した場合は、既に設定されたコマンド内容を表示します。 引数なしで実行した場合は、すべての定義済みエイリアスを表示します。</p>\r
+<p>登録または表示の前にファイルからメモリ上の情報を最新化します。 ファイルを直接修正したり、他のスレッドで変更した内容を反映させたい場合は、 このコマンドを実行してください。 (実装詳細:循環参照などの無限ループ抑制のために、展開の深さは最大100。)</p>\r
+<pre><code>&gt; alias\r
+エイリアスは未定義です。\r
+&gt; alias search select # from\r
+&gt; alias count select count(#) from\r
+&gt; alias search\r
+alias search=[select # from]\r
+&gt; search table1\r
+&gt;&gt; select # from table1\r
+(select # from table1 の結果)\r
+&gt;</code></pre>\r
+<h3 id="unalias---エイリアスコマンド別名の解除-組み込みコマンド"><a href="#TOC">unalias - エイリアス(コマンド別名)の解除 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; unalias &lt;エイリアス&gt;</code></pre>\r
+<p>コマンド別名を解除します。解除する対象がない場合は何もしません。 解除対象の有無にかかわらず、ファイルからメモリ上の情報を最新化します。</p>\r
+<h3 id="exit---終了-組み込みコマンド"><a href="#TOC">exit - 終了 (組み込みコマンド)</a></h3>\r
+<pre><code>&gt; exit</code></pre>\r
+<p>Stewを終了します。確認待ちは行いません。</p>\r
+<p>接続中の全てのコネクションは自動的に切断されます。 ロールバックは、自動ロールバックを設定しているコネクタのみ行われます。 自動ロールバックについては、<a href="#接続設定">使い方-接続設定</a>を参照してください。</p>\r
+<h3 id="load---ファイルから実行"><a href="#TOC">load - ファイルから実行</a></h3>\r
+<pre><code>&gt; load [&lt;SQLファイル&gt; | &lt;データファイル&gt; &lt;テーブル名&gt; [ HEADER ]]</code></pre>\r
+<p>指定されたファイルを読み込んで、SQLを実行します。</p>\r
+<p>パラメータが1個の場合は、ファイルをSQL文と見なして実行します。</p>\r
+<p>パラメータが2個以上の場合は、ファイルをデータファイルと見なして、インポートを実行します。 ファイルの拡張子によって、ファイル形式が自動的に選択されます。 (実装詳細:基本的にはimportと同じ動作ですが、バッチ実行ではなく1件ずつ処理されます。)</p>\r
+<p>*. .csv : CSV形式 *. .xml : XML形式(定義:src/net/argius/stew/io/stew-table.dtd) *. 上記以外 : TAB区切りテキスト形式</p>\r
+<h3 id="import---ファイルのインポート"><a href="#TOC">import - ファイルのインポート</a></h3>\r
+<pre><code>&gt; import [&lt;データファイル&gt; &lt;テーブル名&gt; [ HEADER ]]</code></pre>\r
+<p>ファイルをデータファイルと見なして、インポートを実行します。 ファイルの拡張子によって、ファイル形式が自動的に選択されます。 (実装詳細:基本的には、loadのパラメータ2個以上指定した時と同じ動作ですが、 Statement.addBatch()を使用します。)</p>\r
+<ul>\r
+<li>.csv : CSV形式</li>\r
+<li>.xml : XML形式(定義:src/net/argius/stew/io/stew-table.dtd)</li>\r
+<li>上記以外 : TAB区切りテキスト形式</li>\r
+</ul>\r
+<p>一括で処理する件数を[#property.Import.batch.limit:プロパティ]で設定することができます。</p>\r
+<h3 id="export---検索結果のエクスポート"><a href="#TOC">export - 検索結果のエクスポート</a></h3>\r
+<pre><code>&gt; export &lt;ファイル&gt; [ HEADER ] [command(select|find|report)]</code></pre>\r
+<p>指定したファイルに、コマンドの検索結果を出力します。 ファイルの拡張子によって、ファイル形式が自動的に選択されます。</p>\r
+<ul>\r
+<li>.htm,.html : HTML形式</li>\r
+<li>.csv : CSV形式</li>\r
+<li>.xml : XML形式(定義:src/net/argius/stew/io/stew-table.dtd)</li>\r
+<li>上記以外 : TAB区切りテキスト形式</li>\r
+</ul>\r
+<h3 id="time---実行時間計測"><a href="#TOC">time - 実行時間計測</a></h3>\r
+<pre><code>&gt; time [&lt;回数&gt;] &lt;SQL文&gt;</code></pre>\r
+<p>指定したSQL文を実行し、その実行時間を計測して表示します。</p>\r
+<p>回数が指定された場合は、回数分SQLを繰り返し実行して、 「合計」「平均」「最大」「最小」を集計します。 回数を指定しない場合は、1回の実行時間を表示します。</p>\r
+<pre><code>&gt; time select # from EMPLOYEE\r
+実行時間 : 0.093 秒\r
+&gt; time 100 select # from EMPLOYEE\r
+合計 : 0.484 秒\r
+平均 : 0.005 秒\r
+最大 : 0.094 秒\r
+最小 : 0.000 秒\r
+&gt;</code></pre>\r
+<h3 id="find---テーブル名検索"><a href="#TOC">find - テーブル名検索</a></h3>\r
+<pre><code>&gt; find &lt;テーブル名パターン&gt; [&lt;テーブル種別パターン&gt; [&lt;スキーマ名パターン&gt; [&lt;カタログ名パターン&gt; [ FULL ]]]]</code></pre>\r
+<p>参照可能なテーブルの一覧を表示します。 &quot;パターン&quot;というキーワードを含むパラメータは、ワイルドカード(#,?)が指定できます。</p>\r
+<h3 id="report---データベース情報表示"><a href="#TOC">report - データベース情報表示</a></h3>\r
+<pre><code>&gt; report - | &lt;テーブル名&gt; [ PK | INDEX ]</code></pre>\r
+<p>接続中のコネクションに関する情報を表示します。</p>\r
+<p>-(ハイフン)が指定された場合は、DBとJDBCドライバの名称とバージョン、 接続ユーザとアドレスを表示します。</p>\r
+<p>テーブル名のみが指定された場合は、テーブルの列情報が表示されます。</p>\r
+<p>テーブル名とオプションが指定された場合は、PKを指定するとプライマリキーの一覧が、 INDEXを指定するとインデックスキーの一覧が、それぞれ表示されます。</p>\r
+<h3 id="download---1データごとにダウンロード"><a href="#TOC">download - 1データごとにダウンロード</a></h3>\r
+<pre><code>&gt; download &lt;ルートディレクトリ&gt; SELECT &lt;ダウンロードするデータの列&gt; [, ファイルパス...] FROM ...</code></pre>\r
+<p>1つの列のデータをダウンロードしてファイルに保存します。</p>\r
+<p>どのデータ型でも利用できます。 長めのテキスト項目やラージオブジェクト(BLOB,CLOB)を一挙にファイルに保存する場合などに有用です。</p>\r
+<p>ファイルは、複数ファイルをダウンロードできるようにするため、 データからファイル名を生成できるようになっています。</p>\r
+<p>ファイル名は、2列目以降の列を文字列として結合したものとなります。 これはプライマリキーと拡張子を指定することを想定しています。</p>\r
+<pre><code>&gt; download emp select FULL_NAME, JOB_COUNTRY, &#39;/&#39;, EMP_NO, &#39;.txt&#39; from EMPLOYEE\r
+ディレクトリ[./emp/USA]を作成しました。\r
+ダウンロードされました。 (0.014 Kbytes, file=[./emp/USA/2.txt])\r
+ダウンロードされました。 (0.012 Kbytes, file=[./emp/USA/4.txt])\r
+ ・\r
+ ・\r
+ ・\r
+ダウンロードされました。 (0.012 Kbytes, file=[./emp/USA/24.txt])\r
+ディレクトリ[./emp/England]を作成しました。\r
+ダウンロードされました。 (0.011 Kbytes, file=[./emp/England/28.txt])\r
+ ・\r
+ ・\r
+ ・\r
+ダウンロードされました。 (0.018 Kbytes, file=[./emp/USA/145.txt])\r
+42 件 ヒットしました。</code></pre>\r
+<p>データが1件の場合は、データ列のみ指定すれば、 ファイルパスの前半部分がそのままファイル名となります。</p>\r
+<p>同名のファイルが存在したり、ディレクトリ作成時に権限が無い場合はエラーとなり、 その時点で処理を中断します。</p>\r
+<h3 id="upload---ファイル内容を1データとして登録"><a href="#TOC">upload - ファイル内容を1データとして登録</a></h3>\r
+<pre><code>&gt; upload &lt;ファイルパス&gt; &lt;INSERT文 or UPDATE文&gt;</code></pre>\r
+<p>SQLのプレースホルダで指定された列にファイル内容を登録します。</p>\r
+<h3 id="wait---待機"><a href="#TOC">wait - 待機</a></h3>\r
+<pre><code>&gt; wait 秒(小数第3位まで指定可)</code></pre>\r
+<p>指定された秒数、待機します。</p>\r
+<p>連続したコマンドを実行する場合などに使えるかもしれません。</p>\r
+<h2 id="guiモードメニュー"><a href="#TOC">GUIモード・メニュー</a></h2>\r
+<p>GUIモードのメニューについての説明です。</p>\r
+<h3 id="ファイルf---新しいウィンドウn-ctrl-n"><a href="#TOC">ファイル(F) - 新しいウィンドウ(N) Ctrl-N</a></h3>\r
+<p>新しいウィンドウを開きます。</p>\r
+<p>新しいウィンドウは、元のウィンドウとは独立した接続で処理が行われます。</p>\r
+<h3 id="ファイルf---閉じるc-ctrl-w"><a href="#TOC">ファイル(F) - 閉じる(C) Ctrl-W</a></h3>\r
+<p>ウィンドウを閉じます。 コネクションが接続中の場合は、確認ダイアログを表示します。</p>\r
+<p>開いているウィンドウが1つの場合は、終了(X)と同じ動作となります。</p>\r
+<h3 id="ファイルf---終了x-ctrl-q"><a href="#TOC">ファイル(F) - 終了(X) Ctrl-Q</a></h3>\r
+<p>アプリケーションを終了します。 確認ダイアログが表示され、&quot;はい&quot;を選択すると終了します。</p>\r
+<h3 id="編集e---切り取りt-ctrl-x"><a href="#TOC">編集(E) - 切り取り(T) Ctrl-X</a></h3>\r
+<p>選択範囲をクリップボードにコピーして、選択された部分を削除します。 (基本的に、一般的なアプリケーションと同じ処理です。)</p>\r
+<h3 id="編集e---コピーc-ctrl-c"><a href="#TOC">編集(E) - コピー(C) Ctrl-C</a></h3>\r
+<p>選択範囲をクリップボードにコピーします。 (基本的に、一般的なアプリケーションと同じ処理です。)</p>\r
+<h3 id="編集e---貼り付けp-ctrl-v"><a href="#TOC">編集(E) - 貼り付け(P) Ctrl-V</a></h3>\r
+<p>カーソル位置にクリップボードの内容を貼り付けます。 (基本的に、一般的なアプリケーションと同じ処理です。)</p>\r
+<h3 id="編集e---すべて選択a-ctrl-a"><a href="#TOC">編集(E) - すべて選択(A) Ctrl-A</a></h3>\r
+<p>選択している領域(テーブルまたは入出力欄)を全選択状態にします。</p>\r
+<h3 id="編集e---検索f-ctrl-f"><a href="#TOC">編集(E) - 検索(F) Ctrl-F</a></h3>\r
+<p>選択している領域(テーブル、テーブル列名、入出力欄、情報ツリー)内の文字列を検索します。</p>\r
+<p>テーブル、テーブル列名はセル単位で検索します。 入出力欄は、一致部分をハイライトします。 情報ツリーは、選択しているノードと同じ深さのノードとそれらのサブノード単位で検索します。</p>\r
+<p>オプションで、「正規表現を使用する」「大文字と小文字を区別しない」が指定できます。</p>\r
+<h3 id="編集e---フォーカス切替g-ctrl-g"><a href="#TOC">編集(E) - フォーカス切替(G) Ctrl-G</a></h3>\r
+<p>検索結果エリアと入力欄のフォーカスを入れ替えます(toggle)。</p>\r
+<h3 id="編集e---メッセージのクリア"><a href="#TOC">編集(E) - メッセージのクリア</a></h3>\r
+<p>入出力欄のメッセージをクリアします。</p>\r
+<h3 id="表示v---ステータス-バーb"><a href="#TOC">表示(V) - ステータス バー(B)</a></h3>\r
+<p>選択された場合、ウィンドウの下部にステータスバーを表示します。</p>\r
+<p>設定は保存されます。</p>\r
+<h3 id="表示v---列番号を表示c"><a href="#TOC">表示(V) - 列番号を表示(C)</a></h3>\r
+<p>選択された場合、検索結果の列名に番号を付けます。</p>\r
+<h3 id="表示v---情報ツリーペインを表示i"><a href="#TOC">表示(V) - 情報ツリーペインを表示(I)</a></h3>\r
+<p>選択された場合、データベース情報ツリーペインを表示します。</p>\r
+<h3 id="表示v---常に手前に表示t"><a href="#TOC">表示(V) - 常に手前に表示(T)</a></h3>\r
+<p>選択された場合、ウィンドウを常に手前に表示するようにします。</p>\r
+<h3 id="表示v---最新状態に更新r-f5"><a href="#TOC">表示(V) - 最新状態に更新(R) F5</a></h3>\r
+<p>検索結果を表示した際のクエリを再発行して、最新状態を表示します。</p>\r
+<h3 id="表示v---列幅を拡大w-ctrl-.period"><a href="#TOC">表示(V) - 列幅を拡大(W) Ctrl-.(period)</a></h3>\r
+<p>検索結果の列幅をそれぞれ1.5倍に拡大します。</p>\r
+<h3 id="表示v---列幅を縮小n-ctrl-comma"><a href="#TOC">表示(V) - 列幅を縮小(N) Ctrl-,(comma)</a></h3>\r
+<p>検索結果の列幅をそれぞれ2/3に縮小します。</p>\r
+<h3 id="表示v---列幅を調整a-ctrl-slash"><a href="#TOC">表示(V) - 列幅を調整(A) Ctrl-/(slash)</a></h3>\r
+<p>検索結果の列幅を自動調整します。次項の「列幅自動調整」で選択されたモードで調整されます。</p>\r
+<h3 id="表示v---列幅自動調整m"><a href="#TOC">表示(V) - 列幅自動調整(M)</a></h3>\r
+<p>検索結果表示時に、検索結果の列幅を自動調整するモードを選択します。</p>\r
+<ul>\r
+<li>なし(N) : 自動調整を行いません。</li>\r
+<li>ヘッダ基準(H) : ヘッダ名のサイズを基準に、各列幅を自動調整します。</li>\r
+<li>値基準(V) : 列の中で最も長い値のサイズを基準に、各列幅を自動調整します。</li>\r
+<li>ヘッダと値基準(A) : ヘッダを含めて、列の中で最も長い値のサイズを基準に、各列幅を自動調整します。</li>\r
+</ul>\r
+<h3 id="コマンドc---実行x-ctrl-m"><a href="#TOC">コマンド(C) - 実行(X) Ctrl-M</a></h3>\r
+<p>コマンドを実行します。入出力欄でエンターキーを押したのと同じ効果があります。</p>\r
+<h3 id="コマンドc---中断b-ctrl-pausebreak"><a href="#TOC">コマンド(C) - 中断(B) Ctrl-Pause(Break)</a></h3>\r
+<p>コマンドを中断します。サーバ側の処理はキャンセルされません。</p>\r
+<h3 id="コマンドc---前のコマンド履歴p-ctrl-"><a href="#TOC">コマンド(C) - 前のコマンド履歴(P) Ctrl-↑</a></h3>\r
+<p>コマンド履歴を1つ遡ります。</p>\r
+<h3 id="コマンドc---次のコマンド履歴n-ctrl-"><a href="#TOC">コマンド(C) - 次のコマンド履歴(N) Ctrl-↓</a></h3>\r
+<p>コマンド履歴を1つ進みます。</p>\r
+<h3 id="コマンドc---ロールバックr"><a href="#TOC">コマンド(C) - ロールバック(R)</a></h3>\r
+<p>確認ダイアログを表示し、OKを押すとロールバックを実行します。</p>\r
+<h3 id="コマンドc---コミットm"><a href="#TOC">コマンド(C) - コミット(M)</a></h3>\r
+<p>確認ダイアログを表示し、OKを押すとコミットを実行します。</p>\r
+<h3 id="コマンドc---接続c-ctrl-e"><a href="#TOC">コマンド(C) - 接続(C) Ctrl-E</a></h3>\r
+<p>接続リストを表示します。 接続リストで接続したい接続IDを選択して&quot;了解&quot;を押すと、connectと同じ処理が実行されます。</p>\r
+<h3 id="コマンドc---切断d-ctrl-d"><a href="#TOC">コマンド(C) - 切断(D) Ctrl-D</a></h3>\r
+<p>接続を切断します。disconnectコマンドと同じです。</p>\r
+<h3 id="コマンドc---終了処理0"><a href="#TOC">コマンド(C) - 終了処理(0)</a></h3>\r
+<p>コマンドが終了した時、そのウィンドウが非アクティブの場合に 通知のために発生させるアクションを設定します。</p>\r
+<ul>\r
+<li>なし(N) : 何もしません。</li>\r
+<li>フォーカス(F) : ウィンドウをフォーカス状態にします。 ただし、同じプロセス内のほかのウィンドウがアクティブの場合に限ります。 他のプロセスがアクティブの場合はフォーカスされません。</li>\r
+<li>振動(S) : ウィンドウを振動させます。</li>\r
+<li>点滅(B) : ウィンドウ内を点滅(緑色)させます。</li>\r
+</ul>\r
+<h3 id="コマンドc---暗号鍵の入力k"><a href="#TOC">コマンド(C) - 暗号鍵の入力(K)</a></h3>\r
+<p>起動中のプロセスで接続設定のパスワード暗号化に使用される秘密鍵を入力します。</p>\r
+<h3 id="コマンドc---接続設定e"><a href="#TOC">コマンド(C) - 接続設定(E)</a></h3>\r
+<p>接続設定ダイアログを表示します。</p>\r
+<h3 id="データd---並び替えs-alt-s"><a href="#TOC">データ(D) - 並び替え(S) Alt-S</a></h3>\r
+<p>検索結果をファイルに出力します。 検索結果が表示されている場合のみ有効です。</p>\r
+<h3 id="データd---インポートi"><a href="#TOC">データ(D) - インポート(I)</a></h3>\r
+<p>検索結果のテーブルにファイルをインポートします。 importコマンドとは独立した処理です。検索結果が表示されている場合のみ有効です。</p>\r
+<h3 id="データd---エクスポートe-ctrl-shift-s"><a href="#TOC">データ(D) - エクスポート(E) Ctrl-Shift-S</a></h3>\r
+<p>検索結果をファイルに出力します。 exportコマンドとは独立した処理です。検索結果が表示されている場合のみ有効です。</p>\r
+<h3 id="ヘルプh---ヘルプを表示h"><a href="#TOC">ヘルプ(H) - ヘルプを表示(H)</a></h3>\r
+<p>ヘルプ(このファイル)をデフォルトブラウザで表示します。</p>\r
+<p>環境によっては表示できない場合があります。</p>\r
+<h3 id="ヘルプh---stew-についてa"><a href="#TOC">ヘルプ(H) - Stew について(A)</a></h3>\r
+<p>バージョン情報ダイアログを表示します。</p>\r
+<h2 id="guiモードその他"><a href="#TOC">GUIモード・その他</a></h2>\r
+<h3 id="全体"><a href="#TOC">全体</a></h3>\r
+<p>テキスト入力欄は共通して、 「元に戻す」「やり直す」「切り取り」「コピー」「貼り付け」「すべて選択」の ショートカットが利用できます。</p>\r
+<p>これらは、コンテキストメニューからも操作できます。</p>\r
+<h3 id="結果テーブル"><a href="#TOC">結果テーブル</a></h3>\r
+<p>検索コマンドを実行したときに、結果を表示します。 最左列は行番号が表示されます。列幅自動調整が設定されていれば、列幅が自動的に調整されます。</p>\r
+<p>セルを編集すると、テーブルに反映(UPDATE)されます。 行番号が&quot;+&quot;になっている行は「非リンク行」です。 非リンク行については、コンテキストメニューを参照。</p>\r
+<h3 id="入出力欄"><a href="#TOC">入出力欄</a></h3>\r
+<p>コマンドの入力とメッセージの出力が同居する、コマンドラインのようなインターフェイスです。</p>\r
+<p>エンターキーを押すと、プロンプトから末尾までがコマンドとして解釈され実行されます。 カーソルが末尾にない場合は、カーソルが末尾に移動します。 プロンプトより前の部分は編集不可となっています。</p>\r
+<p>デスクトップからファイルをドロップすると、 「パスの貼り付け」「内容の貼り付け」のいずれかが実行できます。</p>\r
+<h3 id="ステータスバー"><a href="#TOC">ステータスバー</a></h3>\r
+<p>直前のコマンドとその実行時間が表示されます。 実行時間は、timeコマンドとは異なり、コマンドの開始から終了までの所要時間となります。</p>\r
+<h3 id="コンテキストメニュー"><a href="#TOC">コンテキストメニュー</a></h3>\r
+<p>マウスの右クリックにより、そのコントロールに即した機能のメニューが表示されます。 表示されるメニュー内容は、入力部分ごとに異なります。</p>\r
+<p>メニューは以下のとおりです。</p>\r
+<dl>\r
+<dt>この列を並べ替え</dt>\r
+<dd><p>右クリックした列(選択されている列では無く)をソートします。 同じ列を続けてソートすると、逆順でソートします。</p>\r
+</dd>\r
+<dt>コピー</dt>\r
+<dd><p>選択しているセルを、タブ区切りテキスト形式でクリップボードに送ります。 タブ文字や改行文字をエスケープしません。</p>\r
+</dd>\r
+<dt>エスケープ付でコピー</dt>\r
+<dd><p>選択しているセルを、エスケープ付のタブ区切りテキスト形式でクリップボードに送ります。 値に改行文字が含まれている場合でも、直接スプレッドシートなどに貼り付けることができます。</p>\r
+</dd>\r
+<dt>貼り付け</dt>\r
+<dd><p>選択したセルに、クリップボードのデータを貼り付けます(UPDATE)。 選択範囲外のデータは無視されます。 データより選択範囲が大きい場合でも、貼り付けは繰返されません。</p>\r
+</dd>\r
+<dt>すべて選択</dt>\r
+<dd><p>すべてのセルを選択状態にします。</p>\r
+</dd>\r
+<dt>セルの値をクリア</dt>\r
+<dd><p>データ型にかかわらず、選択したセルにNullを設定します(UPDATE)。</p>\r
+</dd>\r
+<dt>現在時刻の貼り付け</dt>\r
+<dd><p>選択したセルに現在時刻を設定します(UPDATE)。</p>\r
+</dd>\r
+<dt>列名をコピー</dt>\r
+<dd><p>現在表示しているヘッダの値を、タブ区切りテキスト形式でクリップボードに送ります。</p>\r
+</dd>\r
+<dt>列名を検索</dt>\r
+<dd><p>現在表示しているヘッダの文字列から列名を検索します。</p>\r
+</dd>\r
+<dt>新しい行を追加</dt>\r
+<dd><p>表の最後に新しい行を追加します。 この時点では、データベースへの反映は行われません(非リンク行)。 値を入力した後で「行をデータベースへリンク」を実行すると、データベースへ反映されます。</p>\r
+</dd>\r
+<dt>クリップボードのデータを追加</dt>\r
+<dd><p>クリップボードのタブ区切りテキストを、データベースに連続して追加します(INSERT)。 エラーの場合は、非リンク行となります。 中断は強制終了以外できませんので、データ量が多い場合はimportコマンドを推奨します。</p>\r
+</dd>\r
+<dt>行を複製</dt>\r
+<dd><p>表の最後に選択した行のコピーを追加します。 この時点では、データベースへの反映は行われません(非リンク行)。 値を入力した後で「行をデータベースへリンク」を実行すると、データベースへ反映されます。</p>\r
+</dd>\r
+<dt>行をデータベースへリンク</dt>\r
+<dd><p>非リンク行をデータベースへ反映します(INSERT)。</p>\r
+</dd>\r
+<dt>行を削除</dt>\r
+<dd><p>選択行を削除するのと同時にデータベース上のテーブルから削除します(DELETE)。 (非リンク行の場合はDELETEは実行されません。) 複数行を選択していても実行できます。</p>\r
+</dd>\r
+</dl>\r
+<p>※COMMIT,ROLLBACKは手動です。</p>\r
+<h3 id="データベース情報ツリー"><a href="#TOC">データベース情報ツリー</a></h3>\r
+<p>データベースのテーブルなどの情報をツリー表示します。</p>\r
+<p>簡易SQL文自動生成機能が利用できます。</p>\r
+<dl>\r
+<dt>コピー</dt>\r
+<dd><p>ノードの文字列表現をコピーします。</p>\r
+</dd>\r
+<dt>単純名をコピー</dt>\r
+<dd><p>ノードを表す単純な名称をコピーします。</p>\r
+</dd>\r
+<dt>完全名をコピー</dt>\r
+<dd><p>カタログ名、スキーマ名を完全修飾した名称をコピーします。</p>\r
+</dd>\r
+<dt>最新状態に更新</dt>\r
+<dd><p>選択したノードの情報を再読み込みして最新状態に更新します。</p>\r
+</dd>\r
+<dt>WHERE句条件部を生成</dt>\r
+<dd><p>選択した列の情報からWHERE句より後の部分を生成します。</p>\r
+</dd>\r
+<dt>SELECT句を生成(WHERE付)</dt>\r
+<dd><p>選択したテーブルと列の情報からSELECT文を生成します。 末尾にWHEREキーワードが付きます。</p>\r
+</dd>\r
+<dt>UPDATE文を生成(WHERE付)</dt>\r
+<dd><p>選択したテーブルと列の情報からUPDATE文を生成します。 末尾にWHEREキーワードが付きます。 テーブルは1つだけ選択できます。</p>\r
+</dd>\r
+<dt>INSERT文を生成</dt>\r
+<dd><p>選択したテーブルと列の情報からINSERT文を生成します。 テーブルは1つだけ選択できます。</p>\r
+</dd>\r
+<dt>この列名の列にジャンプ</dt>\r
+<dd><p>結果セットに同じ列名がある場合はその列にジャンプします。 ダブルクリックでも同じ効果があります。</p>\r
+</dd>\r
+<dt>列番号を表示/非表示</dt>\r
+<dd><p>列ノードの番号表示/非表示を切り替えます。</p>\r
+</dd>\r
+</dl>\r
+<p>接続したときに自動で指定したノードを展開する機能があります(実験的機能)。</p>\r
+<p>システムディレクトリに&quot;autoexpansion.tsv&quot;という名前でファイルを作成します。 内容は、展開したいノードのリストを1行毎にTSV形式で記載します。 実際にツリーに表示されるノード文字列をTABでつないだものになります。</p>\r
+<pre><code># 例: コネクタ&quot;H2 DB1&quot;(IDでなく接続名)のCATALOG1.PUBLIC.TABLE1を展開する\r
+H2 DB1(TAB)CATALOG1(TAB)PUBLIC(TAB)TABLE(TAB)TABLE1</code></pre>\r
+<h3 id="設定保存"><a href="#TOC">設定保存</a></h3>\r
+<p>以下の設定が、ウィンドウを閉じたとき(終了したときではない)に保存され、 次回以降のウィンドウの設定として使用されます。</p>\r
+<ul>\r
+<li>ウィンドウ位置</li>\r
+<li>ウィンドウサイズ</li>\r
+<li>分割バー位置</li>\r
+<li>ステータスバー(表示/非表示)</li>\r
+<li>列番号(表示/非表示)</li>\r
+<li>情報ツリーペイン(表示/非表示)</li>\r
+<li>常に手前に表示(する/しない)</li>\r
+<li>列幅自動調整モード</li>\r
+</ul>\r
+<h3 id="メニューのキー割り当て"><a href="#TOC">メニューのキー割り当て</a></h3>\r
+<p>システムディレクトリに&quot;keybind.conf&quot;という名前のファイルを作成し、 内容には&quot;メニューのアクション名=KeyStrokeの文字列表現&quot;を設定すると、 メニューのキー割り当てが変更できます。</p>\r
+<p>メニュー以外(コンテキストメニューなど)には適用できません。</p>\r
+<pre><code># 例\r
+# アクション名はsrc/net/argius/stew/ui/window/messages.u8pのMenu.#を参照\r
+lastHistory = ctrl K</code></pre>\r
+<h2 id="プロパティ"><a href="#TOC">プロパティ</a></h2>\r
+<p>起動時に値を決定できるパラメータです。</p>\r
+<p>Javaのシステムプロパティ(Javaの-Dオプション)で指定するか、stew.propertiesファイルに記述します。 &quot;設定値&quot;は、設定する値についての説明です。設定値のカッコ内の値は既定値です。 カッコがないものは既定値がありません。</p>\r
+<h3 id="net.argius.stew.properties---プロパティファイルの場所"><a href="#TOC">net.argius.stew.properties - プロパティファイルの場所</a></h3>\r
+<p>設定値:ファイルまたはディレクトリのパス</p>\r
+<p>stew.propertiesファイルを優先的に検索する場所を指定します。</p>\r
+<p>指定されたパスがファイルの場合は、そのファイルをstew.propertiesの代わりに プロパティファイルとして読み込みます。</p>\r
+<p>指定されたパスがディレクトリの場合は、そこにstew.propertiesがあるものとみなされます。</p>\r
+<p>指定されない場合は、クラスパス、システムディレクトリの順に検索されます。 無い場合はプロパティファイルが無いものとみなされます。</p>\r
+<h3 id="net.argius.stew.directory---作業ディレクトリ"><a href="#TOC">net.argius.stew.directory - 作業ディレクトリ</a></h3>\r
+<p>設定値:ディレクトリパス(カレントディレクトリ)</p>\r
+<p>コマンドで使用するディレクトリの開始時のパスを指定します。 デフォルトシステムディレクトリとは異なります。</p>\r
+<h3 id="net.argius.stew.query.timeout---クエリのタイムアウト値"><a href="#TOC">net.argius.stew.query.timeout - クエリのタイムアウト値</a></h3>\r
+<p>設定値:整数秒(0)</p>\r
+<p>コマンド内のクエリが発行時に設定するタイムアウト値を指定します。 0以下はタイムアウト未指定とみなされます。</p>\r
+<p>(実装詳細:java.sql.Statement#setQueryTimeoutに設定する値。)</p>\r
+<h3 id="net.argius.stew.rowcount.limit---出力件数の上限"><a href="#TOC">net.argius.stew.rowcount.limit - 出力件数の上限</a></h3>\r
+<p>設定値:上限値(Max=Integer.MAX_VALUE≒2,147,000,000)</p>\r
+<p>検索結果を出力する上限件数を設定します。exportなどには適用されません。</p>\r
+<h3 id="net.argius.stew.command.import.batch.limit---importのバッチ数の上限値"><a href="#TOC">net.argius.stew.command.Import.batch.limit - Importのバッチ数の上限値</a></h3>\r
+<p>設定値:上限値(10000)</p>\r
+<p>Importコマンドで一度に実行する件数の上限値を決定します。</p>\r
+<h3 id="net.argius.stew.ui.window.resident---windowの常駐"><a href="#TOC">net.argius.stew.ui.window.resident - Windowの常駐</a></h3>\r
+<p>設定値:整数分</p>\r
+<p><strong>実験的な機能です。</strong></p>\r
+<p>指定した分ごとに画面の再描画を行い、アプリケーションがスワップアウトしないようにします。</p>\r
+<h3 id="loggerの設定"><a href="#TOC">Loggerの設定</a></h3>\r
+<p>Stew4は標準のLoggingAPIを(ラップして)使用しています。</p>\r
+<p>ログ出力を有効にする場合は、システムプロパティ&quot;java.util.logging.config.file&quot;を設定します。</p>\r
+<p>以下は起動オプションの例。</p>\r
+<pre><code>-Djava.util.logging.config.file=logging.properties</code></pre>\r
+<hr>\r
+</body>\r
+</html>\r
diff --git a/MANUAL_ja.md b/MANUAL_ja.md
new file mode 100644 (file)
index 0000000..291892c
--- /dev/null
@@ -0,0 +1,962 @@
+% Stew マニュアル
+%
+% version 4.0
+
+
+## Stewとは何ですか?
+
+Stewは、JDBCを使った小規模なデータベースフロントエンドです。
+コマンドラインのようなインターフェイスを持っていて、SQLを入力して実行したりできます。
+ちょっとした処理であれば、バッチのように使用することもできます。
+
+概要については、README_ja.mdをご覧ください。
+
+
+
+## 使用上の注意
+
+
+### パスワードの保存方式
+
+パスワードは、デフォルトではそのまま保存します。
+生のパスワードを保存したくない場合は、暗号化を利用できます。
+
+詳しくは、[使い方-接続設定](#接続設定)を参照してください。
+
+
+### コネクション切断時はrollbackしない
+
+デフォルトでは、disconnectコマンドによりコネクションを切断するとき、rollbackを発行しません。
+DBMSによっては、トランザクションが自動的にコミットされてしまうことがありますので注意が必要です。
+
+「切断時に自動ロールバック」を設定すると、disconnectの際に自動的にrollbackを発行します。
+詳細は、[使い方-接続設定](#接続設定)を参照してください。
+
+
+### その他
+
+プロジェクトサイトにて追加説明を行っていますので、あわせてご利用ください。
+
+<http://stew.sourceforge.jp/>  
+<http://argius.net/wiki/index.php?Stew%20tutorial> (argius.net)
+
+
+
+## インストール
+
+Java実行環境バージョン6(JRE6)以上がインストールされている必要があります。
+また、利用するデータベースのJDBCドライバが必要です。
+
+リリースパッケージ(通常はzipファイル)を任意のディレクトリに展開します。
+
+インストールサイズを最小限にしたい場合は、
+リリースパッケージに含まれている"stew.jar"だけを展開してください。
+
+起動コマンドの設定は、次の起動方法を参照してください。
+
+
+
+## 起動方法
+
+GUIモードで起動する場合は、以下のコマンドを実行します。
+
+    > java -jar stew.jar --gui
+
+CUIモードで起動する場合は、以下のコマンドを実行します。
+
+    > java -jar stew.jar --cui
+
+
+起動スクリプトもしくはショートカットやエイリアスを作成しておくと便利です。
+UNIX系OSの場合は"stew.sh"を、Windowsの場合は"stew.bat"を参照してください。
+
+Stewを実行すると、システムディレクトリ".stew"が作成され、設定の保存に使用されます。
+".stew"ディレクトリは、デフォルトではカレントディレクトリに作成されます。
+
+
+
+## アンインストール
+
+インストールしたファイルと".stew"ディレクトリを削除してください。
+
+
+----------------------------------------------------------------------------------------------------
+
+## 使い方
+
+Stewを使用するには、JDBC接続が可能なデータベースと、JDBCドライバが必要です。
+JDBCドライバ自体の詳細については、各データベースの説明書などを参照してください。
+
+
+### 接続設定
+
+CUIの場合は、起動時に--editオプションを指定、またはコマンドとして--editを実行すると、
+編集プログラムが起動します。
+
+GUIの場合は、メニューの"接続設定"を実行すると、編集ダイアログが開きます。
+
+
+それぞれの設定項目の説明は次のとおりです。
+
+コネクタID
+:   connectコマンドなどに渡すIDです。英数字のみ指定できます。
+
+コネクタ名
+:   プロンプトに表示される接続名です。文字制限は特にありません。
+
+クラスパス
+:   JDBCドライバのクラスパスを指定します。Javaの-CLASSPATHオプションと同じ形式で指定します。
+
+ドライバ
+:   JDBCドライバのDriver実装クラスを指定します。GUIの場合、クラスパスが指定されていれば、
+    "ドライバの検索"から選択できます。
+
+接続先URL
+:   JDBCのURL(DriverManager.getConnection(url)に指定するurlと同じもの)を指定します。
+
+ユーザ
+:   JDBCのURLと同時に指定するユーザIDを指定します。
+
+パスワード
+:   ユーザIDのパスワードを指定します。
+
+暗号化処理
+:   パスワードの保存方法を選択します。次項"パスワードの保存について"を参照してください。
+
+読取専用
+:   コネクションをREADONLYに設定し、更新の有るコマンドを実行できないようにします。
+    (コマンド側のReadOnlyに拠る。)
+
+自動ロールバック
+:   設定すると、切断時に自動でロールバックします。
+
+これらの情報は、".stew/connectors.properties"に保存されます。
+手動で編集することもできますが、その際は注意して行ってください。
+
+
+### パスワードの保存について
+
+パスワードは設定ファイルに保存されるため、そのまま保存すると都合が悪い場合があります。
+
+パスワードの保存方法は、暗号化処理を選択することができます。
+
+PlainTextPassword
+:   (デフォルト)パスワードをそのまま保存します。
+
+PbePassword
+:   PBE暗号を使用してパスワードを保存します。
+    [メニュー-暗号鍵の入力](#コマンドc---暗号鍵の入力k)で暗号鍵を入力した後、
+    接続設定でパスワードを入力して保存すると、暗号化された状態で保存します。
+    次回起動時、再度暗号鍵を入力するまで、パスワードが復号できなくなります。
+
+独自のパスワード暗号化を追加することもできます。
+(実装詳細:Passwordインタフェースの実装クラスを追加します。)
+
+
+### 無名接続
+
+接続設定なしで接続を行うことができます。
+ドライバが特定できるようになっている必要があります。(次の段落で説明)
+
+    > connect <user>/<password>@<URL>
+    
+    (例)
+    > connect user/password1@jdbc:firebirdsql://127.0.0.1//home/argius/test.fdb
+
+最も簡単な方法は、ドライバファイルを起動ディレクトリに置くだけです。
+ドライバ名が指定されていない場合は、このファイルの中を探索してドライバ名を特定します。
+但し、この探索は時間がかかるので、時間を節約するには"jdbc.drivers"システムプロパティを
+指定してください。
+
+
+### 対話モード
+
+通常の起動では、対話モードでの操作となります。
+対話モードでは、コマンド入力待ち状態になった時にコマンドを入力し、
+処理が終わると再びコマンド入力待ちになります。
+
+
+### 一行完結モード(One-Liner)
+
+Stewの処理が一行の入力で完結します。
+"stew"コマンドを設定していると仮定すると、次のような形式で実行します。
+
+    > stew <connector-id> <command>
+
+実行すると、<connector-id>の接続設定で接続を開始し、その接続を使用してコマンドを実行します。
+コマンド実行後、接続を切断して終了します。
+
+このモードでは、ご利用のshell環境の制約(例えば、ワイルドカード,リダイレクト など)を
+受けますのでご注意ください。
+
+
+### エイリアス(alias)
+
+長いコマンドに別名(alias)をつけて、短縮コマンドとして利用できます。
+詳しくは、コマンド[alias](#alias - エイリアス(コマンド別名)の登録 (組み込みコマンド)),
+[unalias](#unalias - エイリアス(コマンド別名)の解除 (組み込みコマンド))を参照してください。
+
+
+----------------------------------------------------------------------------------------------------
+
+## コマンド(Stew内部コマンド)
+
+組み込みコマンドは、Stewから切り離すことができないコマンドです。
+それ以外は、追加コマンドとして実装されています。
+(実装詳細:追加コマンドはCommandクラスのサブクラスによる実装ですが、
+  組み込みコマンドはCommand.invokeメソッドに直接書かれている処理です。)
+
+コマンドでない文が指定された場合は、SQLとして実行されます。
+バインド変数のあるSQL文を使う場合は、最後に";"(セミコロン)をつけ、
+それ以降にカンマ区切りでパラメータを指定できます。
+この機能は、一部のコマンドでも利用できます。
+
+組み込みコマンドのうち、connect/-e/-f/alias/unalias/exitは接続時以外でも使用できます。
+
+
+### connect - データベースに接続する (組み込みコマンド)
+
+    > connect <connector-id>
+    > -c <connector-id>
+
+予め用意した接続設定を使用して、データベースに接続します。
+
+対話モードでは、disconnectまたは強制的に切断されるまで、接続が維持されます。
+すでに接続中の場合は、その接続を切断してから接続を行います。
+
+
+### disconnect - データベースとの接続を切断する (組み込みコマンド)
+
+    > disconnect
+    > -d
+
+データベースとの接続を切断します。
+
+接続設定でrollbackを指定している場合は、rollbackを試みます。
+
+
+### commit - トランザクションのコミット (組み込みコマンド)
+
+現在のトランザクションでの変更をコミットします。
+
+**コミットする際は、誤って必要なデータを削除しないよう注意してください。**
+
+
+### rollback - トランザクションのロールバック (組み込みコマンド)
+
+現在のトランザクションでの変更をロールバックします。
+
+
+### -e - 複数コマンドの評価 (組み込みコマンド)
+
+    > -e <コマンド> -e <コマンド> ...
+
+コマンドを連続して実行させます。
+exportなどを同時に実行する場合や、コマンドラインからの実行の場合に使います。
+
+
+### -f - ファイル内容をコマンドとして実行 (組み込みコマンド)
+
+    > -f <ファイル>
+
+ファイルの内容をコマンドとして実行します。
+再帰的に指定できますが、無限ループは検知できないので注意してください。
+
+
+### -s - ファイル内容をスクリプトとして実行 (組み込みコマンド)
+
+    > -s <ファイル>
+
+ファイルの内容をスクリプト(JavaScript)として実行します。
+
+スクリプト内では、以下の変数が定義済みになります。
+
+ * 接続中のコネクション: connection, conn
+ * パラメーター: parameter, p
+ * 出力制御: outputProcessor, op
+
+
+### cd - カレントディレクトリの移動 (組み込みコマンド)
+
+    > cd <ディレクトリ>
+
+カレントディレクトリを指定したディレクトリに移動します。
+(このカレントディレクトリはStew内部で管理するものです。)
+
+
+### @ - 場所の表示 (組み込みコマンド)
+
+    > @
+
+カレントディレクトリとシステムディレクトリの場所を表示します。
+
+
+### alias - エイリアス(コマンド別名)の登録 (組み込みコマンド)
+
+    > alias [<エイリアス> [<コマンド>]]
+
+コマンド別名を登録します。
+引数が1つで実行した場合は、既に設定されたコマンド内容を表示します。
+引数なしで実行した場合は、すべての定義済みエイリアスを表示します。
+
+登録または表示の前にファイルからメモリ上の情報を最新化します。
+ファイルを直接修正したり、他のスレッドで変更した内容を反映させたい場合は、
+このコマンドを実行してください。
+(実装詳細:循環参照などの無限ループ抑制のために、展開の深さは最大100。)
+
+    > alias
+    エイリアスは未定義です。
+    > alias search select # from
+    > alias count select count(#) from
+    > alias search
+    alias search=[select # from]
+    > search table1
+    >> select # from table1
+    (select # from table1 の結果)
+    >
+
+
+### unalias - エイリアス(コマンド別名)の解除 (組み込みコマンド)
+
+    > unalias <エイリアス>
+
+コマンド別名を解除します。解除する対象がない場合は何もしません。
+解除対象の有無にかかわらず、ファイルからメモリ上の情報を最新化します。
+
+
+### exit - 終了 (組み込みコマンド)
+
+    > exit
+
+Stewを終了します。確認待ちは行いません。
+
+接続中の全てのコネクションは自動的に切断されます。
+ロールバックは、自動ロールバックを設定しているコネクタのみ行われます。
+自動ロールバックについては、[使い方-接続設定](#接続設定)を参照してください。
+
+
+### load - ファイルから実行
+
+    > load [<SQLファイル> | <データファイル> <テーブル名> [ HEADER ]]
+
+指定されたファイルを読み込んで、SQLを実行します。
+
+パラメータが1個の場合は、ファイルをSQL文と見なして実行します。
+
+パラメータが2個以上の場合は、ファイルをデータファイルと見なして、インポートを実行します。
+ファイルの拡張子によって、ファイル形式が自動的に選択されます。
+(実装詳細:基本的にはimportと同じ動作ですが、バッチ実行ではなく1件ずつ処理されます。)
+
+ *. .csv : CSV形式
+ *. .xml : XML形式(定義:src/net/argius/stew/io/stew-table.dtd)
+ *. 上記以外 : TAB区切りテキスト形式
+
+
+### import - ファイルのインポート
+
+    > import [<データファイル> <テーブル名> [ HEADER ]]
+
+ファイルをデータファイルと見なして、インポートを実行します。
+ファイルの拡張子によって、ファイル形式が自動的に選択されます。
+(実装詳細:基本的には、loadのパラメータ2個以上指定した時と同じ動作ですが、
+  Statement.addBatch()を使用します。)
+
+ * .csv : CSV形式
+ * .xml : XML形式(定義:src/net/argius/stew/io/stew-table.dtd)
+ * 上記以外 : TAB区切りテキスト形式
+
+一括で処理する件数を[#property.Import.batch.limit:プロパティ]で設定することができます。
+
+
+### export - 検索結果のエクスポート
+
+    > export <ファイル> [ HEADER ] [command(select|find|report)]
+
+指定したファイルに、コマンドの検索結果を出力します。
+ファイルの拡張子によって、ファイル形式が自動的に選択されます。
+
+ * .htm,.html : HTML形式
+ * .csv : CSV形式
+ * .xml : XML形式(定義:src/net/argius/stew/io/stew-table.dtd)
+ * 上記以外 : TAB区切りテキスト形式
+
+
+### time - 実行時間計測
+
+    > time [<回数>] <SQL文>
+
+指定したSQL文を実行し、その実行時間を計測して表示します。
+
+回数が指定された場合は、回数分SQLを繰り返し実行して、
+「合計」「平均」「最大」「最小」を集計します。
+回数を指定しない場合は、1回の実行時間を表示します。
+
+    > time select # from EMPLOYEE
+    実行時間 : 0.093 秒
+    > time 100 select # from EMPLOYEE
+    合計 : 0.484 秒
+    平均 : 0.005 秒
+    最大 : 0.094 秒
+    最小 : 0.000 秒
+    >
+
+
+### find - テーブル名検索
+
+    > find <テーブル名パターン> [<テーブル種別パターン> [<スキーマ名パターン> [<カタログ名パターン> [ FULL ]]]]
+
+参照可能なテーブルの一覧を表示します。
+"パターン"というキーワードを含むパラメータは、ワイルドカード(#,?)が指定できます。
+
+
+### report - データベース情報表示
+
+    > report - | <テーブル名> [ PK | INDEX ]
+
+接続中のコネクションに関する情報を表示します。
+
+-(ハイフン)が指定された場合は、DBとJDBCドライバの名称とバージョン、
+接続ユーザとアドレスを表示します。
+
+テーブル名のみが指定された場合は、テーブルの列情報が表示されます。
+
+テーブル名とオプションが指定された場合は、PKを指定するとプライマリキーの一覧が、
+INDEXを指定するとインデックスキーの一覧が、それぞれ表示されます。
+
+
+### download - 1データごとにダウンロード
+
+    > download <ルートディレクトリ> SELECT <ダウンロードするデータの列> [, ファイルパス...] FROM ...
+
+1つの列のデータをダウンロードしてファイルに保存します。
+
+どのデータ型でも利用できます。
+長めのテキスト項目やラージオブジェクト(BLOB,CLOB)を一挙にファイルに保存する場合などに有用です。
+
+ファイルは、複数ファイルをダウンロードできるようにするため、
+データからファイル名を生成できるようになっています。
+
+ファイル名は、2列目以降の列を文字列として結合したものとなります。
+これはプライマリキーと拡張子を指定することを想定しています。
+
+    > download emp select FULL_NAME, JOB_COUNTRY, '/', EMP_NO, '.txt' from EMPLOYEE
+    ディレクトリ[./emp/USA]を作成しました。
+    ダウンロードされました。 (0.014 Kbytes, file=[./emp/USA/2.txt])
+    ダウンロードされました。 (0.012 Kbytes, file=[./emp/USA/4.txt])
+     ・
+     ・
+     ・
+    ダウンロードされました。 (0.012 Kbytes, file=[./emp/USA/24.txt])
+    ディレクトリ[./emp/England]を作成しました。
+    ダウンロードされました。 (0.011 Kbytes, file=[./emp/England/28.txt])
+     ・
+     ・
+     ・
+    ダウンロードされました。 (0.018 Kbytes, file=[./emp/USA/145.txt])
+    42 件 ヒットしました。
+
+データが1件の場合は、データ列のみ指定すれば、
+ファイルパスの前半部分がそのままファイル名となります。
+
+同名のファイルが存在したり、ディレクトリ作成時に権限が無い場合はエラーとなり、
+その時点で処理を中断します。
+
+
+### upload - ファイル内容を1データとして登録
+
+    > upload <ファイルパス> <INSERT文 or UPDATE文>
+
+SQLのプレースホルダで指定された列にファイル内容を登録します。
+
+
+### wait - 待機
+
+    > wait 秒(小数第3位まで指定可)
+
+指定された秒数、待機します。
+
+連続したコマンドを実行する場合などに使えるかもしれません。
+
+
+
+## GUIモード・メニュー
+
+GUIモードのメニューについての説明です。
+
+
+### ファイル(F) - 新しいウィンドウ(N) Ctrl-N
+
+新しいウィンドウを開きます。
+
+新しいウィンドウは、元のウィンドウとは独立した接続で処理が行われます。
+
+
+### ファイル(F) - 閉じる(C) Ctrl-W
+
+ウィンドウを閉じます。
+コネクションが接続中の場合は、確認ダイアログを表示します。
+
+開いているウィンドウが1つの場合は、終了(X)と同じ動作となります。
+
+
+### ファイル(F) - 終了(X) Ctrl-Q
+
+アプリケーションを終了します。
+確認ダイアログが表示され、"はい"を選択すると終了します。
+
+
+### 編集(E) - 切り取り(T) Ctrl-X
+
+選択範囲をクリップボードにコピーして、選択された部分を削除します。
+(基本的に、一般的なアプリケーションと同じ処理です。)
+
+
+### 編集(E) - コピー(C) Ctrl-C
+
+選択範囲をクリップボードにコピーします。
+(基本的に、一般的なアプリケーションと同じ処理です。)
+
+
+### 編集(E) - 貼り付け(P) Ctrl-V
+
+カーソル位置にクリップボードの内容を貼り付けます。
+(基本的に、一般的なアプリケーションと同じ処理です。)
+
+
+### 編集(E) - すべて選択(A) Ctrl-A
+
+選択している領域(テーブルまたは入出力欄)を全選択状態にします。
+
+
+### 編集(E) - 検索(F) Ctrl-F
+
+選択している領域(テーブル、テーブル列名、入出力欄、情報ツリー)内の文字列を検索します。
+
+テーブル、テーブル列名はセル単位で検索します。
+入出力欄は、一致部分をハイライトします。
+情報ツリーは、選択しているノードと同じ深さのノードとそれらのサブノード単位で検索します。
+
+オプションで、「正規表現を使用する」「大文字と小文字を区別しない」が指定できます。
+
+
+### 編集(E) - フォーカス切替(G) Ctrl-G
+
+検索結果エリアと入力欄のフォーカスを入れ替えます(toggle)。
+
+
+### 編集(E) - メッセージのクリア
+
+入出力欄のメッセージをクリアします。
+
+
+### 表示(V) - ステータス バー(B)
+
+選択された場合、ウィンドウの下部にステータスバーを表示します。
+
+設定は保存されます。
+
+
+### 表示(V) - 列番号を表示(C)
+
+選択された場合、検索結果の列名に番号を付けます。
+
+
+### 表示(V) - 情報ツリーペインを表示(I)
+
+選択された場合、データベース情報ツリーペインを表示します。
+
+
+### 表示(V) - 常に手前に表示(T)
+
+選択された場合、ウィンドウを常に手前に表示するようにします。
+
+
+### 表示(V) - 最新状態に更新(R) F5
+
+検索結果を表示した際のクエリを再発行して、最新状態を表示します。
+
+
+### 表示(V) - 列幅を拡大(W) Ctrl-.(period)
+
+検索結果の列幅をそれぞれ1.5倍に拡大します。
+
+
+### 表示(V) - 列幅を縮小(N) Ctrl-,(comma)
+
+検索結果の列幅をそれぞれ2/3に縮小します。
+
+
+### 表示(V) - 列幅を調整(A) Ctrl-/(slash)
+
+検索結果の列幅を自動調整します。次項の「列幅自動調整」で選択されたモードで調整されます。
+
+
+### 表示(V) - 列幅自動調整(M)
+
+検索結果表示時に、検索結果の列幅を自動調整するモードを選択します。
+
+ * なし(N) : 自動調整を行いません。
+ * ヘッダ基準(H) : ヘッダ名のサイズを基準に、各列幅を自動調整します。
+ * 値基準(V) : 列の中で最も長い値のサイズを基準に、各列幅を自動調整します。
+ * ヘッダと値基準(A) : ヘッダを含めて、列の中で最も長い値のサイズを基準に、各列幅を自動調整します。
+
+
+### コマンド(C) - 実行(X) Ctrl-M
+
+コマンドを実行します。入出力欄でエンターキーを押したのと同じ効果があります。
+
+
+### コマンド(C) - 中断(B) Ctrl-Pause(Break)
+
+コマンドを中断します。サーバ側の処理はキャンセルされません。
+
+
+### コマンド(C) - 前のコマンド履歴(P) Ctrl-↑
+
+コマンド履歴を1つ遡ります。
+
+
+### コマンド(C) - 次のコマンド履歴(N) Ctrl-↓
+
+コマンド履歴を1つ進みます。
+
+
+### コマンド(C) - ロールバック(R)
+
+確認ダイアログを表示し、OKを押すとロールバックを実行します。
+
+
+### コマンド(C) - コミット(M)
+
+確認ダイアログを表示し、OKを押すとコミットを実行します。
+
+
+### コマンド(C) - 接続(C) Ctrl-E
+
+接続リストを表示します。
+接続リストで接続したい接続IDを選択して"了解"を押すと、connectと同じ処理が実行されます。
+
+
+### コマンド(C) - 切断(D) Ctrl-D
+
+接続を切断します。disconnectコマンドと同じです。
+
+
+### コマンド(C) - 終了処理(0)
+
+コマンドが終了した時、そのウィンドウが非アクティブの場合に
+通知のために発生させるアクションを設定します。
+
+ * なし(N) : 何もしません。
+ * フォーカス(F) : ウィンドウをフォーカス状態にします。
+   ただし、同じプロセス内のほかのウィンドウがアクティブの場合に限ります。
+   他のプロセスがアクティブの場合はフォーカスされません。
+ * 振動(S) : ウィンドウを振動させます。
+ * 点滅(B) : ウィンドウ内を点滅(緑色)させます。
+
+
+### コマンド(C) - 暗号鍵の入力(K)
+
+起動中のプロセスで接続設定のパスワード暗号化に使用される秘密鍵を入力します。
+
+
+### コマンド(C) - 接続設定(E)
+
+接続設定ダイアログを表示します。
+
+
+### データ(D) - 並び替え(S) Alt-S
+
+検索結果をファイルに出力します。
+検索結果が表示されている場合のみ有効です。
+
+
+### データ(D) - インポート(I)
+
+検索結果のテーブルにファイルをインポートします。
+importコマンドとは独立した処理です。検索結果が表示されている場合のみ有効です。
+
+
+### データ(D) - エクスポート(E) Ctrl-Shift-S
+
+検索結果をファイルに出力します。
+exportコマンドとは独立した処理です。検索結果が表示されている場合のみ有効です。
+
+
+### ヘルプ(H) - ヘルプを表示(H)
+
+ヘルプ(このファイル)をデフォルトブラウザで表示します。
+
+環境によっては表示できない場合があります。
+
+
+### ヘルプ(H) - Stew について(A)
+
+バージョン情報ダイアログを表示します。
+
+
+
+## GUIモード・その他
+
+
+### 全体
+
+テキスト入力欄は共通して、
+「元に戻す」「やり直す」「切り取り」「コピー」「貼り付け」「すべて選択」の
+ショートカットが利用できます。
+
+これらは、コンテキストメニューからも操作できます。
+
+
+### 結果テーブル
+
+検索コマンドを実行したときに、結果を表示します。
+最左列は行番号が表示されます。列幅自動調整が設定されていれば、列幅が自動的に調整されます。
+
+セルを編集すると、テーブルに反映(UPDATE)されます。
+行番号が"+"になっている行は「非リンク行」です。
+非リンク行については、コンテキストメニューを参照。
+
+
+### 入出力欄
+
+コマンドの入力とメッセージの出力が同居する、コマンドラインのようなインターフェイスです。
+
+エンターキーを押すと、プロンプトから末尾までがコマンドとして解釈され実行されます。
+カーソルが末尾にない場合は、カーソルが末尾に移動します。
+プロンプトより前の部分は編集不可となっています。
+
+デスクトップからファイルをドロップすると、
+「パスの貼り付け」「内容の貼り付け」のいずれかが実行できます。
+
+
+### ステータスバー
+
+直前のコマンドとその実行時間が表示されます。
+実行時間は、timeコマンドとは異なり、コマンドの開始から終了までの所要時間となります。
+
+
+### コンテキストメニュー
+
+マウスの右クリックにより、そのコントロールに即した機能のメニューが表示されます。
+表示されるメニュー内容は、入力部分ごとに異なります。
+
+メニューは以下のとおりです。
+
+この列を並べ替え
+:   右クリックした列(選択されている列では無く)をソートします。
+    同じ列を続けてソートすると、逆順でソートします。
+
+コピー
+:   選択しているセルを、タブ区切りテキスト形式でクリップボードに送ります。
+    タブ文字や改行文字をエスケープしません。
+
+エスケープ付でコピー
+:   選択しているセルを、エスケープ付のタブ区切りテキスト形式でクリップボードに送ります。
+    値に改行文字が含まれている場合でも、直接スプレッドシートなどに貼り付けることができます。
+
+貼り付け
+:   選択したセルに、クリップボードのデータを貼り付けます(UPDATE)。
+    選択範囲外のデータは無視されます。
+    データより選択範囲が大きい場合でも、貼り付けは繰返されません。
+
+すべて選択
+:   すべてのセルを選択状態にします。
+
+セルの値をクリア
+:   データ型にかかわらず、選択したセルにNullを設定します(UPDATE)。
+
+現在時刻の貼り付け
+:   選択したセルに現在時刻を設定します(UPDATE)。
+
+列名をコピー
+:   現在表示しているヘッダの値を、タブ区切りテキスト形式でクリップボードに送ります。
+
+列名を検索
+:   現在表示しているヘッダの文字列から列名を検索します。
+
+新しい行を追加
+:   表の最後に新しい行を追加します。
+    この時点では、データベースへの反映は行われません(非リンク行)。
+    値を入力した後で「行をデータベースへリンク」を実行すると、データベースへ反映されます。
+
+クリップボードのデータを追加
+:   クリップボードのタブ区切りテキストを、データベースに連続して追加します(INSERT)。
+    エラーの場合は、非リンク行となります。
+    中断は強制終了以外できませんので、データ量が多い場合はimportコマンドを推奨します。
+
+行を複製
+:   表の最後に選択した行のコピーを追加します。
+    この時点では、データベースへの反映は行われません(非リンク行)。
+    値を入力した後で「行をデータベースへリンク」を実行すると、データベースへ反映されます。
+
+行をデータベースへリンク
+:   非リンク行をデータベースへ反映します(INSERT)。
+
+行を削除
+:   選択行を削除するのと同時にデータベース上のテーブルから削除します(DELETE)。
+    (非リンク行の場合はDELETEは実行されません。)
+    複数行を選択していても実行できます。
+
+※COMMIT,ROLLBACKは手動です。
+
+
+### データベース情報ツリー
+
+データベースのテーブルなどの情報をツリー表示します。
+
+簡易SQL文自動生成機能が利用できます。
+
+
+コピー
+:   ノードの文字列表現をコピーします。
+
+単純名をコピー
+:   ノードを表す単純な名称をコピーします。
+
+完全名をコピー
+:   カタログ名、スキーマ名を完全修飾した名称をコピーします。
+
+最新状態に更新
+:   選択したノードの情報を再読み込みして最新状態に更新します。
+
+WHERE句条件部を生成
+:   選択した列の情報からWHERE句より後の部分を生成します。
+
+SELECT句を生成(WHERE付)
+:   選択したテーブルと列の情報からSELECT文を生成します。
+    末尾にWHEREキーワードが付きます。
+
+UPDATE文を生成(WHERE付)
+:   選択したテーブルと列の情報からUPDATE文を生成します。
+    末尾にWHEREキーワードが付きます。
+    テーブルは1つだけ選択できます。
+
+INSERT文を生成
+:   選択したテーブルと列の情報からINSERT文を生成します。
+    テーブルは1つだけ選択できます。
+
+この列名の列にジャンプ
+:   結果セットに同じ列名がある場合はその列にジャンプします。
+    ダブルクリックでも同じ効果があります。
+
+列番号を表示/非表示
+:   列ノードの番号表示/非表示を切り替えます。 
+
+
+接続したときに自動で指定したノードを展開する機能があります(実験的機能)。
+
+システムディレクトリに"autoexpansion.tsv"という名前でファイルを作成します。
+内容は、展開したいノードのリストを1行毎にTSV形式で記載します。
+実際にツリーに表示されるノード文字列をTABでつないだものになります。
+
+    # 例: コネクタ"H2 DB1"(IDでなく接続名)のCATALOG1.PUBLIC.TABLE1を展開する
+    H2 DB1(TAB)CATALOG1(TAB)PUBLIC(TAB)TABLE(TAB)TABLE1
+
+
+### 設定保存
+
+以下の設定が、ウィンドウを閉じたとき(終了したときではない)に保存され、
+次回以降のウィンドウの設定として使用されます。
+
+ * ウィンドウ位置
+ * ウィンドウサイズ
+ * 分割バー位置
+ * ステータスバー(表示/非表示)
+ * 列番号(表示/非表示)
+ * 情報ツリーペイン(表示/非表示)
+ * 常に手前に表示(する/しない)
+ * 列幅自動調整モード
+
+
+### メニューのキー割り当て
+
+システムディレクトリに"keybind.conf"という名前のファイルを作成し、
+内容には"メニューのアクション名=KeyStrokeの文字列表現"を設定すると、
+メニューのキー割り当てが変更できます。
+
+メニュー以外(コンテキストメニューなど)には適用できません。
+
+    # 例
+    # アクション名はsrc/net/argius/stew/ui/window/messages.u8pのMenu.#を参照
+    lastHistory = ctrl K
+
+
+
+## プロパティ
+
+起動時に値を決定できるパラメータです。
+
+Javaのシステムプロパティ(Javaの-Dオプション)で指定するか、stew.propertiesファイルに記述します。
+"設定値"は、設定する値についての説明です。設定値のカッコ内の値は既定値です。
+カッコがないものは既定値がありません。
+
+
+### net.argius.stew.properties - プロパティファイルの場所
+
+設定値:ファイルまたはディレクトリのパス
+
+stew.propertiesファイルを優先的に検索する場所を指定します。
+
+指定されたパスがファイルの場合は、そのファイルをstew.propertiesの代わりに
+プロパティファイルとして読み込みます。
+
+指定されたパスがディレクトリの場合は、そこにstew.propertiesがあるものとみなされます。
+
+指定されない場合は、クラスパス、システムディレクトリの順に検索されます。
+無い場合はプロパティファイルが無いものとみなされます。
+
+
+### net.argius.stew.directory - 作業ディレクトリ
+
+設定値:ディレクトリパス(カレントディレクトリ)
+
+コマンドで使用するディレクトリの開始時のパスを指定します。
+デフォルトシステムディレクトリとは異なります。
+
+
+### net.argius.stew.query.timeout - クエリのタイムアウト値
+
+設定値:整数秒(0)
+
+コマンド内のクエリが発行時に設定するタイムアウト値を指定します。
+0以下はタイムアウト未指定とみなされます。
+
+(実装詳細:java.sql.Statement#setQueryTimeoutに設定する値。)
+
+
+### net.argius.stew.rowcount.limit - 出力件数の上限
+
+設定値:上限値(Max=Integer.MAX_VALUE≒2,147,000,000)
+
+検索結果を出力する上限件数を設定します。exportなどには適用されません。
+
+
+### net.argius.stew.command.Import.batch.limit - Importのバッチ数の上限値
+
+設定値:上限値(10000)
+
+Importコマンドで一度に実行する件数の上限値を決定します。
+
+
+### net.argius.stew.ui.window.resident - Windowの常駐
+
+設定値:整数分
+
+**実験的な機能です。**
+
+指定した分ごとに画面の再描画を行い、アプリケーションがスワップアウトしないようにします。
+
+
+### Loggerの設定
+
+Stew4は標準のLoggingAPIを(ラップして)使用しています。
+
+ログ出力を有効にする場合は、システムプロパティ"java.util.logging.config.file"を設定します。
+
+以下は起動オプションの例。
+
+    -Djava.util.logging.config.file=logging.properties
+
+
+----------------------------------------------------------------------------------------------------
\ No newline at end of file
diff --git a/README_ja.md b/README_ja.md
new file mode 100644 (file)
index 0000000..4b97f3e
--- /dev/null
@@ -0,0 +1,58 @@
+% お読みください - Stew4
+
+
+## 1. 概要
+
+Stewは、JDBCを使ったSQLツール環境です。  
+データベースフロントエンドとしてご利用いただけます。
+
+以下の特徴があります。
+
+ * JDBCドライバ以外の外部ライブラリを使用しない
+ * コマンドラインでも実行できる
+ * カーソルを保持しない:
+     StatementとResultSetは処理が終わった直後に解放するように
+     しています。
+ * コアAPIのみ使用
+ * DBMS固有の処理を実装していない(バージョン2以降):
+     JDBCドライバの実装はばらつきがあるので、できるだけ
+     多くのドライバがサポートしていると思われる機能を使って
+     実装しています。
+     そのため、一般的な同種のアプリケーションで実現している
+     機能を実装できない場合があります。
+ * Windows, MacOSX, Linux をサポート(バージョン3以降):
+     古いバージョンでも使えることは使えますが、Windows以外は
+     一部不具合がありました。
+
+機能の詳細については、説明書(MANUAL_ja.html)を参照してください。
+バージョンアップでの変更点についてはFEATURE_ja.txtを参照してください。
+
+
+## 2. インストール・バージョンアップ・アンインストール
+
+インストール
+    リリースパッケージのZIPファイルを適当なディレクトリに展開するだけです。
+    最小限にしたい場合は本体JARファイル(stew.jar)と起動スクリプト(stew.bat or stew.sh)です。
+    起動スクリプトも自前で用意していただくのであれば不要です。
+
+バージョンアップ
+    インストールしたファイルの最新版を上書きしてください。
+    "connector.properties"はバージョン2以降は互換性がありますので、
+    移行の際はそのままお使いいただけます。
+
+アンインストール
+    インストールしたファイルと、システムディレクトリ(.stew/)を削除するだけです。
+    システムディレクトリは"@"コマンドで表示されます。
+
+
+## 3. ライセンス
+
+Apache License 2.0 を採用しています。
+詳しくは、LICENSEファイルなどを参照してください。
+
+
+## 4. 既知の問題
+
+更新権限のチェックは、更新権限の参照権限が無いと正しく判定できないという問題があります。
+
+Look&Feelの実装によっては上手く動作しない機能があるかもしれません。
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..e09958d
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ====================================================================== 
+     Stew
+     SQL Testing Environment With JDBC
+     ====================================================================== -->
+<project name="Stew4 build" default="build">
+
+    <!-- ENVIRONMENT VERSION INFO -->
+    <echo level="info" message="Ant  = ${ant.version}" />
+    <echo level="info" message="java = ${ant.java.version}" />
+
+    <!-- PROPERTIES -->
+    <property name="jar" value="stew.jar" />
+    <property name="src" value="src" />
+    <property name="bin" value="BLD" />
+
+    <!-- - - - - - - - - - - - - - - - - - 
+          target: clean                      
+         - - - - - - - - - - - - - - - - - -->
+    <target name="clean">
+        <mkdir dir="${bin}" />
+        <delete includeEmptyDirs="yes">
+            <fileset dir="${bin}" includes="**" />
+        </delete>
+    </target>
+
+    <!-- - - - - - - - - - - - - - - - - - 
+          target: compile
+         - - - - - - - - - - - - - - - - - -->
+    <target name="compile">
+        <mkdir dir="${bin}" />
+        <javac fork="yes"
+               srcdir="${src}"
+               destdir="${bin}"
+               source="1.6"
+               target="1.6"
+               encoding="utf-8"
+               optimize="yes"
+               deprecation="no"
+               debug="yes"
+               debuglevel="source,lines">
+            <include name="**/*.java" />
+        </javac>
+        <copy todir="${bin}">
+            <fileset dir="${src}">
+                <include name="**/version" />
+                <include name="**/*.u8p" />
+                <include name="**/*.png" />
+            </fileset>
+        </copy>
+    </target>
+
+    <!-- - - - - - - - - - - - - - - - - - 
+          target: archive
+         - - - - - - - - - - - - - - - - - -->
+    <target name="archive" depends="compile">
+        <jar destfile="${jar}" manifest="${src}/MANIFEST.MF">
+            <fileset dir="${bin}">
+                <include name="net/**" />
+            </fileset>
+        </jar>
+    </target>
+
+    <!-- ================================= 
+          target: build
+         ================================= -->
+    <target name="build" depends="archive" />
+
+</project>
diff --git a/logging.properties b/logging.properties
new file mode 100644 (file)
index 0000000..91fe043
--- /dev/null
@@ -0,0 +1,20 @@
+
+.level = OFF
+
+# ConsoleHandler
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+
+# FileHandler
+java.util.logging.FileHandler.level = ALL
+java.util.logging.FileHandler.pattern = stew.log
+java.util.logging.FileHandler.append = true
+java.util.logging.FileHandler.limit = 4194304
+java.util.logging.FileHandler.count = 2
+java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
+
+# about logger
+#   * stew.level.DEBUG is equivalent to logging.Level.FINE.
+#   * stew.level.TRACE is equivalent to logging.Level.FINER.
+net.argius.stew.level = ALL
+net.argius.stew.handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler
diff --git a/markdown.css b/markdown.css
new file mode 100644 (file)
index 0000000..03df696
--- /dev/null
@@ -0,0 +1,6 @@
+body { font-family: monospace; font-size: 80%; margin-left: 8%; margin-right: 8%; }
+pre { background-color: #cccccc; padding: 2ex; }
+dl { margin-left: 2.5%; }
+dt { text-decoration: underline; }
+strong { color: red; font-weight: bold; }
+.caution { color: red; font-weight: bold; }
diff --git a/src/MANIFEST.MF b/src/MANIFEST.MF
new file mode 100644 (file)
index 0000000..47c2f65
--- /dev/null
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: net.argius.Stew
diff --git a/src/net/argius/Stew.java b/src/net/argius/Stew.java
new file mode 100644 (file)
index 0000000..a0c901d
--- /dev/null
@@ -0,0 +1,17 @@
+package net.argius;
+
+import net.argius.stew.*;
+
+/**
+ * A main class.
+ */
+public final class Stew {
+
+    private Stew() {
+    } // forbidden
+
+    public static void main(String... args) {
+        Bootstrap.main(args);
+    }
+
+}
diff --git a/src/net/argius/logging/BasicFormatter.java b/src/net/argius/logging/BasicFormatter.java
new file mode 100644 (file)
index 0000000..c49b8c8
--- /dev/null
@@ -0,0 +1,53 @@
+package net.argius.logging;
+
+import java.io.*;
+import java.text.*;
+import java.util.logging.*;
+
+/**
+ * LoggingFormatter.
+ */
+public class BasicFormatter extends Formatter {
+
+    @Override
+    public String format(LogRecord record) {
+        final String msg = record.getMessage();
+        final String formatted;
+        Object[] args = record.getParameters();
+        if (args != null && args.length > 0) {
+            formatted = MessageFormat.format(msg, args);
+        } else {
+            formatted = msg;
+        }
+        final String s;
+        final Throwable th = record.getThrown();
+        if (th != null) {
+            Writer w = new StringWriter();
+            PrintWriter out = new PrintWriter(w);
+            th.printStackTrace(out);
+            s = w.toString();
+        } else {
+            s = "";
+        }
+        final String datetime = String.format("%1$tF %1$tT.%1$tL", record.getMillis());
+        return String.format("%s %s %s#%s [%s] %s%n%s",
+                             datetime,
+                             Thread.currentThread().getName(),
+                             record.getSourceClassName(),
+                             record.getSourceMethodName(),
+                             getLevelName(record.getLevel()),
+                             formatted,
+                             s);
+    }
+
+    private static String getLevelName(Level lv) {
+        if (lv.equals(Level.FINE)) {
+            return "DEBUG";
+        }
+        if (lv.equals(Level.FINER)) {
+            return "TRACE";
+        }
+        return lv.toString();
+    }
+
+}
diff --git a/src/net/argius/stew/Alias.java b/src/net/argius/stew/Alias.java
new file mode 100644 (file)
index 0000000..74fbde1
--- /dev/null
@@ -0,0 +1,119 @@
+package net.argius.stew;
+
+import java.io.*;
+import java.util.*;
+import java.util.Map.Entry;
+
+/**
+ * Alias.
+ */
+final class Alias {
+
+    private final Properties properties;
+    private final File file;
+
+    private long timestamp;
+
+    Alias(File file) {
+        this.properties = new Properties();
+        this.file = file;
+        this.timestamp = 0L;
+    }
+
+    String expand(Parameter p) {
+        return expand(p.at(0), p);
+    }
+
+    String expand(String command, Parameter p) {
+        StringBuilder buffer = new StringBuilder(p.asString());
+        String key = command;
+        final int limit = 100;
+        int limitCount = limit;
+        while (containsKey(key)) {
+            final String value = getValue(key);
+            buffer.replace(0, key.length(), value);
+            key = new Parameter(buffer.toString()).at(0);
+            if (--limitCount < 0) {
+                final String mkey = "e.alias-circulation-reference";
+                throw new CommandException(ResourceManager.Default.get(mkey, limit));
+            }
+        }
+        return buffer.toString();
+    }
+
+    String getValue(String key) {
+        return properties.getProperty(key, "");
+    }
+
+    void setValue(String key, String value) {
+        properties.setProperty(key, value);
+    }
+
+    Object remove(String key) {
+        return properties.remove(key);
+    }
+
+    boolean containsKey(String key) {
+        return properties.containsKey(key);
+    }
+
+    boolean isEmpty() {
+        return properties.isEmpty();
+    }
+
+    void load() throws IOException {
+        InputStream is = new FileInputStream(file);
+        try {
+            properties.clear();
+            properties.load(is);
+        } finally {
+            is.close();
+        }
+        timestamp = file.lastModified();
+    }
+
+    /**
+     * Reloads properties from a file if the file was updated.
+     * @throws IOException
+     */
+    void reload() throws IOException {
+        if (updated()) {
+            load();
+        }
+    }
+
+    void save() throws IOException {
+        if (isEmpty()) {
+            if (file.exists()) {
+                if (!file.delete()) {
+                    throw new IOException("file couldn't delete: " + file);
+                }
+            }
+            return;
+        }
+        OutputStream os = new FileOutputStream(file);
+        try {
+            properties.store(os, "");
+        } finally {
+            os.close();
+        }
+        timestamp = file.lastModified();
+    }
+
+    boolean updated() {
+        return file.lastModified() > timestamp;
+    }
+
+    Set<String> keys() {
+        Set<String> set = new LinkedHashSet<String>();
+        for (final Object o : Collections.list(properties.propertyNames())) {
+            set.add((String)o);
+        }
+        return set;
+    }
+
+    Set<Entry<Object, Object>> entrySet() {
+        return properties.entrySet();
+    }
+
+}
diff --git a/src/net/argius/stew/AnonymousConnector.java b/src/net/argius/stew/AnonymousConnector.java
new file mode 100644 (file)
index 0000000..9b7ce9c
--- /dev/null
@@ -0,0 +1,67 @@
+package net.argius.stew;
+
+import java.util.*;
+
+/**
+ * A factory class that creates anonymous connector.
+ */
+final class AnonymousConnector {
+
+    private static final String PREFIX_JDBC = "jdbc:";
+
+    private AnonymousConnector() {
+        // empty
+    }
+
+    /**
+     * Gets an anonymous connector with a URI.
+     * @param uri user/password@URL
+     * @return an anonymous connector
+     */
+    public static Connector getConnector(String uri) {
+        int index1 = uri.indexOf('@');
+        if (index1 < 0) {
+            throw new IllegalArgumentException(uri);
+        }
+        String userInfo = uri.substring(0, index1);
+        String url = uri.substring(index1 + 1);
+        String user;
+        String password;
+        int index2 = userInfo.indexOf('/');
+        if (index2 >= 0) {
+            user = userInfo.substring(0, index2);
+            password = userInfo.substring(index2 + 1);
+        } else {
+            user = userInfo;
+            password = "";
+        }
+        return getConnector(url, user, password);
+    }
+
+    /**
+     * Gets an anonymous connector with a URI, a user name and a password.
+     * @param url the JDBC URL
+     * @param user the database user to connect with it
+     * @param password the user's password
+     * @return an anonymous connector
+     */
+    public static Connector getConnector(String url, String user, String password) {
+        Properties props = new Properties();
+        props.setProperty("name", getName(url, user));
+        props.setProperty("driver", "");
+        props.setProperty("classpath", "");
+        props.setProperty("url", url);
+        props.setProperty("user", user);
+        props.setProperty("readonly", Boolean.FALSE.toString());
+        props.setProperty("rollback", Boolean.FALSE.toString());
+        Connector connector = new Connector("ANONYMOUS", props);
+        connector.getPassword().setRawString(password);
+        return connector;
+    }
+
+    private static String getName(String url, String user) {
+        final String url0 = url.startsWith(PREFIX_JDBC) ? url.substring(PREFIX_JDBC.length()) : url;
+        return String.format("%s@%s", user, url0);
+    }
+
+}
diff --git a/src/net/argius/stew/Bootstrap.java b/src/net/argius/stew/Bootstrap.java
new file mode 100644 (file)
index 0000000..7fb7512
--- /dev/null
@@ -0,0 +1,234 @@
+package net.argius.stew;
+
+import java.io.*;
+import java.util.*;
+
+import net.argius.stew.ui.console.*;
+import net.argius.stew.ui.window.*;
+
+/**
+ * The class which bootstraps this app.
+ */
+public final class Bootstrap {
+
+    private static final Logger log = Logger.getLogger(Bootstrap.class);
+    private static final String PropKey = "net.argius.stew.properties";
+    private static final String PropFileName = "stew.properties";
+    private static final String DefaultDir = ".stew";
+
+    private static final File dir = initializeDirectory();
+    private static Properties props = initializeProperties();
+
+    private static File initializeDirectory() {
+        File directory;
+        String path = System.getProperty(PropKey, System.getProperty(PropFileName, ""));
+        if (path.length() == 0) {
+            directory = new File(DefaultDir);
+        } else {
+            File file = new File(path);
+            if (file.isDirectory()) {
+                directory = file;
+            } else {
+                directory = file.getParentFile();
+                if (directory == null) {
+                    directory = new File(DefaultDir);
+                }
+            }
+        }
+        try {
+            if (!directory.isDirectory()) {
+                if (!directory.mkdirs() || !directory.isDirectory()) {
+                    throw new IOException("can't make directory: " + directory);
+                }
+            }
+        } catch (IOException ex) {
+            throw new IllegalStateException(ex);
+        }
+        return directory;
+    }
+
+    private static Properties initializeProperties() {
+        Properties group3 = System.getProperties();
+        Properties group2 = new Properties(group3);
+        try {
+            group2.putAll(getFileProperties());
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+        Properties group1 = new Properties(group2);
+        if (log.isDebugEnabled()) {
+            int i = 3;
+            for (Properties p : new Properties[]{group3, group2}) {
+                List<String> list = new ArrayList<String>(p.size());
+                for (Object key : p.keySet()) {
+                    list.add((String)key);
+                }
+                Collections.sort(list);
+                Writer buffer = new StringWriter();
+                PrintWriter out = new PrintWriter(buffer);
+                out.println();
+                out.println("--- property group " + (i--) + " ---");
+                for (Iterator<String> it = list.iterator(); it.hasNext();) {
+                    String key = it.next();
+                    out.println(key + '=' + p.getProperty(key));
+                }
+                log.setEnteredMethodName("initializeProperties");
+                log.debug(buffer);
+                log.setEnteredMethodName("");
+            }
+        }
+        return group1;
+    }
+
+    private static Properties getFileProperties() throws IOException {
+        Properties props = new Properties();
+        // system property
+        String path = System.getProperty(PropFileName);
+        if (path != null) {
+            File file = new File(path);
+            if (file.isDirectory()) {
+                file = new File(file, PropFileName);
+            }
+            if (file.exists()) {
+                InputStream is = new FileInputStream(file);
+                try {
+                    props.load(is);
+                    return props;
+                } finally {
+                    is.close();
+                }
+            }
+        }
+        // classpath
+        String resourcePath = "/" + PropFileName;
+        InputStream res = Bootstrap.class.getResourceAsStream(resourcePath);
+        if (res != null) {
+            try {
+                props.load(res);
+                return props;
+            } finally {
+                res.close();
+            }
+        }
+        // system directory
+        File currentdirfile = new File(dir, PropFileName);
+        if (currentdirfile.exists()) {
+            InputStream is = new FileInputStream(currentdirfile);
+            try {
+                props.load(is);
+                return props;
+            } finally {
+                is.close();
+            }
+        }
+        return props;
+    }
+
+    /**
+     * Returns system directory.
+     * @return
+     */
+    public static File getDirectory() {
+        return dir;
+    }
+
+    /**
+     * Returns this app's system property.
+     * @param key
+     * @return
+     */
+    public static String getProperty(String key) {
+        return props.getProperty(key, "");
+    }
+
+    /**
+     * Returns this app's system property.
+     * @param key
+     * @param defaultValue
+     * @return
+     */
+    public static String getProperty(String key, String defaultValue) {
+        return props.getProperty(key, defaultValue);
+    }
+
+    /**
+     * Returns this app's system property as int.
+     * @param key
+     * @param defaultValue
+     * @return
+     */
+    public static int getPropertyAsInt(String key, int defaultValue) {
+        if (props.getProperty(key) != null) {
+            try {
+                return Integer.parseInt(props.getProperty(key, ""));
+            } catch (NumberFormatException ex) {
+                log.warn(ex);
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Returns this app's system property as boolean.
+     * @param key
+     * @return
+     */
+    public static boolean getPropertyAsBoolean(String key) {
+        return Boolean.valueOf(props.getProperty(key, ""));
+    }
+
+    /**
+     * Returns whether the property has specified key or not.
+     * @param key
+     * @return
+     */
+    public static boolean hasProperty(String key) {
+        return props.containsKey(key);
+    }
+
+    /**
+     * Returns app version.
+     * @return
+     */
+    public static String getVersion() {
+        return ResourceManager.Default.read("version", "(UNKNOWN)");
+    }
+
+    /** main **/
+    public static void main(String... args) {
+        int guiCount = 0;
+        int cuiCount = 0;
+        List<String> a = new ArrayList<String>();
+        for (String arg : args) {
+            if (arg.matches("(?i)\\s*--GUI\\s*")) {
+                ++guiCount;
+            } else if (arg.matches("(?i)\\s*--CUI\\s*")) {
+                ++cuiCount;
+            } else {
+                a.add(arg);
+            }
+        }
+        if (guiCount == 0 && cuiCount == 0) {
+            for (String k : new String[]{"stew.bootstrap", "stew.boot",
+                                         "net.argius.stew.bootstrap", "net.argius.stew.boot",}) {
+                final String v = props.getProperty(k, "");
+                if (v.equalsIgnoreCase("GUI")) {
+                    ++guiCount;
+                }
+                if (v.equalsIgnoreCase("CUI")) {
+                    ++cuiCount;
+                }
+            }
+        }
+        if (guiCount > 0 && cuiCount > 0) {
+            throw new IllegalArgumentException("bad option: both --gui and --cui were specified.");
+        }
+        log.debug("cui=%d, gui=%d, new-args=%s", cuiCount, guiCount, a);
+        if (guiCount > 0) {
+            WindowLauncher.main();
+        } else {
+            ConsoleLauncher.main(a.toArray(new String[a.size()]));
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/CipherPassword.java b/src/net/argius/stew/CipherPassword.java
new file mode 100644 (file)
index 0000000..e680514
--- /dev/null
@@ -0,0 +1,120 @@
+package net.argius.stew;
+
+import java.io.*;
+
+import javax.crypto.*;
+
+/**
+ * A skeletal implementation of the Password interface using ciphers.
+ */
+public abstract class CipherPassword implements Password {
+
+    private static final Logger log = Logger.getLogger(CipherPassword.class);
+
+    private static String secretKey = "";
+
+    private String transformedString;
+
+    @Override
+    public final String getTransformedString() {
+        if (transformedString != null) {
+            return transformedString;
+        }
+        return "";
+    }
+
+    @Override
+    public final void setTransformedString(String transformedString) {
+        if (transformedString != null) {
+            this.transformedString = transformedString;
+        }
+    }
+
+    @Override
+    public final String getRawString() {
+        if (transformedString != null) {
+            return decrypt(transformedString);
+        }
+        return "";
+    }
+
+    @Override
+    public final void setRawString(String rowString) {
+        if (rowString != null) {
+            this.transformedString = encrypt(rowString);
+        }
+    }
+
+    @Override
+    public final boolean hasPassword() {
+        return transformedString != null;
+    }
+
+    /**
+     * Sets a secret key.
+     * @param secretKey
+     */
+    public static void setSecretKey(String secretKey) {
+        assert secretKey != null && secretKey.length() > 0;
+        CipherPassword.secretKey = secretKey;
+    }
+
+    /**
+     * Encrypts a password.
+     * @param rowString
+     * @return the encrypted password
+     */
+    private String encrypt(String rowString) {
+        try {
+            Cipher cipher = getCipherInstance(secretKey, Cipher.ENCRYPT_MODE);
+            byte[] encrypted = cipher.doFinal(rowString.getBytes());
+            return toHexString(encrypted);
+        } catch (Exception ex) {
+            log.warn(ex);
+            return "";
+        }
+    }
+
+    /**
+     * Decrypts a password.
+     * @param cryptedString
+     * @return the decrypted password
+     */
+    private String decrypt(String cryptedString) {
+        try {
+            Cipher cipher = getCipherInstance(secretKey, Cipher.DECRYPT_MODE);
+            byte[] decrypted = cipher.doFinal(toBytes(cryptedString));
+            return new String(decrypted);
+        } catch (Exception ex) {
+            log.warn(ex);
+            return "";
+        }
+    }
+
+    private static String toHexString(byte[] bytes) {
+        StringBuffer buffer = new StringBuffer();
+        for (byte b : bytes) {
+            buffer.append(String.format("%02X", b & 0xFF));
+        }
+        return buffer.toString();
+    }
+
+    private static byte[] toBytes(String hexString) {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        for (int i = 0; i < hexString.length(); i += 2) {
+            String s = hexString.substring(i, i + 2);
+            bos.write(Integer.parseInt(s, 16));
+        }
+        return bos.toByteArray();
+    }
+
+    /**
+     * Gets a instance of Cipher class.
+     * @param key
+     * @param mode Cipher.DECRYPT_MODE or Cipher.DECRYPT_MODE
+     * @return the instance of Cipher class
+     * @see Cipher
+     */
+    protected abstract Cipher getCipherInstance(String key, int mode);
+
+}
diff --git a/src/net/argius/stew/ColumnOrder.java b/src/net/argius/stew/ColumnOrder.java
new file mode 100644 (file)
index 0000000..1195793
--- /dev/null
@@ -0,0 +1,78 @@
+package net.argius.stew;
+
+import java.util.*;
+
+/**
+ * A (list of) column order.
+ */
+public final class ColumnOrder {
+
+    private final List<Entry> list;
+
+    /**
+     * A constructor.
+     */
+    public ColumnOrder() {
+        this.list = new ArrayList<Entry>();
+    }
+
+    /**
+     * Adds an order.
+     * @param order
+     */
+    public void addOrder(int order) {
+        addOrder(order, "");
+    }
+
+    /**
+     * Adds an order with a name.
+     * @param order
+     * @param name
+     */
+    public void addOrder(int order, String name) {
+        Entry entry = new Entry(order, name);
+        list.add(entry);
+    }
+
+    /**
+     * Returns the size of this column order's list.
+     * @return size
+     */
+    public int size() {
+        return list.size();
+    }
+
+    /**
+     * Returns the number of the order at the specified index.
+     * @param index
+     * @return the order
+     */
+    public int getOrder(int index) {
+        return list.get(index).order;
+    }
+
+    /**
+     * Returns the name of the order at the specified index.
+     * @param index
+     * @return the name of the order
+     */
+    public String getName(int index) {
+        return list.get(index).name;
+    }
+
+    /**
+     * An order entry.
+     */
+    private static final class Entry {
+
+        int order;
+        String name;
+
+        Entry(int order, String name) {
+            this.order = order;
+            this.name = name;
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/Command.java b/src/net/argius/stew/Command.java
new file mode 100644 (file)
index 0000000..93678c4
--- /dev/null
@@ -0,0 +1,281 @@
+package net.argius.stew;
+
+import java.io.*;
+import java.nio.channels.*;
+import java.sql.*;
+import java.util.*;
+
+import net.argius.stew.io.*;
+import net.argius.stew.ui.*;
+
+/**
+ * The skeletal implementation of the Command.
+ */
+public abstract class Command {
+
+    protected Environment env;
+    protected OutputProcessor op;
+
+    private static final ResourceManager res = ResourceManager.getInstance(Command.class);
+
+    /**
+     * A constructor.
+     */
+    protected Command() {
+        // empty
+    }
+
+    /**
+     * Initializes this.
+     * @throws CommandException
+     */
+    public void initialize() throws CommandException {
+        // empty
+    }
+
+    /**
+     * Executes this command.
+     * @param conn
+     * @param parameter
+     * @throws CommandException
+     */
+    public abstract void execute(Connection conn, Parameter parameter) throws CommandException;
+
+    /**
+     * Closes this command.
+     * Overwrite this to tear down and to do post processes.
+     * @throws CommandException
+     */
+    public void close() throws CommandException {
+        // empty
+    }
+
+    /**
+     * Invokes this command.
+     * @param env
+     * @param parameterString
+     * @return true if it continues, or false if exit this application
+     * @throws CommandException
+     */
+    public static boolean invoke(Environment env, String parameterString) throws CommandException {
+        CommandProcessor processor = new CommandProcessor(env);
+        return processor.invoke(parameterString);
+    }
+
+    /**
+     * Returns whether this command is read-only or not.
+     * Overwrite this method to change the read-only.
+     * @return
+     */
+    @SuppressWarnings("static-method")
+    public boolean isReadOnly() { // overridable
+        return false;
+    }
+
+    /**
+     * Sets the timeout.
+     * It only sets when the config value is more than zero. 
+     * @param stmt Statement
+     * @throws SQLException
+     * @see Statement#setQueryTimeout(int)
+     */
+    protected void setTimeout(Statement stmt) throws SQLException {
+        final int timeoutSeconds = env.getTimeoutSeconds();
+        if (timeoutSeconds >= 0) {
+            stmt.setQueryTimeout(timeoutSeconds);
+        }
+    }
+
+    /**
+     * Sets Environment.
+     * @param env
+     */
+    public final void setEnvironment(Environment env) {
+        this.env = env;
+        this.op = env.getOutputProcessor();
+    }
+
+    /**
+     * Resolves the path.
+     * If the path is relative, this method will convert it to an absolute path to the env's current dir.
+     * @param path
+     * @return
+     */
+    protected final File resolvePath(String path) {
+        return Path.resolve(env.getCurrentDirectory(), path);
+    }
+
+    /**
+     * Resolves the path.
+     * If the path is relative, this method will convert it to an absolute path to the env's current dir.
+     * @param file
+     * @return
+     */
+    protected final File resolvePath(File file) {
+        return Path.resolve(env.getCurrentDirectory(), file);
+    }
+
+    /**
+     * Outputs an Object.
+     * @param object
+     * @throws CommandException
+     */
+    protected final void output(Object object) throws CommandException {
+        op.output(object);
+    }
+
+    /**
+     * Outputs the message specified by that message-key.
+     * @param key
+     * @param args
+     * @throws CommandException
+     */
+    protected final void outputMessage(String key, Object... args) throws CommandException {
+        output(getMessage(key, args));
+    }
+
+    /**
+     * Returns the message specified by that message-key.
+     * @param key
+     * @param args
+     * @return
+     */
+    protected static String getMessage(String key, Object... args) {
+        return res.get(key, args);
+    }
+
+    /**
+     * Converts a pattern string.
+     * It converts with considering identifier that depends on each databases.
+     * @param pattern
+     * @return
+     * @throws SQLException
+     */
+    protected final String convertPattern(String pattern) throws SQLException {
+        String edited;
+        DatabaseMetaData dbmeta = env.getCurrentConnection().getMetaData();
+        if (dbmeta.storesLowerCaseIdentifiers()) {
+            edited = pattern.toLowerCase();
+        } else if (dbmeta.storesUpperCaseIdentifiers()) {
+            edited = pattern.toUpperCase();
+        } else {
+            edited = pattern;
+        }
+        return edited.replace('*', '%').replace('?', '_');
+    }
+
+    /**
+     * Returns USAGE.
+     * @return
+     */
+    protected String getUsage() {
+        final String name = getClass().getName().replaceFirst(".*\\.([^\\.]+)", "$1");
+        return getMessage("usage." + name);
+    }
+
+    /**
+     * Returns whether SQL is SELECT.
+     * @param sql
+     * @return
+     */
+    protected static boolean isSelect(String sql) {
+        Scanner scanner = new Scanner(sql);
+        while (scanner.hasNextLine()) {
+            final String line = scanner.nextLine();
+            final String s = line.replaceAll("/\\*.*?\\*/", "");
+            if (s.matches("\\s*") || s.matches("\\s*--.*")) {
+                continue;
+            }
+            if (!s.matches("\\s*") && s.matches("(?i)\\s*SELECT.*")) {
+                return true;
+            }
+            break;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the string that read from file.
+     * @param file
+     * @return
+     * @throws IOException
+     */
+    protected static String readFileAsString(File file) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        FileInputStream fis = new FileInputStream(file);
+        try {
+            fis.getChannel().transferTo(0, file.length(), Channels.newChannel(bos));
+        } finally {
+            fis.close();
+        }
+        return bos.toString();
+    }
+
+    /**
+     * Prepares Statement.
+     * @param conn
+     * @param sql
+     * @return
+     * @throws SQLException
+     */
+    protected final Statement prepareStatement(Connection conn, String sql) throws SQLException {
+        final int index = sql.indexOf(';');
+        Statement stmt = (index >= 0)
+                ? conn.prepareStatement(sql.substring(0, index))
+                : conn.createStatement();
+        try {
+            if (stmt instanceof PreparedStatement) {
+                PreparedStatement pstmt = (PreparedStatement)stmt;
+                int i = 0;
+                for (String p : sql.substring(index + 1).split(",", -1)) {
+                    pstmt.setString(++i, p);
+                }
+            }
+            setTimeout(stmt);
+            final int limit = Bootstrap.getPropertyAsInt("net.argius.stew.rowcount.limit",
+                                                         Integer.MAX_VALUE);
+            if (limit > 0 && limit != Integer.MAX_VALUE) {
+                stmt.setMaxRows(limit + 1);
+            }
+        } catch (Throwable th) {
+            try {
+                if (th instanceof SQLException) {
+                    throw (SQLException)th;
+                }
+                throw new IllegalStateException(th);
+            } finally {
+                stmt.close();
+            }
+        }
+        return stmt;
+    }
+
+    /**
+     * Executes a query.
+     * @param stmt
+     * @param sql
+     * @return
+     * @throws SQLException
+     */
+    @SuppressWarnings("static-method")
+    protected ResultSet executeQuery(Statement stmt, String sql) throws SQLException {
+        return (stmt instanceof PreparedStatement)
+                ? ((PreparedStatement)stmt).executeQuery()
+                : stmt.executeQuery(sql);
+    }
+
+    /**
+     * Executes Update(SQL).
+     * @param stmt
+     * @param sql
+     * @return
+     * @throws SQLException
+     */
+    @SuppressWarnings("static-method")
+    protected int executeUpdate(Statement stmt, String sql) throws SQLException {
+        return (stmt instanceof PreparedStatement)
+                ? ((PreparedStatement)stmt).executeUpdate()
+                : stmt.executeUpdate(sql);
+    }
+
+}
diff --git a/src/net/argius/stew/Command.u8p b/src/net/argius/stew/Command.u8p
new file mode 100644 (file)
index 0000000..44288ea
--- /dev/null
@@ -0,0 +1,54 @@
+
+i.did-mkdir=ディレクトリ[{0}]を作成しました。
+i.downloaded=ダウンロードされました。 (size={0}, file={1})
+i.exported=エクスポートされました。
+i.loaded=ロードされた {1} 件中 {0} 件 追加されました。
+i.proceeded={0} 件 処理されました。
+#i.selected={0} 件 ヒットしました。
+i.updated={0} 件 更新されました。
+i.wait-time=待機時間 : {0,number,0.000} 秒
+e.no-record=データが見つかりません。
+e.file-already-exists=ファイル[{0}]が存在します。
+e.failed-create-new-file=ファイル[{0}]の作成に失敗しました。
+e.failed-mkdir-filedir=ファイル[{0}]のディレクトリ作成に失敗しました。
+
+usage.Count=<テーブル名> [<WHERE句>]
+usage.Download=<ルートディレクトリ> SELECT <ダウンロードするデータの列> [, ファイルパス...] FROM ... 
+usage.Export=<ファイル> [ HEADER ] [command(select|find|report)] \n        注: "report -"は不可
+usage.Find=<テーブル名パターン> [<テーブル種別パターン> [<スキーマ名パターン> [<カタログ名パターン> [ FULL ]]]]
+usage.Import=[<データファイル> <テーブル名> [ HEADER ]]
+usage.Load=[<SQLファイル> | <データファイル> <テーブル名> [ HEADER ]]
+usage.Report=- | <テーブル名> [ FULL | PK | INDEX ]
+usage.Time=[<回数>] <SQL文>
+usage.Upload=<ファイル> <SQL文(UPDATE|INSERT)>
+usage.Wait=<秒(小数第3位まで)> 
+
+Export.command.usage={0}\n  {1} {2}
+
+Find.label.name=テーブル名
+Find.label.type=テーブル種別
+Find.label.schema=スキーマ名
+Find.label.catalog=カタログ名
+
+Report.label.catalog=カタログ
+Report.label.schema=スキーマ
+Report.label.tablename=テーブル名
+Report.label.sequence=順番
+Report.label.columnname=カラム名
+Report.label.keyname=プライマリキー名
+Report.label.nullable=Null可
+Report.label.type=型
+Report.label.size=サイズ
+Report.dbinfo =\
+製品名             : {0}\n\
+製品バージョン     : {1}\n\
+ドライバ名         : {2}\n\
+ドライババージョン : {3}\n\
+接続ユーザ@URL     : {4}@{5}
+
+Time.once=実行時間 : {0,number,0.000} 秒
+Time.summary=\
+合計 : {0,number,0.000} 秒\n\
+平均 : {1,number,0.000} 秒\n\
+最大 : {2,number,0.000} 秒\n\
+最小 : {3,number,0.000} 秒
diff --git a/src/net/argius/stew/CommandException.java b/src/net/argius/stew/CommandException.java
new file mode 100644 (file)
index 0000000..c7ef05c
--- /dev/null
@@ -0,0 +1,20 @@
+package net.argius.stew;
+
+/**
+ * Thrown when an error occurred while Command is running.
+ */
+public class CommandException extends RuntimeException {
+
+    public CommandException(String message) {
+        super(message);
+    }
+
+    public CommandException(Throwable cause) {
+        super(cause);
+    }
+
+    public CommandException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/net/argius/stew/CommandProcessor.java b/src/net/argius/stew/CommandProcessor.java
new file mode 100644 (file)
index 0000000..02925b9
--- /dev/null
@@ -0,0 +1,435 @@
+package net.argius.stew;
+
+import java.io.*;
+import java.sql.*;
+import java.util.*;
+
+import javax.script.*;
+
+import net.argius.stew.io.*;
+import net.argius.stew.ui.*;
+import net.argius.stew.ui.window.*;
+
+/**
+ * Command Processor.
+ */
+final class CommandProcessor {
+
+    private static Logger log = Logger.getLogger(CommandProcessor.class);
+    private static ResourceManager res = ResourceManager.getInstance(WindowLauncher.class);
+    private static final String HYPHEN_E = "-e";
+
+    private final Environment env;
+    private final OutputProcessor op;
+
+    CommandProcessor(Environment env) {
+        this.env = env;
+        this.op = env.getOutputProcessor();
+    }
+
+    /**
+     * Invokes this command.
+     * @param parameterString
+     * @return whether this application continues or not
+     * @throws CommandException
+     */
+    boolean invoke(String parameterString) throws CommandException {
+        Parameter p = new Parameter(parameterString);
+        if (parameterString.replaceFirst("^\\s+", "").startsWith(HYPHEN_E)) {
+            final int offset = parameterString.indexOf(HYPHEN_E) + 2;
+            for (String s : parameterString.substring(offset).split(HYPHEN_E)) {
+                op.output(" >> " + s);
+                if (!invoke(s)) {
+                    outputMessage("w.exit-not-available-in-sequencial-command");
+                }
+            }
+            return true;
+        }
+        final String commandName = p.at(0);
+        try {
+            return invoke(commandName, new Parameter(parameterString));
+        } catch (UsageException ex) {
+            outputMessage("e.usage", commandName, ex.getMessage());
+        } catch (DynamicLoadingException ex) {
+            log.error(ex);
+            outputMessage("e.not-found", commandName);
+        } catch (CommandException ex) {
+            log.error(ex);
+            Throwable cause = ex.getCause();
+            String message = (cause == null) ? ex.getMessage() : cause.getMessage();
+            outputMessage("e.command", message);
+        } catch (IOException ex) {
+            log.error(ex);
+            outputMessage("e.command", ex.getMessage());
+        } catch (SQLException ex) {
+            log.error(ex);
+            SQLException parent = ex;
+            while (true) {
+                SQLException sqle = parent.getNextException();
+                if (sqle == null || sqle == parent) {
+                    break;
+                }
+                log.error(sqle, "------ SQLException.getNextException ------");
+                parent = sqle;
+            }
+            outputMessage("e.database", ex.getMessage());
+        } catch (UnsupportedOperationException ex) {
+            log.warn(ex);
+            outputMessage("e.unsupported", ex.getMessage());
+        } catch (RuntimeException ex) {
+            log.error(ex);
+            outputMessage("e.runtime", ex.getMessage());
+        } catch (Throwable th) {
+            log.fatal(th);
+            outputMessage("e.fatal", th.getMessage());
+        }
+        try {
+            Connection conn = env.getCurrentConnection();
+            if (conn != null) {
+                boolean isClosed = conn.isClosed();
+                if (isClosed) {
+                    log.info("connection is already closed");
+                    disconnect();
+                }
+            }
+        } catch (SQLException ex) {
+            log.warn(ex);
+        }
+        return true;
+    }
+
+    private boolean invoke(String commandName, Parameter p) throws IOException, SQLException {
+        assert commandName != null;
+        // do nothing if blank
+        if (commandName.length() == 0) {
+            return true;
+        }
+        // exit
+        if (commandName.equalsIgnoreCase("exit")) {
+            disconnect();
+            outputMessage("i.exit");
+            return false;
+        }
+        // connect 
+        if (commandName.equalsIgnoreCase("connect") || commandName.equalsIgnoreCase("-c")) {
+            connect(p);
+            return true;
+        }
+        // from file
+        if (commandName.equals("-f")) {
+            final File file = Path.resolve(env.getCurrentDirectory(), p.at(1));
+            if (!file.isFile()) {
+                throw new UsageException(res.get("usage.-f"));
+            }
+            log.debug("-f %s", file.getAbsolutePath());
+            invoke(String.format("%s%s", Command.readFileAsString(file), p.after(2)));
+            return true;
+        }
+        // script
+        if (commandName.equals("-s")) {
+            final File file = Path.resolve(env.getCurrentDirectory(), p.at(1));
+            if (!file.isFile()) {
+                throw new UsageException(res.get("usage.-s"));
+            }
+            log.debug("-s %s", file.getAbsolutePath());
+            ScriptEngineManager factory = new ScriptEngineManager();
+            ScriptEngine engine = factory.getEngineByName("JavaScript");
+            engine.put("connection", env.getCurrentConnection());
+            engine.put("conn", env.getCurrentConnection());
+            engine.put("patameter", p);
+            engine.put("p", p);
+            engine.put("outputProcessor", op);
+            engine.put("op", op);
+            try {
+                Reader r = new FileReader(file);
+                try {
+                    engine.eval("function using(o, f) { f(o); o.close() }");
+                    engine.eval(r);
+                } finally {
+                    r.close();
+                }
+            } catch (Exception ex) {
+                throw new CommandException(ex);
+            }
+            return true;
+        }
+        // alias
+        Alias alias = env.getAlias();
+        if (commandName.equalsIgnoreCase("alias") || commandName.equalsIgnoreCase("unalias")) {
+            alias.reload();
+            if (commandName.equalsIgnoreCase("alias")) {
+                if (p.has(2)) {
+                    final String keyword = p.at(1);
+                    if (isUsableKeywordForAlias(keyword)) {
+                        outputMessage("w.unusable-keyword-for-alias", keyword);
+                        return true;
+                    }
+                    alias.setValue(keyword, p.after(2));
+                    alias.save();
+                } else if (p.has(1)) {
+                    final String keyword = p.at(1);
+                    if (isUsableKeywordForAlias(keyword)) {
+                        outputMessage("w.unusable-keyword-for-alias", keyword);
+                        return true;
+                    }
+                    if (alias.containsKey(keyword)) {
+                        outputMessage("i.dump-alias", keyword, alias.getValue(keyword));
+                    }
+                } else {
+                    if (alias.isEmpty()) {
+                        outputMessage("i.noalias");
+                    } else {
+                        for (final String key : new TreeSet<String>(alias.keys())) {
+                            outputMessage("i.dump-alias", key, alias.getValue(key));
+                        }
+                    }
+                }
+            } else if (commandName.equalsIgnoreCase("unalias")) {
+                if (p.has(1)) {
+                    alias.remove(p.at(1));
+                    alias.save();
+                } else {
+                    throw new UsageException(res.get("usage.unalias"));
+                }
+            }
+            return true;
+        } else if (alias.containsKey(commandName)) {
+            final String command = alias.expand(commandName, p);
+            op.output(" >> " + command);
+            invoke(command);
+            return true;
+        }
+        // cd
+        if (commandName.equalsIgnoreCase("cd")) {
+            if (!p.has(1)) {
+                throw new UsageException(res.get("usage.cd"));
+            }
+            File olddir = env.getCurrentDirectory();
+            final String path = p.at(1);
+            final File dir = new File(path);
+            final File newdir = ((dir.isAbsolute()) ? dir : new File(olddir, path)).getCanonicalFile();
+            if (!newdir.isDirectory()) {
+                outputMessage("e.dir-not-exists", newdir);
+                return true;
+            }
+            env.setCurrentDirectory(newdir);
+            outputMessage("i.directory-changed", olddir.getAbsolutePath(), newdir.getAbsolutePath());
+            return true;
+        }
+        // at
+        if (commandName.equals("@")) {
+            op.output(String.format("current dir : %s", env.getCurrentDirectory().getAbsolutePath()));
+            op.output(String.format("system  dir : %s", env.getSystemDirectory().getAbsolutePath()));
+            return true;
+        }
+        // report -
+        if (commandName.equals("-")) {
+            return invoke("report -");
+        }
+        // runtime informations
+        if (commandName.equals("?")) {
+            if (p.has(1)) {
+                for (final String k : p.asArray()) {
+                    if (k.equals("?")) {
+                        continue;
+                    }
+                    final String s = System.getProperties().containsKey(k)
+                            ? String.format("[%s]", System.getProperty(k))
+                            : "undefined";
+                    op.output(String.format("%s=%s", k, s));
+                }
+            } else {
+                op.output(String.format("JRE : %s %s",
+                                        System.getProperty("java.runtime.name"),
+                                        System.getProperty("java.runtime.version")));
+                op.output(String.format("OS : %s (osver=%s)",
+                                        System.getProperty("os.name"),
+                                        System.getProperty("os.version")));
+                op.output(String.format("Locale : %s", Locale.getDefault()));
+            }
+            return true;
+        }
+        // connection
+        Connection conn = env.getCurrentConnection();
+        if (conn == null) {
+            outputMessage("e.not-connect");
+        } else if (commandName.equalsIgnoreCase("disconnect") || commandName.equalsIgnoreCase("-d")) {
+            disconnect();
+            outputMessage("i.disconnected");
+        } else if (commandName.equalsIgnoreCase("commit")) {
+            conn.commit();
+            outputMessage("i.committed");
+        } else if (commandName.equalsIgnoreCase("rollback")) {
+            conn.rollback();
+            outputMessage("i.rollbacked");
+        } else {
+            executeDynamicCommand(commandName, conn, p);
+        }
+        return true;
+    }
+
+    private static boolean isUsableKeywordForAlias(String keyword) {
+        return keyword != null && keyword.matches("(?i)-.*|exit|alias|unalias");
+    }
+
+    private void connect(Parameter p) throws SQLException {
+        log.info("connect start");
+        disconnect();
+        final String id = p.at(1);
+        Connector connector;
+        if (!p.has(1)) {
+            connector = AnonymousConnector.getConnector(id, p.at(2), p.at(3));
+        } else if (id.indexOf('@') >= 0) {
+            connector = AnonymousConnector.getConnector(id);
+        } else {
+            connector = env.getConnectorMap().getConnector(id);
+        }
+        if (connector != null) {
+            env.establishConnection(connector);
+        } else {
+            outputMessage("e.no-connector", id);
+        }
+        log.info("connect end");
+    }
+
+    private void disconnect() {
+        log.debug("disconnect start");
+        try {
+            env.releaseConnection();
+        } catch (SQLException ex) {
+            outputMessage("w.connection-closed-abnormally");
+        }
+        log.debug("disconnect end");
+    }
+
+    private void executeDynamicCommand(String commandName, Connection conn, Parameter p) {
+        assert commandName != null && !commandName.contains(" ");
+        final String fqcn;
+        if (commandName.indexOf('.') > 0) {
+            fqcn = commandName;
+        } else {
+            fqcn = "net.argius.stew.command."
+                   + commandName.substring(0, 1).toUpperCase()
+                   + commandName.substring(1).toLowerCase();
+        }
+        Class<? extends Command> c;
+        try {
+            c = DynamicLoader.loadClass(fqcn);
+        } catch (DynamicLoadingException ex) {
+            c = Command.isSelect(p.asString()) ? Select.class : UpdateAndOthers.class;
+        }
+        Command command = DynamicLoader.newInstance(c);
+        try {
+            Connector connector = env.getCurrentConnector();
+            if (connector.isReadOnly() && !command.isReadOnly()) {
+                outputMessage("e.readonly");
+                return;
+            }
+            command.setEnvironment(env);
+            log.info("command: %s start", command);
+            log.debug(p);
+            command.initialize();
+            command.execute(conn, p);
+        } finally {
+            command.close();
+        }
+        log.info("command: %s end", command);
+    }
+
+    /**
+     * Outputs message.
+     * @param id message-id (resource)
+     * @param args
+     * @throws CommandException
+     */
+    void outputMessage(String id, Object... args) throws CommandException {
+        op.output(res.get(id, args));
+    }
+
+    /**
+     * SQL statement command.
+     */
+    abstract static class RawSQL extends Command {
+
+        @Override
+        public final void execute(Connection conn, Parameter parameter) throws CommandException {
+            final String rawString = parameter.asString();
+            try {
+                Statement stmt = prepareStatement(conn, rawString);
+                try {
+                    execute(stmt, rawString);
+                } finally {
+                    stmt.close();
+                }
+            } catch (SQLException ex) {
+                throw new CommandException(ex);
+            }
+        }
+
+        protected abstract void execute(Statement stmt, String sql) throws SQLException;
+
+    }
+
+    /**
+     * Select statement command.
+     */
+    static final class Select extends RawSQL {
+
+        public Select() {
+            // empty
+        }
+
+        @Override
+        public boolean isReadOnly() {
+            return true;
+        }
+
+        @Override
+        public void execute(Statement stmt, String rawString) throws SQLException {
+            final long startTime = System.currentTimeMillis();
+            ResultSet rs = executeQuery(stmt, rawString);
+            try {
+                outputMessage("i.response-time", (System.currentTimeMillis() - startTime) / 1000f);
+                ResultSetReference ref = new ResultSetReference(rs, rawString);
+                output(ref);
+                outputMessage("i.selected", ref.getRecordCount());
+            } finally {
+                rs.close();
+            }
+        }
+
+    }
+
+    /**
+     * Update statement (contains all SQL excepted a Select SQL) command.
+     */
+    static final class UpdateAndOthers extends RawSQL {
+
+        public UpdateAndOthers() {
+            // empty
+        }
+
+        @Override
+        public boolean isReadOnly() {
+            return false;
+        }
+
+        @Override
+        protected void execute(Statement stmt, String sql) throws SQLException {
+            final int updatedCount = executeUpdate(stmt, sql);
+            final String msgId;
+            if (sql.matches("(?i)\\s*UPDATE.*")) {
+                msgId = "i.updated";
+            } else if (sql.matches("(?i)\\\\s*INSERT.*")) {
+                msgId = "i.inserted";
+            } else if (sql.matches("(?i)\\\\s*DELETE.*")) {
+                msgId = "i.deleted";
+            } else {
+                msgId = "i.proceeded";
+            }
+            outputMessage(msgId, updatedCount);
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/Connector.java b/src/net/argius/stew/Connector.java
new file mode 100644 (file)
index 0000000..eeb7c87
--- /dev/null
@@ -0,0 +1,195 @@
+package net.argius.stew;
+
+import java.sql.*;
+import java.util.*;
+
+/**
+ * This class provides functions to manage database connections in this application. 
+ */
+public final class Connector {
+
+    private static final Logger log = Logger.getLogger(Connector.class);
+
+    private final String id;
+    private final Properties props;
+    private final Password password;
+
+    private transient Driver driver;
+
+    /**
+     * A constructor.
+     * @param id
+     * @param props
+     */
+    public Connector(String id, Properties props) {
+        assert id != null;
+        if (!id.matches("[A-Za-z0-9]+")) {
+            throw new IllegalArgumentException("illegal id : " + id);
+        }
+        Properties p = new Properties();
+        p.putAll(props);
+        Password password = createPasswordInstance(props.getProperty("password.class"));
+        password.setTransformedString(props.getProperty("password"));
+        this.id = id;
+        this.props = p;
+        this.password = password;
+    }
+
+    private static Password createPasswordInstance(String className) {
+        if (className != null) {
+            try {
+                return (Password)DynamicLoader.newInstance(className);
+            } catch (Exception ex) {
+                log.warn(ex);
+            }
+        }
+        return new PlainTextPassword();
+    }
+
+    /**
+     * A constructor (for copying).
+     * @param id
+     * @param src
+     */
+    public Connector(String id, Connector src) {
+        this(id, (Properties)src.props.clone());
+    }
+
+    /**
+     * Returns the ID.
+     * @return
+     */
+    public String getId() {
+        return id;
+    }
+
+    /**
+     * Returns the name.
+     * @return
+     */
+    public String getName() {
+        return props.getProperty("name");
+    }
+
+    /**
+     * Returns the classpath.
+     * @return
+     */
+    public String getClasspath() {
+        return props.getProperty("classpath", "");
+    }
+
+    /**
+     * Returns the JDBC driver (class name).
+     * @return
+     */
+    public String getDriver() {
+        final String driver = props.getProperty("driver");
+        log.debug("driver=[%s]", driver);
+        return driver;
+    }
+
+    /**
+     * Returns the URL.
+     * @return
+     */
+    public String getUrl() {
+        return props.getProperty("url");
+    }
+
+    /**
+     * Returns the user.
+     * @return
+     */
+    public String getUser() {
+        return props.getProperty("user");
+    }
+
+    /**
+     * Returns the Password object.
+     * @return
+     */
+    public Password getPassword() {
+        return password;
+    }
+
+    /**
+     * Returns whether the connection is read-only or not.
+     * @return
+     */
+    public boolean isReadOnly() {
+        String s = props.getProperty("readonly");
+        return Boolean.valueOf(s).booleanValue();
+    }
+
+    /**
+     * Returns whether the connection uses auto-rollback or not.
+     * @return
+     */
+    public boolean usesAutoRollback() {
+        String s = props.getProperty("rollback");
+        return Boolean.valueOf(s).booleanValue();
+    }
+
+    /**
+     * Converts this to Properties.
+     * @return
+     */
+    public Properties toProperties() {
+        return (Properties)props.clone();
+    }
+
+    /**
+     * Attempts to establish a connection.
+     * @return
+     * @throws SQLException
+     */
+    public Connection getConnection() throws SQLException {
+        if (driver == null) {
+            driver = ConnectorDriverManager.getDriver(getUrl(), getDriver(), getClasspath());
+            if (driver == null) {
+                throw new SQLException("failed to load driver");
+            }
+            log.debug(driver);
+        }
+        Properties p = new Properties();
+        p.setProperty("user", getUser());
+        p.setProperty("password", getPassword().getRawString());
+        if (!driver.acceptsURL(getUrl())) {
+            throw new SQLException("invalid url: " + getUrl());
+        }
+        log.info("driver.connect start");
+        Connection conn = driver.connect(getUrl(), p);
+        log.info("driver.connect end");
+        if (conn == null) {
+            throw new IllegalStateException("driver returned null");
+        }
+        return conn;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((props == null) ? 0 : props.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof Connector)) {
+            return false;
+        }
+        Connector other = (Connector)obj;
+        return props.equals(other.props);
+    }
+
+    @Override
+    public String toString() {
+        return "Connector:" + id;
+    }
+
+}
\ No newline at end of file
diff --git a/src/net/argius/stew/ConnectorConfiguration.java b/src/net/argius/stew/ConnectorConfiguration.java
new file mode 100644 (file)
index 0000000..58e2d8b
--- /dev/null
@@ -0,0 +1,157 @@
+package net.argius.stew;
+
+import java.io.*;
+import java.nio.channels.*;
+import java.util.*;
+import java.util.regex.*;
+
+/**
+ * ConnectorConfiguration is a helper for ConnectorMap.
+ */
+public final class ConnectorConfiguration {
+
+    private static final Pattern idPattern = Pattern.compile("^([^\\.]+)\\.name *=");
+
+    /**
+     * Loads configurations from a file.
+     * @return
+     * @throws IOException
+     */
+    public static ConnectorMap load() throws IOException {
+        final File f = getPath();
+        InputStream is = (f.exists())
+                ? new FileInputStream(f)
+                : new ByteArrayInputStream(new byte[0]);
+        try {
+            return load(is);
+        } finally {
+            is.close();
+        }
+    }
+
+    /**
+     * Loads configurations from a file.
+     * @param is
+     * @return
+     * @throws IOException
+     */
+    public static ConnectorMap load(InputStream is) throws IOException {
+        // cache for reuse
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[4096];
+        for (int c; (c = is.read(buffer)) >= 0;) {
+            bos.write(buffer, 0, c);
+        }
+        bos.flush();
+        // create ID list
+        List<String> idList = new ArrayList<String>();
+        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+        BufferedReader reader = new BufferedReader(new InputStreamReader(bis));
+        try {
+            for (String line; (line = reader.readLine()) != null;) {
+                Matcher matcher = idPattern.matcher(line);
+                if (matcher.find()) {
+                    idList.add(matcher.group(1));
+                }
+            }
+        } finally {
+            reader.close();
+        }
+        // read as Properties
+        Properties props = new Properties();
+        props.load(new ByteArrayInputStream(bos.toByteArray()));
+        // creates a instance
+        return new ConnectorMap(idList, props);
+    }
+
+    /**
+     * Saves configurations to a file.
+     * @param map
+     * @throws IOException
+     */
+    public static void save(ConnectorMap map) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        save(bos, map);
+        byte[] bytes = bos.toByteArray();
+        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
+        FileOutputStream fos = new FileOutputStream(getPath());
+        try {
+            fos.getChannel().transferFrom(Channels.newChannel(bis), 0, bytes.length);
+        } finally {
+            fos.close();
+        }
+    }
+
+    /**
+     * Saves configurations to a file.
+     * @param os
+     * @param map
+     * @throws IOException
+     */
+    public static void save(OutputStream os, ConnectorMap map) throws IOException {
+        // import using store
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        map.toProperties().store(bos, "");
+        // lines to elements
+        List<String> lines = new ArrayList<String>();
+        Scanner scanner = new Scanner(new ByteArrayInputStream(bos.toByteArray()));
+        try {
+            while (scanner.hasNextLine()) {
+                String line = scanner.nextLine();
+                if (!line.trim().startsWith("#")) {
+                    lines.add(line);
+                }
+            }
+        } finally {
+            scanner.close();
+        }
+        // rewrites records sorted by ID 
+        Comparator<String> c = new ConnectorPropertyComparator(new ArrayList<String>(map.keySet()));
+        Collections.sort(lines, c);
+        PrintWriter out = new PrintWriter(os);
+        try {
+            for (String line : lines) {
+                out.println(line);
+            }
+            out.flush();
+        } finally {
+            out.close();
+        }
+    }
+
+    private static File getPath() {
+        return new File(Bootstrap.getDirectory(), Environment.CONNECTOR_PROPERTIES_NAME);
+    }
+
+    private static final class ConnectorPropertyComparator implements
+                                                          Comparator<String>,
+                                                          Serializable {
+
+        private final List<String> idList;
+
+        ConnectorPropertyComparator(List<String> idList) {
+            this.idList = idList;
+        }
+
+        @Override
+        public int compare(String s1, String s2) {
+            int index1 = getIdIndex(s1);
+            int index2 = getIdIndex(s2);
+            if (index1 == index2) {
+                return s1.compareTo(s2);
+            }
+            return index1 - index2;
+        }
+
+        private int getIdIndex(String s) {
+            String[] sa = s.split("\\.", 2);
+            if (sa.length >= 2) {
+                String id = sa[0];
+                return idList.indexOf(id);
+            }
+            return -1;
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/ConnectorDriverManager.java b/src/net/argius/stew/ConnectorDriverManager.java
new file mode 100644 (file)
index 0000000..b8804e5
--- /dev/null
@@ -0,0 +1,177 @@
+package net.argius.stew;
+
+import static java.io.File.pathSeparator;
+
+import java.io.*;
+import java.net.*;
+import java.sql.*;
+import java.util.*;
+import java.util.zip.*;
+
+/**
+ * A driver manager for Connector.
+ */
+final class ConnectorDriverManager {
+
+    private static final Logger log = Logger.getLogger(ConnectorDriverManager.class);
+
+    static final Set<File> driverFiles = Collections.synchronizedSet(new LinkedHashSet<File>());
+    static final Set<Driver> drivers = Collections.synchronizedSet(new LinkedHashSet<Driver>());
+
+    private ConnectorDriverManager() {
+    } // forbidden
+
+    static Driver getDriver(String url, String driverClassName, String classpath) throws SQLException {
+        assert !isBlank(url);
+        final boolean hasClasspath = !isBlank(classpath);
+        if (!hasClasspath) {
+            for (Driver driver : new ArrayList<Driver>(drivers)) {
+                if (driver.acceptsURL(url)) {
+                    return driver;
+                }
+            }
+        }
+        List<File> jars = new ArrayList<File>();
+        ClassLoader cl;
+        if (hasClasspath) {
+            List<URL> urls = new ArrayList<URL>();
+            for (String path : classpath.split(pathSeparator)) {
+                final File file = new File(path);
+                if (isJarFile(file)) {
+                    jars.add(file);
+                }
+                try {
+                    urls.add(file.toURI().toURL());
+                } catch (MalformedURLException ex) {
+                    log.warn(ex);
+                }
+            }
+            cl = new URLClassLoader(urls.toArray(new URL[urls.size()]));
+        } else {
+            jars.addAll(getJarFiles("."));
+            jars.addAll(driverFiles);
+            List<URL> urls = new ArrayList<URL>();
+            for (File file : jars) {
+                try {
+                    urls.add(file.toURI().toURL());
+                } catch (MalformedURLException ex) {
+                    log.warn(ex);
+                }
+            }
+            cl = new URLClassLoader(urls.toArray(new URL[urls.size()]),
+                                    ClassLoader.getSystemClassLoader());
+        }
+        driverFiles.addAll(jars);
+        final boolean hasDriverClassName = !isBlank(driverClassName);
+        if (hasDriverClassName) {
+            try {
+                Driver driver = DynamicLoader.newInstance(driverClassName, cl);
+                assert driver != null;
+                return driver;
+            } catch (DynamicLoadingException ex) {
+                Throwable cause = (ex.getCause() != ex) ? ex.getCause() : ex;
+                SQLException exception = new SQLException(cause.toString());
+                exception.initCause(cause);
+                throw exception;
+            }
+        }
+        final String jdbcDrivers = System.getProperty("jdbc.drivers");
+        if (!isBlank(jdbcDrivers)) {
+            for (String jdbcDriver : jdbcDrivers.split(":")) {
+                try {
+                    Driver driver = DynamicLoader.newInstance(jdbcDriver, cl);
+                    if (driver != null) {
+                        if (!hasClasspath) {
+                            drivers.add(driver);
+                        }
+                        return driver;
+                    }
+                } catch (DynamicLoadingException ex) {
+                    log.warn(ex);
+                }
+            }
+        }
+        for (File jar : jars) {
+            try {
+                Driver driver = getDriver(jar, url, cl);
+                if (driver != null) {
+                    if (!hasClasspath) {
+                        drivers.add(driver);
+                    }
+                    return driver;
+                }
+            } catch (IOException ex) {
+                log.warn(ex);
+            }
+        }
+        for (String path : System.getProperty("java.class.path", "").split(pathSeparator)) {
+            if (isJarFile(path)) {
+                Driver driver;
+                try {
+                    driver = getDriver(new File(path), url, cl);
+                    if (driver != null) {
+                        drivers.add(driver);
+                        return driver;
+                    }
+                } catch (IOException ex) {
+                    log.warn(ex);
+                }
+            }
+        }
+        throw new SQLException("driver not found");
+    }
+
+    private static Driver getDriver(File jar, String url, ClassLoader cl) throws IOException {
+        ZipFile zipFile = new ZipFile(jar);
+        try {
+            for (ZipEntry entry : Collections.list(zipFile.entries())) {
+                final String name = entry.getName();
+                if (name.endsWith(".class")) {
+                    final String fqcn = name.replaceFirst("\\.class", "").replace('/', '.');
+                    try {
+                        Class<?> c = DynamicLoader.loadClass(fqcn, cl);
+                        if (Driver.class.isAssignableFrom(c)) {
+                            Driver driver = (Driver)c.newInstance();
+                            if (driver.acceptsURL(url)) {
+                                return driver;
+                            }
+                        }
+                    } catch (Exception ex) {
+                        log.trace(ex);
+                    }
+                }
+            }
+        } finally {
+            zipFile.close();
+        }
+        return null;
+    }
+
+    private static List<File> getJarFiles(String path) {
+        File root = new File(path);
+        File[] files = root.listFiles();
+        if (files == null) {
+            return Collections.emptyList();
+        }
+        List<File> jars = new ArrayList<File>();
+        for (File file : files) {
+            if (isJarFile(file)) {
+                jars.add(file);
+            }
+        }
+        return jars;
+    }
+
+    private static boolean isJarFile(File file) {
+        return isJarFile(file.getPath());
+    }
+
+    private static boolean isJarFile(String path) {
+        return path.matches("(?i).+\\.(jar|zip)");
+    }
+
+    private static boolean isBlank(String s) {
+        return s == null || s.trim().length() == 0;
+    }
+
+}
diff --git a/src/net/argius/stew/ConnectorMap.java b/src/net/argius/stew/ConnectorMap.java
new file mode 100644 (file)
index 0000000..c4abdb4
--- /dev/null
@@ -0,0 +1,93 @@
+package net.argius.stew;
+
+import java.util.*;
+
+/**
+ * ConnectorMap provides a mapping to associate an Connector with its own ID. 
+ */
+public final class ConnectorMap extends LinkedHashMap<String, Connector> {
+
+    /**
+     * A constructor.
+     */
+    public ConnectorMap() {
+        // empty
+    }
+
+    /**
+     * A constructor to create from a Properties.
+     * @param idList
+     * @param props
+     */
+    public ConnectorMap(List<String> idList, Properties props) {
+        for (String id : idList) {
+            Properties p = new Properties();
+            copyPropertyById(id, "name", props, p);
+            copyPropertyById(id, "driver", props, p);
+            copyPropertyById(id, "classpath", props, p);
+            copyPropertyById(id, "url", props, p);
+            copyPropertyById(id, "user", props, p);
+            copyPropertyById(id, "password", props, p);
+            copyPropertyById(id, "password.class", props, p);
+            copyPropertyById(id, "readonly", props, p);
+            copyPropertyById(id, "rollback", props, p);
+            Connector connector = new Connector(id, p);
+            put(id, connector);
+        }
+    }
+
+    /**
+     * A copy constructor.
+     * @param src
+     */
+    public ConnectorMap(ConnectorMap src) {
+        putAll(src);
+    }
+
+    private static void copyPropertyById(String id, String key, Properties src, Properties dst) {
+        String fullKey = id + '.' + key;
+        String value = src.getProperty(fullKey, "");
+        dst.setProperty(key, value);
+    }
+
+    /**
+     * Returns the connector specified by ID.
+     * @param id
+     * @return
+     */
+    public Connector getConnector(String id) {
+        return get(id);
+    }
+
+    /**
+     * Sets a connector.
+     * @param id
+     * @param connector
+     */
+    public void setConnector(String id, Connector connector) {
+        put(id, connector);
+    }
+
+    /**
+     * Returns this map as Properties.
+     * @return
+     */
+    public Properties toProperties() {
+        Properties props = new Properties();
+        for (String id : keySet()) {
+            Connector connector = getConnector(id);
+            Password password = connector.getPassword();
+            props.setProperty(id + ".name", connector.getName());
+            props.setProperty(id + ".driver", connector.getDriver());
+            props.setProperty(id + ".classpath", connector.getClasspath());
+            props.setProperty(id + ".url", connector.getUrl());
+            props.setProperty(id + ".user", connector.getUser());
+            props.setProperty(id + ".password", password.getTransformedString());
+            props.setProperty(id + ".password.class", password.getClass().getName());
+            props.setProperty(id + ".readonly", Boolean.toString(connector.isReadOnly()));
+            props.setProperty(id + ".rollback", Boolean.toString(connector.usesAutoRollback()));
+        }
+        return props;
+    }
+
+}
diff --git a/src/net/argius/stew/DaemonThreadFactory.java b/src/net/argius/stew/DaemonThreadFactory.java
new file mode 100644 (file)
index 0000000..340b6b5
--- /dev/null
@@ -0,0 +1,49 @@
+package net.argius.stew;
+
+import java.util.concurrent.*;
+
+/**
+ * This is a ThreadFactory which creates threads as a daemon.
+ */
+public final class DaemonThreadFactory implements ThreadFactory {
+
+    private static final Logger log = Logger.getLogger(DaemonThreadFactory.class);
+
+    private static volatile int count;
+
+    private static ThreadFactory instance;
+
+    private DaemonThreadFactory() {
+    }
+
+    /**
+     * Returns an instance of DaemonThreadFactory (as ThreadFactory).
+     * @return
+     */
+    public static ThreadFactory getInstance() {
+        if (instance == null) {
+            instance = new DaemonThreadFactory();
+        }
+        return instance;
+    }
+
+    @Override
+    public Thread newThread(Runnable r) {
+        final String name = String.format("ChildDaemon%d-of-%s", count++, Thread.currentThread());
+        if (log.isDebugEnabled()) {
+            log.debug("create thread: name=" + name);
+        }
+        Thread thread = new Thread(r, name);
+        thread.setDaemon(true);
+        return thread;
+    }
+
+    /**
+     * Executes a task by DaemonThread.
+     * @param task
+     */
+    public static void execute(Runnable task) {
+        getInstance().newThread(task).start();
+    }
+
+}
diff --git a/src/net/argius/stew/DynamicLoader.java b/src/net/argius/stew/DynamicLoader.java
new file mode 100644 (file)
index 0000000..dc54561
--- /dev/null
@@ -0,0 +1,131 @@
+package net.argius.stew;
+
+import java.net.*;
+import java.security.*;
+
+/**
+ * DynamicLoader provides functions to load Class dynamically.
+ */
+public final class DynamicLoader {
+
+    private DynamicLoader() {
+        // empty
+    }
+
+    /**
+     * Loads the Class specified by the name.
+     * @param <T>
+     * @param className
+     * @return
+     * @throws DynamicLoadingException
+     */
+    public static <T> Class<T> loadClass(String className) throws DynamicLoadingException {
+        return loadClass(className, ClassLoader.getSystemClassLoader());
+    }
+
+    /**
+     * Loads the Class specified by the name and ClassLoader.
+     * @param <T>
+     * @param className
+     * @param classLoader
+     * @return
+     * @throws DynamicLoadingException
+     */
+    public static <T> Class<T> loadClass(String className, ClassLoader classLoader) throws DynamicLoadingException {
+        try {
+            @SuppressWarnings("unchecked")
+            Class<T> c = (Class<T>)classLoader.loadClass(className);
+            return c;
+        } catch (Throwable th) {
+            throw new DynamicLoadingException("class loading error", th);
+        }
+    }
+
+    /**
+     * Creates a new instance.
+     * @param <T>
+     * @param className
+     * @return
+     * @throws DynamicLoadingException
+     */
+    public static <T> T newInstance(String className) throws DynamicLoadingException {
+        // T o = newInstance(className, DynamicLoader.class.getClassLoader());
+        @SuppressWarnings("unchecked")
+        T o = (T)newInstance(className, DynamicLoader.class.getClassLoader());
+        return o;
+    }
+
+    /**
+     * Creates a new instance.
+     * @param <T>
+     * @param className
+     * @param urls
+     * @return
+     * @throws DynamicLoadingException
+     */
+    public static <T> T newInstance(String className, URL... urls) throws DynamicLoadingException {
+        // T o = newInstance(className, getURLClassLoader(urls));
+        @SuppressWarnings("unchecked")
+        T o = (T)newInstance(className, getClassLoader(urls));
+        return o;
+    }
+
+    /**
+     * Creates a new instance.
+     * @param <T>
+     * @param className
+     * @param classLoader
+     * @return
+     * @throws DynamicLoadingException
+     */
+    public static <T> T newInstance(String className, ClassLoader classLoader) throws DynamicLoadingException {
+        try {
+            Class<T> c = loadClass(className, classLoader);
+            return c.newInstance();
+        } catch (DynamicLoadingException ex) {
+            throw ex;
+        } catch (Throwable th) {
+            throw new DynamicLoadingException("load error: " + className, th);
+        }
+    }
+
+    /**
+     * Creates a new instance.
+     * @param <T>
+     * @param classObject
+     * @return
+     * @throws DynamicLoadingException
+     */
+    public static <T> T newInstance(Class<T> classObject) throws DynamicLoadingException {
+        try {
+            return classObject.newInstance();
+        } catch (Throwable th) {
+            throw new DynamicLoadingException("load error: " + classObject, th);
+        }
+    }
+
+    /**
+     * Returns a new URLClassLoader that creates from URL.
+     * @param urls
+     * @return
+     */
+    public static URLClassLoader getClassLoader(URL... urls) {
+        return (URLClassLoader)AccessController.doPrivileged(new GettingURLClassLoaderPrivilegedAction(urls));
+    }
+
+    private static final class GettingURLClassLoaderPrivilegedAction implements PrivilegedAction<Object> {
+
+        private final URL[] urls;
+    
+        GettingURLClassLoaderPrivilegedAction(URL[] urls) {
+            this.urls = urls;
+        }
+    
+        @Override
+        public Object run() {
+            return new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/DynamicLoadingException.java b/src/net/argius/stew/DynamicLoadingException.java
new file mode 100644 (file)
index 0000000..8e4c8bb
--- /dev/null
@@ -0,0 +1,9 @@
+package net.argius.stew;
+
+public class DynamicLoadingException extends RuntimeException {
+
+    public DynamicLoadingException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/net/argius/stew/Environment.java b/src/net/argius/stew/Environment.java
new file mode 100644 (file)
index 0000000..217942e
--- /dev/null
@@ -0,0 +1,267 @@
+package net.argius.stew;
+
+import static net.argius.stew.Bootstrap.getDirectory;
+
+import java.io.*;
+import java.sql.*;
+
+import net.argius.stew.ui.*;
+
+/**
+ * Environment.
+ */
+public final class Environment {
+
+    static final String CONNECTOR_PROPERTIES_NAME = "connector.properties";
+    static final String ALIAS_PROPERTIES_NAME = "alias.properties";
+
+    private static final Logger log = Logger.getLogger(Environment.class);
+    static ResourceManager res = ResourceManager.getInstance(Environment.class);
+
+    private OutputProcessor outputProcessor;
+    private ConnectorMap connectorMap;
+    private Connector connector;
+    private Connection conn;
+
+    private int timeoutSeconds;
+    private File systemDirectory;
+    private File currentDirectory;
+    private long connectorTimestamp;
+    private Alias alias;
+
+    /**
+     * A constructor.
+     */
+    public Environment() {
+        initializeQueryTimeout();
+        // init connections
+        this.connectorMap = new ConnectorMap();
+        loadConnectorMap();
+        // init directories
+        this.systemDirectory = getDirectory();
+        this.currentDirectory = getInitialCurrentDirectory();
+        // init alias
+        final File aliasPropFile = new File(this.systemDirectory, ALIAS_PROPERTIES_NAME);
+        this.alias = new Alias(aliasPropFile);
+        if (aliasPropFile.exists()) {
+            try {
+                alias.load();
+            } catch (IOException ex) {
+                log.warn(ex);
+            }
+        }
+    }
+
+    /**
+     * A constructor (for copy).
+     * @param src
+     */
+    public Environment(Environment src) {
+        // never copy coconnector,conn,op into this
+        this.connectorMap = new ConnectorMap(src.connectorMap);
+        this.timeoutSeconds = src.timeoutSeconds;
+        this.systemDirectory = src.systemDirectory;
+        this.currentDirectory = src.currentDirectory;
+    }
+
+    /**
+     * Releases resouces it keeps.
+     */
+    public void release() {
+        try {
+            releaseConnection();
+            log.debug("released connection");
+        } catch (SQLException ex) {
+            log.error(ex, "release error");
+        } finally {
+            outputProcessor = null;
+            connectorMap = null;
+            connector = null;
+            conn = null;
+            systemDirectory = null;
+            currentDirectory = null;
+        }
+        log.debug("released internal state of Environment");
+    }
+
+    /**
+     * Establishes a connection.
+     * @param connector
+     * @throws SQLException
+     */
+    void establishConnection(Connector connector) throws SQLException {
+        Connection conn = connector.getConnection();
+        try {
+            if (connector.isReadOnly()) {
+                conn.setReadOnly(true);
+            }
+        } catch (RuntimeException ex) {
+            log.warn(ex);
+        }
+        boolean isAutoCommitAvailable;
+        try {
+            conn.setAutoCommit(false);
+            isAutoCommitAvailable = conn.getAutoCommit();
+        } catch (RuntimeException ex) {
+            log.warn(ex);
+            isAutoCommitAvailable = false;
+        }
+        if (isAutoCommitAvailable) {
+            outputMessage("w.auto-commit-not-available");
+        }
+        setCurrentConnection(conn);
+        setCurrentConnector(connector);
+        outputMessage("i.connected");
+        log.debug("connected %s (conn=%08x, env=%08x)", connector.getId(), conn.hashCode(), hashCode());
+        if (Bootstrap.getPropertyAsBoolean("net.argius.stew.print-connected-time")) {
+            outputMessage("i.now", System.currentTimeMillis());
+        }
+    }
+
+    /**
+     * Releases the connection.
+     * @throws SQLException
+     */
+    void releaseConnection() throws SQLException {
+        if (conn == null) {
+            log.debug("not connected");
+            return;
+        }
+        try {
+            if (connector != null && connector.usesAutoRollback()) {
+                try {
+                    conn.rollback();
+                    outputMessage("i.rollbacked");
+                    log.debug("rollbacked %s (%s)", connector.getId(), conn);
+                } catch (SQLException ex) {
+                    log.warn(ex);
+                }
+            }
+            try {
+                conn.close();
+                log.debug("disconnected %s (conn=%08x, env=%08x)", connector.getId(), conn.hashCode(), hashCode());
+                if (Bootstrap.getPropertyAsBoolean("net.argius.stew.print-disconnected-time")) {
+                    outputMessage("i.now", System.currentTimeMillis());
+                }
+            } catch (SQLException ex) {
+                log.warn(ex);
+                throw ex;
+            }
+        } finally {
+            conn = null;
+            connector = null;
+        }
+    }
+
+    private void outputMessage(String id, Object... args) throws CommandException {
+        if (outputProcessor != null) {
+            outputProcessor.output(res.get(id, args));
+        }
+    }
+
+    private static File getInitialCurrentDirectory() {
+        final String propkey = "net.argius.stew.directory";
+        if (Bootstrap.hasProperty(propkey)) {
+            File directory = new File(Bootstrap.getProperty(propkey, ""));
+            if (directory.isDirectory()) {
+                return directory;
+            }
+        }
+        return new File(".");
+    }
+
+    private void initializeQueryTimeout() {
+        this.timeoutSeconds = Bootstrap.getPropertyAsInt("net.argius.stew.query.timeout", -1);
+        if (log.isDebugEnabled()) {
+            log.debug("timeout: " + this.timeoutSeconds);
+        }
+    }
+
+    /**
+     * Loads and refreshes connector map.
+     */
+    public void loadConnectorMap() {
+        File connectorFile = new File(getDirectory(), CONNECTOR_PROPERTIES_NAME);
+        ConnectorMap m;
+        try {
+            InputStream is = new FileInputStream(connectorFile);
+            try {
+                m = ConnectorConfiguration.load(is);
+            } finally {
+                is.close();
+            }
+        } catch (IOException ex) {
+            m = new ConnectorMap();
+        }
+        synchronized (connectorMap) {
+            if (connectorMap.size() > 0) {
+                connectorMap.clear();
+            }
+            connectorMap.putAll(m);
+            connectorTimestamp = connectorFile.lastModified();
+        }
+    }
+
+    /**
+     * Updates connector map.
+     * When file was updated, it calls loadConnectorMap().
+     * @return whether updated or not
+     */
+    public boolean updateConnectorMap() {
+        File connectorFile = new File(getDirectory(), CONNECTOR_PROPERTIES_NAME);
+        if (connectorFile.lastModified() > connectorTimestamp) {
+            loadConnectorMap();
+            return true;
+        }
+        return false;
+    }
+
+    public OutputProcessor getOutputProcessor() {
+        return outputProcessor;
+    }
+
+    public void setOutputProcessor(OutputProcessor outputProcessor) {
+        this.outputProcessor = outputProcessor;
+    }
+
+    public ConnectorMap getConnectorMap() {
+        return connectorMap;
+    }
+
+    public Connector getCurrentConnector() {
+        return connector;
+    }
+
+    void setCurrentConnector(Connector connector) {
+        this.connector = connector;
+    }
+
+    public Connection getCurrentConnection() {
+        return conn;
+    }
+
+    void setCurrentConnection(Connection conn) {
+        this.conn = conn;
+    }
+
+    public int getTimeoutSeconds() {
+        return timeoutSeconds;
+    }
+
+    public File getCurrentDirectory() {
+        return currentDirectory;
+    }
+
+    public void setCurrentDirectory(File currentDirectory) {
+        this.currentDirectory = currentDirectory;
+    }
+
+    public File getSystemDirectory() {
+        return systemDirectory;
+    }
+
+    public Alias getAlias() {
+        return alias;
+    }
+
+}
diff --git a/src/net/argius/stew/Logger.java b/src/net/argius/stew/Logger.java
new file mode 100644 (file)
index 0000000..327b463
--- /dev/null
@@ -0,0 +1,236 @@
+package net.argius.stew;
+
+import java.util.logging.*;
+
+/**
+ * The Logger which has Apache Logging style interface.
+ *
+ * <p>The log levels map to core API's log level as below.</p><ul>
+ * <li> fatal: Level.SEVERE (and "fatal" message)
+ * <li> error: Level.SEVERE
+ * <li> warn: Level.WARNING
+ * <li> info: Level.INFO
+ * <li> debug: Level.FINE
+ * <li> trace: Level.FINER
+ * </ul>
+ * Level.CONFIG is not used.
+ */
+public final class Logger {
+
+    private final java.util.logging.Logger log;
+
+    private String enteredMethodName;
+
+    Logger(String name) {
+        this.log = java.util.logging.Logger.getLogger(name);
+        removeRootLoggerHandlers();
+    }
+
+    Logger(Class<?> c) {
+        this(c.getName());
+    }
+
+    static void removeRootLoggerHandlers() {
+        java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");
+        for (Handler handler : rootLogger.getHandlers()) {
+            rootLogger.removeHandler(handler);
+        }
+    }
+
+    public static Logger getLogger(Object o) {
+        final String name;
+        if (o instanceof Class) {
+            Class<?> c = (Class<?>)o;
+            name = c.getName();
+        } else if (o instanceof String) {
+            name = (String)o;
+        } else {
+            name = String.valueOf(o);
+        }
+        return new Logger(name);
+    }
+
+    /**
+     * The INFO level maps to logging.Level.INFO .
+     * @return info level is enabled
+     */
+    public boolean isInfoEnabled() {
+        return log.isLoggable(Level.INFO);
+    }
+
+    /**
+     * The DEBUG level maps to logging.Level.FINE .
+     * @return debug level is enabled
+     */
+    public boolean isDebugEnabled() {
+        return log.isLoggable(Level.FINE);
+    }
+
+    /**
+     * The TRACE level maps to logging.Level.FINER .
+     * @return debug level is enabled
+     */
+    public boolean isTraceEnabled() {
+        return log.isLoggable(Level.FINER);
+    }
+
+    public String getEnteredMethodName() {
+        return (enteredMethodName == null) ? "" : enteredMethodName;
+    }
+
+    public void setEnteredMethodName(String methodName) {
+        enteredMethodName = (methodName == null) ? "" : methodName;
+    }
+
+    public void log(Level level, Throwable th, String format, Object... args) {
+        if (log.isLoggable(level)) {
+            final String cn = log.getName();
+            final String mn = (enteredMethodName == null) ? "(unknown method)" : enteredMethodName;
+            if (th == null) {
+                log.logp(level, cn, mn, String.format(format, args));
+            } else {
+                log.logp(level, cn, mn, String.format(format, args), th);
+            }
+        }
+    }
+
+    public void fatal(Throwable th) {
+        if (log.isLoggable(Level.SEVERE)) {
+            log(Level.SEVERE, th, "*FATAL*");
+        }
+    }
+
+    public void fatal(Throwable th, String format, Object... args) {
+        if (log.isLoggable(Level.SEVERE)) {
+            log(Level.SEVERE, th, "*FATAL* " + String.format(format, args));
+        }
+    }
+
+    public void error(Throwable th) {
+        if (log.isLoggable(Level.SEVERE)) {
+            log(Level.SEVERE, th, "");
+        }
+    }
+
+    public void error(Throwable th, Object o) {
+        if (log.isLoggable(Level.SEVERE)) {
+            log(Level.SEVERE, th, String.valueOf(o));
+        }
+    }
+
+    public void error(Throwable th, String format, Object arg) {
+        if (log.isLoggable(Level.SEVERE)) {
+            log(Level.SEVERE, th, format, arg);
+        }
+    }
+
+    public void warn(Throwable th) {
+        if (log.isLoggable(Level.WARNING)) {
+            log(Level.WARNING, th, "");
+        }
+    }
+
+    public void warn(Throwable th, Object o) {
+        if (log.isLoggable(Level.WARNING)) {
+            log(Level.WARNING, th, String.valueOf(o));
+        }
+    }
+
+    public void warn(String format, Object... args) {
+        if (log.isLoggable(Level.WARNING)) {
+            log(Level.WARNING, null, format, args);
+        }
+    }
+
+    public void info(Object o) {
+        if (isInfoEnabled()) {
+            log(Level.INFO, null, String.valueOf(o));
+        }
+    }
+
+    public void info(String format, Object arg) {
+        if (isInfoEnabled()) {
+            log(Level.INFO, null, format, arg);
+        }
+    }
+
+    public void info(String format, Object arg1, Object arg2) {
+        if (isInfoEnabled()) {
+            log(Level.INFO, null, format, arg1, arg2);
+        }
+    }
+
+    public void info(String format, Object... args) {
+        if (isInfoEnabled()) {
+            log(Level.INFO, null, format, args);
+        }
+    }
+
+    public void debug(Object o) {
+        if (isDebugEnabled()) {
+            log(Level.FINE, null, String.valueOf(o));
+        }
+    }
+
+    public void debug(String format, Object arg) {
+        if (isDebugEnabled()) {
+            log(Level.FINE, null, format, arg);
+        }
+    }
+
+    public void debug(String format, Object arg1, Object arg2) {
+        if (isDebugEnabled()) {
+            log(Level.FINE, null, format, arg1, arg2);
+        }
+    }
+
+    public void debug(String format, Object... args) {
+        if (isDebugEnabled()) {
+            log(Level.FINE, null, format, args);
+        }
+    }
+
+    public void trace(Throwable th) {
+        if (isTraceEnabled()) {
+            log(Level.FINER, th, "");
+        }
+    }
+
+    public void trace(Object o) {
+        if (isTraceEnabled()) {
+            log(Level.FINER, null, String.valueOf(o));
+        }
+    }
+
+    public void trace(String format, Object arg) {
+        if (isTraceEnabled()) {
+            log(Level.FINER, null, format, arg);
+        }
+    }
+
+    public void trace(String format, Object... args) {
+        if (isTraceEnabled()) {
+            log(Level.FINER, null, format, args);
+        }
+    }
+
+    public void atEnter(String method, Object... args) {
+        // trace("entering method [%s]", method);
+        setEnteredMethodName(method);
+        log.entering(log.getName(), method, args);
+    }
+
+    public void atExit(String method) {
+        // trace("exiting method [%s] with return value [%s]", method, returnValue);
+        log.exiting(log.getName(), method);
+        setEnteredMethodName("");
+    }
+
+    public <T> T atExit(String method, T returnValue) {
+        // trace("exiting method [%s] with return value [%s]", method, returnValue);
+        log.exiting(log.getName(), method, returnValue);
+        setEnteredMethodName("");
+        return returnValue;
+    }
+
+}
diff --git a/src/net/argius/stew/Parameter.java b/src/net/argius/stew/Parameter.java
new file mode 100644 (file)
index 0000000..75f7892
--- /dev/null
@@ -0,0 +1,132 @@
+package net.argius.stew;
+
+import java.util.*;
+
+/**
+ * Parameter.
+ */
+public final class Parameter {
+
+    private final String string;
+    private final String[] array;
+    private final int[] indices;
+
+    /**
+     * A constructor.
+     * @param string
+     */
+    public Parameter(String string) {
+        char[] chars = string.toCharArray();
+        int[] indices = indices(chars);
+        String[] array = array(chars, indices);
+        this.string = string;
+        this.array = array;
+        this.indices = indices;
+    }
+
+    private static int[] indices(char[] chars) {
+        List<Integer> a = new ArrayList<Integer>();
+        boolean prev = true;
+        boolean quoted = false;
+        for (int i = 0; i < chars.length; i++) {
+            final char c = chars[i];
+            if (c == '"') {
+                quoted = !quoted;
+            }
+            final boolean f = isSpaceChar(c);
+            if (!f && f != prev) {
+                a.add(i);
+            }
+            prev = !quoted && f;
+        }
+        a.add(chars.length);
+        int[] indices = new int[a.size()];
+        for (int i = 0; i < indices.length; i++) {
+            indices[i] = a.get(i);
+        }
+        return indices;
+    }
+
+    private static String[] array(char[] chars, int[] indices) {
+        String[] a = new String[indices.length - 1];
+        for (int i = 0; i < a.length; i++) {
+            final int offset = indices[i];
+            int end = indices[i + 1];
+            while (end > offset) {
+                if (!isSpaceChar(chars[end - 1])) {
+                    break;
+                }
+                --end;
+            }
+            final String s = String.valueOf(chars, offset, end - offset);
+            a[i] = (chars[offset] == '"') ? s.substring(1, s.length() - 1) : s;
+        }
+        return a;
+    }
+
+    private static boolean isSpaceChar(char c) {
+        switch (c) {
+            case '\t':
+            case '\n':
+            case '\f':
+            case '\r':
+            case ' ':
+                return true;
+            default:
+        }
+        return false;
+    }
+
+    /**
+     * Returns the parameter at the position specified index.
+     * @param index
+     * @return
+     */
+    public String at(int index) {
+        return has(index) ? array[index] : "";
+    }
+
+    /**
+     * Returns the parameter after the position specified index.
+     * @param index
+     * @return
+     */
+    public String after(int index) {
+        return has(index) ? string.substring(indices[index]) : "";
+    }
+
+    /**
+     * Returns whether a parameter exists at the position specified index.
+     * @param index
+     * @return
+     */
+    public boolean has(int index) {
+        if (index < 0) {
+            throw new IndexOutOfBoundsException("index >= 0: " + index);
+        }
+        return index < array.length;
+    }
+
+    /**
+     * Returns this parameter as an array.
+     * @return
+     */
+    public String[] asArray() {
+        return array.clone();
+    }
+
+    /**
+     * Returns this parameter as String.
+     * is not the same as <code>toString</code>
+     * @return
+     */
+    public String asString() {
+        return string;
+    }
+
+    @Override
+    public String toString() {
+        return "Parameter[" + string + "]";
+    }
+
+}
diff --git a/src/net/argius/stew/Password.java b/src/net/argius/stew/Password.java
new file mode 100644 (file)
index 0000000..084c98f
--- /dev/null
@@ -0,0 +1,38 @@
+package net.argius.stew;
+
+/**
+ * The Password interface that is used by Connector.
+ */
+public interface Password {
+
+    /**
+     * Returns the transformed string.
+     * @return
+     */
+    String getTransformedString();
+
+    /**
+     * Sets the transformed string.
+     * @param transformedString
+     */
+    void setTransformedString(String transformedString);
+
+    /**
+     * Returns the raw string.
+     * @return
+     */
+    String getRawString();
+
+    /**
+     * Sets the raw string.
+     * @param rowString
+     */
+    void setRawString(String rowString);
+
+    /**
+     * Returns true if the password was already set.
+     * @return
+     */
+    boolean hasPassword();
+
+}
diff --git a/src/net/argius/stew/PbePassword.java b/src/net/argius/stew/PbePassword.java
new file mode 100644 (file)
index 0000000..811df6f
--- /dev/null
@@ -0,0 +1,32 @@
+package net.argius.stew;
+
+import java.security.*;
+
+import javax.crypto.*;
+import javax.crypto.spec.*;
+
+/**
+ * The Password implementation using cipher with PBE.
+ */
+public final class PbePassword extends CipherPassword {
+
+    private static final String TRANSFORMATION_NAME = "PBEWithMD5AndDES";
+    private static final byte[] SALT = "0141STEW".getBytes();
+    private static final int ITERATION = 10;
+
+    @Override
+    protected Cipher getCipherInstance(String code, int mode) {
+        try {
+            PBEKeySpec keySpec = new PBEKeySpec(code.toCharArray());
+            SecretKey key = SecretKeyFactory.getInstance(TRANSFORMATION_NAME)
+                                            .generateSecret(keySpec);
+            PBEParameterSpec spec = new PBEParameterSpec(SALT, ITERATION);
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION_NAME);
+            cipher.init(mode, key, spec);
+            return cipher;
+        } catch (GeneralSecurityException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/PlainTextPassword.java b/src/net/argius/stew/PlainTextPassword.java
new file mode 100644 (file)
index 0000000..88d5abf
--- /dev/null
@@ -0,0 +1,37 @@
+package net.argius.stew;
+
+/**
+ * A plain-text password.
+ */
+public final class PlainTextPassword implements Password {
+
+    private String rowString;
+
+    @Override
+    public String getTransformedString() {
+        return getRawString();
+    }
+
+    @Override
+    public void setTransformedString(String transformedString) {
+        setRawString(transformedString);
+    }
+
+    @Override
+    public String getRawString() {
+        return (hasPassword()) ? rowString : "";
+    }
+
+    @Override
+    public void setRawString(String rowString) {
+        if (rowString != null) {
+            this.rowString = rowString;
+        }
+    }
+
+    @Override
+    public boolean hasPassword() {
+        return rowString != null;
+    }
+
+}
diff --git a/src/net/argius/stew/ResourceManager.java b/src/net/argius/stew/ResourceManager.java
new file mode 100644 (file)
index 0000000..51f64d1
--- /dev/null
@@ -0,0 +1,234 @@
+package net.argius.stew;
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * ResourceManager provides a function like a ResourceBundle used UTF-8 instead Unicode escapes.
+ */
+public final class ResourceManager {
+
+    public static final ResourceManager Default = ResourceManager.getInstance(ResourceManager0.class);
+
+    private List<Map<String, String>> list;
+
+    private ResourceManager(List<Map<String, String>> list) {
+        this.list = list;
+    }
+
+    /**
+     * Creates an instance.
+     * @param o
+     * It used as a bundle name.
+     * If String, it will use as a bundle name directly.
+     * Else if Package, it will use its package name + "messages".
+     * Otherwise, it will use as its FQCN.
+     * @return
+     */
+    public static ResourceManager getInstance(Object o) {
+        Locale loc = Locale.getDefault();
+        String[] suffixes = {"_" + loc, "_" + loc.getLanguage(), ""};
+        List<Map<String, String>> a = new ArrayList<Map<String, String>>();
+        for (final String name : getResourceNames(o)) {
+            for (final String suffix : suffixes) {
+                final String key = name + suffix;
+                Map<String, String> m = ResourceManager0.map.get(key);
+                if (m == null) {
+                    m = loadResource(key, "u8p", "utf-8");
+                    if (m == null) {
+                        continue;
+                    }
+                    ResourceManager0.map.putIfAbsent(key, m);
+                }
+                a.add(m);
+            }
+        }
+        return new ResourceManager(a);
+    }
+
+    private static Set<String> getResourceNames(Object o) {
+        Set<String> set = new LinkedHashSet<String>();
+        String cn = null;
+        String pn = null;
+        if (o instanceof String) {
+            cn = (String)o;
+        } else if (o instanceof Package) {
+            pn = ((Package)o).getName();
+        } else if (o != null) {
+            final Class<?> c = (o instanceof Class) ? (Class<?>)o : o.getClass();
+            cn = c.getName();
+            pn = c.getPackage().getName();
+        }
+        if (cn != null) {
+            set.add(cn);
+        }
+        if (pn != null) {
+            set.add(pn + ".messages");
+        }
+        set.add(ResourceManager0.getPackageName() + ".messages");
+        return set;
+    }
+
+    private static Map<String, String> loadResource(String name, String extension, String encname) {
+        final String path = "/" + name.replace('.', '/') + '.' + extension;
+        InputStream is = ResourceManager0.getResourceAsStream(path);
+        if (is == null) {
+            return null;
+        }
+        List<String> lines = new ArrayList<String>();
+        Scanner r = new Scanner(is, encname);
+        try {
+            StringBuilder buffer = new StringBuilder();
+            while (r.hasNextLine()) {
+                final String s = r.nextLine();
+                if (s.matches("^\\s*#.*")) {
+                    continue;
+                }
+                buffer.append(s.replace("\\t", "\t").replace("\\n", "\n").replace("\\=", "="));
+                if (s.endsWith("\\")) {
+                    buffer.setLength(buffer.length() - 1);
+                    continue;
+                }
+                lines.add(buffer.toString());
+                buffer.setLength(0);
+            }
+            if (buffer.length() > 0) {
+                lines.add(buffer.toString());
+            }
+        } finally {
+            r.close();
+        }
+        Map<String, String> m = new HashMap<String, String>();
+        for (final String s : lines) {
+            if (s.contains("=")) {
+                String[] a = s.split("=", 2);
+                m.put(a[0].trim(), a[1].trim().replaceFirst("\\\\$", " ").replace("\\ ", " "));
+            } else {
+                m.put(s.trim(), "");
+            }
+        }
+        return m;
+    }
+
+    /**
+     * @param path resource's path
+     * @param defaultValue defalut value if a resource not found
+     * @return
+     */
+    public String read(String path, String defaultValue) {
+        InputStream in = ResourceManager0.getResourceAsStream(path);
+        if (in == null) {
+            return defaultValue;
+        }
+        StringBuilder buffer = new StringBuilder();
+        Scanner r = new Scanner(in);
+        try {
+            if (r.hasNextLine()) {
+                buffer.append(r.nextLine());
+            }
+            while (r.hasNextLine()) {
+                buffer.append(String.format("%n"));
+                buffer.append(r.nextLine());
+            }
+        } finally {
+            r.close();
+        }
+        return buffer.toString();
+    }
+
+    private String s(String key) {
+        for (final Map<String, String> m : this.list) {
+            final String s = m.get(key);
+            if (s != null) {
+                return s;
+            }
+        }
+        return "";
+    }
+
+    /**
+     * Returns true if this resource contains a value specified by key.
+     * @param key
+     * @return
+     */
+    public boolean containsKey(String key) {
+        for (final Map<String, String> m : this.list) {
+            if (m.containsKey(key)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the value specified by key as a String.
+     * @param key
+     * @param args
+     * @return
+     */
+    public String get(String key, Object... args) {
+        final String s = s(key);
+        return (s.length() == 0) ? key : MessageFormat.format(s(key), args);
+    }
+
+    /**
+     * Returns the value specified by key as a boolean.
+     * @param key
+     * @return
+     */
+    public boolean isTrue(String key) {
+        return s(key).matches("(?i)true|on|yes");
+    }
+
+    /**
+     * Returns the (initial char) value specified by key as a char.
+     * @param key
+     * @return
+     */
+    public char getChar(String key) {
+        final String s = s(key);
+        return (s.length() == 0) ? ' ' : s.charAt(0);
+    }
+
+    /**
+     * Returns the value specified by key as a int.
+     * @param key
+     * @return
+     */
+    public int getInt(String key) {
+        return getInt(key, 0);
+    }
+
+    /**
+     * Returns the value specified by key as a int.
+     * @param key
+     * @param defaultValue
+     * @return
+     */
+    public int getInt(String key, int defaultValue) {
+        final String s = s(key);
+        try {
+            return Integer.parseInt(s);
+        } catch (NumberFormatException ex) {
+            // ignore
+        }
+        return defaultValue;
+    }
+
+}
+
+class ResourceManager0 {
+
+    static final ConcurrentHashMap<String, Map<String, String>> map = new ConcurrentHashMap<String, Map<String, String>>();
+
+    static String getPackageName() {
+        return ResourceManager0.class.getPackage().getName();
+    }
+
+    static InputStream getResourceAsStream(String path) {
+        return ResourceManager0.class.getResourceAsStream(path);
+    }
+
+}
diff --git a/src/net/argius/stew/ResultSetReference.java b/src/net/argius/stew/ResultSetReference.java
new file mode 100644 (file)
index 0000000..46e55f6
--- /dev/null
@@ -0,0 +1,67 @@
+package net.argius.stew;
+
+import java.sql.*;
+
+/**
+ * This object holds the reference of ResultSet.
+ */
+public final class ResultSetReference {
+
+    private final ResultSet rs;
+    private final ColumnOrder order;
+    private final String commandString;
+
+    private int recordCount;
+
+    /**
+     * A constructor.
+     * @param rs ResultSet
+     * @param commandString
+     */
+    public ResultSetReference(ResultSet rs, String commandString) {
+        this.rs = rs;
+        this.order = new ColumnOrder();
+        this.commandString = commandString;
+    }
+
+    /**
+     * Returns the ResultSet.
+     * @return
+     */
+    public ResultSet getResultSet() {
+        return rs;
+    }
+
+    /**
+     * Returns the ColumnOrder.
+     * @return
+     */
+    public ColumnOrder getOrder() {
+        return order;
+    }
+
+    /**
+     * Returns the command string.
+     * @return
+     */
+    public String getCommandString() {
+        return commandString;
+    }
+
+    /**
+     * Returns the count of records.
+     * @return
+     */
+    public int getRecordCount() {
+        return recordCount;
+    }
+
+    /**
+     * Sets the count of records.
+     * @param recordCount
+     */
+    public void setRecordCount(int recordCount) {
+        this.recordCount = recordCount;
+    }
+
+}
diff --git a/src/net/argius/stew/UsageException.java b/src/net/argius/stew/UsageException.java
new file mode 100644 (file)
index 0000000..efdc5db
--- /dev/null
@@ -0,0 +1,12 @@
+package net.argius.stew;
+
+/**
+ * This is a kind of CommandException, only used when "Usage" error occurred.
+ */
+public final class UsageException extends CommandException {
+
+    public UsageException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/net/argius/stew/command/Download.java b/src/net/argius/stew/command/Download.java
new file mode 100644 (file)
index 0000000..b6956f4
--- /dev/null
@@ -0,0 +1,190 @@
+package net.argius.stew.command;
+
+import static java.sql.Types.*;
+
+import java.io.*;
+import java.sql.*;
+
+import net.argius.stew.*;
+
+/**
+ * The Download command used to save selected data to files.
+ */
+public final class Download extends Command {
+
+    private static final Logger log = Logger.getLogger(Download.class);
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        if (!parameter.has(2)) {
+            throw new UsageException(getUsage());
+        }
+        final String root = parameter.at(1);
+        final String sql = parameter.after(2);
+        if (log.isDebugEnabled()) {
+            log.debug("root: " + root);
+            log.debug("SQL: " + sql);
+        }
+        try {
+            Statement stmt = prepareStatement(conn, parameter.asString());
+            try {
+                ResultSet rs = executeQuery(stmt, sql);
+                try {
+                    download(rs, root);
+                } finally {
+                    rs.close();
+                }
+            } finally {
+                stmt.close();
+            }
+        } catch (IOException ex) {
+            throw new CommandException(ex);
+        } catch (SQLException ex) {
+            throw new CommandException(ex);
+        }
+    }
+
+    private void download(ResultSet rs, String root) throws IOException, SQLException {
+        final int targetColumn = 1;
+        ResultSetMetaData meta = rs.getMetaData();
+        final int columnCount = meta.getColumnCount();
+        assert columnCount >= 1;
+        final int columnType = meta.getColumnType(targetColumn);
+        final boolean isBinary;
+        switch (columnType) {
+            case TINYINT:
+            case SMALLINT:
+            case INTEGER:
+            case BIGINT:
+            case FLOAT:
+            case REAL:
+            case DOUBLE:
+            case NUMERIC:
+            case DECIMAL:
+                // numeric to string
+                isBinary = false;
+                break;
+            case BOOLEAN:
+            case BIT:
+            case DATE:
+            case TIME:
+            case TIMESTAMP:
+                // object to string
+                isBinary = false;
+                break;
+            case BINARY:
+            case VARBINARY:
+            case LONGVARBINARY:
+            case BLOB:
+                // binary to stream
+                isBinary = true;
+                break;
+            case CHAR:
+            case VARCHAR:
+            case LONGVARCHAR:
+            case CLOB:
+                // char to binary-stream
+                isBinary = true;
+                break;
+            case OTHER:
+                // ? to binary-stream (experimental)
+                // (e.g.: XML)
+                isBinary = true;
+                break;
+            case DATALINK:
+            case JAVA_OBJECT:
+            case DISTINCT:
+            case STRUCT:
+            case ARRAY:
+            case REF:
+            default:
+                throw new CommandException(String.format("unsupported type: %d", columnType));
+        }
+        byte[] buffer = new byte[(isBinary) ? 0x10000 : 0];
+        int count = 0;
+        while (rs.next()) {
+            ++count;
+            StringBuilder fileName = new StringBuilder();
+            for (int i = 2; i <= columnCount; i++) {
+                fileName.append(rs.getString(i));
+            }
+            final File path = resolvePath(root);
+            final File file = (columnCount == 1) ? path : new File(path, fileName.toString());
+            if (file.exists()) {
+                throw new IOException(getMessage("e.file-already-exists", file.getAbsolutePath()));
+            }
+            if (isBinary) {
+                InputStream is = rs.getBinaryStream(targetColumn);
+                if (is == null) {
+                    mkdirs(file);
+                    if (!file.createNewFile()) {
+                        throw new IOException(getMessage("e.failed-create-new-file",
+                                                         file.getAbsolutePath()));
+                    }
+                } else {
+                    try {
+                        mkdirs(file);
+                        OutputStream os = new FileOutputStream(file);
+                        try {
+                            while (true) {
+                                int readLength = is.read(buffer);
+                                if (readLength <= 0) {
+                                    break;
+                                }
+                                os.write(buffer, 0, readLength);
+                            }
+                        } finally {
+                            os.close();
+                        }
+                    } finally {
+                        is.close();
+                    }
+                }
+            } else {
+                mkdirs(file);
+                PrintWriter out = new PrintWriter(file);
+                try {
+                    out.print(rs.getObject(targetColumn));
+                } finally {
+                    out.close();
+                }
+            }
+            outputMessage("i.downloaded", getSizeString(file.length()), file);
+        }
+        outputMessage("i.selected", count);
+    }
+
+    private void mkdirs(File file) throws IOException {
+        final File dir = file.getParentFile();
+        if (!dir.isDirectory()) {
+            if (log.isDebugEnabled()) {
+                log.debug(String.format("mkdir [%s]", dir.getAbsolutePath()));
+            }
+            if (dir.mkdirs()) {
+                outputMessage("i.did-mkdir", dir);
+            } else {
+                throw new IOException(getMessage("e.failed-mkdir-filedir", file));
+            }
+        }
+    }
+
+    static String getSizeString(long size) {
+        if (size >= 512) {
+            final double convertedSize;
+            final String unit;
+            if (size >= 536870912) {
+                convertedSize = size * 1f / 1073741824f;
+                unit = "GB";
+            } else if (size >= 524288) {
+                convertedSize = size * 1f / 1048576f;
+                unit = "MB";
+            } else {
+                convertedSize = size * 1f / 1024f;
+                unit = "KB";
+            }
+            return String.format("%.3f", convertedSize).replaceFirst("\\.?0+$", "") + unit;
+        }
+        return String.format("%dbyte%s", size, size < 2 ? "" : "s");
+    }
+
+}
diff --git a/src/net/argius/stew/command/Export.java b/src/net/argius/stew/command/Export.java
new file mode 100644 (file)
index 0000000..237f6b6
--- /dev/null
@@ -0,0 +1,146 @@
+package net.argius.stew.command;
+
+import java.io.*;
+import java.sql.*;
+import java.util.*;
+
+import net.argius.stew.*;
+import net.argius.stew.io.*;
+
+/**
+ * The Export command used to export data to a file.
+ * The data is the output of a command which is Select, Find, or Report.
+ * 
+ * The export type will be automatically selected by file's extension. 
+ * @see Exporter
+ */
+public final class Export extends Command {
+
+    private static final Logger log = Logger.getLogger(Export.class);
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        if (!parameter.has(2)) {
+            throw new UsageException(getUsage());
+        }
+        final String path = parameter.at(1);
+        int argsIndex = 2;
+        final boolean withHeader = parameter.at(argsIndex).equalsIgnoreCase("HEADER");
+        if (withHeader) {
+            ++argsIndex;
+        }
+        final String cmd = parameter.after(argsIndex);
+        if (log.isDebugEnabled()) {
+            log.debug(String.format("file: [%s]", path));
+            log.debug("withHeader: " + withHeader);
+            log.debug(String.format("command: [%s]", cmd));
+        }
+        try {
+            final File file = resolvePath(path);
+            if (file.exists()) {
+                throw new CommandException(getMessage("e.file-already-exists", file));
+            }
+            Parameter p = new Parameter(cmd);
+            final String subCommand = p.at(0);
+            final ResultSetReference ref;
+            if (subCommand.equalsIgnoreCase("SELECT")) {
+                Statement stmt = prepareStatement(conn, cmd);
+                try {
+                    ref = new ResultSetReference(executeQuery(stmt, cmd), "");
+                    export(file, ref, withHeader);
+                } finally {
+                    stmt.close();
+                }
+            } else if (subCommand.equalsIgnoreCase("FIND")) {
+                Find find = new Find();
+                try {
+                    find.setEnvironment(env);
+                    ref = find.getResult(conn, p);
+                } catch (UsageException ex) {
+                    throw new UsageException(getMessage("Export.command.usage",
+                                                        getMessage("usage.Export"),
+                                                        cmd,
+                                                        ex.getMessage()));
+                } finally {
+                    find.close();
+                }
+                try {
+                    export(file, ref, withHeader);
+                } finally {
+                    ref.getResultSet().close();
+                }
+            } else if (subCommand.equalsIgnoreCase("REPORT") && !p.at(1).equals("-")) {
+                Report report = new Report();
+                try {
+                    report.setEnvironment(env);
+                    ref = report.getResult(conn, p);
+                } catch (UsageException ex) {
+                    throw new UsageException(getMessage("Export.command.usage",
+                                                        getMessage("usage.Export"),
+                                                        cmd,
+                                                        ex.getMessage()));
+                } finally {
+                    report.close();
+                }
+                try {
+                    export(file, ref, withHeader);
+                } finally {
+                    ref.getResultSet().close();
+                }
+            } else {
+                throw new UsageException(getUsage());
+            }
+            outputMessage("i.selected", ref.getRecordCount());
+            outputMessage("i.exported");
+        } catch (IOException ex) {
+            throw new CommandException(ex);
+        } catch (SQLException ex) {
+            throw new CommandException(ex);
+        }
+    }
+
+    @Override
+    public boolean isReadOnly() {
+        return true;
+    }
+
+    private static void export(File file, ResultSetReference ref, boolean withHeader) throws IOException, SQLException {
+        Exporter exporter = Exporter.getExporter(file);
+        try {
+            ResultSet rs = ref.getResultSet();
+            ColumnOrder order = ref.getOrder();
+            boolean needOrderChange = order.size() > 0;
+            int columnCount;
+            List<String> header = new ArrayList<String>();
+            if (needOrderChange) {
+                columnCount = order.size();
+                for (int i = 0; i < columnCount; i++) {
+                    header.add(order.getName(i));
+                }
+            } else {
+                ResultSetMetaData m = rs.getMetaData();
+                columnCount = m.getColumnCount();
+                for (int i = 0; i < columnCount; i++) {
+                    header.add(m.getColumnName(i + 1));
+                }
+            }
+            if (withHeader) {
+                exporter.addHeader(header.toArray());
+            }
+            int count = 0;
+            while (rs.next()) {
+                ++count;
+                Object[] row = new Object[columnCount];
+                for (int i = 0; i < columnCount; i++) {
+                    int index = (needOrderChange) ? order.getOrder(i) : i + 1;
+                    row[i] = rs.getObject(index);
+                }
+                exporter.addRow(row);
+            }
+            ref.setRecordCount(count);
+        } finally {
+            exporter.close();
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/command/Find.java b/src/net/argius/stew/command/Find.java
new file mode 100644 (file)
index 0000000..fa342e9
--- /dev/null
@@ -0,0 +1,107 @@
+package net.argius.stew.command;
+
+import java.sql.*;
+import java.util.*;
+
+import net.argius.stew.*;
+
+/**
+ * The Find command used to search table names.
+ * @see DatabaseMetaData#getTables(String, String, String, String[])
+ */
+public final class Find extends Command {
+
+    private static final Logger log = Logger.getLogger(Find.class);
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        try {
+            ResultSetReference ref = getResult(conn, parameter);
+            try {
+                output(ref);
+                outputMessage("i.selected", ref.getRecordCount());
+            } finally {
+                ref.getResultSet().close();
+            }
+        } catch (SQLException ex) {
+            throw new CommandException(ex);
+        }
+    }
+
+    ResultSetReference getResult(Connection conn, Parameter p) throws SQLException {
+        if (!p.has(1)) {
+            throw new UsageException(getUsage());
+        }
+        final String p1 = p.at(1);
+        final String p2 = p.at(2);
+        final String p3 = p.at(3);
+        final String p4 = p.at(4);
+        final String p5 = p.at(5);
+        DatabaseMetaData dbmeta = conn.getMetaData();
+        final String tableNamePattern = editNamePattern(p1);
+        final String[] tableTypes = editTableType(p2);
+        final String schemaNamePattern = editNamePattern(p3);
+        final String catalogNamePattern = editNamePattern(p4);
+        final boolean isFull = p5.equalsIgnoreCase("FULL");
+        if (log.isDebugEnabled()) {
+            log.debug("name   : " + tableNamePattern);
+            log.debug("types  : " + (tableTypes == null ? null : Arrays.asList(tableTypes)));
+            log.debug("schema : " + schemaNamePattern);
+            log.debug("catalog: " + catalogNamePattern);
+            log.debug("full?  : " + isFull);
+        }
+        ResultSet rs = dbmeta.getTables(catalogNamePattern,
+                                        schemaNamePattern,
+                                        tableNamePattern,
+                                        tableTypes);
+        try {
+            ResultSetReference ref = new ResultSetReference(rs, p.asString());
+            if (!isFull) {
+                ColumnOrder order = ref.getOrder();
+                order.addOrder(3, getColumnName("name"));
+                order.addOrder(4, getColumnName("type"));
+                order.addOrder(2, getColumnName("schema"));
+                order.addOrder(1, getColumnName("catalog"));
+            }
+            return ref;
+        } catch (Throwable th) {
+            rs.close();
+            if (th instanceof SQLException) {
+                throw (SQLException)th;
+            } else if (th instanceof RuntimeException) {
+                throw (RuntimeException)th;
+            }
+            throw new CommandException(th);
+        }
+    }
+
+    @Override
+    public boolean isReadOnly() {
+        return true;
+    }
+
+    private static String getColumnName(String key) {
+        return getMessage("Find.label." + key);
+    }
+
+    private static String[] editTableType(String pattern) {
+        if (pattern == null || pattern.trim().length() == 0 || pattern.equals("*")) {
+            return null;
+        }
+        return pattern.toUpperCase().split(",");
+    }
+
+    private String editNamePattern(String pattern) throws SQLException {
+        if (pattern == null || pattern.trim().length() == 0) {
+            return null;
+        } else if (pattern.equals("''") || pattern.equals("\"\"")) {
+            return "";
+        }
+        final String edited = convertPattern(pattern);
+        if (log.isDebugEnabled()) {
+            log.debug("table-name-condition : " + edited);
+        }
+        return edited;
+    }
+
+}
diff --git a/src/net/argius/stew/command/Import.java b/src/net/argius/stew/command/Import.java
new file mode 100644 (file)
index 0000000..b8c6046
--- /dev/null
@@ -0,0 +1,136 @@
+package net.argius.stew.command;
+
+import java.io.*;
+import java.sql.*;
+
+import net.argius.stew.*;
+import net.argius.stew.io.*;
+
+/**
+ * Import command used to import a file into database.
+ *
+ * The export type will be automatically selected by file's extension:
+ * *.csv as CSV, otherwise as TSV.
+ *
+ * Unlike Load command, this uses "executeBatch".
+ */
+public final class Import extends Load {
+
+    private static final Logger log = Logger.getLogger(Import.class);
+    private static final String PROP_BATCH_LIMIT = "net.argius.stew.command.Import.batch.limit";
+    private static final int DEFAULT_BATCH_LIMIT = 10000;
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        if (!parameter.has(2)) {
+            throw new UsageException(getUsage());
+        }
+        final File file = resolvePath(parameter.at(1));
+        final String table = parameter.at(2);
+        final boolean hasHeader = parameter.at(3).equalsIgnoreCase("HEADER");
+        if (log.isDebugEnabled()) {
+            log.debug("file: " + file.getAbsolutePath());
+            log.debug("table: " + table);
+            log.debug("hasHeader: " + hasHeader);
+        }
+        try {
+            loadRecord(conn, file, table, hasHeader);
+        } catch (IOException ex) {
+            throw new CommandException(ex);
+        } catch (SQLException ex) {
+            SQLException next = ex.getNextException();
+            if (next != null && next != ex) {
+                log.error(next, "next exception: ");
+            }
+            throw new CommandException(ex);
+        }
+    }
+
+    @Override
+    protected void insertRecords(PreparedStatement stmt, Importer importer) throws IOException, SQLException {
+        final int batchLimit = Bootstrap.getPropertyAsInt(PROP_BATCH_LIMIT, DEFAULT_BATCH_LIMIT);
+        if (log.isDebugEnabled()) {
+            log.debug("batch limit = " + batchLimit);
+        }
+        int recordCount = 0;
+        int insertedCount = 0;
+        int errorCount = 0;
+        while (true) {
+            Object[] row = importer.nextRow();
+            final boolean eof = row.length == 0;
+            if (!eof) {
+                ++recordCount;
+                try {
+                    for (int i = 0; i < row.length; i++) {
+                        stmt.setObject(i + 1, row[i]);
+                    }
+                    stmt.addBatch();
+                } catch (SQLException ex) {
+                    String message = "error occurred at " + recordCount;
+                    if (log.isTraceEnabled()) {
+                        log.trace(message, ex);
+                    } else if (log.isDebugEnabled()) {
+                        log.debug(message + " : " + ex);
+                    }
+                    ++errorCount;
+                }
+            }
+            if (recordCount % batchLimit == 0 || eof) {
+                int inserted = executeBatch(stmt);
+                insertedCount += inserted;
+                if (log.isDebugEnabled()) {
+                    log.debug("record/inserted = " + recordCount + "/" + insertedCount);
+                }
+                if (eof) {
+                    break;
+                }
+            }
+        }
+        if (errorCount > 0) {
+            log.warn("error count = " + errorCount);
+        }
+        outputMessage("i.loaded", insertedCount, recordCount);
+    }
+
+    /**
+     * Executes batch.
+     * @param stmt
+     * @return updated record count
+     * @throws SQLException
+     */
+    private static int executeBatch(PreparedStatement stmt) throws SQLException {
+        int[] results = stmt.executeBatch();
+        int resultCount = 0;
+        int noInfoCount = 0;
+        int failedCount = 0;
+        for (int i = 0; i < results.length; i++) {
+            int result = results[i];
+            switch (result) {
+                case 1:
+                    ++resultCount;
+                    break;
+                case Statement.SUCCESS_NO_INFO:
+                    ++noInfoCount;
+                    ++resultCount;
+                    break;
+                case Statement.EXECUTE_FAILED:
+                    ++failedCount;
+                    break;
+                default:
+                    throw new IllegalStateException("result=" + result);
+            }
+        }
+        if (failedCount > 0) {
+            log.warn("failedCount = " + failedCount);
+        }
+        if (noInfoCount > 0) {
+            log.warn("noInfoCount = " + noInfoCount);
+        }
+        if (resultCount != results.length) {
+            log.warn("array size = " + results.length + ", but result count = " + resultCount);
+        }
+        stmt.clearBatch();
+        return resultCount;
+    }
+
+}
diff --git a/src/net/argius/stew/command/Load.java b/src/net/argius/stew/command/Load.java
new file mode 100644 (file)
index 0000000..c3f4da5
--- /dev/null
@@ -0,0 +1,152 @@
+package net.argius.stew.command;
+
+import static net.argius.stew.text.TextUtilities.join;
+
+import java.io.*;
+import java.sql.*;
+import java.util.*;
+
+import net.argius.stew.*;
+import net.argius.stew.io.*;
+
+/**
+ * The Load command used to execute SQL from a file.
+ *
+ * This command has two mode:
+ *   if it gived one argument, it will execute SQL read from a file,
+ *   or it it will load data from file.
+ *
+ * The file type to load will be automatically selected by file's extension:
+ * @see Importer
+ */
+public class Load extends Command {
+
+    private static final Logger log = Logger.getLogger(Load.class);
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        if (!parameter.has(1)) {
+            throw new UsageException(getUsage());
+        }
+        try {
+            final File file = resolvePath(parameter.at(1));
+            if (log.isDebugEnabled()) {
+                log.debug("file: " + file.getAbsolutePath());
+            }
+            if (parameter.has(2)) {
+                final String table = parameter.at(2);
+                final boolean hasHeader = parameter.at(3).equalsIgnoreCase("HEADER");
+                if (log.isDebugEnabled()) {
+                    log.debug("table: " + table);
+                    log.debug("hasHeader: " + hasHeader);
+                }
+                loadRecord(conn, file, table, hasHeader);
+            } else {
+                loadSql(conn, file);
+            }
+        } catch (IOException ex) {
+            throw new CommandException(ex);
+        } catch (SQLException ex) {
+            throw new CommandException(ex);
+        }
+    }
+
+    private void loadSql(Connection conn, File file) throws IOException, SQLException {
+        final String sql = readFileAsString(file);
+        if (log.isDebugEnabled()) {
+            log.debug("sql : " + sql);
+        }
+        Statement stmt = prepareStatement(conn, sql);
+        try {
+            if (isSelect(sql)) {
+                ResultSet rs = executeQuery(stmt, sql);
+                try {
+                    ResultSetReference ref = new ResultSetReference(rs, sql);
+                    output(ref);
+                    outputMessage("i.selected", ref.getRecordCount());
+                } finally {
+                    rs.close();
+                }
+            } else {
+                final int count = stmt.executeUpdate(sql);
+                outputMessage("i.proceeded", count);
+            }
+        } finally {
+            stmt.close();
+        }
+    }
+
+    protected void loadRecord(Connection conn, File file, String tableName, boolean hasHeader) throws IOException, SQLException {
+        Importer importer = Importer.getImporter(file);
+        try {
+            final Object[] header;
+            if (hasHeader) {
+                header = importer.nextRow();
+            } else {
+                Importer importer2 = Importer.getImporter(file);
+                try {
+                    Object[] a = importer2.nextRow();
+                    Arrays.fill(a, "");
+                    header = a;
+                } finally {
+                    importer2.close();
+                }
+            }
+            final List<Object> headerList = Arrays.asList(header);
+            final String columns = (hasHeader) ? String.format("(%s)", join(",", headerList)) : "";
+            final List<Object> valueList = new ArrayList<Object>(headerList);
+            Collections.fill(valueList, "?");
+            final String sql = String.format("INSERT INTO %s %s VALUES (%s)",
+                                             tableName,
+                                             columns,
+                                             join(",", valueList));
+            if (log.isDebugEnabled()) {
+                log.debug("SQL : " + sql);
+            }
+            PreparedStatement stmt = conn.prepareStatement(sql);
+            try {
+                insertRecords(stmt, importer);
+            } finally {
+                stmt.close();
+            }
+        } finally {
+            importer.close();
+        }
+    }
+
+    protected void insertRecords(PreparedStatement stmt, Importer importer) throws IOException, SQLException {
+        int recordCount = 0;
+        int insertedCount = 0;
+        int errorCount = 0;
+        while (true) {
+            Object[] row = importer.nextRow();
+            if (row == null || row.length == 0) {
+                break;
+            }
+            ++recordCount;
+            try {
+                for (int i = 0; i < row.length; i++) {
+                    int index = i + 1;
+                    Object o = row[i];
+                    stmt.setObject(index, o);
+                }
+                insertedCount += stmt.executeUpdate();
+            } catch (SQLException ex) {
+                String message = "error occurred at " + recordCount;
+                if (log.isTraceEnabled()) {
+                    log.trace(message, ex);
+                } else if (log.isDebugEnabled()) {
+                    log.debug(message + " : " + ex);
+                }
+                ++errorCount;
+            }
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("record   = " + recordCount);
+            log.debug("inserted = " + insertedCount);
+            log.debug("error    = " + errorCount);
+        }
+        outputMessage("i.loaded", insertedCount, recordCount);
+    }
+
+}
diff --git a/src/net/argius/stew/command/Report.java b/src/net/argius/stew/command/Report.java
new file mode 100644 (file)
index 0000000..b6aba96
--- /dev/null
@@ -0,0 +1,170 @@
+package net.argius.stew.command;
+
+import java.sql.*;
+
+import net.argius.stew.*;
+
+/**
+ * The Report command used to show database informations.
+ */
+public final class Report extends Command {
+
+    private static final Logger log = Logger.getLogger(Report.class);
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        if (!parameter.has(1)) {
+            throw new UsageException(getUsage());
+        }
+        try {
+            final String p1 = parameter.at(1);
+            if (p1.equals("-")) {
+                reportDBInfo(conn);
+            } else {
+                ResultSetReference ref = getResult(conn, parameter);
+                try {
+                    output(ref);
+                    outputMessage("i.selected", ref.getRecordCount());
+                } finally {
+                    ref.getResultSet().close();
+                }
+            }
+        } catch (SQLException ex) {
+            throw new CommandException(ex);
+        }
+    }
+
+    @Override
+    public boolean isReadOnly() {
+        return true;
+    }
+
+    ResultSetReference getResult(Connection conn, Parameter p) throws SQLException {
+        if (!p.has(1)) {
+            throw new UsageException(getUsage());
+        }
+        final String cmd = p.asString();
+        final String tableName = p.at(1);
+        final String option = p.at(2);
+        try {
+            DatabaseMetaData dbmeta = conn.getMetaData();
+            if (option.equalsIgnoreCase("FULL")) {
+                return getTableFullDescription(dbmeta, tableName, cmd);
+            } else if (option.equalsIgnoreCase("PK")) {
+                return getPrimaryKeyInfo(dbmeta, tableName, cmd);
+            } else if (option.equalsIgnoreCase("INDEX")) {
+                return getIndexInfo(dbmeta, tableName, cmd);
+            }
+            return getTableDescription(dbmeta, tableName, cmd);
+        } catch (Throwable th) {
+            if (th instanceof SQLException) {
+                throw (SQLException)th;
+            } else if (th instanceof RuntimeException) {
+                throw (RuntimeException)th;
+            }
+            throw new CommandException(th);
+        }
+    }
+
+    private ResultSetReference getTableFullDescription(DatabaseMetaData dbmeta,
+                                                       String tableName,
+                                                       String cmd) throws Throwable {
+        if (log.isDebugEnabled()) {
+            log.debug("report table-full-description of : " + tableName);
+        }
+        ResultSet rs = dbmeta.getColumns(null, null, convertPattern(tableName), null);
+        try {
+            return new ResultSetReference(rs, cmd);
+        } catch (Throwable th) {
+            rs.close();
+            throw th;
+        }
+    }
+
+    private ResultSetReference getTableDescription(DatabaseMetaData dbmeta,
+                                                   String tableName,
+                                                   String cmd) throws Throwable {
+        if (log.isDebugEnabled()) {
+            log.debug("report table-description of : " + tableName);
+        }
+        ResultSet rs = dbmeta.getColumns(null, null, convertPattern(tableName), null);
+        try {
+            ResultSetReference ref = new ResultSetReference(rs, cmd);
+            ColumnOrder order = ref.getOrder();
+            order.addOrder(17, getColumnName("sequence"));
+            order.addOrder(4, getColumnName("columnname"));
+            order.addOrder(18, getColumnName("nullable"));
+            order.addOrder(6, getColumnName("type"));
+            order.addOrder(7, getColumnName("size"));
+            order.addOrder(2, getColumnName("schema"));
+            return ref;
+        } catch (Throwable th) {
+            rs.close();
+            throw th;
+        }
+    }
+
+    private ResultSetReference getPrimaryKeyInfo(DatabaseMetaData dbmeta,
+                                                 String tableName,
+                                                 String cmd) throws Throwable {
+        if (log.isDebugEnabled()) {
+            log.debug("report primary-key of : " + tableName);
+        }
+        ResultSet rs = dbmeta.getPrimaryKeys(null, null, convertPattern(tableName));
+        try {
+            ResultSetReference ref = new ResultSetReference(rs, cmd);
+            ColumnOrder order = ref.getOrder();
+            order.addOrder(1, getColumnName("catalog"));
+            order.addOrder(2, getColumnName("schema"));
+            order.addOrder(3, getColumnName("tablename"));
+            order.addOrder(5, getColumnName("sequence"));
+            order.addOrder(4, getColumnName("columnname"));
+            order.addOrder(6, getColumnName("keyname"));
+            return ref;
+        } catch (Throwable th) {
+            rs.close();
+            throw th;
+        }
+    }
+
+    private ResultSetReference getIndexInfo(DatabaseMetaData dbmeta, String tableName, String cmd) throws Throwable {
+        if (log.isDebugEnabled()) {
+            log.debug("report index of : " + tableName);
+        }
+        ResultSet rs = dbmeta.getIndexInfo(null, null, convertPattern(tableName), false, false);
+        try {
+            ResultSetReference ref = new ResultSetReference(rs, cmd);
+            ColumnOrder order = ref.getOrder();
+            order.addOrder(1, getColumnName("catalog"));
+            order.addOrder(2, getColumnName("schema"));
+            order.addOrder(3, getColumnName("tablename"));
+            order.addOrder(8, getColumnName("sequence"));
+            order.addOrder(9, getColumnName("columnname"));
+            order.addOrder(6, getColumnName("keyname"));
+            return ref;
+        } catch (Throwable th) {
+            rs.close();
+            throw th;
+        }
+    }
+
+    private void reportDBInfo(Connection conn) throws SQLException {
+        if (log.isDebugEnabled()) {
+            log.debug("report dbinfo");
+        }
+        DatabaseMetaData meta = conn.getMetaData();
+        final String userName = meta.getUserName();
+        outputMessage("Report.dbinfo",
+                      meta.getDatabaseProductName(),
+                      meta.getDatabaseProductVersion(),
+                      meta.getDriverName(),
+                      meta.getDriverVersion(),
+                      (userName == null) ? "" : userName,
+                      meta.getURL());
+    }
+
+    private static String getColumnName(String key) {
+        return getMessage("Report.label." + key);
+    }
+
+}
diff --git a/src/net/argius/stew/command/Time.java b/src/net/argius/stew/command/Time.java
new file mode 100644 (file)
index 0000000..fe81188
--- /dev/null
@@ -0,0 +1,119 @@
+package net.argius.stew.command;
+
+import java.sql.*;
+
+import net.argius.stew.*;
+
+/**
+ * The Time command used to measure execution times.
+ */
+public final class Time extends Command {
+
+    private static final Logger log = Logger.getLogger(Time.class);
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        if (!parameter.has(1)) {
+            throw new UsageException(getUsage());
+        }
+        int argsIndex = 0;
+        final String p1 = parameter.at(++argsIndex);
+        final int times;
+        if (p1.matches("\\d+")) {
+            ++argsIndex;
+            int number = 1;
+            try {
+                number = Integer.parseInt(p1);
+            } catch (NumberFormatException ex) {
+                log.warn("", ex);
+            }
+            times = number;
+        } else {
+            times = 1;
+        }
+        if (!parameter.has(argsIndex)) {
+            throw new UsageException(getUsage());
+        }
+        final String sql = parameter.after(argsIndex);
+        try {
+            if (times > 1) {
+                tryManyTimes(conn, sql, times);
+            } else {
+                tryOnce(conn, sql);
+            }
+        } catch (SQLException ex) {
+            throw new CommandException(ex);
+        }
+    }
+
+    private void tryOnce(Connection conn, String sql) throws SQLException {
+        log.debug("tryOnce");
+        Statement stmt = prepareStatement(conn, sql);
+        try {
+            final long beginningTime;
+            final long endTime;
+            if (isSelect(sql)) {
+                beginningTime = System.currentTimeMillis();
+                ResultSet rs = executeQuery(stmt, sql);
+                try {
+                    endTime = System.currentTimeMillis();
+                } finally {
+                    rs.close();
+                }
+            } else {
+                beginningTime = System.currentTimeMillis();
+                stmt.executeUpdate(sql);
+                endTime = System.currentTimeMillis();
+            }
+            if (log.isDebugEnabled()) {
+                log.debug("beginning: " + beginningTime);
+                log.debug("      end: " + endTime);
+            }
+            outputMessage("Time.once", (endTime - beginningTime) / 1000f);
+        } finally {
+            stmt.close();
+        }
+    }
+
+    private void tryManyTimes(Connection conn, String sql, int times) throws SQLException {
+        log.debug("tryManyTimes");
+        final boolean isSelect = isSelect(sql);
+        Statement stmt = prepareStatement(conn, sql);
+        try {
+            long total = 0;
+            long maximum = 0;
+            long minimun = Long.MAX_VALUE;
+            for (int i = 1; i <= times; i++) {
+                final long beginningTime;
+                final long endTime;
+                log.trace("beginning: %d", i);
+                if (isSelect) {
+                    beginningTime = System.currentTimeMillis();
+                    ResultSet rs = executeQuery(stmt, sql);
+                    try {
+                        endTime = System.currentTimeMillis();
+                    } finally {
+                        rs.close();
+                    }
+                } else {
+                    beginningTime = System.currentTimeMillis();
+                    stmt.executeUpdate(sql);
+                    endTime = System.currentTimeMillis();
+                }
+                log.trace("      end: %d", i);
+                final long result = endTime - beginningTime;
+                total += result;
+                maximum = Math.max(result, maximum);
+                minimun = Math.min(result, minimun);
+            }
+            outputMessage("Time.summary",
+                          total / 1000f,
+                          total / 1000f / times,
+                          maximum / 1000f,
+                          minimun / 1000f);
+        } finally {
+            stmt.close();
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/command/Upload.java b/src/net/argius/stew/command/Upload.java
new file mode 100644 (file)
index 0000000..7ca71d1
--- /dev/null
@@ -0,0 +1,52 @@
+package net.argius.stew.command;
+
+import java.io.*;
+import java.sql.*;
+
+import net.argius.stew.*;
+
+/**
+ * The Upload command used to upload a file into specified column.
+ */
+public final class Upload extends Command {
+
+    private static final Logger log = Logger.getLogger(Upload.class);
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        if (!parameter.has(2)) {
+            throw new UsageException(getUsage());
+        }
+        final File file = resolvePath(parameter.at(1));
+        final String sql = parameter.after(2);
+        if (log.isDebugEnabled()) {
+            log.debug("file: " + file.getAbsolutePath());
+            log.debug("SQL: " + sql);
+        }
+        if (file.length() > Integer.MAX_VALUE) {
+            throw new CommandException("file too large: " + file);
+        }
+        final int length = (int)file.length();
+        try {
+            InputStream is = new FileInputStream(file);
+            try {
+                PreparedStatement stmt = conn.prepareStatement(sql);
+                try {
+                    setTimeout(stmt);
+                    stmt.setBinaryStream(1, is, length);
+                    final int updatedCount = stmt.executeUpdate();
+                    outputMessage("i.updated", updatedCount);
+                } finally {
+                    stmt.close();
+                }
+            } finally {
+                is.close();
+            }
+        } catch (IOException ex) {
+            throw new CommandException(ex);
+        } catch (SQLException ex) {
+            throw new CommandException(ex);
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/command/Wait.java b/src/net/argius/stew/command/Wait.java
new file mode 100644 (file)
index 0000000..6d82b47
--- /dev/null
@@ -0,0 +1,27 @@
+package net.argius.stew.command;
+
+import java.sql.*;
+
+import net.argius.stew.*;
+
+/**
+ * The Wait command used to suspend a specified period of time(seconds).
+ */
+public final class Wait extends Command {
+
+    @Override
+    public void execute(Connection conn, Parameter parameter) throws CommandException {
+        try {
+            final double seconds = Double.parseDouble(parameter.at(1));
+            outputMessage("i.wait-time", seconds);
+            try {
+                Thread.sleep((long)(seconds * 1000L));
+            } catch (InterruptedException ex) {
+                throw new CommandException(ex);
+            }
+        } catch (NumberFormatException ex) {
+            throw new UsageException(getUsage());
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/io/CsvFormatter.java b/src/net/argius/stew/io/CsvFormatter.java
new file mode 100644 (file)
index 0000000..b91f403
--- /dev/null
@@ -0,0 +1,146 @@
+package net.argius.stew.io;
+
+/**
+ * CSV Formatter.
+ */
+@SuppressWarnings("hiding")
+public final class CsvFormatter {
+
+    /**
+     * Format Type.
+     */
+    public enum FormatType {
+
+        /**
+         * Encloses by <code>=" "</code> to recognize it as String.
+         */
+        STRING,
+
+        /**
+         * Encloses by <code>" "</code> to escape special characters.
+         * (CR, LF, comma, etc)
+         */
+        ESCAPE,
+
+        /**
+         * Detects type automatically.
+         */
+        AUTO,
+
+        /**
+         * Raw.
+         */
+        RAW;
+
+        static FormatType of(String s) {
+            try {
+                return valueOf(s);
+            } catch (IllegalArgumentException ex) {
+                return RAW;
+            }
+        }
+
+    }
+
+    /** @see FormatType */
+    public static final CsvFormatter RAW = new CsvFormatter(FormatType.RAW);
+
+    /** @see FormatType */
+    public static final CsvFormatter STRING = new CsvFormatter(FormatType.STRING);
+
+    /** @see FormatType */
+    public static final CsvFormatter ESCAPE = new CsvFormatter(FormatType.ESCAPE);
+
+    /** @see FormatType */
+    public static final CsvFormatter AUTO = new CsvFormatter(FormatType.AUTO);
+
+    private final FormatType type;
+
+    private CsvFormatter(FormatType type) {
+        this.type = type;
+    }
+
+    /**
+     * Formats a string.
+     * @param value
+     * @return
+     */
+    public String format(String value) {
+        switch (type) {
+            case STRING:
+                return editAsStringValue(value);
+            case ESCAPE:
+                return editAsEscapeValue(value);
+            case AUTO:
+                return editAuto(value);
+            case RAW:
+            default:
+                return value;
+        }
+    }
+
+    private static String editAuto(String value) {
+        // null or empty string -> RAW
+        if (value == null || value.length() == 0) {
+            return value;
+        }
+        // including double quotation -> STRING
+        if (value.indexOf('"') >= 0) {
+            return editAsEscapeValue(value);
+        }
+        // including CR or LF -> STRING
+        if (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0) {
+            return editAsEscapeValue(value);
+        }
+        final String trimmed = value.trim();
+        if (trimmed.length() > 0) {
+            // including separator (comma) -> STRING
+            if (trimmed.indexOf(',') >= 0) {
+                return editAsEscapeValue(value);
+            }
+            final char initial = trimmed.charAt(0);
+            // starting zero and is at least 32 characters -> parsing Double + STRING
+            if (initial == '0' && trimmed.length() >= 2) {
+                try {
+                    Double.parseDouble(value);
+                    return editAsStringValue(value);
+                } catch (NumberFormatException ex) {
+                    // ignore
+                }
+            }
+            // is decimal number and ends with zero -> parsing Double + STRING
+            if (trimmed.indexOf('.') >= 0 && trimmed.charAt(trimmed.length() - 1) == '0') {
+                try {
+                    Double.parseDouble(value);
+                    return editAsStringValue(value);
+                } catch (NumberFormatException ex) {
+                    // ignore
+                }
+            }
+            // more than the max of int -> STRING
+            if ('0' <= initial && initial <= '9') {
+                try {
+                    if (Long.parseLong(trimmed) > Integer.MAX_VALUE) {
+                        return editAsStringValue(value);
+                    }
+                } catch (NumberFormatException ex) {
+                    return editAsStringValue(value);
+                }
+            }
+        }
+        return value;
+    }
+
+    private static String editAsStringValue(String value) {
+        return String.format("=\"%s\"", escapeQuote(value));
+    }
+
+    private static String editAsEscapeValue(String value) {
+        return String.format("\"%s\"", escapeQuote(value));
+    }
+
+    private static String escapeQuote(String string) {
+        return string.replaceAll("\"", "\"\"");
+    }
+
+}
diff --git a/src/net/argius/stew/io/Exporter.java b/src/net/argius/stew/io/Exporter.java
new file mode 100644 (file)
index 0000000..04ec652
--- /dev/null
@@ -0,0 +1,102 @@
+package net.argius.stew.io;
+
+import java.io.*;
+
+/**
+ * A basic implementation of Exporter.
+ */
+public abstract class Exporter {
+
+    protected OutputStream os;
+    protected boolean wasWrittenHeader;
+
+    private boolean closed;
+
+    /**
+     * A constructor.
+     * @param os
+     */
+    protected Exporter(OutputStream os) {
+        this.os = os;
+        this.wasWrittenHeader = false;
+    }
+
+    /**
+     * Ensures that this stream is opened.
+     * @throws IOException this stream was closed
+     */
+    protected final void ensureOpen() throws IOException {
+        if (closed) {
+            throw new IOException("stream closed");
+        }
+    }
+
+    /**
+     * Adds a header.
+     * This method can be called only once after opening stream.
+     * @param header
+     * @throws IOException a header was already written, or another I/O error
+     */
+    public void addHeader(Object[] header) throws IOException {
+        ensureOpen();
+        if (wasWrittenHeader) {
+            throw new IOException("header was already written");
+        }
+        writeHeader(header);
+        wasWrittenHeader = true;
+    }
+
+    /**
+     * Writes a header.
+     * @param header
+     * @throws IOException
+     */
+    protected void writeHeader(Object[] header) throws IOException {
+        ensureOpen();
+        addRow(header);
+    }
+
+    /**
+     * Closes stream.
+     * @throws IOException
+     */
+    public void close() throws IOException {
+        ensureOpen();
+        if (os != null) {
+            try {
+                os.close();
+            } finally {
+                os = null;
+            }
+        }
+    }
+
+    /**
+     * Returns an Exporter.
+     * @param file
+     * @return
+     * @throws IOException
+     */
+    public static Exporter getExporter(File file) throws IOException {
+        return ExporterFactory.createExporter(new Path(file));
+    }
+
+    /**
+     * Returns an Exporter.
+     * @param fileName
+     * @return
+     * @throws IOException
+     */
+    public static Exporter getExporter(String fileName) throws IOException {
+        return ExporterFactory.createExporter(new Path(fileName));
+    }
+
+    /**
+     * Adds a row.
+     * @param values
+     * @throws IOException
+     */
+
+    public abstract void addRow(Object[] values) throws IOException;
+
+}
\ No newline at end of file
diff --git a/src/net/argius/stew/io/ExporterFactory.java b/src/net/argius/stew/io/ExporterFactory.java
new file mode 100644 (file)
index 0000000..84932a6
--- /dev/null
@@ -0,0 +1,37 @@
+package net.argius.stew.io;
+
+import java.io.*;
+
+/**
+ * A factory to create an Exporter.
+ */
+final class ExporterFactory {
+
+    private ExporterFactory() {
+        // empty
+    }
+
+    /**
+     * Returns an Exporter.
+     * @param path
+     * @return
+     * @throws IOException
+     */
+    static Exporter createExporter(Path path) throws IOException {
+        final String ext = path.getExtension();
+        if (ext.equalsIgnoreCase("xml")) {
+            return new XmlExporter(openFile(path));
+        } else if (ext.equalsIgnoreCase("htm") || ext.equalsIgnoreCase("html")) {
+            return new HtmlExporter(openFile(path), "");
+        } else if (ext.equalsIgnoreCase("csv")) {
+            return new SimpleExporter(openFile(path), ",");
+        } else {
+            return new SimpleExporter(openFile(path), "\t");
+        }
+    }
+
+    private static OutputStream openFile(File file) throws IOException {
+        return new FileOutputStream(file);
+    }
+
+}
diff --git a/src/net/argius/stew/io/HtmlExporter.java b/src/net/argius/stew/io/HtmlExporter.java
new file mode 100644 (file)
index 0000000..f55bc4a
--- /dev/null
@@ -0,0 +1,69 @@
+package net.argius.stew.io;
+
+import java.io.*;
+
+/**
+ * The Exporter for HTML.
+ */
+public final class HtmlExporter extends Exporter {
+
+    private PrintWriter out;
+
+    /**
+     * A constructor.
+     * @param os
+     * @param title
+     */
+    public HtmlExporter(OutputStream os, String title) {
+        super(os);
+        this.out = new PrintWriter(os);
+        out.println("<html>");
+        out.println("<head>");
+        out.printf("<title>%s</title>%n", title);
+        out.println("</head>");
+        out.println("<body>");
+        out.printf("<h1>%s</h1>%n", title);
+        out.println("<table>");
+        out.flush();
+    }
+
+    @Override
+    protected void writeHeader(Object[] header) throws IOException {
+        ensureOpen();
+        out.println("<tr>");
+        for (Object o : header) {
+            out.printf("<th>%s</th>%n", o);
+        }
+        out.println("</tr>");
+        out.flush();
+    }
+
+    @Override
+    public void addRow(Object[] values) throws IOException {
+        ensureOpen();
+        out.println("<tr>");
+        for (Object o : values) {
+            out.printf("<td>%s</td>%n", o);
+        }
+        out.println("</tr>");
+        out.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+        ensureOpen();
+        try {
+            if (out != null) {
+                out.println("</table>");
+                out.println("</body>");
+                out.println("</html>");
+                out.flush();
+                out.close();
+            }
+        } finally {
+            out = null;
+            super.close();
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/io/Importer.java b/src/net/argius/stew/io/Importer.java
new file mode 100644 (file)
index 0000000..b011ecb
--- /dev/null
@@ -0,0 +1,99 @@
+package net.argius.stew.io;
+
+import java.io.*;
+
+/**
+ * A basic implementation of Importer.
+ */
+public abstract class Importer {
+
+    protected InputStream is;
+    protected boolean wasReadHeader;
+    protected boolean closed;
+
+    /**
+     * A constructor.
+     * @param is InputStream
+     */
+    protected Importer(InputStream is) {
+        this.is = is;
+        this.wasReadHeader = false;
+        this.closed = false;
+    }
+
+    /**
+     * Ensures that this stream is opened.
+     * @throws IOException this stream was closed
+     */
+    protected final void ensureOpen() throws IOException {
+        if (closed) {
+            throw new IOException("stream closed");
+        }
+    }
+
+    /**
+     * Returns the header.
+     * @return
+     * @throws IOException
+     */
+    public Object[] getHeader() throws IOException {
+        ensureOpen();
+        Object[] header = readHeader();
+        wasReadHeader = true;
+        return header;
+    }
+
+    /**
+     * Reads the header.
+     * @return
+     * @throws IOException 
+     */
+    protected Object[] readHeader() throws IOException {
+        ensureOpen();
+        return nextRow();
+    }
+
+    /**
+     * Closes this stream.
+     * @throws IOException
+     */
+    public void close() throws IOException {
+        ensureOpen();
+        if (is != null) {
+            try {
+                is.close();
+            } finally {
+                closed = true;
+                is = null;
+            }
+        }
+    }
+
+    /**
+     * Returns an Importer.
+     * @param file
+     * @return
+     * @throws IOException
+     */
+    public static Importer getImporter(File file) throws IOException {
+        return ImporterFactory.createImporter(new Path(file));
+    }
+
+    /**
+     * Returns an Importer.
+     * @param fileName
+     * @return
+     * @throws IOException
+     */
+    public static Importer getImporter(String fileName) throws IOException {
+        return ImporterFactory.createImporter(new Path(fileName));
+    }
+
+    /**
+     * Returns the next row.
+     * @return nex row. when it has no more row, returns empty array
+     * @throws IOException
+     */
+    public abstract Object[] nextRow() throws IOException;
+
+}
\ No newline at end of file
diff --git a/src/net/argius/stew/io/ImporterFactory.java b/src/net/argius/stew/io/ImporterFactory.java
new file mode 100644 (file)
index 0000000..fabc5b9
--- /dev/null
@@ -0,0 +1,35 @@
+package net.argius.stew.io;
+
+import java.io.*;
+
+/**
+ * A factory of Importer.
+ */
+final class ImporterFactory {
+
+    private ImporterFactory() {
+        // empty
+    }
+
+    /**
+     * Returns an Importer.
+     * @param path
+     * @return
+     * @throws IOException
+     */
+    static Importer createImporter(Path path) throws IOException {
+        final String ext = path.getExtension();
+        if (ext.equalsIgnoreCase("xml")) {
+            return new XmlImporter(openFile(path));
+        } else if (ext.equalsIgnoreCase("csv")) {
+            return new SmartImporter(openFile(path), ",");
+        } else {
+            return new SmartImporter(openFile(path), "\t");
+        }
+    }
+
+    private static InputStream openFile(File file) throws IOException {
+        return new FileInputStream(file);
+    }
+
+}
diff --git a/src/net/argius/stew/io/Path.java b/src/net/argius/stew/io/Path.java
new file mode 100644 (file)
index 0000000..7069283
--- /dev/null
@@ -0,0 +1,145 @@
+package net.argius.stew.io;
+
+import java.io.*;
+import java.net.*;
+
+/**
+ * Path is an extended java.util.File.
+ */
+public final class Path extends File {
+
+    private static final long serialVersionUID = 6787315355616650978L;
+
+    private static final String PATTERN_EXTENSION = "^.*\\.([^\\.]+)$";
+
+    /**
+     * A constructor.
+     * @param file
+     */
+    public Path(File file) {
+        super(file.getPath());
+    }
+
+    /**
+     * A constructor.
+     * @param parent
+     * @param child
+     */
+    public Path(File parent, String child) {
+        super(parent, child);
+    }
+
+    /**
+     * A constructor.
+     * @param pathname
+     */
+    public Path(String pathname) {
+        super(pathname);
+    }
+
+    /**
+     * A constructor.
+     * @param parent
+     * @param child
+     */
+    public Path(String parent, String child) {
+        super(new File(parent), child);
+    }
+
+    /**
+     * A constructor.
+     * @param uri URI
+     */
+    public Path(URI uri) {
+        super(uri);
+    }
+
+    /**
+     * Resolves the path.
+     * @param parent
+     * @param child
+     * @return
+     */
+    public static Path resolve(File parent, File child) {
+        if (child.isAbsolute()) {
+            return new Path(child.getAbsolutePath());
+        }
+        return new Path(parent, child.getPath());
+    }
+
+    /**
+     * Resolves the path.
+     * @param parent
+     * @param child
+     * @return
+     */
+    public static Path resolve(File parent, String child) {
+        return resolve(parent, new File(child));
+    }
+
+    /**
+     * Resolves the path.
+     * @param parent
+     * @param child
+     * @return
+     */
+    public static Path resolve(String parent, String child) {
+        return resolve(new File(parent), child);
+    }
+
+    /**
+     * Returns the extension of this path.
+     * @return
+     * @see #getExtension(String)
+     */
+    public String getExtension() {
+        return getExtension(getPath());
+    }
+
+    /**
+     * Returns the extension of this path.
+     * @param file
+     * @return
+     * @see #getExtension(String)
+     */
+    public static String getExtension(File file) {
+        return getExtension(file.getName());
+    }
+
+    /**
+     * Returns the extension of this path.
+     * If the path string contains periods,
+     * the extension is a string from after the last period to the end.
+     * Otherwise, returns an empty string.
+     * @param path
+     * @return
+     */
+    public static String getExtension(String path) {
+        if (path.matches(PATTERN_EXTENSION)) {
+            return path.replaceFirst(PATTERN_EXTENSION, "$1");
+        }
+        return "";
+    }
+
+    /**
+     * Creates directories if not exists.
+     * @throws IOException If it failed to create directories
+     */
+    public void makeDirectory() throws IOException {
+        makeDirectory(this);
+    }
+
+    /**
+     * Creates directories if not exists.
+     * @param file
+     * @throws IOException If it failed to create directories
+     */
+    public static void makeDirectory(File file) throws IOException {
+        if (!file.isDirectory()) {
+            if (!file.mkdirs() || !file.isDirectory()) {
+                throw new IOException("can't make directory: " + file);
+            }
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/io/SimpleExporter.java b/src/net/argius/stew/io/SimpleExporter.java
new file mode 100644 (file)
index 0000000..2ca4ef5
--- /dev/null
@@ -0,0 +1,112 @@
+package net.argius.stew.io;
+
+import java.io.*;
+
+import net.argius.stew.*;
+import net.argius.stew.io.CsvFormatter.FormatType;
+
+/**
+ * A simple implementation of Exporter.
+ */
+public final class SimpleExporter extends Exporter {
+
+    private static final String PROP_FORMAT = SimpleExporter.class.getName() + ".format";
+
+    private final String separator;
+
+    private PrintWriter out;
+    private CsvFormatter formatter;
+
+    /**
+     * A constructor.
+     * @param os
+     * @param separator
+     */
+    public SimpleExporter(OutputStream os, String separator) {
+        this(os, separator, getDefaultFormatter());
+    }
+
+    /**
+     * A constructor.
+     * @param os
+     * @param separator
+     * @param formatter
+     */
+    public SimpleExporter(OutputStream os, String separator, CsvFormatter formatter) {
+        super(os);
+        this.out = new PrintWriter(os);
+        this.separator = separator;
+        this.formatter = formatter;
+    }
+
+    /**
+     * Returns the formatter.
+     * @return
+     */
+    public CsvFormatter getFormatter() {
+        return formatter;
+    }
+
+    /**
+     * Sets a formatter.
+     * @param formatter
+     */
+    public void setFormatter(CsvFormatter formatter) {
+        this.formatter = formatter;
+    }
+
+    private static CsvFormatter getDefaultFormatter() {
+        try {
+            switch (FormatType.of(Bootstrap.getProperty(PROP_FORMAT).toUpperCase())) {
+                case STRING:
+                    return CsvFormatter.STRING;
+                case ESCAPE:
+                    return CsvFormatter.ESCAPE;
+                case AUTO:
+                    return CsvFormatter.AUTO;
+                case RAW:
+                default:
+                    return CsvFormatter.RAW;
+            }
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    @Override
+    public void addRow(Object[] values) throws IOException {
+        ensureOpen();
+        for (int i = 0; i < values.length; i++) {
+            Object o = values[i];
+            if (i > 0) {
+                out.print(separator);
+            }
+            String value;
+            if (o instanceof String) {
+                value = (String)o;
+            } else if (values[i] != null) {
+                value = o.toString();
+            } else {
+                value = "";
+            }
+            out.print(formatter.format(value));
+        }
+        out.println();
+        out.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+        ensureOpen();
+        try {
+            if (out != null) {
+                out.flush();
+                out.close();
+            }
+        } finally {
+            out = null;
+            super.close();
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/io/SmartImporter.java b/src/net/argius/stew/io/SmartImporter.java
new file mode 100644 (file)
index 0000000..3556406
--- /dev/null
@@ -0,0 +1,200 @@
+package net.argius.stew.io;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * An implementation of Importer  smart () 
+ */
+public final class SmartImporter extends Importer {
+
+    private final String separator;
+    private final int separatorLength;
+
+    private Reader reader;
+    private StringBuilder buffer;
+    private char[] chars;
+
+    /**
+     * An constructor.
+     * @param is
+     * @param separator
+     */
+    public SmartImporter(InputStream is, String separator) {
+        super(is);
+        this.separator = separator;
+        this.separatorLength = separator.length();
+        this.reader = new InputStreamReader(is);
+        this.buffer = new StringBuilder();
+        this.chars = new char[0x4000];
+    }
+
+    /**
+     * An constructor.
+     * @param reader
+     * @param separator
+     */
+    public SmartImporter(Reader reader, String separator) {
+        this(new InputStream() {
+
+            @Override
+            public int read() throws IOException {
+                throw new IllegalStateException("cannot use");
+            }
+
+        }, separator);
+        this.reader = reader;
+    }
+
+    @Override
+    public Object[] nextRow() throws IOException {
+        ensureOpen();
+        List<String> row = new ArrayList<String>();
+        while (true) {
+            if (!fillBuffer(1)) {
+                break;
+            }
+            int start;
+            int offset;
+            int quoteEnd;
+            boolean isQuote = false;
+            char initial = buffer.charAt(0);
+            if (initial == '"') {
+                // quote
+                if (fillBuffer(2)) {
+                    int fromIndex = 1;
+                    while (true) {
+                        int i = indexOf(initial, fromIndex, true);
+                        if (i < 0) {
+                            start = 0;
+                            quoteEnd = buffer.length();
+                            break;
+                        }
+                        if (fillBuffer(i + 2) && buffer.charAt(i + 1) == initial) {
+                            fromIndex = i + 2;
+                        } else {
+                            start = 1;
+                            quoteEnd = i;
+                            isQuote = true;
+                            break;
+                        }
+                    }
+                } else {
+                    start = 0;
+                    quoteEnd = buffer.length();
+                }
+                offset = quoteEnd + 1;
+            } else {
+                // not quote
+                start = 0;
+                quoteEnd = -1;
+                offset = 0;
+            }
+            int index;
+            int drawLength = 0;
+            boolean isRowEnd = false;
+            while (true) {
+                int length = buffer.length();
+                int indexLS = indexOf('\r', offset, false);
+                boolean hasCR = indexLS >= 0;
+                if (!hasCR) {
+                    indexLS = indexOf('\n', offset, false);
+                }
+                int indexCS = indexOf(separator, offset, false);
+                if (indexLS < 0 && indexCS < 0) {
+                    // not found
+                    if (read() <= 0) {
+                        index = length;
+                        drawLength = length;
+                        isRowEnd = true;
+                        break;
+                    }
+                    offset = length;
+                    continue;
+                } else if (indexLS >= 0 && (indexLS < indexCS || indexCS < 0)) {
+                    // end of line
+                    int lssize = 1;
+                    if (hasCR && length > indexLS) {
+                        if (fillBuffer(indexLS + 2) && buffer.charAt(indexLS + 1) == '\n') {
+                            lssize += 1;
+                        }
+                    }
+                    index = indexLS;
+                    drawLength = indexLS + lssize;
+                    isRowEnd = true;
+                    break;
+                } else if (indexCS >= 0) {
+                    // separator
+                    index = indexCS;
+                    drawLength = indexCS + separatorLength;
+                    break;
+                } else {
+                    assert false;
+                }
+            }
+            final int end = (isQuote ? quoteEnd : index);
+            final CharSequence column = buffer.subSequence(0, drawLength);
+            buffer.delete(0, drawLength);
+            addColumn(row, column.subSequence(start, end));
+            if (isRowEnd) {
+                break;
+            }
+        }
+        return row.toArray();
+    }
+
+    @Override
+    public void close() throws IOException {
+        ensureOpen();
+        reader = null;
+        buffer = null;
+        chars = null;
+        super.close();
+    }
+
+    int indexOf(char c, int fromIndex, boolean autoFill) throws IOException {
+        return indexOf(String.valueOf(c), fromIndex, autoFill);
+    }
+
+    int indexOf(String s, int fromIndex, boolean autoFill) throws IOException {
+        while (true) {
+            int index = buffer.indexOf(s, fromIndex);
+            if (index >= 0) {
+                return index;
+            }
+            if (!autoFill || read() <= 0) {
+                return -1;
+            }
+        }
+    }
+
+    private int read() throws IOException {
+        final int length = reader.read(chars);
+        if (length > 0) {
+            buffer.append(chars, 0, length);
+        }
+        return length;
+    }
+
+    private boolean fillBuffer(int size) throws IOException {
+        while (buffer.length() < size) {
+            final int rest = size - buffer.length();
+            final int length = reader.read(chars, 0, rest);
+            if (length <= 0) {
+                break;
+            }
+            buffer.append(chars, 0, length);
+        }
+        return buffer.length() >= size;
+    }
+
+    private static void addColumn(List<String> row, CharSequence cs) {
+        StringBuilder buffer = new StringBuilder(cs.length());
+        for (String s : cs.toString().split("\"\"")) {
+            buffer.append('"');
+            buffer.append(s);
+        }
+        row.add(buffer.substring(1));
+    }
+
+}
diff --git a/src/net/argius/stew/io/StringBasedSerializer.java b/src/net/argius/stew/io/StringBasedSerializer.java
new file mode 100644 (file)
index 0000000..3acfade
--- /dev/null
@@ -0,0 +1,242 @@
+package net.argius.stew.io;
+
+import java.io.*;
+import java.math.*;
+import java.util.*;
+
+/**
+ * A serializer with String.
+ */
+public final class StringBasedSerializer {
+
+    private StringBasedSerializer() {
+        // empty
+    }
+
+    /**
+     * Serializes an object.
+     * @param object
+     * @return
+     * @throws IOException
+     */
+    public static Element serialize(Object object) throws IOException {
+        String name;
+        String value;
+        if (object == null) {
+            name = Element.NULL;
+            value = null;
+        } else if (object instanceof String) {
+            name = Element.STRING;
+            value = (String)object;
+        } else if (object instanceof Boolean) {
+            name = Element.BOOLEAN;
+            value = object.toString();
+        } else if (object instanceof Byte) {
+            name = Element.BYTE;
+            value = object.toString();
+        } else if (object instanceof Short) {
+            name = Element.SHORT;
+            value = object.toString();
+        } else if (object instanceof Integer) {
+            name = Element.INT;
+            value = object.toString();
+        } else if (object instanceof Long) {
+            name = Element.LONG;
+            value = object.toString();
+        } else if (object instanceof Float) {
+            name = Element.FLOAT;
+            value = object.toString();
+        } else if (object instanceof Double) {
+            name = Element.DOUBLE;
+            value = object.toString();
+        } else if (object instanceof BigDecimal) {
+            name = Element.DECIMAL;
+            value = object.toString();
+        } else if (object instanceof Date) {
+            name = Element.TIME;
+            Date date = (Date)object;
+            value = Long.toString(date.getTime());
+        } else {
+            name = Element.OBJECT;
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ObjectOutputStream oos = new ObjectOutputStream(bos);
+            try {
+                oos.writeObject(object);
+                value = toHexString(bos.toByteArray());
+            } finally {
+                oos.close();
+            }
+        }
+        return new Element(name, value);
+    }
+
+    /**
+     * Deserializes an object.
+     * @param element
+     * @return
+     * @throws IOException
+     */
+    public static Object deserialize(Element element) throws IOException {
+        return deserialize(element.getType(), element.getValue());
+    }
+
+    /**
+     * Deserializes an object.
+     * @param name
+     * @param value
+     * @return
+     * @throws IOException
+     */
+    public static Object deserialize(String name, String value) throws IOException {
+        Object o;
+        if (name.equals(Element.NULL)) {
+            o = null;
+        } else if (name.equals(Element.STRING)) {
+            o = value;
+        } else if (name.equals(Element.BOOLEAN)) {
+            o = Boolean.valueOf(value);
+        } else if (name.equals(Element.BYTE)) {
+            o = Byte.valueOf(value);
+        } else if (name.equals(Element.SHORT)) {
+            o = Short.valueOf(value);
+        } else if (name.equals(Element.INT)) {
+            o = Integer.valueOf(value);
+        } else if (name.equals(Element.LONG)) {
+            o = Long.valueOf(value);
+        } else if (name.equals(Element.FLOAT)) {
+            o = Float.valueOf(value);
+        } else if (name.equals(Element.DOUBLE)) {
+            o = Double.valueOf(value);
+        } else if (name.equals(Element.DECIMAL)) {
+            o = new BigDecimal(value);
+        } else if (name.equals(Element.TIME)) {
+            o = new Date(Long.parseLong(value));
+        } else if (name.equals(Element.OBJECT)) {
+            ByteArrayInputStream bis = new ByteArrayInputStream(toBytes(value));
+            ObjectInputStream ois = new ObjectInputStream(bis);
+            try {
+                o = ois.readObject();
+            } catch (ClassNotFoundException ex) {
+                IOException ioe = new IOException(ex.getMessage());
+                ioe.initCause(ex);
+                throw ioe;
+            } finally {
+                ois.close();
+            }
+        } else {
+            throw new IOException("unknown element : " + name);
+        }
+        return o;
+    }
+
+    private static String toHexString(byte[] bytes) {
+        StringBuilder buffer = new StringBuilder(bytes.length * 2);
+        for (byte b : bytes) {
+            buffer.append(String.format("%02X", b & 0xFF));
+        }
+        return buffer.toString();
+    }
+
+    private static byte[] toBytes(String hexString) {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        for (int i = 0; i < hexString.length(); i += 2) {
+            String s = hexString.substring(i, i + 2);
+            bos.write(Integer.parseInt(s, 16));
+        }
+        return bos.toByteArray();
+    }
+
+    /**
+     * Element (type).
+     */
+    public static final class Element {
+
+        /**
+         * <code>NULL</code>
+         */
+        public static final String NULL = "null";
+        /**
+         * <code>STRING</code>
+         */
+        public static final String STRING = "string";
+        /**
+         * <code>BOOLEAN</code>
+         */
+        public static final String BOOLEAN = "boolean";
+        /**
+         * <code>BYTE</code>
+         */
+        public static final String BYTE = "byte";
+        /**
+         * <code>SHORT</code>
+         */
+        public static final String SHORT = "short";
+        /**
+         * <code>INT</code>
+         */
+        public static final String INT = "int";
+        /**
+         * <code>LONG</code>
+         */
+        public static final String LONG = "long";
+        /**
+         * <code>FLOAT</code>
+         */
+        public static final String FLOAT = "float";
+        /**
+         * <code>DOUBLE</code>
+         */
+        public static final String DOUBLE = "double";
+        /**
+         * <code>DECIMAL</code>
+         */
+        public static final String DECIMAL = "decimal";
+        /**
+         * <code>TIME</code>
+         */
+        public static final String TIME = "time";
+        /**
+         * <code>OBJECT</code>
+         */
+        public static final String OBJECT = "object";
+
+        private final String type;
+        private final String value;
+
+        Element(String type, String value) {
+            this.type = (type == null) ? NULL : type;
+            this.value = value;
+        }
+
+        /**
+         * Returns this type.
+         * @return
+         */
+        public String getType() {
+            return type;
+        }
+
+        /**
+         * Returns this value.
+         * @return
+         */
+        public String getValue() {
+            return value;
+        }
+
+        /**
+         * Tests whether this element is null.
+         * @return
+         */
+        public boolean isNull() {
+            return (type == NULL || value == null);
+        }
+
+        @Override
+        public String toString() {
+            return type + ":" + value;
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/io/XmlExporter.java b/src/net/argius/stew/io/XmlExporter.java
new file mode 100644 (file)
index 0000000..ebad5de
--- /dev/null
@@ -0,0 +1,117 @@
+package net.argius.stew.io;
+
+import java.io.*;
+
+import net.argius.stew.io.StringBasedSerializer.Element;
+
+/**
+ * The Exporter for XML.
+ */
+public final class XmlExporter extends Exporter {
+
+    private static final String ENCODING = "utf-8";
+    private static final String TAG_TABLE = "table";
+    private static final String TAG_TABLE_START = "<"
+                                                  + TAG_TABLE
+                                                  + " writer=\""
+                                                  + XmlExporter.class.getName()
+                                                  + "\">";
+    private static final String TAG_TABLE_END = "</" + TAG_TABLE + ">";
+    private static final String TAG_HEADERROW = "headerrow";
+    private static final String TAG_HEADERROW_END = "</" + TAG_HEADERROW + ">";
+    private static final String TAG_HEADERROW_START = "<" + TAG_HEADERROW + ">";
+    private static final String TAG_HEADER = "header";
+    private static final String TAG_HEADER_START = "<" + TAG_HEADER;
+    private static final String TAG_HEADER_END = "</" + TAG_HEADER + ">";
+    private static final String TAG_ROW = "row";
+    private static final String TAG_ROW_START = "<" + TAG_ROW + ">";
+    private static final String TAG_ROW_END = "</" + TAG_ROW + ">";
+
+    private PrintWriter out;
+
+    /**
+     * An constructor.
+     * @param outputStream 
+     */
+    public XmlExporter(OutputStream outputStream) {
+        super(outputStream);
+        try {
+            this.out = new PrintWriter(new OutputStreamWriter(outputStream, ENCODING));
+            out.println("<?xml version=\"1.0\" encoding=\"" + ENCODING + "\"?>");
+            out.println("<!DOCTYPE " + TAG_TABLE + " SYSTEM \"stew-table.dtd\">");
+            out.println(TAG_TABLE_START);
+        } catch (UnsupportedEncodingException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    @Override
+    protected void writeHeader(Object[] header) throws IOException {
+        ensureOpen();
+        out.println(TAG_HEADERROW_START);
+        for (int i = 0; i < header.length; i++) {
+            Object o = header[i];
+            out.printf(TAG_HEADER_START + " index=\"%d\">%s" + TAG_HEADER_END + "%n",
+                       i,
+                       convertCData(String.valueOf(o)));
+        }
+        out.println(TAG_HEADERROW_END);
+        out.flush();
+    }
+
+    @Override
+    public void addRow(Object[] values) throws IOException {
+        ensureOpen();
+        out.print(TAG_ROW_START);
+        for (int i = 0; i < values.length; i++) {
+            Object o = values[i];
+            Element element = StringBasedSerializer.serialize(o);
+            String type = element.getType();
+            if (element.isNull()) {
+                out.print("<" + type + "/>");
+            } else {
+                out.print("<" + type);
+                if (type.equals(Element.OBJECT)) {
+                    out.print(" class=\"");
+                    out.print(o.getClass().getName());
+                    out.print("\"");
+                } else if (type.equals(Element.TIME)) {
+                    out.print(" display=\"");
+                    out.print(o);
+                    out.print("\"");
+                }
+                out.print(">");
+                out.print(convertCData(element.getValue()));
+                out.print("</" + type + ">");
+            }
+        }
+        out.println(TAG_ROW_END);
+        out.flush();
+    }
+
+    private static String convertCData(String string) {
+        String s = string;
+        if (s.indexOf('<') >= 0 || s.indexOf('>') >= 0) {
+            if (s.contains("]]>")) {
+                s = s.replaceAll("\\]\\]>", "]]&gt;");
+            }
+            return "<![CDATA[" + s + "]]>";
+        }
+        return s;
+    }
+
+    @Override
+    public void close() throws IOException {
+        ensureOpen();
+        try {
+            if (out != null) {
+                out.print(TAG_TABLE_END);
+                out.close();
+            }
+        } finally {
+            out = null;
+            super.close();
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/io/XmlImporter.java b/src/net/argius/stew/io/XmlImporter.java
new file mode 100644 (file)
index 0000000..7961270
--- /dev/null
@@ -0,0 +1,255 @@
+package net.argius.stew.io;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+/**
+ * The Importer for XML.
+ */
+public final class XmlImporter extends Importer {
+
+    private static final Pattern PATTERN_TAG = Pattern.compile("<(/?)([A-Za-z0-9]+)([^/>]+)?(/?) *>");
+    private static final Pattern PATTERN_TAG_HEADER_ATTRIBUTE = Pattern.compile("(?i)index=\"[^0-9\"]*([0-9]+)[^0-9\"]*\"");
+    private static final String SLASH = "/";
+    private static final String TAG_TABLE = "table";
+    private static final String TAG_HEADERROW = "headerrow";
+    private static final String TAG_HEADER = "header";
+    private static final String TAG_ROW = "row";
+    private static final String TAG_NULL = "null";
+
+    private BufferedReader reader;
+    private StringBuilder buffer;
+    private char[] chars;
+    private boolean hasMoreData;
+
+    /**
+     * An constructor.
+     * @param is
+     * @throws IOException
+     */
+    public XmlImporter(InputStream is) throws IOException {
+        super(is);
+        String enc = getEncoding();
+        this.reader = new BufferedReader(new InputStreamReader(is, enc));
+        this.buffer = new StringBuilder(1024);
+        this.chars = new char[1024];
+        this.hasMoreData = seekStartTag(TAG_TABLE);
+    }
+
+    @Override
+    protected Object[] readHeader() throws IOException {
+        Map<Integer, Object> map = new HashMap<Integer, Object>();
+        boolean isHeaderRow = false;
+        while (true) {
+            Matcher m = PATTERN_TAG.matcher(buffer);
+            if (m.find()) {
+                String g1 = m.group(1);
+                String name = m.group(2);
+                String attribute = String.valueOf(m.group(3));
+                String g4 = m.group(4);
+                buffer.delete(0, m.end());
+                if (name.equalsIgnoreCase(TAG_HEADERROW)) {
+                    if (g1.equals(SLASH) || g4.equals(SLASH)) {
+                        isHeaderRow = false;
+                        break;
+                    }
+                    isHeaderRow = true;
+                    continue;
+                } else if (name.equalsIgnoreCase(TAG_TABLE) || name.equalsIgnoreCase(TAG_ROW)) {
+                    break;
+                } else if (isHeaderRow && name.equalsIgnoreCase(TAG_HEADER)) {
+                    Matcher mAttr = PATTERN_TAG_HEADER_ATTRIBUTE.matcher(attribute);
+                    final int index;
+                    if (mAttr.find()) {
+                        index = Integer.parseInt(mAttr.group(1));
+                    } else {
+                        index = map.size();
+                    }
+                    while (true) {
+                        Matcher mEnd = PATTERN_TAG.matcher(buffer);
+                        if (mEnd.find() && mEnd.group(1).equals(SLASH)) {
+                            int iEnd = mEnd.start();
+                            Object value = buffer.subSequence(0, iEnd);
+                            map.put(index, parseCData(value));
+                            buffer.delete(0, mEnd.end());
+                            break;
+                        }
+                        if (readChars() <= 0) {
+                            break;
+                        }
+                    }
+                }
+            } else {
+                if (readChars() <= 0) {
+                    break;
+                }
+            }
+        }
+        if (map.isEmpty()) {
+            return new Object[0];
+        }
+        final int max = Collections.max(map.keySet());
+        Object[] headers = new Object[max + 1];
+        for (int i = 0; i <= max; i++) {
+            if (map.containsKey(i)) {
+                headers[i] = map.get(i);
+            }
+        }
+        return headers;
+    }
+
+    @Override
+    public Object[] nextRow() throws IOException {
+        ensureOpen();
+        while (hasMoreData) {
+            Matcher m = PATTERN_TAG.matcher(buffer);
+            if (m.find()) {
+                String name = m.group(2);
+                String g4 = m.group(4);
+                buffer.delete(0, m.end());
+                if (name.equalsIgnoreCase(TAG_ROW)) {
+                    if (g4.equals(SLASH)) {
+                        return new Object[0];
+                    }
+                    return parseRow();
+                } else if (name.equalsIgnoreCase(TAG_TABLE)) {
+                    hasMoreData = false;
+                }
+            } else {
+                if (readChars() <= 0) {
+                    hasMoreData = false;
+                    break;
+                }
+            }
+        }
+        return new Object[0];
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            if (reader != null) {
+                reader.close();
+            }
+        } finally {
+            reader = null;
+            buffer = null;
+            super.close();
+        }
+    }
+
+    private String getEncoding() throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        for (int c; (c = is.read()) >= 0;) {
+            bos.write(c);
+            if (c == '>') {
+                break;
+            }
+        }
+        return parseEncoding(bos.toString("ISO8859-1"));
+    }
+
+    private static String parseEncoding(String declaration) {
+        Pattern p = Pattern.compile("<\\?xml.+encoding=\"([^\"]+)\"\\?>", Pattern.CASE_INSENSITIVE);
+        Matcher m = p.matcher(declaration);
+        if (m.find()) {
+            return m.group(1);
+        }
+        return "utf-8";
+    }
+
+    private boolean seekStartTag(String tagName) throws IOException {
+        while (true) {
+            Matcher m = PATTERN_TAG.matcher(buffer);
+            if (m.find()) {
+                String g1 = m.group(1);
+                if (!g1.equals(SLASH)) {
+                    if (tagName.equalsIgnoreCase(m.group(2))) {
+                        int position;
+                        if (m.group(4).equals(SLASH)) {
+                            position = m.start();
+                        } else {
+                            position = m.end();
+                        }
+                        buffer.delete(0, position);
+                        return true;
+                    }
+                }
+            }
+            if (readChars() <= 0) {
+                break;
+            }
+        }
+        return false;
+    }
+
+    private Object[] parseRow() throws IOException {
+        List<Object> list = new ArrayList<Object>();
+        while (hasMoreData) {
+            Matcher m = PATTERN_TAG.matcher(buffer);
+            if (m.find()) {
+                String g1 = m.group(1);
+                String name = m.group(2);
+                String g4 = m.group(4);
+                buffer.delete(0, m.end());
+                if (g1.equals(SLASH)) {
+                    if (name.equals(TAG_ROW)) {
+                        return list.toArray();
+                    } else if (name.equals(TAG_TABLE)) {
+                        break;
+                    }
+                } else if (name.equalsIgnoreCase(TAG_NULL)) {
+                    list.add(null);
+                } else if (g4.equals(SLASH)) {
+                    list.add(deserialize(name, ""));
+                } else {
+                    while (true) {
+                        Matcher mEnd = PATTERN_TAG.matcher(buffer);
+                        if (mEnd.find() && mEnd.group(1).equals(SLASH)) {
+                            int iEnd = mEnd.start();
+                            list.add(deserialize(name, buffer.subSequence(0, iEnd).toString()));
+                            buffer.delete(0, mEnd.end());
+                            break;
+                        }
+                        if (readChars() <= 0) {
+                            break;
+                        }
+                    }
+                }
+            } else {
+                if (readChars() <= 0) {
+                    break;
+                }
+            }
+        }
+        throw new IOException("</row> not found");
+    }
+
+    private static Object deserialize(String name, String value) throws IOException {
+        return parseCData(StringBasedSerializer.deserialize(name, value));
+    }
+
+    private static Object parseCData(Object o) {
+        if (o instanceof String) {
+            String s = (String)o;
+            if (s.matches("^<!\\[CDATA\\[.*\\]\\]>$")) {
+                String value = s.substring(9, s.length() - 3);
+                if (value.contains("]]&gt;")) {
+                    value = value.replaceAll("\\]\\]&gt;", "]]>");
+                }
+                return value;
+            }
+        }
+        return o;
+    }
+
+    private int readChars() throws IOException {
+        final int length = reader.read(chars);
+        if (length > 0) {
+            buffer.append(chars, 0, length);
+        }
+        return length;
+    }
+
+}
\ No newline at end of file
diff --git a/src/net/argius/stew/io/stew-table.dtd b/src/net/argius/stew/io/stew-table.dtd
new file mode 100644 (file)
index 0000000..4a5d0b2
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version = "1.0" encoding="utf-8"?>
+
+<!-- Stew Table Data XML -->
+
+<!ELEMENT table (headerrow?,row*)>
+
+<!ELEMENT headerrow (header)*>
+<!ELEMENT header (#PCDATA)>
+
+<!ELEMENT row (null    |
+               string  |
+               boolean |
+               byte    |
+               short   |
+               int     |
+               long    |
+               float   |
+               double  |
+               decimal |
+               time    |
+               object)*>
+
+<!ELEMENT null    EMPTY>
+<!ELEMENT string  (#PCDATA)>
+<!ELEMENT boolean (#PCDATA)>
+<!ELEMENT byte    (#PCDATA)>
+<!ELEMENT short   (#PCDATA)>
+<!ELEMENT int     (#PCDATA)>
+<!ELEMENT long    (#PCDATA)>
+<!ELEMENT float   (#PCDATA)>
+<!ELEMENT double  (#PCDATA)>
+<!ELEMENT decimal (#PCDATA)>
+<!ELEMENT time    (#PCDATA)>
+<!ELEMENT object  (#PCDATA)>
+
+<!ATTLIST table  writer  CDATA "">
+<!ATTLIST header index   CDATA "">
+<!ATTLIST time   display CDATA "">
+<!ATTLIST object class   CDATA "">
diff --git a/src/net/argius/stew/messages.u8p b/src/net/argius/stew/messages.u8p
new file mode 100644 (file)
index 0000000..44b193d
--- /dev/null
@@ -0,0 +1,41 @@
+.title=Stew
+.about=Stew - SQL Tool Environment With JDBC \nversion: {0}
+
+i.committed=Commited.
+i.connected=Connected.
+i.deleted=Deleted {0} records.
+i.directory-changed=The current directory was changed [{0}] to [{1}].
+i.disconnected=Disconnected.
+i.dump-alias={0}\=[{1}]
+i.exit=Stewを終了します。
+i.inserted=Inserted {0} records.
+i.noalias=No aliases.
+i.now=({0,date,yyyy-MM-dd}T{0,time,HH:mm:ss}{0,time,ZZZZ})
+i.proceeded=Proceeded {0} records.
+i.response-time=[ response time: {0,number,#.###} seconds ]
+i.rollbacked=Rollbacked.
+i.selected=Selected {0} records.
+i.updated=Updated {0} records.
+w.auto-commit-not-available=Warning: Auto-commit mode is not available.
+w.connection-closed-abnormally=Warning: Connection was closed abnormally.
+w.error-occurred-on-auto-rollback=Warning: An error occurred when auto-rollback. ({0})
+w.exceeded-limit=Warning: Over limit({0}).
+w.exit-not-available-in-sequencial-command=Warning: 連続コマンド内のEXITは無効です。
+w.unusable-keyword-for-alias=キーワード[{0}]はエイリアスに使用できません。
+e.alias-circulation-reference=エイリアスの展開回数が上限({0})を超えたため展開を中止しました。
+e.command=Error: {0}
+e.database=Database Error: {0}
+e.dir-not-exists=Directory[{0}] does not exist.
+e.fatal=Fatal Error: {0}
+e.no-connector=Connector [{0}] does not exist.
+e.not-connect=Not connected.
+e.not-found=Error: Command [{0}] was not found.
+e.readonly=Error: Connector is read-only.
+e.runtime=Runtime Error: {0}
+e.unsupported=[{0}] is unsupported.
+e.usage=Usage: {0} {1}
+
+usage.cd=<DIRECTORY>
+usage.-f=<FILE>
+usage.-s=<SCRIPT FILE>
+usage.unalias=<ALIAS>
diff --git a/src/net/argius/stew/messages_ja.u8p b/src/net/argius/stew/messages_ja.u8p
new file mode 100644 (file)
index 0000000..cb4d180
--- /dev/null
@@ -0,0 +1,39 @@
+.title=Stew
+.about=Stew - SQL Tool Environment With JDBC \nversion: {0}
+
+i.committed=コミットされました。
+i.connected=接続されました。
+i.deleted={0} 件 削除されました。
+i.directory-changed=カレントディレクトリを[{0}]から[{1}]へ移動しました。
+i.disconnected=切断されました。
+i.dump-alias={0}\=[{1}]
+i.exit=Stewを終了します。
+i.inserted={0} 件 追加されました。
+i.noalias=エイリアスは未定義です。
+i.proceeded={0} 件 処理されました。
+i.response-time=[ 応答時間: {0,number,#.###} 秒 ]
+i.rollbacked=ロールバックされました。
+i.selected={0} 件 ヒットしました。
+i.updated={0} 件 更新されました。
+w.auto-commit-not-available=警告: 自動コミットモードが無効です。
+w.connection-closed-abnormally=警告: コネクションは正常に切断されませんでした。
+w.error-occurred-on-auto-rollback=警告: 自動ロールバック時にエラーが発生しました。({0})
+w.exceeded-limit=警告: 上限件数({0})を超えました。
+w.exit-not-available-in-sequencial-command=警告: 連続コマンド内のEXITは無効です。
+w.unusable-keyword-for-alias=キーワード[{0}]はエイリアスに使用できません。
+e.alias-circulation-reference=エイリアスの展開回数が上限({0})を超えたため展開を中止しました。
+e.command=エラー: {0}
+e.database=データベースエラー: {0}
+e.dir-not-exists=ディレクトリ[{0}]は存在しません。
+e.fatal=致命的なエラー: {0}
+e.no-connector=コネクタ [{0}] はありません。
+e.not-connect=接続されていません。
+e.not-found=エラー: コマンド [{0}] は見つかりませんでした。
+e.readonly=エラー: コネクタは読取専用です。
+e.runtime=実行時エラー: {0}
+e.unsupported=[{0}]は実装されていません。
+e.usage=使い方: {0} {1}
+
+usage.cd=<ディレクトリ>
+usage.-f=<ファイル>
+usage.unalias=<短縮名>
diff --git a/src/net/argius/stew/text/PrintFormat.java b/src/net/argius/stew/text/PrintFormat.java
new file mode 100644 (file)
index 0000000..b99eeca
--- /dev/null
@@ -0,0 +1,94 @@
+package net.argius.stew.text;
+
+import static java.util.FormattableFlags.LEFT_JUSTIFY;
+
+import java.util.*;
+
+/**
+ * The text formatter supports the width of
+ *  multi-byte characters and full-width characters.
+ */
+public final class PrintFormat {
+
+    /**
+     * Formats text.
+     * @param format
+     * @param args
+     * @return
+     */
+    public static String format(String format, Object... args) {
+        List<Object> a = new ArrayList<Object>();
+        for (Object arg : args) {
+            final Object o;
+            if (arg == null || arg instanceof CharSequence) {
+                o = new FullwidthFormatter((arg == null) ? "" : arg.toString());
+            } else {
+                o = arg;
+            }
+            a.add(o);
+        }
+        try {
+            return String.format(format, a.toArray());
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private static final class FullwidthFormatter implements Formattable {
+
+        private static final char SPACE = ' ';
+
+        private final char[] chars;
+        private final int length;
+
+        FullwidthFormatter(String string) {
+            this.chars = string.toCharArray();
+            this.length = chars.length;
+        }
+
+        @Override
+        public void formatTo(Formatter formatter, int flags, int width, int precision) {
+            final boolean leftJustify = (flags & LEFT_JUSTIFY) == LEFT_JUSTIFY;
+            StringBuilder buffer = new StringBuilder(width);
+            int count = 0;
+            for (int i = 0; i < length; i++) {
+                final int index = leftJustify ? i : length - 1 - i;
+                char c = chars[index];
+                if (leftJustify) {
+                    buffer.append(c);
+                } else {
+                    buffer.insert(0, c);
+                }
+                final int w = getWidth(c);
+                if (++count >= width) {
+                    if (w == 2) {
+                        buffer.setCharAt(index, SPACE);
+                    }
+                    break;
+                }
+                if (w == 2) {
+                    ++count;
+                }
+            }
+            if (count < width) {
+                char[] chars = new char[width - count];
+                Arrays.fill(chars, SPACE);
+                if (leftJustify) {
+                    buffer.append(chars);
+                } else {
+                    buffer.insert(0, chars);
+                }
+            }
+            formatter.format(buffer.toString());
+        }
+
+        private static int getWidth(char ch) {
+            // U+0000 <= C <= U+00FF: half-width character
+            // U+FF61 <= C <= U+FF9F: half-width katakana (Japanese)
+            // otherwise            : full-width character
+            return (ch > '\u00FF' && (ch < '\uFF61' || ch > '\uFF9F')) ? 2 : 1;
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/text/TextUtilities.java b/src/net/argius/stew/text/TextUtilities.java
new file mode 100644 (file)
index 0000000..7962f27
--- /dev/null
@@ -0,0 +1,26 @@
+package net.argius.stew.text;
+
+import java.util.*;
+
+/**
+ * Utilities for text.
+ */
+public final class TextUtilities {
+
+    private TextUtilities() {
+    } // forbidden
+
+
+    public static String join(String delimiter, Collection<?> a) {
+        if (a.isEmpty()) {
+            return "";
+        }
+        StringBuilder buffer = new StringBuilder();
+        for (Object o : a) {
+            buffer.append(delimiter);
+            buffer.append(o);
+        }
+        return buffer.substring(delimiter.length());
+    }
+
+}
diff --git a/src/net/argius/stew/ui/Launcher.java b/src/net/argius/stew/ui/Launcher.java
new file mode 100644 (file)
index 0000000..2ed8cc7
--- /dev/null
@@ -0,0 +1,9 @@
+package net.argius.stew.ui;
+
+import net.argius.stew.*;
+
+public interface Launcher {
+
+    void launch(Environment env);
+
+}
diff --git a/src/net/argius/stew/ui/OutputProcessor.java b/src/net/argius/stew/ui/OutputProcessor.java
new file mode 100644 (file)
index 0000000..9a535e3
--- /dev/null
@@ -0,0 +1,8 @@
+package net.argius.stew.ui;
+
+public interface OutputProcessor {
+
+    void output(Object object);
+    void close();
+
+}
diff --git a/src/net/argius/stew/ui/Prompt.java b/src/net/argius/stew/ui/Prompt.java
new file mode 100644 (file)
index 0000000..caf53b4
--- /dev/null
@@ -0,0 +1,20 @@
+package net.argius.stew.ui;
+
+import net.argius.stew.*;
+
+public final class Prompt {
+
+    private final Environment env;
+
+    public Prompt(Environment env) {
+        this.env = env;
+    }
+
+    @Override
+    public String toString() {
+        Connector connector = env.getCurrentConnector();
+        final String currentConnectorName = (connector == null) ? "" : connector.getName();
+        return currentConnectorName + " > ";
+    }
+
+}
diff --git a/src/net/argius/stew/ui/console/ConnectorMapEditor.java b/src/net/argius/stew/ui/console/ConnectorMapEditor.java
new file mode 100644 (file)
index 0000000..1eaa4ec
--- /dev/null
@@ -0,0 +1,313 @@
+package net.argius.stew.ui.console;
+
+import java.io.*;
+import java.sql.*;
+import java.util.Map.Entry;
+import java.util.*;
+
+import net.argius.stew.*;
+
+/**
+ * The Connector Editor for console mode.
+ */
+public final class ConnectorMapEditor {
+
+    private static final ResourceManager res = ResourceManager.getInstance(ConnectorMapEditor.class);
+    private static final String[] PROP_KEYS = {"name", "classpath", "driver", "url", "user",
+                                               "password", "readonly", "rollback"};
+
+    private final ConnectorMap map;
+
+    private ConnectorMap oldContent;
+
+    private ConnectorMapEditor() throws IOException {
+        ConnectorMap m;
+        try {
+            m = ConnectorConfiguration.load();
+        } catch (FileNotFoundException ex) {
+            printMessage("main.notice.filenotexists");
+            printLine(String.format("(%s)", ex.getMessage()));
+            m = new ConnectorMap();
+        }
+        this.oldContent = m;
+        this.map = new ConnectorMap(this.oldContent);
+    }
+
+    /**
+     * Processes to input properties.
+     * @param id
+     * @param props
+     * @return true if the configuration was changed, otherwise false.
+     */
+    private static boolean proceedInputProperties(String id, Properties props) {
+        while (true) {
+            printMessage("property.start1");
+            printMessage("property.start2");
+            for (String key : PROP_KEYS) {
+                String value = props.getProperty(key);
+                print(res.get("property.input", key, value));
+                String input = getInput("");
+                if (input != null && input.length() > 0) {
+                    props.setProperty(key, input);
+                }
+            }
+            for (String key : PROP_KEYS) {
+                printLine(key + "=" + props.getProperty(key));
+            }
+            if (confirmYes("property.tryconnect.confirm")) {
+                try {
+                    Connector c = new Connector(id, props);
+                    c.getConnection();
+                    printMessage("property.tryconnect.succeeded");
+                } catch (SQLException ex) {
+                    printMessage("property.tryconnect.failed", ex.getMessage());
+                }
+            }
+            if (confirmYes("property.update.confirm")) {
+                return true;
+            }
+            if (!confirmYes("property.retry.confirm")) {
+                printMessage("property.update.cancel");
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Adds a Connector.
+     * @param id
+     */
+    private void proceedAdd(String id) {
+        if (map.containsKey(id)) {
+            printMessage("proc.alreadyexists", id);
+            return;
+        }
+        Properties props = new Properties();
+        // setting default
+        for (String key : PROP_KEYS) {
+            props.setProperty(key, "");
+        }
+        if (proceedInputProperties(id, props)) {
+            map.put(id, new Connector(id, props));
+            printMessage("proc.added", id);
+        }
+    }
+
+    /**
+     * Modifies a Connector.
+     * @param id
+     */
+    private void proceedModify(String id) {
+        if (!map.containsKey(id)) {
+            printMessage("proc.notexists", id);
+            return;
+        }
+        Properties props = map.getConnector(id).toProperties();
+        if (proceedInputProperties(id, props)) {
+            map.put(id, new Connector(id, props));
+            printMessage("proc.modified", id);
+        }
+    }
+
+    /**
+     * Deletes a Connector.
+     * @param id
+     */
+    private void proceedRemove(String id) {
+        Connector connector = map.getConnector(id);
+        printLine("ID[" + id + "]:" + connector.getName());
+        if (confirmYes("proc.remove.confirm")) {
+            map.remove(id);
+            printMessage("proc.remove.finished");
+        } else {
+            printMessage("proc.remove.canceled");
+        }
+        printLine(map);
+    }
+
+    /**
+     * Copies from a Connector to another.
+     * @param src
+     * @param dst
+     */
+    private void proceedCopy(String src, String dst) {
+        if (!map.containsKey(src)) {
+            printMessage("proc.notexists", src);
+            printMessage("proc.copy.canceled");
+            return;
+        }
+        if (map.containsKey(dst)) {
+            printMessage("proc.alreadyexists", dst);
+            printMessage("proc.copy.canceled");
+            return;
+        }
+        map.put(dst, new Connector(dst, map.getConnector(src)));
+        printMessage("proc.copy.finished");
+    }
+
+    /**
+     * Displays all of the Connectors.
+     */
+    private void proceedDisplayIds() {
+        for (Entry<String, Connector> entry : map.entrySet()) {
+            String id = entry.getKey();
+            printLine(String.format("%10s : %s", id, map.getConnector(id).getName()));
+        }
+    }
+
+    /**
+     * Displays the Connector info specified by ID.
+     * @param id
+     */
+    private void proceedDisplayDetail(String id) {
+        if (!map.containsKey(id)) {
+            printMessage("proc.notexists", id);
+            return;
+        }
+        Properties props = map.getConnector(id).toProperties();
+        for (String key : PROP_KEYS) {
+            printLine(String.format("%10s : %s", key, props.getProperty(key)));
+        }
+    }
+
+    /**
+     * Saves the configuration to a file.
+     * @throws IOException
+     */
+    private void proceedSave() throws IOException {
+        if (map.equals(oldContent)) {
+            printMessage("proc.nomodification");
+        } else if (confirmYes("proc.save.confirm")) {
+            ConnectorConfiguration.save(map);
+            oldContent = new ConnectorMap(map);
+            printMessage("proc.save.finished");
+        } else {
+            printMessage("proc.save.canceled");
+        }
+    }
+
+    /**
+     * Loads from file.
+     * @throws IOException
+     */
+    private void proceedLoad() throws IOException {
+        ConnectorMap m = ConnectorConfiguration.load();
+        if (m.equals(oldContent) && m.equals(map)) {
+            printMessage("proc.nomodification");
+            return;
+        }
+        printMessage("proc.load.confirm1");
+        if (confirmYes("proc.load.confirm2")) {
+            map.clear();
+            map.putAll(ConnectorConfiguration.load());
+            printMessage("proc.load.finished");
+        } else {
+            printMessage("proc.load.canceled");
+        }
+    }
+
+    /**
+     * Returns the input string from STDIN.
+     * @param messageId
+     * @param args
+     * @return
+     */
+    private static String getInput(String messageId, Object... args) {
+        if (messageId.length() > 0) {
+            print(res.get(messageId, args));
+        }
+        print(res.get("proc.prompt"));
+        Scanner scanner = new Scanner(System.in);
+        if (scanner.hasNextLine()) {
+            return scanner.nextLine();
+        }
+        return "";
+    }
+
+    /**
+     * Returns the result to confirm Yes or No as boolean.
+     * @param messageId
+     * @return
+     */
+    private static boolean confirmYes(String messageId) {
+        if (messageId.length() > 0) {
+            print(res.get(messageId));
+        }
+        print("(y/N)");
+        return getInput("").equalsIgnoreCase("y");
+    }
+
+    private static void print(Object o) {
+        System.out.print(o);
+    }
+
+    private static void printLine() {
+        System.out.println();
+    }
+
+    private static void printLine(Object o) {
+        System.out.println(o);
+    }
+
+    private static void printMessage(String messageId, Object... args) {
+        printLine(res.get(messageId, args));
+    }
+
+    /**
+     * Invokes this editor.
+     */
+    static void invoke() {
+        printLine();
+        printMessage("main.start");
+        printLine();
+        try {
+            ConnectorMapEditor editor = new ConnectorMapEditor();
+            while (true) {
+                Parameter p = new Parameter(getInput("main.wait"));
+                final String command = p.at(0);
+                final String id = p.at(1);
+                if (command.equalsIgnoreCase("help")) {
+                    printMessage("help");
+                } else if (command.equalsIgnoreCase("a")) {
+                    editor.proceedAdd(id);
+                } else if (command.equalsIgnoreCase("m")) {
+                    editor.proceedModify(id);
+                } else if (command.equalsIgnoreCase("r")) {
+                    editor.proceedRemove(id);
+                } else if (command.equalsIgnoreCase("copy")) {
+                    editor.proceedCopy(id, p.at(2));
+                } else if (command.equalsIgnoreCase("disp")) {
+                    if (id.length() == 0) {
+                        editor.proceedDisplayIds();
+                    } else {
+                        editor.proceedDisplayDetail(id);
+                    }
+                } else if (command.equalsIgnoreCase("save")) {
+                    editor.proceedSave();
+                } else if (command.equalsIgnoreCase("load")) {
+                    editor.proceedLoad();
+                } else if (command.equalsIgnoreCase("exit")) {
+                    if (editor.map.equals(editor.oldContent)) {
+                        break;
+                    } else if (confirmYes("proc.exit.confirm")) {
+                        break;
+                    }
+                }
+                printLine();
+            }
+        } catch (Throwable th) {
+            th.printStackTrace();
+        }
+        printLine();
+        printMessage("main.end");
+    }
+
+    /**
+     * @param args
+     */
+    public static void main(String... args) {
+        System.out.println("Stew " + Bootstrap.getVersion());
+        invoke();
+    }
+
+}
diff --git a/src/net/argius/stew/ui/console/ConnectorMapEditor.u8p b/src/net/argius/stew/ui/console/ConnectorMapEditor.u8p
new file mode 100644 (file)
index 0000000..2ecead9
--- /dev/null
@@ -0,0 +1,44 @@
+
+main.start=接続設定エディタ
+main.wait=コマンドを入力 (help: コマンド一覧を表示)
+main.end=接続設定エディタを終了します。
+main.notice.filenotexists=お知らせ:接続設定ファイルが存在しません。保存時に新規に作成されます。
+proc.prompt =\ >\ 
+proc.added=ID "{0}" を追加しました。
+proc.modified=ID "{0}" を変更しました。
+proc.notexists=ID "{0}" は存在しません。
+proc.alreadyexists=ID "{0}" は既に存在します。
+proc.nomodification=変更はありません。
+proc.remove.confirm=削除してよろしいですか?
+proc.remove.finished=削除しました。
+proc.remove.canceled=削除を中止します。
+proc.copy.finished=コピーしました。
+proc.copy.canceled=コピーを中止します。
+proc.save.confirm=保存してよろしいですか?
+proc.save.finished=保存しました。
+proc.save.canceled=保存を中止しました。
+proc.load.confirm1=復元を実行すると現在の変更は破棄されます。
+proc.load.confirm2=復元しますか?
+proc.load.finished=復元しました。
+proc.load.canceled=復元を中止しました。
+proc.exit.confirm=終了すると、保存されていない変更は破棄されます。よろしいですか?
+property.start1=プロパティの設定。
+property.start2=現在の値を変更しない場合は何も入力しないでENTER
+property.input={0}の入力 (現在の値=[{1}])
+property.tryconnect.confirm=接続試験しますか?
+property.tryconnect.succeeded=成功しました。
+property.tryconnect.failed=失敗しました。({0})
+property.update.confirm=これでよろしいですか?
+property.retry.confirm=変更をやり直しますか?
+property.update.cancel=変更を中止します。
+help =\
+ ---ヘルプ(コマンド一覧)---\n\
+     help     : このヘルプ\n\
+     a    id  : 接続設定の追加(add)\n\
+     m    id  : 接続設定の変更(modify)\n\
+     r    id  : 接続設定の削除(remove)\n\
+     copy A B : 接続設定のコピー\n\
+     disp [id]: 接続設定の表示(一覧/詳細)\n\
+     save     : 接続設定の保存\n\
+     load     : 接続設定の復元(変更をリセット)\n\
+     exit     : 終了
diff --git a/src/net/argius/stew/ui/console/ConsoleLauncher.java b/src/net/argius/stew/ui/console/ConsoleLauncher.java
new file mode 100644 (file)
index 0000000..8777ebe
--- /dev/null
@@ -0,0 +1,68 @@
+package net.argius.stew.ui.console;
+
+import static net.argius.stew.text.TextUtilities.join;
+
+import java.util.*;
+
+import net.argius.stew.*;
+import net.argius.stew.ui.*;
+
+/**
+ * The Launcher implementation of console mode.
+ */
+public final class ConsoleLauncher implements Launcher {
+
+    private static Logger log = Logger.getLogger(ConsoleLauncher.class);
+    private static final boolean END = false;
+
+    @Override
+    public void launch(Environment env) {
+        log.info("start");
+        OutputProcessor out = env.getOutputProcessor();
+        Prompt prompt = new Prompt(env);
+        Scanner scanner = new Scanner(System.in);
+        while (true) {
+            out.output(prompt);
+            if (!scanner.hasNextLine()) {
+                break;
+            }
+            final String line = scanner.nextLine();
+            log.debug("input : %s", line);
+            if (String.valueOf(line).trim().equals("--edit")) {
+                ConnectorMapEditor.invoke();
+                env.updateConnectorMap();
+            } else if (Command.invoke(env, line) == END) {
+                break;
+            }
+        }
+        log.info("end");
+    }
+
+    /** main **/
+    public static void main(String... args) {
+        List<String> a = new ArrayList<String>(Arrays.asList(args));
+        if (a.contains("-v") || a.contains("--version")) {
+            System.out.println("Stew " + Bootstrap.getVersion());
+            return;
+        }
+        Environment env = new Environment();
+        try {
+            env.setOutputProcessor(new ConsoleOutputProcessor());
+            final String about = ResourceManager.Default.get(".about", Bootstrap.getVersion());
+            env.getOutputProcessor().output(about);
+            if (!a.isEmpty() && !a.get(0).startsWith("-")) {
+                Command.invoke(env, "connect " + a.remove(0));
+            }
+            if (!a.isEmpty()) {
+                Command.invoke(env, join(" ", a));
+                Command.invoke(env, "disconnect");
+            } else {
+                Launcher o = new ConsoleLauncher();
+                o.launch(env);
+            }
+        } finally {
+            env.release();
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/ui/console/ConsoleOutputProcessor.java b/src/net/argius/stew/ui/console/ConsoleOutputProcessor.java
new file mode 100644 (file)
index 0000000..e44ba06
--- /dev/null
@@ -0,0 +1,113 @@
+package net.argius.stew.ui.console;
+
+import java.sql.*;
+import java.util.*;
+
+import net.argius.stew.*;
+import net.argius.stew.text.*;
+import net.argius.stew.ui.*;
+
+/**
+ * This is the implementation of OutputProcessor for console.
+ */
+public final class ConsoleOutputProcessor implements OutputProcessor {
+
+    private static final int WIDTH_LIMIT = 30;
+
+    @Override
+    public void output(Object o) {
+        if (o instanceof ResultSetReference) {
+            outputResult((ResultSetReference)o);
+        } else if (o instanceof ResultSet) {
+            outputResult(new ResultSetReference((ResultSet)o, ""));
+        } else if (o instanceof Prompt) {
+            System.err.print(o);
+        } else {
+            System.out.println(o);
+        }
+    }
+
+    private static void outputResult(ResultSetReference ref) {
+        try {
+            // result
+            ResultSet rs = ref.getResultSet();
+            ColumnOrder order = ref.getOrder();
+            ResultSetMetaData rsmeta = rs.getMetaData();
+            final boolean needsOrderChange = order.size() > 0;
+            System.err.println();
+            // column info
+            final int columnCount = (needsOrderChange) ? order.size() : rsmeta.getColumnCount();
+            int maxWidth = 1;
+            StringBuilder borderFormat = new StringBuilder();
+            for (int i = 0; i < columnCount; i++) {
+                final int index = (needsOrderChange) ? order.getOrder(i) : i + 1;
+                int size = rsmeta.getColumnDisplaySize(index);
+                if (size > WIDTH_LIMIT) {
+                    size = WIDTH_LIMIT;
+                } else if (size < 1) {
+                    size = 1;
+                }
+                maxWidth = Math.max(maxWidth, size);
+                final int widthExpression;
+                switch (rsmeta.getColumnType(index)) {
+                    case Types.TINYINT:
+                    case Types.SMALLINT:
+                    case Types.INTEGER:
+                    case Types.BIGINT:
+                    case Types.REAL:
+                    case Types.DOUBLE:
+                    case Types.FLOAT:
+                    case Types.DECIMAL:
+                    case Types.NUMERIC:
+                        widthExpression = size;
+                        break;
+                    default:
+                        widthExpression = -size;
+                }
+                final String format = "%" + widthExpression + "s";
+                borderFormat.append(" " + format);
+                if (i != 0) {
+                    System.out.print(' ');
+                }
+                final String name = (needsOrderChange) ? order.getName(i) : rsmeta.getColumnName(index);
+                System.out.print(PrintFormat.format(format, name));
+            }
+            System.out.println();
+            // border
+            String format = borderFormat.substring(1);
+            char[] borderChars = new char[maxWidth];
+            Arrays.fill(borderChars, '-');
+            Object[] borders = new String[columnCount];
+            Arrays.fill(borders, String.valueOf(borderChars));
+            System.out.println(PrintFormat.format(format, borders));
+            // beginning of loop
+            Object[] a = new Object[columnCount];
+            final int limit = Bootstrap.getPropertyAsInt("net.argius.stew.rowcount.limit",
+                                                         Integer.MAX_VALUE);
+            int count = 0;
+            while (rs.next()) {
+                if (count >= limit) {
+                    System.err.println(ResourceManager.Default.get("w.exceeded-limit", limit));
+                    break;
+                }
+                ++count;
+                for (int i = 0; i < columnCount; i++) {
+                    final int index = (needsOrderChange) ? order.getOrder(i) : i + 1;
+                    a[i] = rs.getString(index);
+                }
+                System.out.println(PrintFormat.format(format, a));
+            }
+            System.out.println();
+            // end of loop
+            ref.setRecordCount(count);
+        } catch (SQLException ex) {
+            ex.printStackTrace(System.err);
+        }
+    }
+
+    @Override
+    public void close() {
+        // do nothing
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/AnyAction.java b/src/net/argius/stew/ui/window/AnyAction.java
new file mode 100644 (file)
index 0000000..1298683
--- /dev/null
@@ -0,0 +1,262 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.KeyEvent.VK_Y;
+import static java.awt.event.KeyEvent.VK_Z;
+import static javax.swing.KeyStroke.getKeyStroke;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import javax.swing.*;
+import javax.swing.text.*;
+import javax.swing.undo.*;
+
+import net.argius.stew.*;
+
+/**
+ * These class are members of the suite of event-handling as an internal framework.
+ */
+final class AnyAction extends AbstractAction implements Runnable {
+
+    /** AnyActionListener, JComponent, or JTextComponent */
+    private Object o;
+
+    /** mappings (a method name and a Method) for dynamic invoking */
+    private Map<String, Method> m;
+
+    /** event command string */
+    private String eventCommand;
+
+    /** `o' can listen AnyAction (that is, it implemented AnyActionListener) */
+    private boolean canListenAnyAction;
+
+    /** `o' has InputMap (that is, it is a JComponent) */
+    private boolean hasInputMap;
+
+    AnyAction(Object o) {
+        this(o, "");
+    }
+
+    AnyAction(Object o, String eventCommand) {
+        super(eventCommand);
+        if (eventCommand == null) {
+            throw new IllegalArgumentException("eventCommand is null");
+        }
+        this.o = o;
+        this.m = new LinkedHashMap<String, Method>();
+        this.eventCommand = eventCommand;
+        this.canListenAnyAction = (o instanceof AnyActionListener);
+        this.hasInputMap = (o instanceof JComponent);
+    }
+
+    void doNow(String methodName, final Object... args) {
+        try {
+            resolveMethod(methodName).invoke(o, args);
+        } catch (Exception ex) {
+            throw new RuntimeException("at AnyAction#doNow", ex);
+        }
+    }
+
+    void doLater(String methodName, Object... args) {
+        final String label = "AnyAction#doLater";
+        EventQueue.invokeLater(new Task(label, o, resolveMethod(methodName), args));
+    }
+
+    void doParallel(String methodName, final Object... args) {
+        final String label = "AnyAction#doParallel";
+        DaemonThreadFactory.execute(new Task(label, o, resolveMethod(methodName), args));
+    }
+
+    private static final class Task implements Runnable {
+
+        private final String label;
+        private final Object o;
+        private final Method method;
+        private final Object[] args;
+
+        Task(String label, Object o, Method method, Object... args) {
+            this.label = label;
+            this.o = o;
+            this.method = method;
+            this.args = args;
+        }
+
+        @Override
+        public void run() {
+            try {
+                method.invoke(o, args);
+            } catch (Exception ex) {
+                throw new RuntimeException("at " + label, ex);
+            }
+        }
+
+    }
+
+    private Method resolveMethod(String methodName) {
+        if (m.containsKey(methodName)) {
+            return m.get(methodName);
+        }
+        for (Method method : o.getClass().getDeclaredMethods()) {
+            if (method.getName().equals(methodName)) {
+                method.setAccessible(true);
+                m.put(methodName, method);
+                return method;
+            }
+        }
+        throw new IllegalArgumentException("method not found: " + methodName);
+    }
+
+    @Override
+    public void run() {
+        actionPerformed(new ActionEvent(o, 0, eventCommand));
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        if (!canListenAnyAction) {
+            throw new RuntimeException(editErrorMessage(1, e));
+        }
+        final String cmd;
+        if (eventCommand.length() == 0) {
+            cmd = e.getActionCommand();
+        } else {
+            cmd = eventCommand;
+        }
+        AnyActionEvent aae = new AnyActionEvent(e.getSource(), cmd);
+        ((AnyActionListener)o).anyActionPerformed(aae);
+    }
+
+    void bind(AnyActionListener dst, Object actionKey, KeyStroke... keyStrokes) {
+        bind(dst, false, actionKey, keyStrokes);
+    }
+
+    void bind(AnyActionListener dst,
+              boolean whenAncestor,
+              Object actionKey,
+              KeyStroke... keyStrokes) {
+        if (!hasInputMap && keyStrokes.length > 0) {
+            throw new RuntimeException(editErrorMessage(2, o));
+        }
+        bind((JComponent)o, dst, whenAncestor, actionKey, keyStrokes);
+    }
+
+    void bindKeyStroke(boolean whenAncestor, Object actionKey, KeyStroke... keyStrokes) {
+        if (!hasInputMap && keyStrokes.length > 0) {
+            throw new RuntimeException(editErrorMessage(2, o));
+        }
+        bindKeyStroke((JComponent)o, whenAncestor, String.valueOf(actionKey), keyStrokes);
+    }
+
+    void bindSelf(Object actionKey, KeyStroke... keyStrokes) {
+        if (!canListenAnyAction) {
+            throw new RuntimeException(editErrorMessage(1, o));
+        }
+        if (keyStrokes.length > 0 && !hasInputMap) {
+            throw new RuntimeException(editErrorMessage(2, o));
+        }
+        bind((JComponent)o, (AnyActionListener)o, false, actionKey, keyStrokes);
+    }
+
+    UndoManager setUndoAction() {
+        if (o instanceof JTextComponent) {
+            return setUndoAction((JTextComponent)o);
+        }
+        throw new RuntimeException(editErrorMessage(3, o));
+    }
+
+    static UndoManager setUndoAction(JTextComponent text) {
+        final UndoManager um = new UndoManager();
+        if (text == null) {
+            return um;
+        }
+        text.getDocument().addUndoableEditListener(um);
+        ActionMap amap = text.getActionMap();
+        InputMap imap = text.getInputMap();
+        final int shortcutKey = Utilities.getMenuShortcutKeyMask();
+        amap.put("undo", new UndoAction(um));
+        imap.put(getKeyStroke(VK_Z, shortcutKey), "undo");
+        amap.put("redo", new RedoAction(um));
+        imap.put(getKeyStroke(VK_Y, shortcutKey), "redo");
+        return um;
+    }
+
+    private static final class UndoAction extends AbstractAction {
+        private UndoManager um;
+        UndoAction(UndoManager um) {
+            this.um = um;
+        }
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (um.canUndo()) {
+                um.undo();
+            }
+        }
+    }
+
+    private static final class RedoAction extends AbstractAction {
+        private UndoManager um;
+        RedoAction(UndoManager um) {
+            this.um = um;
+        }
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (um.canRedo()) {
+                um.redo();
+            }
+        }
+    }
+
+    static String editErrorMessage(int type, Object o) {
+        final String cn = getClassName(o);
+        switch (type) {
+            case 0: // unexpected error
+                return "unexpected error";
+            case 1: // can not listen AnyAction
+                return String.format("%s can not listen AnyAction", cn);
+            case 2: // does not have InputMap
+                return String.format("%s does not have InputMap", cn);
+            case 3: // is not JTextComponent
+                return String.format("This is not JTextComponent, but %s", cn);
+            default:
+                return "";
+        }
+    }
+
+    private static void bind(JComponent src,
+                             AnyActionListener dst,
+                             boolean whenAncestor,
+                             Object actionKey,
+                             KeyStroke... keyStrokes) {
+        final String key = String.valueOf(actionKey);
+        bindAction(src, dst, key, keyStrokes);
+        bindKeyStroke(src, whenAncestor, key, keyStrokes);
+    }
+
+    private static void bindAction(JComponent src,
+                                   AnyActionListener dst,
+                                   String actionKeyString,
+                                   KeyStroke... keyStrokes) {
+        src.getActionMap().put(actionKeyString, new AnyAction(dst, actionKeyString));
+    }
+
+    private static void bindKeyStroke(JComponent src,
+                                      boolean whenAncestor,
+                                      String actionKeyString,
+                                      KeyStroke... keyStrokes) {
+        if (keyStrokes != null) {
+            final int focusMode = whenAncestor
+                    ? JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
+                    : JComponent.WHEN_FOCUSED;
+            for (KeyStroke ks : keyStrokes) {
+                src.getInputMap(focusMode).put(ks, actionKeyString);
+            }
+        }
+    }
+
+    private static String getClassName(Object o) {
+        return (o == null) ? "null" : o.getClass().getSimpleName();
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/AnyActionEvent.java b/src/net/argius/stew/ui/window/AnyActionEvent.java
new file mode 100644 (file)
index 0000000..fd2a450
--- /dev/null
@@ -0,0 +1,44 @@
+package net.argius.stew.ui.window;
+
+import java.awt.event.*;
+
+/**
+ * @see AnyAction
+ */
+final class AnyActionEvent extends ActionEvent {
+
+    private final Object[] args;
+
+    AnyActionEvent(Object source, Object actionKey, Object... args) {
+        this(source, System.currentTimeMillis(), String.valueOf(actionKey), args);
+    }
+
+    private AnyActionEvent(Object source, long when, String actionKeystring, Object... args) {
+        super(source, ACTION_PERFORMED, actionKeystring, when, 0);
+        this.args = args;
+    }
+
+    Object[] getArgs() {
+        return args.clone();
+    }
+
+    void validate() {
+        // the 2 second rule
+        if (getWhen() < System.currentTimeMillis() - 2000L) {
+            final String msg = String.format("timeout: command [%s] cancelled", getActionCommand());
+            throw new RuntimeException(msg);
+        }
+    }
+
+    boolean isAnyOf(Object... a) {
+        final String cmd = getActionCommand();
+        for (Object o : a) {
+            final String key = String.valueOf(o);
+            if (key.equals(cmd)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/AnyActionKey.java b/src/net/argius/stew/ui/window/AnyActionKey.java
new file mode 100644 (file)
index 0000000..fa299a7
--- /dev/null
@@ -0,0 +1,61 @@
+package net.argius.stew.ui.window;
+
+/**
+ * @see AnyAction
+ */
+public enum AnyActionKey {
+
+    cut, copy, paste, selectAll, undo, redo,
+    //
+    execute,
+    refresh,
+    //
+    newWindow,
+    closeWindow,
+    quit,
+    find,
+    toggleFocus,
+    clearMessage,
+    showStatusBar,
+    showInfoTree,
+    showColumnNumber,
+    showAlwaysOnTop,
+    widenColumnWidth,
+    narrowColumnWidth,
+    adjustColumnWidth,
+    autoAdjustMode,
+    autoAdjustModeNone,
+    autoAdjustModeHeader,
+    autoAdjustModeValue,
+    autoAdjustModeHeaderAndValue,
+    executeCommand,
+    breakCommand,
+    lastHistory,
+    nextHistory,
+    sendRollback,
+    sendCommit,
+    connect,
+    disconnect,
+    postProcessMode,
+    postProcessModeNone,
+    postProcessModeFocus,
+    postProcessModeShake,
+    postProcessModeBlink,
+    inputEcryptionKey,
+    editConnectors,
+    sortResult,
+    importFile,
+    exportFile,
+    showHelp,
+    showAbout,
+    unknown;
+
+    static AnyActionKey of(String name) {
+        try {
+            return valueOf(name);
+        } catch (IllegalArgumentException ex) {
+            return unknown;
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/AnyActionListener.java b/src/net/argius/stew/ui/window/AnyActionListener.java
new file mode 100644 (file)
index 0000000..d68faa3
--- /dev/null
@@ -0,0 +1,10 @@
+package net.argius.stew.ui.window;
+
+/**
+ * @see AnyAction
+ */
+interface AnyActionListener {
+
+    void anyActionPerformed(AnyActionEvent ev);
+
+}
diff --git a/src/net/argius/stew/ui/window/ClipboardHelper.java b/src/net/argius/stew/ui/window/ClipboardHelper.java
new file mode 100644 (file)
index 0000000..f632e20
--- /dev/null
@@ -0,0 +1,66 @@
+package net.argius.stew.ui.window;
+
+import java.awt.*;
+import java.awt.datatransfer.*;
+import java.io.*;
+
+import net.argius.stew.*;
+
+final class ClipboardHelper {
+
+    private static final Logger log = Logger.getLogger(ClipboardHelper.class);
+
+    private ClipboardHelper() {
+        // ignore
+    }
+
+    static String getString() {
+        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+        try {
+            Object o = clipboard.getData(DataFlavor.stringFlavor);
+            if (log.isTraceEnabled()) {
+                log.trace("received from clipboard: [%s]", o);
+            }
+            return (String)o;
+        } catch (UnsupportedFlavorException ex) {
+            throw new RuntimeException("at ClipboardHelper.getString", ex);
+        } catch (IOException ex) {
+            throw new RuntimeException("at ClipboardHelper.getString", ex);
+        }
+    }
+
+    static void setStrings(Iterable<String> rows) {
+        StringWriter buffer = new StringWriter();
+        PrintWriter out = new PrintWriter(buffer);
+        try {
+            for (String s : rows) {
+                out.println(s);
+            }
+        } finally {
+            out.close();
+        }
+        setString(buffer.toString().replaceFirst("[\\r\\n]+$", ""));
+    }
+
+    static void setString(String s) {
+        if (log.isTraceEnabled()) {
+            log.trace("sending to clipboard: [%s]", s);
+        }
+        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+        StringSelection sselection = new StringSelection(s);
+        clipboard.setContents(sselection, sselection);
+    }
+
+    static Reader getReaderForText() {
+        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+        Transferable content = clipboard.getContents(null);
+        try {
+            return DataFlavor.stringFlavor.getReaderForText(content);
+        } catch (UnsupportedFlavorException ex) {
+            throw new RuntimeException("at ClipboardHelper.getReaderForText", ex);
+        } catch (IOException ex) {
+            throw new RuntimeException("at ClipboardHelper.getReaderForText", ex);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/net/argius/stew/ui/window/ConnectorEditDialog.java b/src/net/argius/stew/ui/window/ConnectorEditDialog.java
new file mode 100644 (file)
index 0000000..66f9ec6
--- /dev/null
@@ -0,0 +1,492 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.KeyEvent.VK_ESCAPE;
+import static javax.swing.JOptionPane.*;
+import static net.argius.stew.ui.window.ConnectorEditDialog.ActionKey.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.net.*;
+import java.sql.*;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.List;
+import java.util.zip.*;
+
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+
+import net.argius.stew.*;
+
+final class ConnectorEditDialog extends JDialog implements AnyActionListener {
+
+    enum ActionKey {
+        referToOthers, searchFile, searchDriver, submit, tryToConnect, cancel
+    }
+
+    private static final ResourceManager res = ResourceManager.getInstance(ConnectorEditDialog.class);
+    private static final int TEXT_SIZE = 32;
+    private static final Insets TEXT_MARGIN = new Insets(1, 3, 1, 0);
+    private static final Color COLOR_ESSENTIAL = new Color(0xFFF099);
+
+    private final Connector connector;
+    private final JTextField tId;
+    private final JTextField tName;
+    private final JTextField tClasspath;
+    private final JTextField tDriver;
+    private final JTextField tUrl;
+    private final JTextField tUser;
+    private final JPasswordField tPassword;
+    private final JComboBox cPasswordClass;
+    private final JCheckBox cReadOnly;
+    private final JCheckBox cUsesAutoRollback;
+
+    private List<ChangeListener> listenerList;
+    private File currentDirectory;
+
+    ConnectorEditDialog(JDialog owner, Connector connector) {
+        // [Init Instances]
+        super(owner);
+        this.connector = connector;
+        this.listenerList = new ArrayList<ChangeListener>();
+        setTitle(res.get("title"));
+        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+        // [Init Components]
+        final FlexiblePanel p = new FlexiblePanel();
+        add(p);
+        // setup and layout text-fields
+        String id = connector.getId();
+        tId = new JTextField(id, TEXT_SIZE);
+        tId.setEditable(false);
+        tId.setMargin(TEXT_MARGIN);
+        p.addComponent(new JLabel(res.get("connector.id")), false);
+        p.addComponent(tId, true);
+        tName = createJTextField(connector.getName(), false);
+        p.addComponent(new JLabel(res.get("connector.name")), false);
+        p.addComponent(tName, false);
+        p.addComponent(createJButton(referToOthers), true);
+        tClasspath = createJTextField(connector.getClasspath(), false);
+        p.addComponent(new JLabel(res.get("connector.classpath")), false);
+        p.addComponent(tClasspath, false);
+        p.addComponent(createJButton(searchFile), true);
+        tDriver = createJTextField(connector.getDriver(), false);
+        p.addComponent(new JLabel(res.get("connector.driver")), false);
+        p.addComponent(tDriver, false);
+        p.addComponent(createJButton(searchDriver), true);
+        tUrl = createJTextField(connector.getUrl(), true);
+        p.addComponent(new JLabel(res.get("connector.url")), false);
+        p.addComponent(tUrl, true);
+        tUser = createJTextField(connector.getUser(), true);
+        p.addComponent(new JLabel(res.get("connector.user")), false);
+        p.addComponent(tUser, true);
+        tPassword = new JPasswordField(TEXT_SIZE);
+        tPassword.setBackground(COLOR_ESSENTIAL);
+        tPassword.setMargin(TEXT_MARGIN);
+        Password password = connector.getPassword();
+        if (id.length() > 0 && password.hasPassword()) {
+            tPassword.setText(password.getRawString());
+        }
+        p.addComponent(new JLabel(res.get("connector.password")), false);
+        p.addComponent(tPassword, true);
+        PasswordItem[] items = {new PasswordItem(PlainTextPassword.class),
+                                new PasswordItem(PbePassword.class)};
+        cPasswordClass = new JComboBox(items);
+        cPasswordClass.setEditable(true);
+        int passwordClassSelectedIndex = -1;
+        for (int i = 0; i < items.length; i++) {
+            if (items[i].getPasswordClass() == password.getClass()) {
+                passwordClassSelectedIndex = i;
+                cPasswordClass.setSelectedIndex(i);
+                break;
+            }
+        }
+        if (passwordClassSelectedIndex < 0) {
+            cPasswordClass.setSelectedItem(password.getClass().getName());
+        }
+        p.addComponent(new JLabel(res.get("connector.encryption")), false);
+        p.addComponent(cPasswordClass, true);
+        cReadOnly = new JCheckBox(res.get("connector.readonly"), connector.isReadOnly());
+        p.addComponent(cReadOnly, true);
+        cUsesAutoRollback = new JCheckBox(res.get("connector.autorollback"),
+                                          connector.usesAutoRollback());
+        p.addComponent(cUsesAutoRollback, true);
+        // setup and layout buttons
+        JPanel p2 = new JPanel(new GridLayout(1, 3, 16, 0));
+        p2.add(createJButton(submit));
+        p2.add(createJButton(tryToConnect));
+        p2.add(createJButton(cancel));
+        p.c.gridwidth = GridBagConstraints.REMAINDER;
+        p.c.insets = new Insets(12, 0, 12, 0);
+        p.c.anchor = GridBagConstraints.CENTER;
+        p.addComponent(p2, false);
+        pack();
+        // [Events]
+        AnyAction aa = new AnyAction(rootPane);
+        // ESC
+        aa.bind(this, true, cancel, KeyStroke.getKeyStroke(VK_ESCAPE, 0));
+    }
+
+    @Override
+    protected void processWindowEvent(WindowEvent e) {
+        super.processWindowEvent(e);
+        if (e.getID() == WindowEvent.WINDOW_CLOSING) {
+            anyActionPerformed(new AnyActionEvent(this, cancel));
+        }
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        try {
+            if (ev.isAnyOf(referToOthers)) {
+                referToOtherConnectors();
+            } else if (ev.isAnyOf(searchFile)) {
+                chooseClasspath();
+            } else if (ev.isAnyOf(searchDriver)) {
+                chooseDriverClass();
+            } else if (ev.isAnyOf(submit)) {
+                requestClose(true);
+            } else if (ev.isAnyOf(tryToConnect)) {
+                tryToConnect();
+            } else if (ev.isAnyOf(cancel)) {
+                requestClose(false);
+            }
+        } catch (Exception ex) {
+            WindowOutputProcessor.showErrorDialog(this, ex);
+        }
+    }
+
+    private static JTextField createJTextField(String value, boolean isEssential) {
+        final JTextField text = new JTextField(TEXT_SIZE);
+        text.setMargin(TEXT_MARGIN);
+        if (value != null) {
+            text.setText(value);
+            text.setCaretPosition(0);
+        }
+        if (isEssential) {
+            text.setBackground(COLOR_ESSENTIAL);
+        }
+        ContextMenu.createForText(text);
+        return text;
+    }
+
+    private JButton createJButton(ActionKey key) {
+        final String cmd = key.toString();
+        JButton c = new JButton(res.get("button." + cmd));
+        c.setActionCommand(cmd);
+        c.addActionListener(new AnyAction(this));
+        return c;
+    }
+
+    void addChangeListener(ChangeListener listener) {
+        listenerList.add(listener);
+    }
+
+    private Connector createConnector() {
+        final String id = tId.getText();
+        Properties props = new Properties();
+        props.setProperty("name", tName.getText());
+        props.setProperty("driver", tDriver.getText());
+        props.setProperty("classpath", tClasspath.getText());
+        props.setProperty("url", tUrl.getText());
+        props.setProperty("user", tUser.getText());
+        props.setProperty("readonly", Boolean.toString(cReadOnly.isSelected()));
+        props.setProperty("rollback", Boolean.toString(cUsesAutoRollback.isSelected()));
+        props.setProperty("password.class", getPasswordClassName());
+        Connector connector = new Connector(id, props);
+        Password password = connector.getPassword();
+        password.setRawString(String.valueOf(tPassword.getPassword()));
+        props.setProperty("password", password.getTransformedString());
+        return new Connector(id, props);
+    }
+
+    private String getPasswordClassName() {
+        final Object item = cPasswordClass.getSelectedItem();
+        if (item != null) {
+            if (item instanceof PasswordItem) {
+                PasswordItem passwordItem = (PasswordItem)item;
+                return passwordItem.getName();
+            }
+            if (item instanceof CharSequence) {
+                return item.toString();
+            }
+        }
+        return PlainTextPassword.class.getName();
+    }
+
+    void referToOtherConnectors() {
+        ConnectorMap m;
+        try {
+            m = ConnectorConfiguration.load();
+        } catch (IOException ex) {
+            WindowOutputProcessor.showErrorDialog(this, ex);
+            return;
+        }
+        String[] names = {"id", "name", "classpath", "driver", "url", "user"};
+        Vector<Vector<String>> data = new Vector<Vector<String>>();
+        for (Entry<String, Connector> entry : m.entrySet()) {
+            Vector<String> a = new Vector<String>(names.length);
+            Properties p = entry.getValue().toProperties();
+            p.setProperty("id", entry.getKey());
+            for (String name : names) {
+                a.add(p.getProperty(name));
+            }
+            data.add(a);
+        }
+        JTable t = new JTable();
+        t.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+        t.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        t.setDefaultEditor(Object.class, null);
+        Vector<String> headers = new Vector<String>();
+        for (String name : names) {
+            headers.add(res.get("connector." + name));
+        }
+        t.setModel(new DefaultTableModel(data, headers));
+        JPanel p = new JPanel(new BorderLayout());
+        p.add(new JLabel(res.get("dialog.refer-to-others.message")), BorderLayout.PAGE_START);
+        p.add(new JScrollPane(t), BorderLayout.CENTER);
+        if (showConfirmDialog(this, p, null, OK_CANCEL_OPTION) != OK_OPTION
+            || t.getSelectedRow() < 0) {
+            return;
+        }
+        final Properties selected = m.get(t.getValueAt(t.getSelectedRow(), 0)).toProperties();
+        Map<String, JTextField> pairs = new HashMap<String, JTextField>();
+        pairs.put("name", tName);
+        pairs.put("classpath", tClasspath);
+        pairs.put("driver", tDriver);
+        pairs.put("url", tUrl);
+        pairs.put("user", tUser);
+        for (Entry<String, JTextField> entry : pairs.entrySet()) {
+            JTextField text = entry.getValue();
+            if (text.getText().trim().length() == 0) {
+                text.setText(selected.getProperty(entry.getKey()));
+            }
+        }
+    }
+
+    void chooseClasspath() {
+        if (currentDirectory == null) {
+            synchronized (this) {
+                if (currentDirectory == null) {
+                    File file = new File(tClasspath.getText().split(File.pathSeparator)[0]);
+                    if (file.isDirectory()) {
+                        currentDirectory = file;
+                    } else {
+                        final File parent = file.getParentFile();
+                        if (parent != null && parent.isDirectory()) {
+                            currentDirectory = parent;
+                        }
+                    }
+                }
+            }
+        }
+        JFileChooser fileChooser = new JFileChooser();
+        fileChooser.setCurrentDirectory(currentDirectory);
+        fileChooser.setDialogTitle(res.get("dialog.search.file.header"));
+        fileChooser.setApproveButtonText(res.get("dialog.search.file.button"));
+        fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+        fileChooser.showDialog(this, null);
+        File file = fileChooser.getSelectedFile();
+        if (file != null) {
+            tClasspath.setText(file.getPath());
+            synchronized (this) {
+                currentDirectory = file.getParentFile();
+            }
+        }
+    }
+
+    void chooseDriverClass() {
+        String text = tClasspath.getText();
+        if (text.trim().length() == 0) {
+            String message = res.get("confirm.searchsystemclasspath");
+            if (showConfirmDialog(this, message, null, OK_CANCEL_OPTION) != OK_OPTION) {
+                return;
+            }
+            text = Bootstrap.getProperty("java.class.path");
+        }
+        final Set<String> classes = new LinkedHashSet<String>();
+        for (String path : text.split(File.pathSeparator)) {
+            File file = new File(path);
+            final URL[] urls;
+            try {
+                urls = new URL[]{file.toURI().toURL()};
+            } catch (MalformedURLException ex) {
+                continue;
+            }
+            DriverClassFinder finder = new DriverClassFinder(path, classes);
+            finder.setClassLoader(DynamicLoader.getClassLoader(urls));
+            finder.setFailMode(true);
+            finder.find(file);
+        }
+        if (classes.isEmpty()) {
+            showMessageDialog(this, res.get("search.driver.classnotfound"), null, ERROR_MESSAGE);
+        } else {
+            final String message = res.get("selectDriverClass");
+            Object[] a = classes.toArray();
+            Object value = showInputDialog(this, message, "", PLAIN_MESSAGE, null, a, a[0]);
+            if (value != null) {
+                tDriver.setText((String)value);
+                tDriver.setCaretPosition(0);
+            }
+        }
+    }
+
+    void tryToConnect() throws SQLException {
+        Connection conn = createConnector().getConnection();
+        try {
+            DatabaseMetaData dbmeta = conn.getMetaData();
+            final String message = res.get("try.connect",
+                                           dbmeta.getDatabaseProductName(),
+                                           dbmeta.getDatabaseProductVersion());
+            WindowOutputProcessor.showInformationMessageDialog(this, message, "");
+        } finally {
+            conn.close();
+        }
+    }
+
+    void requestClose(boolean withSaving) {
+        Connector newConnector = createConnector();
+        if (withSaving) {
+            ChangeEvent event = new ChangeEvent(newConnector);
+            for (ChangeListener listener : listenerList) {
+                listener.stateChanged(event);
+            }
+        } else if (!newConnector.equals(this.connector)) {
+            if (showConfirmDialog(this,
+                                  res.get("i.confirm-without-register"),
+                                  null,
+                                  OK_CANCEL_OPTION) != OK_OPTION) {
+                return;
+            }
+        }
+        dispose();
+    }
+
+    private static final class PasswordItem {
+
+        final Class<? extends Password> passwordClass;
+
+        PasswordItem(Class<? extends Password> passwordClass) {
+            this.passwordClass = passwordClass;
+        }
+
+        Class<? extends Password> getPasswordClass() {
+            return passwordClass;
+        }
+
+        String getName() {
+            return toString();
+        }
+
+        @Override
+        public String toString() {
+            return passwordClass.getName();
+        }
+
+    }
+
+    private static final class DriverClassFinder {
+
+        private static final Logger log = Logger.getLogger(DriverClassFinder.class);
+
+        private final String rootPath;
+        private final Set<String> classes;
+
+        private ClassLoader classLoader;
+        private boolean failMode;
+
+        DriverClassFinder(String rootPath, Set<String> classes) {
+            this.rootPath = normalizePath(rootPath);
+            this.classes = classes;
+            this.classLoader = ClassLoader.getSystemClassLoader();
+        }
+
+        void setClassLoader(ClassLoader classLoader) {
+            this.classLoader = classLoader;
+        }
+
+        void setFailMode(boolean failMode) {
+            this.failMode = failMode;
+        }
+
+        final void find(File file) {
+            try {
+                final File f = file.getCanonicalFile();
+                if (f.isDirectory()) {
+                    File[] files = f.listFiles();
+                    if (files != null) {
+                        for (File child : files) {
+                            find(child);
+                        }
+                    }
+                } else {
+                    final String name = f.getName();
+                    if (name.matches("(?i).+\\.class")) {
+                        try {
+                            filter(resolveClass(f.getPath()));
+                        } catch (Throwable ex) {
+                            if (failMode) {
+                                fail(f, ex);
+                            } else {
+                                throw new RuntimeException(ex);
+                            }
+                        }
+                    } else if (name.matches("(?i).+\\.(jar|zip)")) {
+                        find(new ZipFile(f));
+                    }
+                }
+            } catch (IOException ex) {
+                if (failMode) {
+                    fail(file, ex);
+                } else {
+                    throw new RuntimeException(ex);
+                }
+            }
+        }
+
+        void find(ZipFile zipFile) {
+            Enumeration<?> en = zipFile.entries();
+            while (en.hasMoreElements()) {
+                ZipEntry entry = (ZipEntry)en.nextElement();
+                String name = entry.getName();
+                if (name.matches("(?i).+\\.class")) {
+                    try {
+                        filter(resolveClass(name));
+                    } catch (Throwable ex) {
+                        if (failMode) {
+                            fail(name, ex);
+                        } else {
+                            throw new RuntimeException(ex);
+                        }
+                    }
+                }
+            }
+        }
+
+        void filter(Class<?> c) {
+            if (Driver.class.isAssignableFrom(c)) {
+                classes.add(c.getName());
+            }
+        }
+
+        static void fail(Object object, Throwable cause) {
+            log.trace("%s at object %s", cause, object);
+        }
+
+        Class<?> resolveClass(String path) throws Throwable {
+            String s = normalizePath(path);
+            if (s.startsWith(rootPath)) {
+                s = s.substring(rootPath.length() + 1);
+            }
+            s = s.replaceFirst("\\.class$", "").replace('/', '.');
+            return Class.forName(s, false, classLoader);
+        }
+
+        private static String normalizePath(String path) {
+            return path.replaceAll("\\\\", "/");
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/ConnectorEditDialog.u8p b/src/net/argius/stew/ui/window/ConnectorEditDialog.u8p
new file mode 100644 (file)
index 0000000..2614617
--- /dev/null
@@ -0,0 +1,28 @@
+title=コネクタの設定
+connector.id=コネクタID
+connector.name=コネクタ名
+connector.classpath=クラスパス
+button.searchFile=ファイル
+connector.driver=ドライバ
+button.searchDriver=ドライバの検索
+connector.url=接続先URL
+connector.user=ユーザ
+connector.password=パスワード
+connector.encryption=暗号化処理
+connector.readonly=読取専用コマンドのみ使用する
+connector.autorollback=切断時に(自動で)ロールバックする
+button.submit=登録
+button.tryToConnect=接続試験
+button.cancel=キャンセル
+selectDriverClass=ドライバクラスの選択
+confirm.searchsystemclasspath=クラスパスが未指定のため、システムクラスパスから検索します。
+dialog.search.file.header=クラスパスの選択
+dialog.search.file.button=選択
+try.connect=\
+接続に成功しました!\n\
+接続先名称: {0}\n\
+バージョン: {1}
+
+search.driver.classnotfound=ドライバクラスが見つかりません。
+button.referToOthers=他の設定を参照
+dialog.referToOthers.message=選択した行の内容を未入力の箇所に貼り付けます。
diff --git a/src/net/argius/stew/ui/window/ConnectorEntry.java b/src/net/argius/stew/ui/window/ConnectorEntry.java
new file mode 100644 (file)
index 0000000..522bb60
--- /dev/null
@@ -0,0 +1,93 @@
+package net.argius.stew.ui.window;
+
+import java.util.*;
+
+import net.argius.stew.*;
+
+/**
+ * ConnectorEntry.
+ */
+final class ConnectorEntry {
+
+    private final String id;
+    private final Connector connector;
+
+    /**
+     * A constructor.
+     * @param id
+     * @param connector
+     */
+    ConnectorEntry(String id, Connector connector) {
+        this.id = id;
+        this.connector = connector;
+    }
+
+    /**
+     * Creates the list from a connector list.
+     * @param iterable
+     * @return
+     */
+    static List<ConnectorEntry> toList(Iterable<Connector> iterable) {
+        List<ConnectorEntry> a = new ArrayList<ConnectorEntry>();
+        for (Connector c : iterable) {
+            a.add(new ConnectorEntry(c.getId(), c));
+        }
+        return a;
+    }
+
+    /**
+     * Returns this ID.
+     * @return
+     */
+    public String getId() {
+        return id;
+    }
+
+    /**
+     * Returns this connector.
+     * @return
+     */
+    public Connector getConnector() {
+        return connector;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((id == null) ? 0 : id.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof ConnectorEntry)) {
+            return false;
+        }
+        ConnectorEntry other = (ConnectorEntry)obj;
+        if (id == null) {
+            if (other.id != null) {
+                return false;
+            }
+        } else if (!id.equals(other.id)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        final String name = connector.getName();
+        if (name == null || name.length() == 0) {
+            return id;
+        }
+        return String.format("%s (%s)", id, name);
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/ConnectorMapEditDialog.java b/src/net/argius/stew/ui/window/ConnectorMapEditDialog.java
new file mode 100644 (file)
index 0000000..285cf80
--- /dev/null
@@ -0,0 +1,254 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.KeyEvent.VK_ESCAPE;
+import static javax.swing.JOptionPane.*;
+import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER;
+import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS;
+import static net.argius.stew.ui.window.ConnectorMapEditDialog.ActionKey.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+
+import javax.swing.*;
+import javax.swing.event.*;
+
+import net.argius.stew.*;
+
+final class ConnectorMapEditDialog extends JDialog implements ChangeListener, AnyActionListener {
+
+    enum ActionKey {
+        addNew, modify, rename, remove, up, down, submit, cancel
+    }
+
+    private static final ResourceManager res = ResourceManager.getInstance(ConnectorMapEditDialog.class);
+
+    private final ConnectorMap connectorMap;
+    private final JList idList;
+    private final DefaultListModel listModel;
+
+    ConnectorMapEditDialog(JFrame owner, Environment env) {
+        // [instance]
+        super(owner);
+        final DefaultListModel listModel = new DefaultListModel();
+        this.connectorMap = new ConnectorMap(env.getConnectorMap());
+        this.idList = new JList(listModel);
+        this.listModel = listModel;
+        setTitle(res.get("title"));
+        setResizable(false);
+        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+        FlexiblePanel p = new FlexiblePanel();
+        p.c.anchor = GridBagConstraints.CENTER;
+        p.c.insets = new Insets(8, 12, 8, 0);
+        add(p);
+        // [components]
+        // List
+        final JList idList = this.idList;
+        idList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        for (ConnectorEntry entry : ConnectorEntry.toList(connectorMap.values())) {
+            listModel.addElement(entry);
+        }
+        JScrollPane pane = new JScrollPane(idList,
+                                           VERTICAL_SCROLLBAR_ALWAYS,
+                                           HORIZONTAL_SCROLLBAR_NEVER);
+        pane.setWheelScrollingEnabled(true);
+        idList.addMouseListener(new IdListMouseListener());
+        p.addComponent(pane, false);
+        // Button 1
+        JPanel p1 = new JPanel(new GridLayout(6, 1, 4, 2));
+        p1.add(createJButton(addNew));
+        p1.add(createJButton(modify));
+        p1.add(createJButton(rename));
+        p1.add(createJButton(remove));
+        p1.add(createJButton(up));
+        p1.add(createJButton(down));
+        p.c.gridwidth = GridBagConstraints.REMAINDER;
+        p.c.insets = new Insets(8, 32, 8, 32);
+        p.addComponent(p1, true);
+        // Button 2
+        JPanel p2 = new JPanel(new GridLayout(1, 2, 16, 8));
+        p2.add(createJButton(submit));
+        p2.add(createJButton(cancel));
+        p.c.gridwidth = GridBagConstraints.REMAINDER;
+        p.c.fill = GridBagConstraints.NONE;
+        p.addComponent(p2, false);
+        // [events]
+        AnyAction aa = new AnyAction(rootPane);
+        // ESC key
+        aa.bind(this, true, cancel, KeyStroke.getKeyStroke(VK_ESCAPE, 0));
+    }
+
+    private final class IdListMouseListener extends MouseAdapter {
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            if (e.getClickCount() % 2 == 0) {
+                ConnectorEntry entry = (ConnectorEntry)idList.getSelectedValue();
+                openConnectorEditDialog(entry.getConnector());
+            }
+        }
+    }
+
+    @Override
+    protected void processWindowEvent(WindowEvent e) {
+        super.processWindowEvent(e);
+        if (e.getID() == WindowEvent.WINDOW_CLOSING) {
+            anyActionPerformed(new AnyActionEvent(this, cancel));
+        }
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        if (ev.isAnyOf(addNew)) {
+            final String id = showInputDialog(this, res.get("i.input-new-connector-id"));
+            if (id == null) {
+                return;
+            }
+            if (connectorMap.containsKey(id)) {
+                final String message = res.get("e.id-already-exists", id);
+                showMessageDialog(this, message, null, ERROR_MESSAGE);
+            } else {
+                openConnectorEditDialog(new Connector(id, new Properties()));
+            }
+        } else if (ev.isAnyOf(modify)) {
+            ConnectorEntry entry = (ConnectorEntry)idList.getSelectedValue();
+            if (entry != null) {
+                openConnectorEditDialog(entry.getConnector());
+            }
+        } else if (ev.isAnyOf(rename)) {
+            Object o = idList.getSelectedValue();
+            if (o == null) {
+                return;
+            }
+            ConnectorEntry entry = (ConnectorEntry)o;
+            final String newId = showInputDialog(this,
+                                                 res.get("i.input-new-connector-id"),
+                                                 entry.getId());
+            if (newId == null || newId.equals(entry.getId())) {
+                return;
+            }
+            connectorMap.remove(entry);
+            connectorMap.put(newId, entry.getConnector());
+            DefaultListModel m = (DefaultListModel)idList.getModel();
+            Connector newConnector = new Connector(newId, entry.getConnector());
+            m.set(m.indexOf(entry), new ConnectorEntry(newId, newConnector));
+            idList.repaint();
+        } else if (ev.isAnyOf(remove)) {
+            if (showConfirmDialog(this, res.get("i.confirm-remove"), "", OK_CANCEL_OPTION) != OK_OPTION) {
+                return;
+            }
+            ConnectorEntry selected = (ConnectorEntry)idList.getSelectedValue();
+            connectorMap.remove(selected.getId());
+            DefaultListModel m = (DefaultListModel)idList.getModel();
+            m.removeElement(selected);
+        } else if (ev.isAnyOf(up)) {
+            shiftSelectedElementUpward();
+        } else if (ev.isAnyOf(down)) {
+            shiftSelectedElementDownward();
+        } else if (ev.isAnyOf(submit)) {
+            requestClose(true);
+        } else if (ev.isAnyOf(cancel)) {
+            requestClose(false);
+        }
+    }
+
+    @Override
+    public void stateChanged(ChangeEvent e) {
+        Object source = e.getSource();
+        if (source instanceof Connector) {
+            Connector connector = (Connector)source;
+            final String id = connector.getId();
+            connectorMap.setConnector(id, connector);
+            ConnectorEntry changed = new ConnectorEntry(id, connector);
+            final int index = listModel.indexOf(changed);
+            if (index >= 0) {
+                listModel.set(index, changed);
+                idList.setSelectedIndex(index);
+            } else {
+                listModel.add(0, changed);
+                idList.setSelectedIndex(0);
+            }
+        }
+    }
+
+    private JButton createJButton(Object cmdObject) {
+        final String cmd = String.valueOf(cmdObject);
+        JButton button = new JButton(res.get("button." + cmd));
+        button.setActionCommand(cmd);
+        button.addActionListener(new AnyAction(this));
+        return button;
+    }
+
+    private void openConnectorEditDialog(Connector connector) {
+        ConnectorEditDialog dialog = new ConnectorEditDialog(this, connector);
+        dialog.addChangeListener(this);
+        dialog.setModal(true);
+        dialog.setLocationRelativeTo(getParent());
+        dialog.setSize(dialog.getPreferredSize());
+        dialog.setVisible(true);
+    }
+
+    private void shiftSelectedElementUpward() {
+        final int index = idList.getSelectedIndex();
+        if (index == 0) {
+            return;
+        }
+        final int newIndex = index - 1;
+        swap(listModel, index, newIndex);
+        idList.setSelectedIndex(newIndex);
+        idList.ensureIndexIsVisible(newIndex);
+    }
+
+    private void shiftSelectedElementDownward() {
+        final int index = idList.getSelectedIndex();
+        final int size = listModel.getSize();
+        if (index == size - 1) {
+            return;
+        }
+        final int newIndex = index + 1;
+        swap(listModel, index, newIndex);
+        idList.setSelectedIndex(newIndex);
+        idList.ensureIndexIsVisible(newIndex);
+    }
+
+    private static void swap(DefaultListModel listModel, int index1, int index2) {
+        Object o = listModel.get(index1);
+        listModel.set(index1, listModel.get(index2));
+        listModel.set(index2, o);
+    }
+
+    private void requestClose(boolean withSaving) {
+        if (withSaving) {
+            if (showConfirmDialog(this, res.get("i.confirm-save"), "", YES_NO_OPTION) != YES_OPTION) {
+                return;
+            }
+            ConnectorMap m = new ConnectorMap();
+            for (Object o : listModel.toArray()) {
+                ConnectorEntry entry = (ConnectorEntry)o;
+                final String id = entry.getId();
+                m.setConnector(id, connectorMap.getConnector(id));
+            }
+            try {
+                ConnectorConfiguration.save(m);
+            } catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+            connectorMap.clear();
+            connectorMap.putAll(m);
+        } else {
+            ConnectorMap fileContent;
+            try {
+                fileContent = ConnectorConfiguration.load();
+            } catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+            if (!connectorMap.equals(fileContent)) {
+                if (showConfirmDialog(this, res.get("i.confirm-without-save"), "", OK_CANCEL_OPTION) != OK_OPTION) {
+                    return;
+                }
+            }
+        }
+        dispose();
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/ConnectorMapEditDialog.u8p b/src/net/argius/stew/ui/window/ConnectorMapEditDialog.u8p
new file mode 100644 (file)
index 0000000..160ff06
--- /dev/null
@@ -0,0 +1,9 @@
+title=接続設定
+button.addNew=新規
+button.modify=変更
+button.rename=ID変更
+button.remove=削除
+button.up=上へ
+button.down=下へ
+button.submit=決定
+button.cancel=キャンセル
diff --git a/src/net/argius/stew/ui/window/ConsoleTextArea.java b/src/net/argius/stew/ui/window/ConsoleTextArea.java
new file mode 100644 (file)
index 0000000..0ac57eb
--- /dev/null
@@ -0,0 +1,267 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.InputEvent.ALT_DOWN_MASK;
+import static java.awt.event.KeyEvent.*;
+import static javax.swing.KeyStroke.getKeyStroke;
+import static net.argius.stew.ui.window.AnyActionKey.breakCommand;
+import static net.argius.stew.ui.window.AnyActionKey.execute;
+import static net.argius.stew.ui.window.ConsoleTextArea.ActionKey.*;
+
+import java.awt.event.*;
+import java.util.*;
+
+import javax.swing.*;
+import javax.swing.text.*;
+import javax.swing.text.Highlighter.Highlight;
+import javax.swing.text.Highlighter.HighlightPainter;
+import javax.swing.undo.*;
+
+import net.argius.stew.*;
+import net.argius.stew.text.*;
+
+/**
+ * The console style text area.
+ */
+final class ConsoleTextArea extends JTextArea implements AnyActionListener, TextSearch {
+
+    enum ActionKey {
+        submit, copyOrBreak, addNewLine, jumpToHomePosition, outputMessage, insertText, doNothing;
+    }
+
+    private static final Logger log = Logger.getLogger(ConsoleTextArea.class);
+    private static final String BREAK_PROMPT = "[BREAK] > ";
+
+    private final AnyActionListener anyActionListener;
+    private final UndoManager undoManager;
+
+    private int homePosition;
+
+    ConsoleTextArea(AnyActionListener anyActionListener) {
+        // [Instances]
+        this.anyActionListener = anyActionListener;
+        this.undoManager = AnyAction.setUndoAction(this);
+        ((AbstractDocument)getDocument()).setDocumentFilter(new ConsoleTextAreaDocumentFilter());
+        // [Actions]
+        final int shortcutKey = Utilities.getMenuShortcutKeyMask();
+        AnyAction aa = new AnyAction(this);
+        aa.setUndoAction();
+        aa.bindSelf(submit, getKeyStroke(VK_ENTER, 0));
+        aa.bindSelf(copyOrBreak, getKeyStroke(VK_C, shortcutKey));
+        aa.bindSelf(breakCommand, getKeyStroke(VK_B, ALT_DOWN_MASK));
+        aa.bindSelf(addNewLine, getKeyStroke(VK_ENTER, shortcutKey));
+        aa.bindSelf(jumpToHomePosition, getKeyStroke(VK_HOME, 0));
+    }
+
+    private final class ConsoleTextAreaDocumentFilter extends DocumentFilter {
+
+        ConsoleTextAreaDocumentFilter() {
+        } // empty
+
+        @Override
+        public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
+            if (isEditablePosition(offset)) {
+                super.insertString(fb, offset, string, attr);
+            }
+        }
+
+        @Override
+        public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
+            if (isEditablePosition(offset)) {
+                super.remove(fb, offset, length);
+            }
+        }
+
+        @Override
+        public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
+            if (isEditablePosition(offset)) {
+                super.replace(fb, offset, length, text, attrs);
+            }
+        }
+
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        log.atEnter("anyActionPerformed", ev);
+        if (ev.isAnyOf(submit)) {
+            final int ep = getEndPosition();
+            if (getCaretPosition() == ep) {
+                anyActionListener.anyActionPerformed(new AnyActionEvent(this, execute));
+            } else {
+                setCaretPosition(ep);
+            }
+        } else if (ev.isAnyOf(copyOrBreak)) {
+            if (getSelectedText() == null) {
+                sendBreak();
+            } else {
+                Action copyAction = new DefaultEditorKit.CopyAction();
+                copyAction.actionPerformed(ev);
+            }
+        } else if (ev.isAnyOf(breakCommand)) {
+            sendBreak();
+        } else if (ev.isAnyOf(addNewLine)) {
+            insert("\n", getCaretPosition());
+        } else if (ev.isAnyOf(jumpToHomePosition)) {
+            setCaretPosition(getHomePosition());
+        } else if (ev.isAnyOf(insertText)) {
+            if (!isEditablePosition(getCaretPosition())) {
+                setCaretPosition(getEndPosition());
+            }
+            replaceSelection(TextUtilities.join(" ", Arrays.asList(ev.getArgs())));
+            requestFocus();
+        } else if (ev.isAnyOf(outputMessage)) {
+            for (Object o : ev.getArgs()) {
+                output(String.valueOf(o));
+            }
+        } else if (ev.isAnyOf(doNothing)) {
+            // do nothing
+        } else {
+            log.warn("not expected: Event=%s", ev);
+        }
+        log.atExit("anyActionPerformed");
+    }
+
+    boolean canUndo() {
+        return undoManager.canUndo();
+    }
+
+    boolean canRedo() {
+        return undoManager.canRedo();
+    }
+
+    /**
+     * Appends text.
+     * @param s
+     * @param movesCaretToEnd true if it moves Caret to the end, otherwise false
+     */
+    void append(String s, boolean movesCaretToEnd) {
+        super.append(s);
+        if (movesCaretToEnd) {
+            setCaretPosition(getEndPosition());
+        }
+    }
+
+    /**
+     * Outputs text.
+     * @param s
+     */
+    void output(String s) {
+        super.append(s);
+        undoManager.discardAllEdits();
+        homePosition = getEndPosition();
+        setCaretPosition(homePosition);
+    }
+
+    /**
+     * Replaces text from prompt to the end.
+     * @param s
+     */
+    void replace(String s) {
+        replaceRange(s, homePosition, getEndPosition());
+    }
+
+    /**
+     * Clears text.
+     */
+    void clear() {
+        homePosition = 0;
+        setText("");
+    }
+
+    /**
+     * Returns the text that is editable.
+     * @return
+     */
+    String getEditableText() {
+        try {
+            return getText(homePosition, getEndPosition() - homePosition);
+        } catch (BadLocationException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    /**
+     * Tests whether the specified position is editable.
+     * @param position
+     * @return
+     */
+    boolean isEditablePosition(int position) {
+        return (position >= homePosition);
+    }
+
+    int getHomePosition() {
+        return homePosition;
+    }
+
+    int getEndPosition() {
+        Document document = getDocument();
+        Position position = document.getEndPosition();
+        return position.getOffset() - 1;
+    }
+
+    void resetHomePosition() {
+        undoManager.discardAllEdits();
+        homePosition = getEndPosition();
+    }
+
+    void sendBreak() {
+        append(BREAK_PROMPT);
+        resetHomePosition();
+        validate();
+    }
+
+    @Override
+    public void updateUI() {
+        if (getCaret() == null) {
+            super.updateUI();
+        } else {
+            final int p = getCaretPosition();
+            super.updateUI();
+            setCaretPosition(p);
+        }
+    }
+
+    // text search
+
+    @Override
+    public boolean search(Matcher matcher) {
+        removeHighlights();
+        try {
+            Highlighter highlighter = getHighlighter();
+            HighlightPainter painter = TextSearch.Matcher.getHighlightPainter();
+            final String text = getText();
+            int start = 0;
+            boolean matched = false;
+            while (matcher.find(text, start)) {
+                matched = true;
+                int matchedIndex = matcher.getStart();
+                highlighter.addHighlight(matchedIndex, matcher.getEnd(), painter);
+                start = matchedIndex + 1;
+            }
+            addKeyListener(new TextSearchKeyListener());
+            return matched;
+        } catch (BadLocationException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private final class TextSearchKeyListener extends KeyAdapter {
+        @Override
+        public void keyTyped(KeyEvent e) {
+            removeKeyListener(this);
+            removeHighlights();
+        }
+    }
+    
+    @Override
+    public void reset() {
+        removeHighlights();
+    }
+
+    void removeHighlights() {
+        for (Highlight highlight : getHighlighter().getHighlights()) {
+            getHighlighter().removeHighlight(highlight);
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/ContextMenu.java b/src/net/argius/stew/ui/window/ContextMenu.java
new file mode 100644 (file)
index 0000000..99426e4
--- /dev/null
@@ -0,0 +1,248 @@
+package net.argius.stew.ui.window;
+
+import java.awt.event.*;
+import java.util.*;
+
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+import javax.swing.undo.*;
+
+import net.argius.stew.*;
+
+final class ContextMenu {
+
+    private static final Logger log = Logger.getLogger(ContextMenu.class);
+    private static final ResourceManager res = ResourceManager.getInstance(ContextMenu.class);
+
+    static JPopupMenu create(final JComponent target, final AnyActionListener dst) {
+        return create(target, dst, target.getClass().getSimpleName());
+    }
+
+    static JPopupMenu create(final AnyActionListener dst) {
+        JComponent c = (dst instanceof JComponent) ? (JComponent)dst : null;
+        return create(c, dst, dst.getClass().getSimpleName());
+    }
+
+    static JPopupMenu create(final JComponent target, final AnyActionListener dst, String name) {
+        log.atEnter("set", dst, name);
+        final JPopupMenu menu = new JPopupMenu();
+        final Map<String, KeyStroke> keyBounds = extractKeyBinds(target);
+        final boolean autoMnemonic = res.getInt("auto-mnemonic") == 1;
+        AnyAction aa = new AnyAction(dst);
+        for (final String itemId : res.get(name + ".items").split(",", -1)) {
+            if (itemId.length() == 0) {
+                menu.add(new JSeparator());
+                continue;
+            }
+            final String id = "item." + itemId;
+            final JMenuItem item = new JMenuItem(itemId);
+            final char mn = res.getChar(id + ".mnemonic");
+            item.setText((res.containsKey(id))
+                    ? res.get(id) + (autoMnemonic ? "(" + mn + ")" : "")
+                    : itemId);
+            item.setMnemonic(mn);
+            item.setActionCommand(itemId);
+            item.addActionListener(aa);
+            final String shortcutId = id + ".shortcut";
+            if (res.containsKey(shortcutId)) {
+                KeyStroke shortcutKey = Utilities.getKeyStroke(res.get(shortcutId));
+                if (shortcutKey != null) {
+                    item.setAccelerator(shortcutKey);
+                }
+            } else if (keyBounds.containsKey(itemId)) {
+                item.setAccelerator(keyBounds.get(itemId));
+            }
+            menu.add(item);
+        }
+        if (target == null && dst instanceof JComponent) {
+            ((JComponent)dst).setComponentPopupMenu(menu);
+        } else if (target != null) {
+            target.setComponentPopupMenu(menu);
+        }
+        if (dst instanceof PopupMenuListener) {
+            menu.addPopupMenuListener((PopupMenuListener)dst);
+        }
+        return log.atExit("set", menu);
+    }
+
+    private static Map<String, KeyStroke> extractKeyBinds(JComponent c) {
+        Map<String, KeyStroke> m = new HashMap<String, KeyStroke>();
+        if (c != null) {
+            InputMap imap = c.getInputMap();
+            if (imap != null) {
+                KeyStroke[] a = imap.allKeys();
+                if (a != null) {
+                    for (KeyStroke ks : a) {
+                        m.put(String.valueOf(imap.get(ks)), ks);
+                    }
+                }
+            }
+        }
+        return m;
+    }
+
+    static JPopupMenu createForText(final JTextComponent text) {
+        AnyAction aa = new AnyAction(text);
+        return createForText(text, aa.setUndoAction());
+    }
+
+    static JPopupMenu createForText(final JTextComponent text, UndoManager um) {
+        final JPopupMenu menu = new JPopupMenu();
+        TextPopupMenuListener textPopupListener = new TextPopupMenuListener(text, um);
+        final boolean autoMnemonic = res.getInt("auto-mnemonic") == 1;
+        final String name = "TextComponent";
+        for (final String itemId : res.get(name + ".items").split(",", -1)) {
+            if (itemId.length() == 0) {
+                menu.add(new JSeparator());
+                continue;
+            }
+            final String id = "item." + itemId;
+            final JMenuItem item = new JMenuItem(itemId);
+            final char mn = res.getChar(id + ".mnemonic");
+            item.setText((res.containsKey(id))
+                    ? res.get(id) + (autoMnemonic ? "(" + mn + ")" : "")
+                    : itemId);
+            item.setMnemonic(mn);
+            item.setActionCommand(itemId);
+            item.addActionListener(textPopupListener);
+            final String shortcutId = id + ".shortcut";
+            if (res.containsKey(shortcutId)) {
+                KeyStroke shortcutKey = Utilities.getKeyStroke(res.get(shortcutId));
+                if (shortcutKey != null) {
+                    item.setAccelerator(shortcutKey);
+                }
+            }
+            menu.add(item);
+            textPopupListener.putPopupMenuItem(itemId, item);
+        }
+        menu.addPopupMenuListener(textPopupListener);
+        text.setComponentPopupMenu(menu);
+        return menu;
+    }
+
+    static final class TextPopupMenuListener implements ActionListener, PopupMenuListener {
+
+        private final JTextComponent text;
+        private final UndoManager um;
+        private final Map<String, JMenuItem> itemMap;
+
+        TextPopupMenuListener(JTextComponent c, UndoManager um) {
+            this.text = c;
+            this.um = um;
+            this.itemMap = new HashMap<String, JMenuItem>();
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            final String cmd = ev.getActionCommand();
+            final String key;
+            if (cmd.equals("cut")) {
+                key = "cut-to-clipboard";
+            } else if (cmd.equals("copy")) {
+                key = "copy-to-clipboard";
+            } else if (cmd.equals("paste")) {
+                key = "paste-from-clipboard";
+            } else if (cmd.equals("selectAll")) {
+                key = "select-all";
+            } else {
+                key = cmd;
+            }
+            text.getActionMap().get(key).actionPerformed(ev);
+        }
+
+        @Override
+        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+            itemMap.get("undo").setEnabled(um.canUndo());
+            itemMap.get("redo").setEnabled(um.canRedo());
+            final boolean textSelected = text.getSelectionEnd() > text.getSelectionStart();
+            itemMap.get("cut").setEnabled(textSelected);
+            itemMap.get("copy").setEnabled(textSelected);
+        }
+
+        @Override
+        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+            // empty
+        }
+
+        @Override
+        public void popupMenuCanceled(PopupMenuEvent e) {
+            // empty
+        }
+
+        void putPopupMenuItem(String key, JMenuItem item) {
+            itemMap.put(key, item);
+        }
+
+    }
+
+    enum ActionKey {
+        // infotree
+        copySimpleName,
+        copyFullName,
+        generateWherePhrase,
+        generateSelectPhrase,
+        generateUpdateStatement,
+        generateInsertStatement,
+        jumpToColumnByName,
+        toggleShowColumnNumber,
+        // rst
+        copyWithEscape,
+        clearSelectedCellValue,
+        setCurrentTimeValue,
+        copyColumnName,
+        findColumnName,
+        addEmptyRow,
+        insertFromClipboard,
+        duplicateRows,
+        linkRowsToDatabase,
+        deleteRows,
+        adjustColumnWidth,
+        sort,
+        doNothing,
+        // textarea
+        submit,
+        copyOrBreak,
+        addNewLine,
+        jumpToHomePosition,
+        outputMessage,
+        insertText,
+        // others
+        cut,
+        copy,
+        paste,
+        selectAll,
+        undo,
+        redo,
+        execute,
+        refresh,
+        newWindow,
+        closeWindow,
+        quit,
+        find,
+        toggleFocus,
+        clearMessage,
+        showStatusBar,
+        showInfoTree,
+        showColumnNumber,
+        showAlwaysOnTop,
+        widenColumnWidth,
+        narrowColumnWidth,
+        executeCommand,
+        breakCommand,
+        lastHistory,
+        nextHistory,
+        sendRollback,
+        sendCommit,
+        connect,
+        disconnect,
+        inputEcryptionKey,
+        editConnectors,
+        sortResult,
+        importFile,
+        exportFile,
+        showHelp,
+        showAbout
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/ContextMenu.u8p b/src/net/argius/stew/ui/window/ContextMenu.u8p
new file mode 100644 (file)
index 0000000..ab4d5eb
--- /dev/null
@@ -0,0 +1,53 @@
+## ContextMenu mappings
+
+# for common
+TextComponent.items=undo,redo,,cut,copy,paste,selectAll
+
+# for DatabaseInfoTree
+DatabaseInfoTree.items=copy,copySimpleName,copyFullName,,refresh,,generateWherePhrase,generateSelectPhrase,generateUpdateStatement,generateInsertStatement,,jumpToColumnByName,toggleShowColumnNumber
+item.copySimpleName=Copy Simple Name
+item.copySimpleName.mnemonic=N
+item.copyFullName=Copy Full Name
+item.copyFullName.mnemonic=F
+item.generateWherePhrase=Generate WHERE Phrase
+item.generateWherePhrase.mnemonic=W
+item.generateSelectPhrase=Generate SELECT Statement (with WHERE)
+item.generateSelectPhrase.mnemonic=S
+item.generateUpdateStatement=Generate UPDATE Statement (with WHERE)
+item.generateUpdateStatement.mnemonic=U
+item.generateInsertStatement=Generate INSERT Statement
+item.generateInsertStatement.mnemonic=I
+item.jumpToColumnByName=Jump To Column By Name
+item.jumpToColumnByName.mnemonic=J
+item.toggleShowColumnNumber=Toggle Show Column Number
+item.toggleShowColumnNumber.mnemonic=T
+
+# for ResultSetTable
+ResultSetTable.items=copy,copyWithEscape,paste,selectAll,,clearSelectedCellValue,setCurrentTimeValue,,copyColumnName,findColumnName,,addEmptyRow,insertFromClipboard,duplicateRows,linkRowsToDatabase,deleteRows
+ResultSetTableColumnHeader.items=sort,,copy,copyWithEscape,paste,selectAll,,copyColumnName,findColumnName,,addEmptyRow,insertFromClipboard,duplicateRows
+item.sort=Sort This Column
+item.sort.mnemonic=S
+item.copyWithEscape=Copy With Escape
+item.copyWithEscape.mnemonic=W
+item.copyColumnName=Copy Column Names
+item.copyColumnName.mnemonic=N
+item.findColumnName=Find Column Names
+item.findColumnName.mnemonic=F
+item.clearSelectedCellValue=Clear Selected Cell Value
+item.clearSelectedCellValue.mnemonic=X
+item.clearSelectedCellValue.shortcut=Delete
+item.setCurrentTimeValue=Set Current Time Value
+item.setCurrentTimeValue.mnemonic=T
+item.addEmptyRow=Add New (Empty) Row
+item.addEmptyRow.mnemonic=E
+item.insertFromClipboard=Insert From Clipboard
+item.insertFromClipboard.mnemonic=I
+item.duplicateRows=Duplicate Rows
+item.duplicateRows.mnemonic=S
+item.linkRowsToDatabase=Link Rows Into Database
+item.linkRowsToDatabase.mnemonic=L
+item.deleteRows=Delete Rows
+item.deleteRows.mnemonic=D
+
+# for ConsoleTextArea
+ConsoleTextArea.items=undo,redo,,cut,copy,paste,selectAll
diff --git a/src/net/argius/stew/ui/window/ContextMenu_ja.u8p b/src/net/argius/stew/ui/window/ContextMenu_ja.u8p
new file mode 100644 (file)
index 0000000..b7b0622
--- /dev/null
@@ -0,0 +1,23 @@
+auto-mnemonic=1
+
+item.sort=この列をソート
+item.copyWithEscape=エスケープ付でコピー
+item.clearSelectedCellValue=セルの値をクリア
+item.setCurrentTimeValue=現在時刻を貼り付け
+item.copyColumnName=列名をコピー
+item.findColumnName=列名を検索
+item.addEmptyRow=新しい行を追加
+item.insertFromClipboard=クリップボードのデータを追加
+item.duplicateRows=行を複製
+item.linkRowsToDatabase=行をデータベースへリンク
+item.deleteRows=行を削除
+
+# for InfoTree
+item.copySimpleName=単純名をコピー
+item.copyFullName=完全名をコピー
+item.generateWherePhrase=WHERE句条件部を生成
+item.generateSelectPhrase=SELECT文を生成(WHERE付)
+item.generateUpdateStatement=UPDATE文を生成(WHERE付)
+item.generateInsertStatement=INSERT文を生成
+item.jumpToColumnByName=この列名の列へジャンプ
+item.toggleShowColumnNumber=列番号を表示/非表示
diff --git a/src/net/argius/stew/ui/window/DatabaseInfoTree.java b/src/net/argius/stew/ui/window/DatabaseInfoTree.java
new file mode 100644 (file)
index 0000000..921e4aa
--- /dev/null
@@ -0,0 +1,912 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.EventQueue.invokeLater;
+import static java.awt.event.InputEvent.ALT_DOWN_MASK;
+import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
+import static java.awt.event.KeyEvent.VK_C;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.nCopies;
+import static javax.swing.KeyStroke.getKeyStroke;
+import static net.argius.stew.ui.window.AnyActionKey.copy;
+import static net.argius.stew.ui.window.AnyActionKey.refresh;
+import static net.argius.stew.ui.window.DatabaseInfoTree.ActionKey.*;
+import static net.argius.stew.ui.window.WindowOutputProcessor.showInformationMessageDialog;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.sql.*;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.List;
+
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+
+import net.argius.stew.*;
+import net.argius.stew.text.*;
+
+/**
+ * The Database Information Tree is a tree pane that provides to
+ * display database object information from DatabaseMetaData.
+ */
+final class DatabaseInfoTree extends JTree implements AnyActionListener, TextSearch {
+
+    enum ActionKey {
+        copySimpleName,
+        copyFullName,
+        generateWherePhrase,
+        generateSelectPhrase,
+        generateUpdateStatement,
+        generateInsertStatement,
+        jumpToColumnByName,
+        toggleShowColumnNumber
+    }
+
+    private static final Logger log = Logger.getLogger(DatabaseInfoTree.class);
+    private static final ResourceManager res = ResourceManager.getInstance(DatabaseInfoTree.class);
+
+    static volatile boolean showColumnNumber;
+
+    private Connector currentConnector;
+    private DatabaseMetaData dbmeta;
+    private AnyActionListener anyActionListener;
+
+    DatabaseInfoTree(AnyActionListener anyActionListener) {
+        this.anyActionListener = anyActionListener;
+        setRootVisible(false);
+        setShowsRootHandles(false);
+        setScrollsOnExpand(true);
+        setCellRenderer(new Renderer());
+        setModel(new DefaultTreeModel(null));
+        // [Events]
+        int sckm = Utilities.getMenuShortcutKeyMask();
+        AnyAction aa = new AnyAction(this);
+        aa.bindKeyStroke(false, copy, KeyStroke.getKeyStroke(VK_C, sckm));
+        aa.bindSelf(copySimpleName, getKeyStroke(VK_C, sckm | ALT_DOWN_MASK));
+        aa.bindSelf(copyFullName, getKeyStroke(VK_C, sckm | SHIFT_DOWN_MASK));
+    }
+
+    @Override
+    protected void processMouseEvent(MouseEvent e) {
+        super.processMouseEvent(e);
+        if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() % 2 == 0) {
+            anyActionPerformed(new AnyActionEvent(this, jumpToColumnByName));
+        }
+    }
+
+    static Set<String> collectTableName(TreePath[] paths, List<ColumnNode> out) {
+        Set<String> a = new LinkedHashSet<String>();
+        for (TreePath path : paths) {
+            InfoNode node = (InfoNode)path.getLastPathComponent();
+            if (node instanceof TableNode) {
+                TableNode tableNode = (TableNode)node;
+                if (tableNode.isKindOfTable()) {
+                    a.add(tableNode.getNodeFullName());
+                }
+            } else if (node instanceof ColumnNode) {
+                ColumnNode columnNode = (ColumnNode)node;
+                if (columnNode.getTableNode().isKindOfTable()) {
+                    a.add(columnNode.getTableNode().getName());
+                    out.add(columnNode);
+                }
+            }
+        }
+        return a;
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        log.atEnter("anyActionPerformed", ev);
+        if (ev.isAnyOf(copySimpleName)) {
+            copySimpleName();
+        } else if (ev.isAnyOf(copyFullName)) {
+            copyFullName();
+        } else if (ev.isAnyOf(refresh)) {
+            for (TreePath path : getSelectionPaths()) {
+                refresh((InfoNode)path.getLastPathComponent());
+            }
+        } else if (ev.isAnyOf(generateWherePhrase)) {
+            TreePath[] paths = getSelectionPaths();
+            List<ColumnNode> columns = new ArrayList<ColumnNode>();
+            Set<String> tableNames = collectTableName(paths, columns);
+            if (!tableNames.isEmpty()) {
+                insertTextToTextArea(generateEquivalentJoinClause(columns));
+            }
+        } else if (ev.isAnyOf(generateSelectPhrase)) {
+            TreePath[] paths = getSelectionPaths();
+            List<ColumnNode> columns = new ArrayList<ColumnNode>();
+            Set<String> tableNames = collectTableName(paths, columns);
+            if (!tableNames.isEmpty()) {
+                List<String> columnNames = new ArrayList<String>();
+                final boolean one = tableNames.size() == 1;
+                for (ColumnNode node : columns) {
+                    columnNames.add(one ? node.getName() : node.getNodeFullName());
+                }
+                final String columnString = (columnNames.isEmpty())
+                        ? "*"
+                        : TextUtilities.join(", ", columnNames);
+                final String tableString = joinByComma(new ArrayList<String>(tableNames));
+                insertTextToTextArea(String.format("SELECT %s FROM %s WHERE ", columnString, tableString));
+            }
+        } else if (ev.isAnyOf(generateUpdateStatement)) {
+            TreePath[] paths = getSelectionPaths();
+            List<ColumnNode> columns = new ArrayList<ColumnNode>();
+            Set<String> tableNames = collectTableName(paths, columns);
+            if (tableNames.isEmpty()) {
+                return;
+            }
+            if (tableNames.size() != 1 || columns.isEmpty()) {
+                showInformationMessageDialog(this, res.get("e.enables-select-just-1-table"), "");
+            } else {
+                final String tableName = tableNames.toArray(new String[1])[0];
+                List<String> columnExpressions = new ArrayList<String>();
+                for (ColumnNode columnNode : columns) {
+                    columnExpressions.add(columnNode.getName() + "=?");
+                }
+                insertTextToTextArea(String.format("UPDATE %s SET %s WHERE ",
+                                                   tableName,
+                                                   joinByComma(columnExpressions)));
+            }
+        } else if (ev.isAnyOf(generateInsertStatement)) {
+            generateInsertStatement();
+        } else if (ev.isAnyOf(jumpToColumnByName)) {
+            jumpToColumnByName();
+        } else if (ev.isAnyOf(toggleShowColumnNumber)) {
+            showColumnNumber = !showColumnNumber;
+            repaint();
+        } else {
+            log.warn("not expected: Event=%s", ev);
+        }
+        log.atExit("anyActionPerformed");
+    }
+
+    private void insertTextToTextArea(String s) {
+        AnyActionEvent ev = new AnyActionEvent(this, ConsoleTextArea.ActionKey.insertText, s);
+        anyActionListener.anyActionPerformed(ev);
+    }
+
+    private static String joinByComma(List<?> a) {
+        StringBuilder buffer = new StringBuilder();
+        for (int i = 0, n = a.size(); i < n; i++) {
+            if (i != 0) {
+                buffer.append(", ");
+            }
+            buffer.append(a.get(i));
+        }
+        return buffer.toString();
+    }
+
+    private void copySimpleName() {
+        TreePath[] paths = getSelectionPaths();
+        if (paths == null || paths.length == 0) {
+            return;
+        }
+        List<String> names = new ArrayList<String>(paths.length);
+        for (TreePath path : paths) {
+            if (path == null) {
+                continue;
+            }
+            Object o = path.getLastPathComponent();
+            assert o instanceof InfoNode;
+            final String name;
+            if (o instanceof ColumnNode) {
+                name = ((ColumnNode)o).getName();
+            } else if (o instanceof TableNode) {
+                name = ((TableNode)o).getName();
+            } else {
+                name = o.toString();
+            }
+            names.add(name);
+        }
+        ClipboardHelper.setStrings(names);
+    }
+
+    private void copyFullName() {
+        TreePath[] paths = getSelectionPaths();
+        if (paths == null || paths.length == 0) {
+            return;
+        }
+        List<String> names = new ArrayList<String>(paths.length);
+        for (TreePath path : paths) {
+            if (path == null) {
+                continue;
+            }
+            Object o = path.getLastPathComponent();
+            assert o instanceof InfoNode;
+            names.add(((InfoNode)o).getNodeFullName());
+        }
+        ClipboardHelper.setStrings(names);
+    }
+
+    private void generateInsertStatement() {
+        TreePath[] paths = getSelectionPaths();
+        List<ColumnNode> columns = new ArrayList<ColumnNode>();
+        Set<String> tableNames = collectTableName(paths, columns);
+        if (tableNames.isEmpty()) {
+            return;
+        }
+        if (tableNames.size() != 1) {
+            showInformationMessageDialog(this, res.get("e.enables-select-just-1-table"), "");
+            return;
+        }
+        final String tableName = tableNames.toArray(new String[1])[0];
+        List<String> columnNames = new ArrayList<String>();
+        final Iterable<ColumnNode> columnNodes;
+        if (columns.isEmpty()) {
+            final TreePath path = paths[0];
+            TableNode tableNode = (TableNode)path.getLastPathComponent();
+            if (tableNode.getChildCount() == 0) {
+                expandPath(path);
+            }
+            @SuppressWarnings("unchecked")
+            Iterable<ColumnNode> it = Collections.list(tableNode.children());
+            columnNodes = it;
+        } else {
+            columnNodes = columns;
+        }
+        for (ColumnNode node : columnNodes) {
+            columnNames.add(node.getName());
+        }
+        insertTextToTextArea(String.format("INSERT INTO %s (%s) VALUES (%s);",
+                                           tableName,
+                                           joinByComma(columnNames),
+                                           joinByComma(nCopies(columnNames.size(), "?"))));
+    }
+
+    private void jumpToColumnByName() {
+        TreePath[] paths = getSelectionPaths();
+        if (paths == null || paths.length == 0) {
+            return;
+        }
+        final TreePath path = paths[0];
+        Object o = path.getLastPathComponent();
+        if (o instanceof ColumnNode) {
+            ColumnNode node = (ColumnNode)o;
+            AnyActionEvent ev = new AnyActionEvent(this,
+                                                   ResultSetTable.ActionKey.jumpToColumn,
+                                                   node.getName());
+            anyActionListener.anyActionPerformed(ev);
+        }
+    }
+
+    @Override
+    public boolean search(Matcher matcher) {
+        return search(resolveTargetPath(getSelectionPath()), matcher);
+    }
+
+    private static TreePath resolveTargetPath(TreePath path) {
+        if (path != null) {
+            TreePath parent = path.getParentPath();
+            if (parent != null) {
+                return parent;
+            }
+        }
+        return path;
+    }
+
+    private boolean search(TreePath path, Matcher matcher) {
+        if (path == null) {
+            return false;
+        }
+        TreeNode node = (TreeNode)path.getLastPathComponent();
+        if (node == null) {
+            return false;
+        }
+        boolean found = false;
+        found = matcher.find(node.toString());
+        if (found) {
+            addSelectionPath(path);
+        } else {
+            removeSelectionPath(path);
+        }
+        if (!node.isLeaf() && node.getChildCount() >= 0) {
+            @SuppressWarnings("unchecked")
+            Iterable<DefaultMutableTreeNode> children = Collections.list(node.children());
+            for (DefaultMutableTreeNode child : children) {
+                if (search(path.pathByAddingChild(child), matcher)) {
+                    found = true;
+                }
+            }
+        }
+        return found;
+    }
+
+    @Override
+    public void reset() {
+        // empty
+    }
+
+    /**
+     * Refreshes the root and its children.
+     * @param env Environment
+     * @throws SQLException
+     */
+    void refreshRoot(Environment env) throws SQLException {
+        Connector c = env.getCurrentConnector();
+        if (c == null) {
+            if (log.isDebugEnabled()) {
+                log.debug("not connected");
+            }
+            currentConnector = null;
+            return;
+        }
+        if (c == currentConnector && getModel().getRoot() != null) {
+            if (log.isDebugEnabled()) {
+                log.debug("not changed");
+            }
+            return;
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("updating");
+        }
+        // initializing models
+        ConnectorNode connectorNode = new ConnectorNode(c.getName());
+        DefaultTreeModel model = new DefaultTreeModel(connectorNode);
+        setModel(model);
+        final DefaultTreeSelectionModel m = new DefaultTreeSelectionModel();
+        m.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
+        setSelectionModel(m);
+        // initializing nodes
+        final DatabaseMetaData dbmeta = env.getCurrentConnection().getMetaData();
+        final Set<InfoNode> createdStatusSet = new HashSet<InfoNode>();
+        expandNode(connectorNode, dbmeta);
+        createdStatusSet.add(connectorNode);
+        // events
+        addTreeWillExpandListener(new TreeWillExpandListener() {
+            @Override
+            public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
+                TreePath path = event.getPath();
+                final Object lastPathComponent = path.getLastPathComponent();
+                if (!createdStatusSet.contains(lastPathComponent)) {
+                    InfoNode node = (InfoNode)lastPathComponent;
+                    if (node.isLeaf()) {
+                        return;
+                    }
+                    createdStatusSet.add(node);
+                    try {
+                        expandNode(node, dbmeta);
+                    } catch (SQLException ex) {
+                        throw new RuntimeException(ex);
+                    }
+                }
+            }
+            @Override
+            public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
+                // ignore
+            }
+        });
+        this.dbmeta = dbmeta;
+        // showing
+        model.reload();
+        setRootVisible(true);
+        this.currentConnector = c;
+        // auto-expansion
+        try {
+            File confFile = new File(Bootstrap.getDirectory(), "autoexpansion.tsv");
+            if (confFile.exists() && confFile.length() > 0) {
+                AnyAction aa = new AnyAction(this);
+                Scanner r = new Scanner(confFile);
+                try {
+                    while (r.hasNextLine()) {
+                        final String line = r.nextLine();
+                        if (line.matches("^\\s*#.*")) {
+                            continue;
+                        }
+                        aa.doParallel("expandNodes", Arrays.asList(line.split("\t")));
+                    }
+                } finally {
+                    r.close();
+                }
+            }
+        } catch (IOException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    void expandNodes(List<String> a) {
+        long startTime = System.currentTimeMillis();
+        AnyAction aa = new AnyAction(this);
+        int index = 1;
+        while (index < a.size()) {
+            final String s = a.subList(0, index + 1).toString();
+            for (int i = 0, n = getRowCount(); i < n; i++) {
+                TreePath target;
+                try {
+                    target = getPathForRow(i);
+                } catch (IndexOutOfBoundsException ex) {
+                    // FIXME when IndexOutOfBoundsException was thrown at expandNodes
+                    log.warn(ex);
+                    break;
+                }
+                if (target != null && target.toString().equals(s)) {
+                    if (!isExpanded(target)) {
+                        aa.doLater("expandLater", target);
+                        Utilities.sleep(200L);
+                    }
+                    index++;
+                    break;
+                }
+            }
+            if (System.currentTimeMillis() - startTime > 5000L) {
+                break; // timeout
+            }
+        }
+    }
+
+    // called by expandNodes
+    @SuppressWarnings("unused")
+    private void expandLater(TreePath parent) {
+        expandPath(parent);
+    }
+
+    /**
+     * Refreshes a node and its children.
+     * @param node
+     */
+    void refresh(InfoNode node) {
+        if (dbmeta == null) {
+            return;
+        }
+        node.removeAllChildren();
+        final DefaultTreeModel model = (DefaultTreeModel)getModel();
+        model.reload(node);
+        try {
+            expandNode(node, dbmeta);
+        } catch (SQLException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    /**
+     * Expands a node.
+     * @param parent
+     * @param dbmeta
+     * @throws SQLException
+     */
+    void expandNode(final InfoNode parent, final DatabaseMetaData dbmeta) throws SQLException {
+        if (parent.isLeaf()) {
+            return;
+        }
+        final DefaultTreeModel model = (DefaultTreeModel)getModel();
+        final InfoNode tmpNode = new InfoNode(res.get("i.paren-in-processing")) {
+            @Override
+            protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
+                return Collections.emptyList();
+            }
+            @Override
+            public boolean isLeaf() {
+                return true;
+            }
+        };
+        invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                model.insertNodeInto(tmpNode, parent, 0);
+            }
+        });
+        // asynchronous
+        DaemonThreadFactory.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    final List<InfoNode> children = new ArrayList<InfoNode>(parent.createChildren(dbmeta));
+                    invokeLater(new Runnable() {
+                        @Override
+                        public void run() {
+                            for (InfoNode child : children) {
+                                model.insertNodeInto(child, parent, parent.getChildCount());
+                            }
+                            model.removeNodeFromParent(tmpNode);
+                        }
+                    });
+                } catch (SQLException ex) {
+                    throw new RuntimeException(ex);
+                }
+            }
+        });
+    }
+
+    /**
+     * Clears (root).
+     */
+    void clear() {
+        for (TreeWillExpandListener listener : getListeners(TreeWillExpandListener.class).clone()) {
+            removeTreeWillExpandListener(listener);
+        }
+        setModel(new DefaultTreeModel(null));
+        currentConnector = null;
+        dbmeta = null;
+        if (log.isDebugEnabled()) {
+            log.debug("cleared");
+        }
+    }
+
+    static String generateEquivalentJoinClause(List<ColumnNode> nodes) {
+        if (nodes.isEmpty()) {
+            return "";
+        }
+        ListMap tm = new ListMap();
+        ListMap cm = new ListMap();
+        for (ColumnNode node : nodes) {
+            final String tableName = node.getTableNode().getName();
+            final String columnName = node.getName();
+            tm.add(tableName, columnName);
+            cm.add(columnName, String.format("%s.%s", tableName, columnName));
+        }
+        List<String> expressions = new ArrayList<String>();
+        if (tm.size() == 1) {
+            for (ColumnNode node : nodes) {
+                expressions.add(String.format("%s=?", node.getName()));
+            }
+        } else {
+            final String tableName = nodes.get(0).getTableNode().getName();
+            for (String c : tm.get(tableName)) {
+                expressions.add(String.format("%s.%s=?", tableName, c));
+            }
+            for (Entry<String, List<String>> entry : cm.entrySet()) {
+                if (!entry.getKey().equals(tableName) && entry.getValue().size() == 1) {
+                    expressions.add(String.format("%s=?", entry.getValue().get(0)));
+                }
+            }
+            for (Entry<String, List<String>> entry : cm.entrySet()) {
+                Object[] a = entry.getValue().toArray();
+                final int n = a.length;
+                for (int i = 0; i < n; i++) {
+                    for (int j = i + 1; j < n; j++) {
+                        expressions.add(String.format("%s=%s", a[i], a[j]));
+                    }
+                }
+            }
+        }
+        return TextUtilities.join(" AND ", expressions) + ';';
+    }
+
+    @Override
+    public TreePath[] getSelectionPaths() {
+        TreePath[] a = super.getSelectionPaths();
+        if (a == null) {
+            return new TreePath[0];
+        }
+        return a;
+    }
+
+    // subclasses
+
+    private static final class ListMap extends LinkedHashMap<String, List<String>> {
+
+        ListMap() {
+            // empty
+        }
+
+        void add(String key, String value) {
+            if (get(key) == null) {
+                put(key, new ArrayList<String>());
+            }
+            get(key).add(value);
+        }
+
+    }
+
+    private static class Renderer extends DefaultTreeCellRenderer {
+
+        Renderer() {
+            // empty
+        }
+
+        @Override
+        public Component getTreeCellRendererComponent(JTree tree,
+                                                      Object value,
+                                                      boolean sel,
+                                                      boolean expanded,
+                                                      boolean leaf,
+                                                      int row,
+                                                      boolean hasFocus) {
+            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+            if (value instanceof InfoNode) {
+                setIcon(Utilities.getImageIcon(((InfoNode)value).getIconName()));
+            }
+            if (value instanceof ColumnNode) {
+                if (showColumnNumber) {
+                    TreePath path = tree.getPathForRow(row);
+                    if (path != null) {
+                        TreePath parent = path.getParentPath();
+                        if (parent != null) {
+                            final int index = row - tree.getRowForPath(parent);
+                            setText(String.format("%d %s", index, getText()));
+                        }
+                    }
+                }
+            }
+            return this;
+        }
+
+    }
+
+    private abstract static class InfoNode extends DefaultMutableTreeNode {
+
+        InfoNode(Object userObject) {
+            super(userObject, true);
+        }
+
+        @Override
+        public boolean isLeaf() {
+            return false;
+        }
+
+        abstract protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException;
+
+        String getIconName() {
+            final String className = getClass().getName();
+            final String nodeType = className.replaceFirst(".+?([^\\$]+)Node$", "$1");
+            return "node-" + nodeType.toLowerCase() + ".png";
+        }
+
+        protected String getNodeFullName() {
+            return String.valueOf(userObject);
+        }
+
+        static List<TableTypeNode> getTableTypeNodes(DatabaseMetaData dbmeta,
+                                                     String catalog,
+                                                     String schema) throws SQLException {
+            List<TableTypeNode> a = new ArrayList<TableTypeNode>();
+            ResultSet rs = dbmeta.getTableTypes();
+            try {
+                while (rs.next()) {
+                    TableTypeNode typeNode = new TableTypeNode(catalog, schema, rs.getString(1));
+                    if (typeNode.hasItems(dbmeta)) {
+                        a.add(typeNode);
+                    }
+                }
+            } finally {
+                rs.close();
+            }
+            if (a.isEmpty()) {
+                a.add(new TableTypeNode(catalog, schema, "TABLE"));
+            }
+            return a;
+        }
+
+    }
+
+    private static class ConnectorNode extends InfoNode {
+
+        ConnectorNode(String name) {
+            super(name);
+        }
+
+        @Override
+        protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
+            List<InfoNode> a = new ArrayList<InfoNode>();
+            if (dbmeta.supportsCatalogsInDataManipulation()) {
+                ResultSet rs = dbmeta.getCatalogs();
+                try {
+                    while (rs.next()) {
+                        a.add(new CatalogNode(rs.getString(1)));
+                    }
+                } finally {
+                    rs.close();
+                }
+            } else if (dbmeta.supportsSchemasInDataManipulation()) {
+                ResultSet rs = dbmeta.getSchemas();
+                try {
+                    while (rs.next()) {
+                        a.add(new SchemaNode(null, rs.getString(1)));
+                    }
+                } finally {
+                    rs.close();
+                }
+            } else {
+                a.addAll(getTableTypeNodes(dbmeta, null, null));
+            }
+            return a;
+        }
+
+    }
+
+    private static final class CatalogNode extends InfoNode {
+
+        private final String name;
+
+        CatalogNode(String name) {
+            super(name);
+            this.name = name;
+        }
+
+        @Override
+        protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
+            List<InfoNode> a = new ArrayList<InfoNode>();
+            if (dbmeta.supportsSchemasInDataManipulation()) {
+                ResultSet rs = dbmeta.getSchemas();
+                try {
+                    while (rs.next()) {
+                        a.add(new SchemaNode(name, rs.getString(1)));
+                    }
+                } finally {
+                    rs.close();
+                }
+            } else {
+                a.addAll(getTableTypeNodes(dbmeta, name, null));
+            }
+            return a;
+        }
+
+    }
+
+    private static final class SchemaNode extends InfoNode {
+
+        private final String catalog;
+        private final String schema;
+
+        SchemaNode(String catalog, String schema) {
+            super(schema);
+            this.catalog = catalog;
+            this.schema = schema;
+        }
+
+        @Override
+        protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
+            List<InfoNode> a = new ArrayList<InfoNode>();
+            a.addAll(getTableTypeNodes(dbmeta, catalog, schema));
+            return a;
+        }
+
+    }
+
+    private static final class TableTypeNode extends InfoNode {
+
+        private static final String ICON_NAME_FORMAT = "node-tabletype-%s.png";
+
+        private final String catalog;
+        private final String schema;
+        private final String tableType;
+
+        TableTypeNode(String catalog, String schema, String tableType) {
+            super(tableType);
+            this.catalog = catalog;
+            this.schema = schema;
+            this.tableType = tableType;
+        }
+
+        @Override
+        protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
+            List<InfoNode> a = new ArrayList<InfoNode>();
+            ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
+            try {
+                while (rs.next()) {
+                    final String table = rs.getString(3);
+                    final String type = rs.getString(4);
+                    final boolean kindOfTable = type.matches("TABLE|VIEW|SYNONYM");
+                    a.add(new TableNode(catalog, schema, table, kindOfTable));
+                }
+            } finally {
+                rs.close();
+            }
+            return a;
+        }
+
+        @Override
+        String getIconName() {
+            final String name = String.format(ICON_NAME_FORMAT, getUserObject());
+            if (getClass().getResource("icon/" + name) == null) {
+                return String.format(ICON_NAME_FORMAT, "");
+            }
+            return name;
+        }
+
+        boolean hasItems(DatabaseMetaData dbmeta) throws SQLException {
+            ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
+            try {
+                return rs.next();
+            } finally {
+                rs.close();
+            }
+        }
+
+    }
+
+    static final class TableNode extends InfoNode {
+
+        private final String catalog;
+        private final String schema;
+        private final String name;
+        private final boolean kindOfTable;
+
+        TableNode(String catalog, String schema, String name, boolean kindOfTable) {
+            super(name);
+            this.catalog = catalog;
+            this.schema = schema;
+            this.name = name;
+            this.kindOfTable = kindOfTable;
+        }
+
+        @Override
+        protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
+            List<InfoNode> a = new ArrayList<InfoNode>();
+            ResultSet rs = dbmeta.getColumns(catalog, schema, name, null);
+            try {
+                while (rs.next()) {
+                    a.add(new ColumnNode(rs.getString(4),
+                                         rs.getString(6),
+                                         rs.getInt(7),
+                                         rs.getString(18),
+                                         this));
+                }
+            } finally {
+                rs.close();
+            }
+            return a;
+        }
+
+        @Override
+        public boolean isLeaf() {
+            return false;
+        }
+
+        @Override
+        protected String getNodeFullName() {
+            List<String> a = new ArrayList<String>();
+            if (catalog != null) {
+                a.add(catalog);
+            }
+            if (schema != null) {
+                a.add(schema);
+            }
+            a.add(name);
+            return TextUtilities.join(".", a);
+        }
+
+        String getName() {
+            return name;
+        }
+
+        boolean isKindOfTable() {
+            return kindOfTable;
+        }
+
+    }
+
+    static final class ColumnNode extends InfoNode {
+
+        private final String name;
+        private final TableNode tableNode;
+
+        ColumnNode(String name, String type, int size, String nulls, TableNode tableNode) {
+            super(format(name, type, size, nulls));
+            setAllowsChildren(false);
+            this.name = name;
+            this.tableNode = tableNode;
+        }
+
+        String getName() {
+            return name;
+        }
+
+        TableNode getTableNode() {
+            return tableNode;
+        }
+
+        private static String format(String name, String type, int size, String nulls) {
+            final String nonNull = "NO".equals(nulls) ? " NOT NULL" : "";
+            return String.format("%s [%s(%d)%s]", name, type, size, nonNull);
+        }
+
+        @Override
+        public boolean isLeaf() {
+            return true;
+        }
+
+        @Override
+        protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
+            return emptyList();
+        }
+
+        @Override
+        protected String getNodeFullName() {
+            return String.format("%s.%s", tableNode.getNodeFullName(), name);
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/FlexiblePanel.java b/src/net/argius/stew/ui/window/FlexiblePanel.java
new file mode 100644 (file)
index 0000000..ac06e1d
--- /dev/null
@@ -0,0 +1,39 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.GridBagConstraints.*;
+
+import java.awt.*;
+
+import javax.swing.*;
+
+public class FlexiblePanel extends JPanel {
+
+    protected final GridBagLayout g;
+    protected final GridBagConstraints c;
+
+    public FlexiblePanel() {
+        this.g = new GridBagLayout();
+        this.c = new GridBagConstraints();
+        setLayout(g);
+        c.gridwidth = 8;
+        c.fill = NONE;
+        c.ipadx = 2;
+        c.ipady = 0;
+        c.insets = new Insets(2, 4, 2, 4);
+        c.anchor = WEST;
+        c.weightx = 1.0;
+        c.weighty = 1.0;
+    }
+
+    public void addComponent(Component component, boolean isRowEnd) {
+        if (isRowEnd) {
+            c.gridwidth = REMAINDER;
+        }
+        g.setConstraints(component, c);
+        add(component);
+        if (isRowEnd) {
+            c.gridwidth = LINE_START;
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/FontControlLookAndFeel.java b/src/net/argius/stew/ui/window/FontControlLookAndFeel.java
new file mode 100644 (file)
index 0000000..9e24482
--- /dev/null
@@ -0,0 +1,107 @@
+package net.argius.stew.ui.window;
+
+import java.awt.*;
+import java.util.Map.Entry;
+
+import javax.swing.*;
+import javax.swing.UIDefaults.ActiveValue;
+import javax.swing.plaf.*;
+import javax.swing.plaf.basic.*;
+
+final class FontControlLookAndFeel extends BasicLookAndFeel {
+
+    private static final LookAndFeel BASE = UIManager.getLookAndFeel();
+
+    private final String fontFamily;
+    private final int fontStyleMask;
+    private final double sizeRate;
+
+    FontControlLookAndFeel(String fontFamily, int fontStyleMask, double sizeRate) {
+        this.fontFamily = fontFamily;
+        this.fontStyleMask = fontStyleMask;
+        this.sizeRate = sizeRate;
+    }
+
+    static void change(String fontFamily, int fontStyleMask, double sizeRate) {
+        try {
+            UIManager.setLookAndFeel(new FontControlLookAndFeel(fontFamily, fontStyleMask, sizeRate));
+        } catch (UnsupportedLookAndFeelException ex) {
+            throw new IllegalStateException(ex.toString());
+        }
+    }
+
+    @Override
+    public UIDefaults getDefaults() {
+        UIDefaults defaults = BASE.getDefaults();
+        for (Entry<?, Object> entry : defaults.entrySet()) {
+            Object key = entry.getKey();
+            if (String.valueOf(key).endsWith("font")) {
+                Object value = entry.getValue();
+                if (value instanceof UIDefaults.ActiveValue) {
+                    entry.setValue(new FontControlActiveValue((ActiveValue)value,
+                                                              fontFamily,
+                                                              fontStyleMask,
+                                                              sizeRate));
+                }
+            }
+        }
+        return defaults;
+    }
+
+    @Override
+    public String getDescription() {
+        return BASE.getDescription();
+    }
+
+    @Override
+    public String getID() {
+        return BASE.getID();
+    }
+
+    @Override
+    public String getName() {
+        return BASE.getName();
+    }
+
+    @Override
+    public boolean isNativeLookAndFeel() {
+        return BASE.isNativeLookAndFeel();
+    }
+
+    @Override
+    public boolean isSupportedLookAndFeel() {
+        return BASE.isSupportedLookAndFeel();
+    }
+
+    private static final class FontControlActiveValue implements ActiveValue {
+
+        private final ActiveValue base;
+        private final String fontFamily;
+        private final int fontStyleMask;
+        private final double sizeRate;
+
+        FontControlActiveValue(ActiveValue base,
+                               String fontFamily,
+                               int fontStyleMask,
+                               double sizeRate) {
+            this.base = base;
+            this.fontFamily = fontFamily;
+            this.fontStyleMask = fontStyleMask;
+            this.sizeRate = sizeRate;
+        }
+
+        @Override
+        public Object createValue(UIDefaults table) {
+            Object o = base.createValue(table);
+            if (o instanceof Font) {
+                Font font = (Font)o;
+                final int style = font.getStyle() & fontStyleMask;
+                final int size = (int)(font.getSize() * sizeRate);
+                return new FontUIResource(fontFamily, style, size);
+            }
+            return o;
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/Menu.java b/src/net/argius/stew/ui/window/Menu.java
new file mode 100644 (file)
index 0000000..be3f987
--- /dev/null
@@ -0,0 +1,254 @@
+package net.argius.stew.ui.window;
+
+import static net.argius.stew.ui.window.Menu.Item.*;
+import static net.argius.stew.ui.window.Utilities.getImageIcon;
+import static net.argius.stew.ui.window.Utilities.getMenuShortcutKeyMask;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.beans.*;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+
+import net.argius.stew.*;
+
+/**
+ * The menu bar.
+ */
+final class Menu extends JMenuBar implements PropertyChangeListener {
+
+    private static final ResourceManager res = ResourceManager.getInstance(Menu.class);
+
+    /**
+     * Menu Items.
+     */
+    enum Item {
+        newWindow,
+        closeWindow,
+        quit,
+        cut,
+        copy,
+        paste,
+        selectAll,
+        find,
+        toggleFocus,
+        clearMessage,
+        showStatusBar,
+        showColumnNumber,
+        showInfoTree,
+        showAlwaysOnTop,
+        refresh,
+        widenColumnWidth,
+        narrowColumnWidth,
+        adjustColumnWidth,
+        autoAdjustMode,
+        autoAdjustModeNone,
+        autoAdjustModeHeader,
+        autoAdjustModeValue,
+        autoAdjustModeHeaderAndValue,
+        executeCommand,
+        breakCommand,
+        lastHistory,
+        nextHistory,
+        sendRollback,
+        sendCommit,
+        connect,
+        disconnect,
+        postProcessMode,
+        postProcessModeNone,
+        postProcessModeFocus,
+        postProcessModeShake,
+        postProcessModeBlink,
+        inputEcryptionKey,
+        editConnectors,
+        sortResult,
+        importFile,
+        exportFile,
+        showHelp,
+        showAbout;
+    }
+
+    private List<JMenuItem> lockingTargets;
+    private List<JMenuItem> unlockingTargets;
+    private EnumMap<Item, JMenuItem> itemToCompMap;
+    private Map<JMenuItem, Item> compToItemMap;
+
+    Menu(final AnyActionListener anyActionListener) {
+        this.lockingTargets = new ArrayList<JMenuItem>();
+        this.unlockingTargets = new ArrayList<JMenuItem>();
+        this.itemToCompMap = new EnumMap<Item, JMenuItem>(Item.class);
+        this.compToItemMap = new HashMap<JMenuItem, Item>();
+        final boolean autoMnemonic = res.getInt("auto-mnemonic") == 1;
+        AnyAction aa = new AnyAction(anyActionListener);
+        for (final String groupId : res.get("groups").split(",", -1)) {
+            final String groupKey = "group." + groupId;
+            JMenu group = add(buildGroup(groupId, autoMnemonic));
+            for (final String itemId : res.get(groupKey + ".items").split(",", -1)) {
+                if (itemId.length() == 0) {
+                    group.add(new JSeparator());
+                    continue;
+                }
+                JMenuItem m = group.add(buildItem(itemId, autoMnemonic));
+                Item item;
+                try {
+                    item = Item.valueOf(itemId);
+                } catch (Exception ex) {
+                    assert false : ex.toString();
+                    continue;
+                }
+                m.addActionListener(aa);
+                itemToCompMap.put(item, m);
+                compToItemMap.put(m, item);
+                final String shortcutId = "item." + itemId + ".shortcut";
+                if (res.containsKey(shortcutId)) {
+                    KeyStroke shortcutKey = KeyStroke.getKeyStroke(res.get(shortcutId));
+                    if (shortcutKey != null) {
+                        setAccelerator(item, shortcutKey);
+                    }
+                }
+                switch (item) {
+                    case closeWindow:
+                    case quit:
+                    case cut:
+                    case copy:
+                    case paste:
+                    case selectAll:
+                    case find:
+                    case clearMessage:
+                    case refresh:
+                    case widenColumnWidth:
+                    case narrowColumnWidth:
+                    case adjustColumnWidth:
+                    case autoAdjustMode:
+                    case executeCommand:
+                    case lastHistory:
+                    case nextHistory:
+                    case connect:
+                    case disconnect:
+                    case postProcessMode:
+                    case sortResult:
+                    case exportFile:
+                        lockingTargets.add(m);
+                        break;
+                    case breakCommand:
+                        unlockingTargets.add(m);
+                        break;
+                    default:
+                }
+            }
+        }
+        for (final Item item : EnumSet.of(autoAdjustModeNone,
+                                          autoAdjustModeHeader,
+                                          autoAdjustModeValue,
+                                          autoAdjustModeHeaderAndValue,
+                                          postProcessMode,
+                                          postProcessModeNone,
+                                          postProcessModeFocus,
+                                          postProcessModeShake,
+                                          postProcessModeBlink)) {
+            itemToCompMap.get(item).addActionListener(aa);
+        }
+        setEnabledStates(false);
+    }
+
+    private static JMenu buildGroup(String groupId, boolean autoMnemonic) {
+        final String key = (res.containsKey("group." + groupId) ? "group" : "item") + '.' + groupId;
+        final char mn = res.getChar(key + ".mnemonic");
+        final String groupString = res.get(key) + (autoMnemonic ? "(" + mn + ")" : "");
+        JMenu group = new JMenu(groupString);
+        group.setMnemonic(mn);
+        return group;
+    }
+
+    private JMenuItem buildItem(String itemId, boolean autoMnemonic) {
+        final String itemKey = "item." + itemId;
+        final char mn = res.getChar(itemKey + ".mnemonic");
+        final JMenuItem m;
+        if (res.isTrue(itemKey + ".checkbox")) {
+            m = new JCheckBoxMenuItem();
+        } else if (res.isTrue(itemKey + ".subgroup")) {
+            m = buildGroup(itemId, autoMnemonic);
+            ButtonGroup buttonGroup = new ButtonGroup();
+            boolean selected = false;
+            for (final String id : res.get(itemKey + ".items").split(",", -1)) {
+                final JMenuItem sub = buildItem(itemId + id, autoMnemonic);
+                m.add(sub);
+                buttonGroup.add(sub);
+                if (!selected) {
+                    sub.setSelected(true);
+                    selected = true;
+                }
+                Item subItem = Item.valueOf(itemId + id);
+                itemToCompMap.put(subItem, sub);
+                compToItemMap.put(sub, subItem);
+            }
+        } else {
+            m = new JMenuItem();
+        }
+        m.setText(res.get(itemKey) + (autoMnemonic ? "(" + mn + ")" : ""));
+        m.setMnemonic(mn);
+        m.setActionCommand(itemId);
+        m.setIcon(getImageIcon(String.format("menu-%s.png", itemId)));
+        m.setDisabledIcon(getImageIcon(String.format("menu-disabled-%s.png", itemId)));
+        return m;
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent e) {
+        final String propertyName = e.getPropertyName();
+        final Object source = e.getSource();
+        if (source instanceof JLabel && propertyName.equals("ancestor")) {
+            itemToCompMap.get(showStatusBar).setSelected(((JLabel)source).isVisible());
+        } else if (source instanceof ResultSetTable && propertyName.equals("showNumber")) {
+            itemToCompMap.get(showColumnNumber).setSelected((Boolean)e.getNewValue());
+        } else if (source instanceof DatabaseInfoTree) {
+            itemToCompMap.get(showInfoTree).setSelected(((Component)source).isEnabled());
+        } else if (source instanceof JFrame && propertyName.equals("alwaysOnTop")) {
+            itemToCompMap.get(showAlwaysOnTop).setSelected((Boolean)e.getNewValue());
+        } else if (source instanceof WindowOutputProcessor
+                   && propertyName.equals("autoAdjustMode|postProcessMode")) {
+            final String itemName = e.getNewValue().toString();
+            if (!itemName.matches("[A-Z_]+")) { // ignore old version
+                itemToCompMap.get(Item.valueOf(itemName)).setSelected(true);
+            }
+        }
+    }
+
+    /**
+     * Sets a Accelerator (shortcut key).
+     * @param item
+     * @param ks
+     */
+    void setAccelerator(Item item, KeyStroke ks) {
+        int m = 0;
+        final String s = ks.toString();
+        if (s.contains("ctrl")) {
+            m |= getMenuShortcutKeyMask();
+        }
+        if (s.contains("alt")) {
+            m |= InputEvent.ALT_DOWN_MASK;
+        }
+        if (s.contains("shift")) {
+            m |= InputEvent.SHIFT_DOWN_MASK;
+        }
+        itemToCompMap.get(item).setAccelerator(KeyStroke.getKeyStroke(ks.getKeyCode(), m));
+    }
+
+    /**
+     * Sets the state that command was started or not.
+     * @param commandStarted
+     */
+    void setEnabledStates(boolean commandStarted) {
+        final boolean lockingTargetsState = !commandStarted;
+        for (JMenuItem item : lockingTargets) {
+            item.setEnabled(lockingTargetsState);
+        }
+        final boolean unlockingTargetsState = commandStarted;
+        for (JMenuItem item : unlockingTargets) {
+            item.setEnabled(unlockingTargetsState);
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/Menu.u8p b/src/net/argius/stew/ui/window/Menu.u8p
new file mode 100644 (file)
index 0000000..67a6bd8
--- /dev/null
@@ -0,0 +1,138 @@
+
+groups=file,edit,view,command,data,help
+
+group.file=File
+group.file.mnemonic=F
+group.file.items=newWindow,closeWindow,,quit
+item.newWindow=New Window
+item.newWindow.mnemonic=N
+item.newWindow.shortcut=ctrl N
+item.closeWindow=Close Window
+item.closeWindow.mnemonic=C
+item.closeWindow.shortcut=ctrl W
+item.quit=Quit
+item.quit.mnemonic=Q
+item.quit.shortcut=ctrl Q
+
+group.edit=Edit
+group.edit.mnemonic=E
+group.edit.items=cut,copy,paste,selectAll,,find,,toggleFocus,clearMessage
+# see messages.u8p about: cut, copy, paste, selectAll
+item.find=Find
+item.find.mnemonic=F
+item.find.shortcut=ctrl F
+item.toggleFocus=Toggle focus
+item.toggleFocus.mnemonic=G
+item.toggleFocus.shortcut=ctrl G
+item.clearMessage=Clear Message
+item.clearMessage.mnemonic=M
+
+group.view=View
+group.view.mnemonic=V
+group.view.items=showStatusBar,showColumnNumber,showInfoTree,showAlwaysOnTop,,refresh,,widenColumnWidth,narrowColumnWidth,adjustColumnWidth,autoAdjustMode
+item.showStatusBar=Show Status Bar
+item.showStatusBar.mnemonic=S
+item.showStatusBar.checkbox=yes
+item.showColumnNumber=Show Column Number
+item.showColumnNumber.mnemonic=C
+item.showColumnNumber.checkbox=yes
+item.showInfoTree=Show Info Tree
+item.showInfoTree.mnemonic=I
+item.showInfoTree.checkbox=yes
+item.showAlwaysOnTop=Always On Top
+item.showAlwaysOnTop.mnemonic=T
+item.showAlwaysOnTop.checkbox=yes
+item.refresh=Refresh
+item.refresh.mnemonic=R
+item.refresh.shortcut=F5
+item.widenColumnWidth=Widen Column Width
+item.widenColumnWidth.mnemonic=W
+item.widenColumnWidth.shortcut=ctrl PERIOD
+item.narrowColumnWidth=Narrow Column Width
+item.narrowColumnWidth.mnemonic=N
+item.narrowColumnWidth.shortcut=ctrl COMMA
+item.adjustColumnWidth=Adjust Column Width
+item.adjustColumnWidth.mnemonic=A
+item.adjustColumnWidth.shortcut=ctrl SLASH
+item.autoAdjustMode=Auto Adjust Mode
+item.autoAdjustMode.mnemonic=M
+item.autoAdjustMode.subgroup=yes
+item.autoAdjustMode.items=None,Header,Value,HeaderAndValue
+item.autoAdjustModeNone=None
+item.autoAdjustModeNone.mnemonic=N
+item.autoAdjustModeNone.checkbox=yes
+item.autoAdjustModeHeader=Header
+item.autoAdjustModeHeader.mnemonic=H
+item.autoAdjustModeHeader.checkbox=yes
+item.autoAdjustModeValue=Value
+item.autoAdjustModeValue.mnemonic=V
+item.autoAdjustModeValue.checkbox=yes
+item.autoAdjustModeHeaderAndValue=Header And Value
+item.autoAdjustModeHeaderAndValue.mnemonic=A
+item.autoAdjustModeHeaderAndValue.checkbox=yes
+
+group.command=Command
+group.command.mnemonic=C
+group.command.items=executeCommand,breakCommand,,lastHistory,nextHistory,sendRollback,sendCommit,,connect,disconnect,,postProcessMode,inputEcryptionKey,editConnectors
+item.executeCommand=Execute
+item.executeCommand.mnemonic=X
+item.executeCommand.shortcut=ctrl M
+item.breakCommand=BREAK
+item.breakCommand.mnemonic=B
+item.breakCommand.shortcut=ctrl PAUSE
+item.lastHistory=History Back
+item.lastHistory.mnemonic=P
+item.lastHistory.shortcut=ctrl UP
+item.nextHistory=History Next
+item.nextHistory.mnemonic=N
+item.nextHistory.shortcut=ctrl DOWN
+item.sendRollback=Rollback
+item.sendRollback.mnemonic=R
+item.sendCommit=Commit
+item.sendCommit.mnemonic=M
+item.connect=Connect
+item.connect.mnemonic=C
+item.connect.shortcut=ctrl E
+item.disconnect=Disconnect
+item.disconnect.mnemonic=D
+item.disconnect.shortcut=ctrl D
+item.postProcessMode=Post-Process(0)
+item.postProcessMode.mnemonic=0
+item.postProcessMode.subgroup=yes
+item.postProcessMode.items=None,Focus,Shake,Blink
+item.postProcessModeNone=None
+item.postProcessModeNone.mnemonic=N
+item.postProcessModeNone.checkbox=yes
+item.postProcessModeFocus=Focus
+item.postProcessModeFocus.mnemonic=F
+item.postProcessModeFocus.checkbox=yes
+item.postProcessModeShake=Shake
+item.postProcessModeShake.mnemonic=S
+item.postProcessModeShake.checkbox=yes
+item.postProcessModeBlink=Blink
+item.postProcessModeBlink.mnemonic=B
+item.postProcessModeBlink.checkbox=yes
+item.inputEcryptionKey=Encryption Key
+item.inputEcryptionKey.mnemonic=K
+item.editConnectors=Edit Connectors
+item.editConnectors.mnemonic=E
+
+group.data=Data
+group.data.mnemonic=D
+group.data.items=sortResult,importFile,exportFile
+item.sortResult=Sort
+item.sortResult.mnemonic=S
+item.sortResult.shortcut=alt S
+item.importFile=Import File
+item.importFile.mnemonic=I
+item.exportFile=Export File
+item.exportFile.mnemonic=X
+item.exportFile.shortcut=shift ctrl S
+
+group.help=Help
+group.help.mnemonic=H
+group.help.items=showHelp,,showAbout
+item.showHelp=Show Help
+item.showHelp.mnemonic=H
+item.showAbout=About
+item.showAbout.mnemonic=A
diff --git a/src/net/argius/stew/ui/window/Menu_ja.u8p b/src/net/argius/stew/ui/window/Menu_ja.u8p
new file mode 100644 (file)
index 0000000..5bcfd36
--- /dev/null
@@ -0,0 +1,52 @@
+auto-mnemonic=1
+
+group.file=ファイル
+item.newWindow=新しいウィンドウ
+item.closeWindow=閉じる
+item.quit=終了
+
+group.edit=編集
+item.find=検索
+item.toggleFocus=フォーカス切替
+item.clearMessage=メッセージのクリア
+
+group.view=表示
+item.showStatusBar=ステータス バー
+item.showColumnNumber=列番号を表示
+item.showInfoTree=情報ツリーペインを表示
+item.showAlwaysOnTop=常に手前に表示
+item.refresh=最新状態に更新
+item.widenColumnWidth=列幅を拡げる
+item.narrowColumnWidth=列幅を狭める
+item.adjustColumnWidth=列幅を自動調整
+item.autoAdjustMode=列幅自動調整モード
+item.autoAdjustModeNone=なし
+item.autoAdjustModeHeader=ヘッダ基準
+item.autoAdjustModeValue=値基準
+item.autoAdjustModeHeaderAndValue=ヘッダと値基準
+
+group.command=コマンド
+item.executeCommand=実行
+item.breakCommand=中断
+item.lastHistory=前のコマンド履歴
+item.nextHistory=次のコマンド履歴
+item.sendRollback=ロールバック
+item.sendCommit=コミット
+item.connect=接続
+item.disconnect=切断
+item.postProcessMode=終了処理
+item.postProcessModeNone=なし
+item.postProcessModeFocus=フォーカス
+item.postProcessModeShake=振動
+item.postProcessModeBlink=点滅
+item.inputEcryptionKey=暗号鍵の入力
+item.editConnectors=接続設定
+
+group.data=データ
+item.sortResult=並び替え
+item.importFile=インポート
+item.exportFile=エクスポート
+
+group.help=ヘルプ
+item.showHelp=ヘルプを表示
+item.showAbout=Stewについて
diff --git a/src/net/argius/stew/ui/window/ResultSetTable.java b/src/net/argius/stew/ui/window/ResultSetTable.java
new file mode 100644 (file)
index 0000000..3a7c901
--- /dev/null
@@ -0,0 +1,1038 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
+import static java.awt.event.KeyEvent.*;
+import static java.awt.event.MouseEvent.MOUSE_DRAGGED;
+import static java.awt.event.MouseEvent.MOUSE_PRESSED;
+import static javax.swing.KeyStroke.getKeyStroke;
+import static net.argius.stew.ui.window.AnyActionKey.*;
+import static net.argius.stew.ui.window.ResultSetTable.ActionKey.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.beans.*;
+import java.io.*;
+import java.sql.*;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+import javax.swing.text.*;
+
+import net.argius.stew.*;
+import net.argius.stew.io.*;
+import net.argius.stew.text.*;
+
+/**
+ * Table for Result Set.
+ */
+final class ResultSetTable extends JTable implements AnyActionListener, TextSearch {
+
+    enum ActionKey {
+        copyWithEscape,
+        clearSelectedCellValue,
+        setCurrentTimeValue,
+        copyColumnName,
+        findColumnName,
+        addEmptyRow,
+        insertFromClipboard,
+        duplicateRows,
+        linkRowsToDatabase,
+        deleteRows,
+        sort,
+        jumpToColumn,
+        doNothing,
+    }
+
+    static final String TAB = "\t";
+
+    private static final Logger log = Logger.getLogger(ResultSetTable.class);
+    private static final TableCellRenderer nullRenderer = new NullValueRenderer();
+
+    private final AnyActionListener anyActionListener;
+    private final ColumnHeaderCellRenderer columnHeaderRenderer;
+    private final RowHeader rowHeader;
+    private final Point mousePositionForColumnHeader = new Point();
+
+    private int lastSortedIndex;
+    private boolean lastSortedIsReverse;
+    private String autoAdjustMode;
+
+    // It is used by the process that has no key-event.
+    private volatile KeyEvent lastKeyEvent;
+
+    /**
+     * Constructor.
+     */
+    ResultSetTable(AnyActionListener anyActionListener) {
+        this.anyActionListener = anyActionListener;
+        JTableHeader columnHeader = getTableHeader();
+        TableCellRenderer columnHeaderDefaultRenderer = columnHeader.getDefaultRenderer();
+        final RowHeader rowHeader = new RowHeader(this);
+        this.columnHeaderRenderer = new ColumnHeaderCellRenderer(columnHeaderDefaultRenderer);
+        this.rowHeader = rowHeader;
+        setColumnSelectionAllowed(true);
+        setAutoResizeMode(AUTO_RESIZE_OFF);
+        columnHeader.setDefaultRenderer(columnHeaderRenderer);
+        columnHeader.setReorderingAllowed(false);
+        // [Events]
+        // column header
+        MouseInputListener colHeaderMouseListener = new ColumnHeaderMouseInputListener();
+        columnHeader.addMouseListener(colHeaderMouseListener);
+        columnHeader.addMouseMotionListener(colHeaderMouseListener);
+        // row header
+        MouseInputListener rowHeaderMouseListener = new RowHeaderMouseInputListener(rowHeader);
+        rowHeader.addMouseListener(rowHeaderMouseListener);
+        rowHeader.addMouseMotionListener(rowHeaderMouseListener);
+        // cursor
+        for (int i = 0; i < 2; i++) {
+            final boolean withSelect = (i == 1);
+            bindJumpAction("home", VK_HOME, withSelect);
+            bindJumpAction("end", VK_END, withSelect);
+            bindJumpAction("top", VK_UP, withSelect);
+            bindJumpAction("bottom", VK_DOWN, withSelect);
+            bindJumpAction("leftmost", VK_LEFT, withSelect);
+            bindJumpAction("rightmost", VK_RIGHT, withSelect);
+        }
+        // key binds
+        final int shortcutKey = Utilities.getMenuShortcutKeyMask();
+        AnyAction aa = new AnyAction(this);
+        aa.bindSelf(paste, getKeyStroke(VK_V, shortcutKey));
+        aa.bindKeyStroke(true, adjustColumnWidth, getKeyStroke(VK_SLASH, shortcutKey));
+        aa.bindKeyStroke(false, doNothing, getKeyStroke(VK_ESCAPE, 0));
+    }
+
+    private final class RowHeaderMouseInputListener extends MouseInputAdapter {
+    
+        @SuppressWarnings("hiding")
+        private final RowHeader rowHeader;
+        private int dragStartRow;
+    
+        RowHeaderMouseInputListener(RowHeader rowHeader) {
+            this.rowHeader = rowHeader;
+        }
+    
+        @Override
+        public void mousePressed(MouseEvent e) {
+            changeSelection(e);
+        }
+    
+        @Override
+        public void mouseDragged(MouseEvent e) {
+            changeSelection(e);
+        }
+    
+        private void changeSelection(MouseEvent e) {
+            Point p = new Point(e.getX(), e.getY());
+            if (SwingUtilities.isLeftMouseButton(e)) {
+                int id = e.getID();
+                boolean isMousePressed = (id == MOUSE_PRESSED);
+                boolean isMouseDragged = (id == MOUSE_DRAGGED);
+                if (isMousePressed || isMouseDragged) {
+                    if (p.y >= rowHeader.getBounds().height) {
+                        return;
+                    }
+                    if (!e.isControlDown() && !e.isShiftDown()) {
+                        clearSelection();
+                    }
+                    int rowIndex = rowAtPoint(p);
+                    if (rowIndex < 0 || getRowCount() < rowIndex) {
+                        return;
+                    }
+                    final int index0;
+                    final int index1;
+                    if (isMousePressed) {
+                        if (e.isShiftDown()) {
+                            index0 = dragStartRow;
+                            index1 = rowIndex;
+                        } else {
+                            dragStartRow = rowIndex;
+                            index0 = rowIndex;
+                            index1 = rowIndex;
+                        }
+                    } else if (isMouseDragged) {
+                        index0 = dragStartRow;
+                        index1 = rowIndex;
+                    } else {
+                        return;
+                    }
+                    addRowSelectionInterval(index0, index1);
+                    addColumnSelectionInterval(getColumnCount() - 1, 0);
+                    requestFocus();
+                    // justify a position between table and its row header
+                    JViewport tableView = (JViewport)getParent();
+                    Point viewPosition = tableView.getViewPosition();
+                    viewPosition.y = ((JViewport)rowHeader.getParent()).getViewPosition().y;
+                    tableView.setViewPosition(viewPosition);
+                }
+            }
+        }
+    }
+
+    private final class ColumnHeaderMouseInputListener extends MouseInputAdapter {
+    
+        private int dragStartColumn;
+    
+        ColumnHeaderMouseInputListener() {
+        } // empty
+
+        @SuppressWarnings("synthetic-access")
+        @Override
+        public void mousePressed(MouseEvent e) {
+            if (SwingUtilities.isLeftMouseButton(e)) {
+                changeSelection(e);
+            }
+            mousePositionForColumnHeader.setLocation(e.getPoint());
+        }
+    
+        @Override
+        public void mouseDragged(MouseEvent e) {
+            if (SwingUtilities.isLeftMouseButton(e)) {
+                changeSelection(e);
+            }
+        }
+    
+        private void changeSelection(MouseEvent e) {
+            final Point p = e.getPoint();
+            int id = e.getID();
+            boolean isMousePressed = (id == MOUSE_PRESSED);
+            boolean isMouseDragged = (id == MOUSE_DRAGGED);
+            if (isMousePressed || isMouseDragged) {
+                if (!e.isControlDown() && !e.isShiftDown()) {
+                    clearSelection();
+                }
+                int columnIndex = columnAtPoint(p);
+                if (columnIndex < 0 || getColumnCount() <= columnIndex) {
+                    return;
+                }
+                final int index0;
+                final int index1;
+                if (isMousePressed) {
+                    if (e.isShiftDown()) {
+                        index0 = dragStartColumn;
+                        index1 = columnIndex;
+                    } else {
+                        dragStartColumn = columnIndex;
+                        index0 = columnIndex;
+                        index1 = columnIndex;
+                    }
+                } else if (isMouseDragged) {
+                    index0 = dragStartColumn;
+                    index1 = columnIndex;
+                } else {
+                    return;
+                }
+                selectColumn(index0, index1);
+                requestFocus();
+            }
+        }
+    }
+
+    @Override
+    protected void processKeyEvent(KeyEvent e) {
+        super.processKeyEvent(e);
+        lastKeyEvent = e;
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        try {
+            processAnyActionEvent(ev);
+        } catch (Exception ex) {
+            WindowOutputProcessor.showErrorDialog(this, ex);
+        }
+    }
+
+    public void processAnyActionEvent(AnyActionEvent ev) throws Exception {
+        if (ev.isAnyOf(copy, selectAll)) {
+            final String cmd = ev.getActionCommand();
+            getActionMap().get(cmd).actionPerformed(new ActionEvent(this, 0, cmd));
+        } else if (ev.isAnyOf(copyWithEscape)) {
+            List<String> rows = new ArrayList<String>();
+            for (int rowIndex : getSelectedRows()) {
+                List<Object> row = new ArrayList<Object>();
+                for (int columnIndex : getSelectedColumns()) {
+                    final Object o = getValueAt(rowIndex, columnIndex);
+                    row.add(CsvFormatter.AUTO.format(o == null ? "" : String.valueOf(o)));
+                }
+                rows.add(TextUtilities.join(TAB, row));
+            }
+            ClipboardHelper.setStrings(rows);
+        } else if (ev.isAnyOf(paste)) {
+            try {
+                InputStream is = new ByteArrayInputStream(ClipboardHelper.getString().getBytes());
+                Importer importer = new SmartImporter(is, TAB);
+                try {
+                    int[] selectedColumns = getSelectedColumns();
+                    for (int rowIndex : getSelectedRows()) {
+                        Object[] values = importer.nextRow();
+                        final int limit = Math.min(selectedColumns.length, values.length);
+                        for (int x = 0; x < limit; x++) {
+                            setValueAt(values[x], rowIndex, selectedColumns[x]);
+                        }
+                    }
+                } finally {
+                    importer.close();
+                }
+                repaint();
+            } finally {
+                editingCanceled(new ChangeEvent(ev.getSource()));
+            }
+        } else if (ev.isAnyOf(clearSelectedCellValue)) {
+            try {
+                setValueAtSelectedCells(null);
+                repaint();
+            } finally {
+                editingCanceled(new ChangeEvent(ev.getSource()));
+            }
+        } else if (ev.isAnyOf(setCurrentTimeValue)) {
+            try {
+                setValueAtSelectedCells(new Timestamp(System.currentTimeMillis()));
+                repaint();
+            } finally {
+                editingCanceled(new ChangeEvent(ev.getSource()));
+            }
+        } else if (ev.isAnyOf(copyColumnName)) {
+            List<String> a = new ArrayList<String>();
+            ResultSetTableModel m = getResultSetTableModel();
+            if (ev.getModifiers() == 0) {
+                for (int i = 0, n = m.getColumnCount(); i < n; i++) {
+                    a.add(m.getColumnName(i));
+                }
+            } else {
+                for (final int i : getSelectedColumns()) {
+                    a.add(m.getColumnName(i));
+                }   
+            }
+            ClipboardHelper.setString(TextUtilities.join(TAB, a));
+        } else if (ev.isAnyOf(findColumnName)) {
+            anyActionListener.anyActionPerformed(ev);
+        } else if (ev.isAnyOf(addEmptyRow)) {
+            ResultSetTableModel m = getResultSetTableModel();
+            int[] selectedRows = getSelectedRows();
+            if (selectedRows.length > 0) {
+                final int nextRow = selectedRows[selectedRows.length - 1] + 1;
+                for (int i = 0; i < selectedRows.length; i++) {
+                    m.insertUnlinkedRow(nextRow, new Object[m.getColumnCount()]);
+                }
+            } else {
+                m.addUnlinkedRow(new Object[m.getColumnCount()]);
+            }
+        } else if (ev.isAnyOf(insertFromClipboard)) {
+            try {
+                Importer importer = new SmartImporter(ClipboardHelper.getReaderForText(), TAB);
+                try {
+                    ResultSetTableModel m = getResultSetTableModel();
+                    while (true) {
+                        Object[] row = importer.nextRow();
+                        if (row.length == 0) {
+                            break;
+                        }
+                        m.addUnlinkedRow(row);
+                        m.linkRow(m.getRowCount() - 1);
+                    }
+                    repaintRowHeader("model");
+                } finally {
+                    importer.close();
+                }
+            } finally {
+                editingCanceled(new ChangeEvent(ev.getSource()));
+            }
+        } else if (ev.isAnyOf(duplicateRows)) {
+            ResultSetTableModel m = getResultSetTableModel();
+            List<?> rows = m.getDataVector();
+            int[] selectedRows = getSelectedRows();
+            int index = selectedRows[selectedRows.length - 1];
+            for (int rowIndex : selectedRows) {
+                m.insertUnlinkedRow(++index, (Vector<?>)((Vector<?>)rows.get(rowIndex)).clone());
+            }
+            repaint();
+            repaintRowHeader("model");
+        } else if (ev.isAnyOf(linkRowsToDatabase)) {
+            ResultSetTableModel m = getResultSetTableModel();
+            try {
+                for (int rowIndex : getSelectedRows()) {
+                    m.linkRow(rowIndex);
+                }
+            } finally {
+                repaintRowHeader("unlinkedRowStatus");
+            }
+        } else if (ev.isAnyOf(deleteRows)) {
+            try {
+                ResultSetTableModel m = getResultSetTableModel();
+                while (true) {
+                    final int selectedRow = getSelectedRow();
+                    if (selectedRow < 0) {
+                        break;
+                    }
+                    if (m.isLinkedRow(selectedRow)) {
+                        final boolean removed = m.removeLinkedRow(selectedRow);
+                        assert removed;
+                    } else {
+                        m.removeRow(selectedRow);
+                    }
+                }
+            } finally {
+                repaintRowHeader("model");
+            }
+        } else if (ev.isAnyOf(adjustColumnWidth)) {
+            adjustColumnWidth();
+        } else if (ev.isAnyOf(widenColumnWidth)) {
+            changeTableColumnWidth(1.5f);
+        } else if (ev.isAnyOf(narrowColumnWidth)) {
+            changeTableColumnWidth(1 / 1.5f);
+        } else if (ev.isAnyOf(sort)) {
+            doSort(getTableHeader().columnAtPoint(mousePositionForColumnHeader));
+        } else if (ev.isAnyOf(jumpToColumn)) {
+            Object[] args = ev.getArgs();
+            if (args != null && args.length > 0) {
+                jumpToColumn(String.valueOf(args[0]));
+            }
+        } else if (ev.isAnyOf(showColumnNumber)) {
+            setShowColumnNumber(!columnHeaderRenderer.fixesColumnNumber);
+            updateUI();
+        } else {
+            log.warn("not expected: Event=%s", ev);
+        }
+    }
+
+    @Override
+    public void editingStopped(ChangeEvent e) {
+        try {
+            super.editingStopped(e);
+        } catch (Exception ex) {
+            WindowOutputProcessor.showErrorDialog(getParent(), ex);
+        }
+    }
+
+    @Override
+    public boolean editCellAt(int row, int column, EventObject e) {
+        boolean succeeded = super.editCellAt(row, column, e);
+        if (succeeded) {
+            if (editorComp instanceof JTextField) {
+                // make it selected when starting edit-mode
+                if (lastKeyEvent != null && lastKeyEvent.getKeyCode() != VK_F2) {
+                    JTextField editor = (JTextField)editorComp;
+                    initializeEditorComponent(editor);
+                    editor.requestFocus();
+                    editor.selectAll();
+                }
+            }
+        }
+        return succeeded;
+    }
+
+    @Override
+    public TableCellEditor getCellEditor() {
+        TableCellEditor editor = super.getCellEditor();
+        if (editor instanceof DefaultCellEditor) {
+            DefaultCellEditor d = (DefaultCellEditor)editor;
+            initializeEditorComponent(d.getComponent());
+        }
+        return editor;
+    }
+
+    private void initializeEditorComponent(Component c) {
+        final Color bgColor = Color.ORANGE;
+        if (c != null && c.getBackground() != bgColor) {
+            // determines initialized state by bgcolor
+            if (!c.isEnabled()) {
+                c.setEnabled(true);
+            }
+            c.setFont(getFont());
+            c.setBackground(bgColor);
+            if (c instanceof JTextComponent) {
+                final JTextComponent text = (JTextComponent)c;
+                AnyAction aa = new AnyAction(text);
+                aa.setUndoAction();
+                c.addFocusListener(new FocusAdapter() {
+                    @Override
+                    public void focusLost(FocusEvent e) {
+                        editingCanceled(new ChangeEvent(e.getSource()));
+                    }
+                });
+            }
+        }
+    }
+
+    @Override
+    public TableCellRenderer getCellRenderer(int row, int column) {
+        final Object v = getValueAt(row, column);
+        if (v == null) {
+            return nullRenderer;
+        }
+        return super.getCellRenderer(row, column);
+    }
+
+    @Override
+    public void updateUI() {
+        super.updateUI();
+        adjustRowHeight(this);
+    }
+
+    static void adjustRowHeight(JTable table) {
+        Component c = new JLabel("0");
+        final int height = c.getPreferredSize().height;
+        if (height > 0) {
+            table.setRowHeight(height);
+        }
+    }
+
+    @Override
+    protected void configureEnclosingScrollPane() {
+        super.configureEnclosingScrollPane();
+        Container p = getParent();
+        if (p instanceof JViewport) {
+            Container gp = p.getParent();
+            if (gp instanceof JScrollPane) {
+                JScrollPane scrollPane = (JScrollPane)gp;
+                JViewport viewport = scrollPane.getViewport();
+                if (viewport == null || viewport.getView() != this) {
+                    return;
+                }
+                scrollPane.setRowHeaderView(rowHeader);
+            }
+        }
+    }
+
+    @Override
+    public void setModel(TableModel dataModel) {
+        dataModel.addTableModelListener(rowHeader);
+        super.setModel(dataModel);
+    }
+
+    /**
+     * Selects colomns.
+     * @param index0 index of start
+     * @param index1 index of end
+     */
+    void selectColumn(int index0, int index1) {
+        if (getRowCount() > 0) {
+            addColumnSelectionInterval(index0, index1);
+            addRowSelectionInterval(getRowCount() - 1, 0);
+        }
+    }
+
+    /**
+     * Jumps to specified column.
+     * @param index
+     * @return whether the column exists or not
+     */
+    boolean jumpToColumn(int index) {
+        final int columnCount = getColumnCount();
+        if (0 <= index && index < columnCount) {
+            if (getSelectedRowCount() == 0) {
+                changeSelection(-1, index, false, false);
+            } else {
+                int[] selectedRows = getSelectedRows();
+                changeSelection(getSelectedRow(), index, false, false);
+                for (final int selectedRow : selectedRows) {
+                    changeSelection(selectedRow, index, false, true);
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Jumps to specified column.
+     * @param name
+     * @return whether the column exists or not
+     */
+    boolean jumpToColumn(String name) {
+        TableColumnModel columnModel = getColumnModel();
+        for (int i = 0, n = columnModel.getColumnCount(); i < n; i++) {
+            if (name.equals(String.valueOf(columnModel.getColumn(i).getHeaderValue()))) {
+                jumpToColumn(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    RowHeader getRowHeader() {
+        return rowHeader;
+    }
+
+    ResultSetTableModel getResultSetTableModel() {
+        return (ResultSetTableModel)getModel();
+    }
+
+    boolean isShowColumnNumber() {
+        return columnHeaderRenderer.fixesColumnNumber;
+    }
+
+    void setShowColumnNumber(boolean showColumnNumber) {
+        final boolean oldValue = this.columnHeaderRenderer.fixesColumnNumber;
+        this.columnHeaderRenderer.fixesColumnNumber = showColumnNumber;
+        firePropertyChange("showNumber", oldValue, showColumnNumber);
+    }
+
+    void repaintRowHeader(String propName) {
+        if (rowHeader != null) {
+            rowHeader.propertyChange(new PropertyChangeEvent(this, propName, null, null));
+        }
+    }
+
+    String getAutoAdjustMode() {
+        return autoAdjustMode;
+    }
+
+    void setAutoAdjustMode(String autoAdjustMode) {
+        final String oldValue = this.autoAdjustMode;
+        this.autoAdjustMode = autoAdjustMode;
+        firePropertyChange("autoAdjustMode", oldValue, autoAdjustMode);
+    }
+
+    private void bindJumpAction(String suffix, int key, boolean withSelect) {
+        final String actionKey = String.format("%s-to-%s", (withSelect) ? "select" : "jump", suffix);
+        final CellCursor c = new CellCursor(this, withSelect);
+        final int modifiers = Utilities.getMenuShortcutKeyMask()
+                              | (withSelect ? SHIFT_DOWN_MASK : 0);
+        KeyStroke[] keyStrokes = {getKeyStroke(key, modifiers)};
+        c.putValue(Action.ACTION_COMMAND_KEY, actionKey);
+        InputMap im = this.getInputMap();
+        for (KeyStroke ks : keyStrokes) {
+            im.put(ks, actionKey);
+        }
+        this.getActionMap().put(actionKey, c);
+    }
+
+    private static final class CellCursor extends AbstractAction {
+
+        private final JTable table;
+        private final boolean extend;
+
+        CellCursor(JTable table, boolean extend) {
+            this.table = table;
+            this.extend = extend;
+        }
+
+        int getColumnPosition() {
+            int[] a = table.getSelectedColumns();
+            if (a == null || a.length == 0) {
+                return -1;
+            }
+            ListSelectionModel csm = table.getColumnModel().getSelectionModel();
+            return (a[0] == csm.getAnchorSelectionIndex()) ? a[a.length - 1] : a[0];
+        }
+
+        int getRowPosition() {
+            int[] a = table.getSelectedRows();
+            if (a == null || a.length == 0) {
+                return -1;
+            }
+            ListSelectionModel rsm = table.getSelectionModel();
+            return (a[0] == rsm.getAnchorSelectionIndex()) ? a[a.length - 1] : a[0];
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            final String cmd = e.getActionCommand();
+            final int ri;
+            final int ci;
+            if (cmd == null) {
+                assert false : "command is null";
+                return;
+            } else if (cmd.endsWith("-to-home")) {
+                ri = 0;
+                ci = 0;
+            } else if (cmd.endsWith("-to-end")) {
+                ri = table.getRowCount() - 1;
+                ci = table.getColumnCount() - 1;
+            } else if (cmd.endsWith("-to-top")) {
+                ri = 0;
+                ci = getColumnPosition();
+            } else if (cmd.endsWith("-to-bottom")) {
+                ri = table.getRowCount() - 1;
+                ci = getColumnPosition();
+            } else if (cmd.endsWith("-to-leftmost")) {
+                ri = getRowPosition();
+                ci = 0;
+            } else if (cmd.endsWith("-to-rightmost")) {
+                ri = getRowPosition();
+                ci = table.getColumnCount() - 1;
+            } else {
+                assert false : "unknown command: " + cmd;
+                return;
+            }
+            table.changeSelection(ri, ci, false, extend);
+        }
+        
+    }
+
+    private static final class NullValueRenderer extends DefaultTableCellRenderer {
+
+        NullValueRenderer() {
+        } // empty
+
+        @Override
+        public Component getTableCellRendererComponent(JTable table,
+                                                       Object value,
+                                                       boolean isSelected,
+                                                       boolean hasFocus,
+                                                       int row,
+                                                       int column) {
+            Component c = super.getTableCellRendererComponent(table,
+                                                              "NULL",
+                                                              isSelected,
+                                                              hasFocus,
+                                                              row,
+                                                              column);
+            c.setForeground(new Color(63, 63, 192, 192));
+            Font font = c.getFont();
+            c.setFont(font.deriveFont(font.getSize() * 0.8f));
+            return c;
+        }
+
+    }
+
+    private static final class ColumnHeaderCellRenderer implements TableCellRenderer {
+
+        TableCellRenderer renderer;
+        boolean fixesColumnNumber;
+
+        ColumnHeaderCellRenderer(TableCellRenderer renderer) {
+            this.renderer = renderer;
+            this.fixesColumnNumber = false;
+        }
+
+        @Override
+        public Component getTableCellRendererComponent(JTable table,
+                                                       Object value,
+                                                       boolean isSelected,
+                                                       boolean hasFocus,
+                                                       int row,
+                                                       int column) {
+            Object o = fixesColumnNumber ? String.format("%d %s", column + 1, value) : value;
+            return renderer.getTableCellRendererComponent(table,
+                                                          o,
+                                                          isSelected,
+                                                          hasFocus,
+                                                          row,
+                                                          column);
+        }
+
+    }
+
+    private static final class RowHeader extends JTable implements PropertyChangeListener {
+
+        private static final int DEFAULT_WIDTH = 40;
+
+        private DefaultTableModel model;
+        private TableCellRenderer renderer;
+
+        RowHeader(JTable table) {
+            this.model = new DefaultTableModel(0, 1) {
+                @Override
+                public boolean isCellEditable(int row, int column) {
+                    return false;
+                }
+            };
+            final ResultSetTable t = (ResultSetTable)table;
+            this.renderer = new DefaultTableCellRenderer() {
+                @Override
+                public Component getTableCellRendererComponent(JTable table,
+                                                               Object value,
+                                                               boolean isSelected,
+                                                               boolean hasFocus,
+                                                               int row,
+                                                               int column) {
+                    assert t.getModel() instanceof ResultSetTableModel;
+                    final boolean rowLinked = t.getResultSetTableModel().isLinkedRow(row);
+                    JLabel label = new JLabel(String.format("%s ", rowLinked ? row + 1 : "+"));
+                    label.setHorizontalAlignment(RIGHT);
+                    label.setFont(table.getFont());
+                    label.setOpaque(true);
+                    return label;
+                }
+            };
+            setModel(model);
+            setWidth(table);
+            setFocusable(false);
+            table.addPropertyChangeListener(this);
+        }
+
+        @Override
+        public void propertyChange(PropertyChangeEvent e) {
+            JTable table = (JTable)e.getSource();
+            if (table == null) {
+                return;
+            }
+            String propertyName = e.getPropertyName();
+            if (propertyName.equals("enabled")) {
+                boolean isEnabled = table.isEnabled();
+                setVisible(isEnabled);
+                if (isEnabled) {
+                    setWidth(table);
+                    resetViewPosition(table);
+                }
+            } else if (propertyName.equals("font")) {
+                setFont(table.getFont());
+            } else if (propertyName.equals("rowHeight")) {
+                setRowHeight(table.getRowHeight());
+            } else if (propertyName.equals("model")) {
+                model.setRowCount(table.getRowCount());
+            } else if (propertyName.equals("unlinkedRowStatus")) {
+                repaint();
+            } else if (propertyName.equals("ancestor")) {
+                // empty
+            } else {
+                // empty
+            }
+            validate();
+        }
+
+        @Override
+        public void tableChanged(TableModelEvent e) {
+            Object src = e.getSource();
+            if (model != null && src != null) {
+                model.setRowCount(((TableModel)src).getRowCount());
+            }
+            super.tableChanged(e);
+        }
+
+        @Override
+        public void updateUI() {
+            super.updateUI();
+            adjustRowHeight(this);
+        }
+
+        void setWidth(JTable table) {
+            // XXX unstable
+            final int rowCount = table.getRowCount();
+            model.setRowCount(rowCount);
+            JLabel label = new JLabel(String.valueOf(rowCount * 1000L));
+            Dimension d = getSize();
+            d.width = Math.max(label.getPreferredSize().width, DEFAULT_WIDTH);
+            setPreferredScrollableViewportSize(d);
+        }
+
+        private void resetViewPosition(JTable table) {
+            // forces to reset its view position to table's view position
+            Container p1 = table.getParent();
+            Container p2 = getParent();
+            if (p1 instanceof JViewport && p2 instanceof JViewport) {
+                JViewport v1 = (JViewport)p1;
+                JViewport v2 = (JViewport)p2;
+                v2.setViewPosition(v1.getViewPosition());
+            }
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return false;
+        }
+
+        @Override
+        public TableCellRenderer getCellRenderer(int row, int column) {
+            return renderer;
+        }
+
+    }
+
+    // text search
+
+    @Override
+    public boolean search(Matcher matcher) {
+        final int rowCount = getRowCount();
+        if (rowCount <= 0) {
+            return false;
+        }
+        final int columnCount = getColumnCount();
+        final boolean backward = matcher.isBackward();
+        final int amount = backward ? -1 : 1;
+        final int rowStart = backward ? rowCount - 1 : 0;
+        final int rowEnd = backward ? 0 : rowCount - 1;
+        final int columnStart = backward ? columnCount - 1 : 0;
+        final int columnEnd = backward ? 0 : columnCount - 1;
+        int row = rowStart;
+        int column = columnStart;
+        if (getSelectedColumnCount() > 0) {
+            column = getSelectedColumn();
+            row = getSelectedRow() + amount;
+            if (backward) {
+                if (row < 0) {
+                    --column;
+                    if (column < 0) {
+                        return false;
+                    }
+                    row = rowStart;
+                }
+            } else {
+                if (row >= rowCount) {
+                    ++column;
+                    if (column >= columnCount) {
+                        return false;
+                    }
+                    row = rowStart;
+                }
+            }
+        }
+        final TableModel m = getModel();
+        for (; backward ? column >= columnEnd : column <= columnEnd; column += amount) {
+            for (; backward ? row >= rowEnd : row <= rowEnd; row += amount) {
+                if (matcher.find(String.valueOf(m.getValueAt(row, column)))) {
+                    changeSelection(row, column, false, false);
+                    return true;
+                }
+            }
+            row = rowStart;
+        }
+        return false;
+    }
+
+    @Override
+    public void reset() {
+        // empty
+    }
+
+    static final class TableHeaderTextSearch implements TextSearch {
+        private ResultSetTable rstable;
+        private JTableHeader tableHeader;
+        TableHeaderTextSearch(ResultSetTable rstable, JTableHeader tableHeader) {
+            this.rstable = rstable;
+            this.tableHeader = tableHeader;
+        }
+        @Override
+        public boolean search(Matcher matcher) {
+            TableColumnModel m = tableHeader.getColumnModel();
+            int columnCount = m.getColumnCount();
+            if (columnCount < 1) {
+                return false;
+            }
+            final boolean backward = matcher.isBackward();
+            final int amount = backward ? -1 : 1;
+            final int columnStart = backward ? columnCount - 1 : 0;
+            final int columnEnd = backward ? 0 : columnCount - 1;
+            int column = columnStart;
+            if (rstable.getSelectedColumnCount() > 0) {
+                column = rstable.getSelectedColumn() + amount;
+            }
+            for (; backward ? column >= columnEnd : column <= columnEnd; column += amount) {
+                if (matcher.find(String.valueOf(m.getColumn(column).getHeaderValue()))) {
+                    rstable.jumpToColumn(column);
+                    return true;
+                }
+            }
+            return false;
+        }
+        @Override
+        public void reset() {
+            // empty
+        }
+    }
+
+    // event-handlers
+
+    private void setValueAtSelectedCells(Object value) {
+        int[] selectedColumns = getSelectedColumns();
+        for (int rowIndex : getSelectedRows()) {
+            for (int colIndex : selectedColumns) {
+                setValueAt(value, rowIndex, colIndex);
+            }
+        }
+    }
+
+    private void adjustColumnWidth() {
+        final int rowCount = getRowCount();
+        final boolean byHeader;
+        final boolean byValue;
+        switch (AnyActionKey.of(autoAdjustMode)) {
+            case autoAdjustModeNone:
+                byHeader = false;
+                byValue = false;
+                break;
+            case autoAdjustModeHeader:
+                byHeader = true;
+                byValue = false;
+                break;
+            case autoAdjustModeValue:
+                byHeader = false;
+                byValue = true;
+                break;
+            case autoAdjustModeHeaderAndValue:
+                byHeader = true;
+                byValue = true;
+                break;
+            default:
+                log.warn("autoAdjustMode=%s", autoAdjustMode);
+                return;
+        }
+        if (!byHeader && rowCount == 0) {
+            return;
+        }
+        final float max = getParent().getWidth() * 0.8f;
+        TableColumnModel columnModel = getColumnModel();
+        JTableHeader header = getTableHeader();
+        for (int columnIndex = 0, n = getColumnCount(); columnIndex < n; columnIndex++) {
+            float size = 0f;
+            if (byHeader) {
+                TableColumn column = columnModel.getColumn(columnIndex);
+                TableCellRenderer renderer = column.getHeaderRenderer();
+                if (renderer == null) {
+                    renderer = header.getDefaultRenderer();
+                }
+                if (renderer != null) {
+                    Component c = renderer.getTableCellRendererComponent(this,
+                                                                         column.getHeaderValue(),
+                                                                         false,
+                                                                         false,
+                                                                         0,
+                                                                         columnIndex);
+                    size = c.getPreferredSize().width * 1.5f;
+                }
+            }
+            if (byValue) {
+                for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+                    TableCellRenderer renderer = getCellRenderer(rowIndex,
+                                                                                columnIndex);
+                    if (renderer == null) {
+                        continue;
+                    }
+                    Object value = getValueAt(rowIndex, columnIndex);
+                    Component c = renderer.getTableCellRendererComponent(this,
+                                                                         value,
+                                                                         false,
+                                                                         false,
+                                                                         rowIndex,
+                                                                         columnIndex);
+                    size = Math.max(size, c.getPreferredSize().width);
+                    if (size >= max) {
+                        break;
+                    }
+                }
+            }
+            int width = Math.round(size > max ? max : size) + 1;
+            columnModel.getColumn(columnIndex).setPreferredWidth(width);
+        }
+    }
+
+    void doSort(int columnIndex) {
+        if (getColumnCount() == 0) {
+            return;
+        }
+        final boolean reverse;
+        if (lastSortedIndex == columnIndex) {
+            reverse = (lastSortedIsReverse == false);
+        } else {
+            lastSortedIndex = columnIndex;
+            reverse = false;
+        }
+        lastSortedIsReverse = reverse;
+        getResultSetTableModel().sort(columnIndex, reverse);
+        repaint();
+        repaintRowHeader("unlinkedRowStatus");
+    }
+
+    void changeTableColumnWidth(double rate) {
+        for (TableColumn column : Collections.list(getColumnModel().getColumns())) {
+            column.setPreferredWidth((int)(column.getWidth() * rate));
+        }
+    }
+
+
+}
diff --git a/src/net/argius/stew/ui/window/ResultSetTableModel.java b/src/net/argius/stew/ui/window/ResultSetTableModel.java
new file mode 100644 (file)
index 0000000..7828343
--- /dev/null
@@ -0,0 +1,633 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.EventQueue.invokeLater;
+import static java.sql.Types.*;
+import static java.util.Collections.nCopies;
+import static net.argius.stew.text.TextUtilities.join;
+
+import java.math.*;
+import java.sql.*;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.concurrent.*;
+import java.util.regex.*;
+
+import javax.swing.table.*;
+
+import net.argius.stew.*;
+
+/**
+ * The TableModel for ResultSetTable.
+ * It mainly provides to synchronize with databases.
+ */
+final class ResultSetTableModel extends DefaultTableModel {
+
+    private static final Logger log = Logger.getLogger(ResultSetTableModel.class);
+
+    private static final long serialVersionUID = -8861356207097438822L;
+    private static final String PTN1 = "\\s*SELECT\\s.+?\\sFROM\\s+([^\\s]+).*";
+
+    private final int[] types;
+    private final String commandString;
+
+    private Connection conn;
+    private Object tableName;
+    private String[] primaryKeys;
+    private boolean updatable;
+    private boolean linkable;
+
+    ResultSetTableModel(ResultSetReference ref) throws SQLException {
+        super(0, getColumnCount(ref));
+        ResultSet rs = ref.getResultSet();
+        ColumnOrder order = ref.getOrder();
+        final String cmd = ref.getCommandString();
+        final boolean orderIsEmpty = order.size() == 0;
+        ResultSetMetaData meta = rs.getMetaData();
+        final int columnCount = getColumnCount();
+        int[] types = new int[columnCount];
+        for (int i = 0; i < columnCount; i++) {
+            final int type;
+            final String name;
+            if (orderIsEmpty) {
+                type = meta.getColumnType(i + 1);
+                name = meta.getColumnName(i + 1);
+            } else {
+                type = meta.getColumnType(order.getOrder(i));
+                name = order.getName(i);
+            }
+            types[i] = type;
+            @SuppressWarnings({"unchecked", "unused"})
+            Object o = columnIdentifiers.set(i, name);
+        }
+        this.types = types;
+        this.commandString = cmd;
+        try {
+            analyzeForLinking(rs, cmd);
+        } catch (Exception ex) {
+            log.warn(ex);
+        }
+    }
+
+    private static final class UnlinkedRow extends Vector<Object> {
+
+        UnlinkedRow(Vector<?> rowData) {
+            super(rowData);
+        }
+
+        UnlinkedRow(Object[] rowData) {
+            super(rowData.length);
+            for (final Object o : rowData) {
+                add(o);
+            }
+        }
+
+    }
+
+    private static int getColumnCount(ResultSetReference ref) throws SQLException {
+        final int size = ref.getOrder().size();
+        return (size == 0) ? ref.getResultSet().getMetaData().getColumnCount() : size;
+    }
+
+    @Override
+    public Class<?> getColumnClass(int columnIndex) {
+        switch (types[columnIndex]) {
+            case CHAR:
+            case VARCHAR:
+            case LONGVARCHAR:
+                return String.class;
+            case BOOLEAN:
+            case BIT:
+                return Boolean.class;
+            case TINYINT:
+                return Byte.class;
+            case SMALLINT:
+                return Short.class;
+            case INTEGER:
+                return Integer.class;
+            case BIGINT:
+                return Long.class;
+            case REAL:
+                return Float.class;
+            case DOUBLE:
+            case FLOAT:
+                return Double.class;
+            case DECIMAL:
+            case NUMERIC:
+                return BigDecimal.class;
+            default:
+                return Object.class;
+        }
+    }
+
+    @Override
+    public boolean isCellEditable(int row, int column) {
+        if (primaryKeys == null || primaryKeys.length == 0) {
+            return false;
+        }
+        return super.isCellEditable(row, column);
+    }
+
+    @Override
+    public void setValueAt(Object newValue, int row, int column) {
+        if (!linkable) {
+            return;
+        }
+        final Object oldValue = getValueAt(row, column);
+        final boolean changed;
+        if (newValue == null) {
+            changed = (newValue != oldValue);
+        } else {
+            changed = !newValue.equals(oldValue);
+        }
+        if (changed) {
+            if (isLinkedRow(row)) {
+                Object[] keys = columnIdentifiers.toArray();
+                try {
+                    executeUpdate(getRowData(keys, row), keys[column], newValue);
+                } catch (Exception ex) {
+                    log.error(ex);
+                    throw new RuntimeException(ex);
+                }
+            } else {
+                if (log.isTraceEnabled()) {
+                    log.debug("update unlinked row");
+                }
+            }
+        } else {
+            if (log.isDebugEnabled()) {
+                log.debug("skip to update");
+            }
+        }
+        super.setValueAt(newValue, row, column);
+    }
+
+    void addUnlinkedRow(Object[] rowData) {
+        addUnlinkedRow(convertToVector(rowData));
+    }
+
+    void addUnlinkedRow(Vector<?> rowData) {
+        addRow(new UnlinkedRow(rowData));
+    }
+
+    void insertUnlinkedRow(int row, Object[] rowData) {
+        insertUnlinkedRow(row, new UnlinkedRow(rowData));
+    }
+
+    void insertUnlinkedRow(int row, Vector<?> rowData) {
+        insertRow(row, new UnlinkedRow(rowData));
+    }
+
+    /**
+     * Links a row with database.
+     * @param rowIndex
+     * @return true if it successed, false if already linked
+     * @throws SQLException failed to link by SQL error
+     */
+    boolean linkRow(int rowIndex) throws SQLException {
+        if (isLinkedRow(rowIndex)) {
+            return false;
+        }
+        executeInsert(getRowData(columnIdentifiers.toArray(), rowIndex));
+        @SuppressWarnings("unchecked")
+        Vector<Object> rows = getDataVector();
+        rows.set(rowIndex, new Vector<Object>((Vector<?>)rows.get(rowIndex)));
+        return true;
+    }
+
+    /**
+     * Removes a linked row.
+     * @param rowIndex
+     * @return true if it successed, false if already linked
+     * @throws SQLException failed to link by SQL error
+     */
+    boolean removeLinkedRow(int rowIndex) throws SQLException {
+        if (!isLinkedRow(rowIndex)) {
+            return false;
+        }
+        executeDelete(getRowData(columnIdentifiers.toArray(), rowIndex));
+        super.removeRow(rowIndex);
+        return true;
+    }
+
+    private Map<Object, Object> getRowData(Object[] keys, int rowIndex) {
+        Map<Object, Object> rowData = new LinkedHashMap<Object, Object>();
+        for (int columnIndex = 0, n = keys.length; columnIndex < n; columnIndex++) {
+            rowData.put(keys[columnIndex], getValueAt(rowIndex, columnIndex));
+        }
+        return rowData;
+    }
+
+    /**
+     * Sorts this table.
+     * @param columnIndex
+     * @param descending
+     */
+    void sort(final int columnIndex, boolean descending) {
+        final int f = (descending) ? -1 : 1;
+        @SuppressWarnings("unchecked")
+        List<List<Object>> dataVector = getDataVector();
+        Collections.sort(dataVector, new RowComparator(f, columnIndex));
+    }
+
+    private static final class RowComparator implements Comparator<List<Object>> {
+    
+        private final int f;
+        private final int columnIndex;
+    
+        RowComparator(int f, int columnIndex) {
+            this.f = f;
+            this.columnIndex = columnIndex;
+        }
+    
+        @Override
+        public int compare(List<Object> row1, List<Object> row2) {
+            return c(row1, row2) * f;
+        }
+    
+        private int c(List<Object> row1, List<Object> row2) {
+            if (row1 == null || row2 == null) {
+                return row1 == null ? row2 == null ? 0 : -1 : 1;
+            }
+            final Object o1 = row1.get(columnIndex);
+            final Object o2 = row2.get(columnIndex);
+            if (o1 == null || o2 == null) {
+                return o1 == null ? o2 == null ? 0 : -1 : 1;
+            }
+            if (o1 instanceof Comparable<?> && o1.getClass() == o2.getClass()) {
+                @SuppressWarnings("unchecked")
+                Comparable<Object> c1 = (Comparable<Object>)o1;
+                @SuppressWarnings("unchecked")
+                Comparable<Object> c2 = (Comparable<Object>)o2;
+                return c1.compareTo(c2);
+            }
+            return o1.toString().compareTo(o2.toString());
+        }
+
+    }
+
+    /**
+     * Checks whether this table is updatable.
+     * @return
+     */
+    boolean isUpdatable() {
+        return updatable;
+    }
+
+    /**
+     * Checks whether this table is linkable. 
+     * @return
+     */
+    boolean isLinkable() {
+        return linkable;
+    }
+
+    /**
+     * Checks whether the specified row is linked.
+     * @param rowIndex
+     * @return
+     */
+    boolean isLinkedRow(int rowIndex) {
+        return !(getDataVector().get(rowIndex) instanceof UnlinkedRow);
+    }
+
+    /**
+     * Checks whether this table has unlinked rows.
+     * @return
+     */
+    boolean hasUnlinkedRows() {
+        for (final Object row : getDataVector()) {
+            if (row instanceof UnlinkedRow) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether specified connection is same as the connection it has.
+     * @return
+     */
+    boolean isSameConnection(Connection conn) {
+        return conn == this.conn;
+    }
+
+    /**
+     * Returns the command string that creates this.
+     * @return
+     */
+    String getCommandString() {
+        return commandString;
+    }
+
+    private void executeUpdate(Map<Object, Object> keyMap, Object targetKey, Object targetValue) throws SQLException {
+        final String sql = String.format("UPDATE %s SET %s=? WHERE %s",
+                                         tableName,
+                                         quoteIfNeeds(targetKey),
+                                         toKeyPhrase(primaryKeys));
+        List<Object> a = new ArrayList<Object>();
+        a.add(targetValue);
+        for (Object pk : primaryKeys) {
+            a.add(keyMap.get(pk));
+        }
+        executeSql(sql, a.toArray());
+    }
+
+    private void executeInsert(Map<Object, Object> rowData) throws SQLException {
+        final int dataSize = rowData.size();
+        List<Object> keys = new ArrayList<Object>(dataSize);
+        List<Object> values = new ArrayList<Object>(dataSize);
+        for (Entry<?, ?> entry : rowData.entrySet()) {
+            keys.add(quoteIfNeeds(String.valueOf(entry.getKey())));
+            values.add(entry.getValue());
+        }
+        final String sql = String.format("INSERT INTO %s (%s) VALUES (%s)",
+                                         tableName,
+                                         join(",", keys),
+                                         join(",", nCopies(dataSize, "?")));
+        executeSql(sql, values.toArray());
+    }
+
+    private void executeDelete(Map<Object, Object> keyMap) throws SQLException {
+        final String sql = String.format("DELETE FROM %s WHERE %s",
+                                         tableName,
+                                         toKeyPhrase(primaryKeys));
+        List<Object> a = new ArrayList<Object>();
+        for (Object pk : primaryKeys) {
+            a.add(keyMap.get(pk));
+        }
+        executeSql(sql, a.toArray());
+    }
+
+    private void executeSql(final String sql, final Object[] parameters) throws SQLException {
+        if (log.isDebugEnabled()) {
+            log.debug("SQL: " + sql);
+            log.debug("parameters: " + Arrays.asList(parameters));
+        }
+        final CountDownLatch latch = new CountDownLatch(1);
+        final List<SQLException> errors = new ArrayList<SQLException>();
+        // asynchronous execution
+        DaemonThreadFactory.execute(new Runnable() {
+            @SuppressWarnings("synthetic-access")
+            @Override
+            public void run() {
+                try {
+                    if (conn.isClosed()) {
+                        throw new SQLException(ResourceManager.Default.get("e.not-connect"));
+                    }
+                    final PreparedStatement stmt = conn.prepareStatement(sql);
+                    try {
+                        ValueTransporter transporter = ValueTransporter.getInstance("");
+                        int index = 0;
+                        for (Object o : parameters) {
+                            boolean isNull = false;
+                            if (o == null || String.valueOf(o).length() == 0) {
+                                if (getColumnClass(index) != String.class) {
+                                    isNull = true;
+                                }
+                            }
+                            ++index;
+                            if (isNull) {
+                                stmt.setNull(index, types[index - 1]);
+                            } else {
+                                transporter.setObject(stmt, index, o);
+                            }
+                        }
+                        final int updatedCount = stmt.executeUpdate();
+                        if (updatedCount != 1) {
+                            throw new SQLException("updated count is not 1, but " + updatedCount);
+                        }
+                    } finally {
+                        stmt.close();
+                    }
+                } catch (SQLException ex) {
+                    log.error(ex);
+                    errors.add(ex);
+                } catch (Throwable th) {
+                    log.error(th);
+                    SQLException ex = new SQLException();
+                    ex.initCause(th);
+                    errors.add(ex);
+                }
+                latch.countDown();
+            }
+        });
+        try {
+            // waits for a task to stop
+            latch.await(3L, TimeUnit.SECONDS);
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(ex);
+        }
+        if (latch.getCount() != 0) {
+            DaemonThreadFactory.execute(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        latch.await();
+                    } catch (InterruptedException ex) {
+                        log.warn(ex);
+                    }
+                    if (!errors.isEmpty()) {
+                        invokeLater(new Runnable() {
+                            @Override
+                            public void run() {
+                                WindowOutputProcessor.showErrorDialog(null, errors.get(0));
+                            }
+                        });
+                    }
+                }
+            });
+        } else if (!errors.isEmpty()) {
+            if (log.isDebugEnabled()) {
+                for (final Exception ex : errors) {
+                    log.debug("", ex);
+                }
+            }
+            throw errors.get(0);
+        }
+    }
+
+    private static String toKeyPhrase(Object[] keys) {
+        List<String> a = new ArrayList<String>(keys.length);
+        for (final Object key : keys) {
+            a.add(String.format("%s=?", key));
+        }
+        return join(" AND ", a);
+    }
+
+    private static String quoteIfNeeds(Object o) {
+        final String s = String.valueOf(o);
+        if (s.matches(".*\\W.*")) {
+            return String.format("\"%s\"", s);
+        }
+        return s;
+    }
+
+    private void analyzeForLinking(ResultSet rs, String cmd) throws SQLException {
+        if (rs == null) {
+            return;
+        }
+        Statement stmt = rs.getStatement();
+        if (stmt == null) {
+            return;
+        }
+        Connection conn = stmt.getConnection();
+        if (conn == null) {
+            return;
+        }
+        this.conn = conn;
+        if (conn.isReadOnly()) {
+            return;
+        }
+        final String tableName = findTableName(cmd);
+        if (tableName.length() == 0) {
+            return;
+        }
+        this.tableName = tableName;
+        this.updatable = true;
+        List<String> pkList = findPrimaryKeys(conn, tableName);
+        if (pkList.isEmpty()) {
+            return;
+        }
+        @SuppressWarnings("unchecked")
+        final Collection<Object> columnIdentifiers = this.columnIdentifiers;
+        if (!columnIdentifiers.containsAll(pkList)) {
+            return;
+        }
+        if (findUnion(cmd)) {
+            return;
+        }
+        this.primaryKeys = pkList.toArray(new String[pkList.size()]);
+        this.linkable = true;
+    }
+
+    /**
+     * Finds a table name.
+     * @param cmd command string or SQL
+     * @return table name if it found only a table, or empty string
+     */
+    static String findTableName(String cmd) {
+        if (cmd != null) {
+            StringBuilder buffer = new StringBuilder();
+            Scanner scanner = new Scanner(cmd);
+            try {
+                while (scanner.hasNextLine()) {
+                    final String line = scanner.nextLine();
+                    buffer.append(line.replaceAll("/\\*.*?\\*/|//.*", ""));
+                    buffer.append(' ');
+                }
+            } finally {
+                scanner.close();
+            }
+            Pattern p = Pattern.compile(PTN1, Pattern.CASE_INSENSITIVE);
+            Matcher m = p.matcher(buffer);
+            if (m.matches()) {
+                String afterFrom = m.group(1);
+                String[] words = afterFrom.split("\\s");
+                boolean foundComma = false;
+                for (int i = 0; i < 2 && i < words.length; i++) {
+                    String word = words[i];
+                    if (word.indexOf(',') >= 0) {
+                        foundComma = true;
+                    }
+                }
+                if (!foundComma) {
+                    String word = words[0];
+                    if (word.matches("[A-Za-z0-9_\\.]+")) {
+                        return word;
+                    }
+                }
+            }
+        }
+        return "";
+    }
+
+    private static List<String> findPrimaryKeys(Connection conn, String tableName) throws SQLException {
+        DatabaseMetaData dbmeta = conn.getMetaData();
+        final String cp0;
+        final String sp0;
+        final String tp0;
+        if (tableName.contains(".")) {
+            String[] splitted = tableName.split("\\.");
+            if (splitted.length >= 3) {
+                cp0 = splitted[0];
+                sp0 = splitted[1];
+                tp0 = splitted[2];
+            } else {
+                cp0 = null;
+                sp0 = splitted[0];
+                tp0 = splitted[1];
+            }
+        } else {
+            cp0 = null;
+            sp0 = dbmeta.getUserName();
+            tp0 = tableName;
+        }
+        final String cp;
+        final String sp;
+        final String tp;
+        if (dbmeta.storesLowerCaseIdentifiers()) {
+            cp = (cp0 == null) ? null : cp0.toLowerCase();
+            sp = (sp0 == null) ? null : sp0.toLowerCase();
+            tp = tp0.toLowerCase();
+        } else if (dbmeta.storesUpperCaseIdentifiers()) {
+            cp = (cp0 == null) ? null : cp0.toUpperCase();
+            sp = (sp0 == null) ? null : sp0.toUpperCase();
+            tp = tp0.toUpperCase();
+        } else {
+            cp = cp0;
+            sp = sp0;
+            tp = tp0;
+        }
+        if (cp == null && sp == null) {
+            return getPrimaryKeys(dbmeta, null, null, tp);
+        }
+        List<String> a = getPrimaryKeys(dbmeta, cp, sp, tp);
+        if (a.isEmpty()) {
+            return getPrimaryKeys(dbmeta, null, null, tp);
+        }
+        return a;
+    }
+
+    private static List<String> getPrimaryKeys(DatabaseMetaData dbmeta,
+                                               String catalog,
+                                               String schema,
+                                               String table) throws SQLException {
+        ResultSet rs = dbmeta.getPrimaryKeys(catalog, schema, table);
+        try {
+            Map<Short, String> result = new TreeMap<Short, String>();
+            Set<String> schemaSet = new HashSet<String>();
+            while (rs.next()) {
+                result.put(rs.getShort(5), rs.getString(4));
+                schemaSet.add(rs.getString(2));
+            }
+            if (result.isEmpty() || schemaSet.size() != 1) {
+                return Collections.emptyList();
+            }
+            List<String> pkList = new ArrayList<String>();
+            for (Short key : result.keySet()) {
+                pkList.add(result.get(key));
+            }
+            return pkList;
+        } finally {
+            rs.close();
+        }
+    }
+
+    private static boolean findUnion(String sql) {
+        String s = sql;
+        if (s.indexOf('\'') >= 0) {
+            if (s.indexOf("\\'") >= 0) {
+                s = s.replaceAll("\\'", "");
+            }
+            s = s.replaceAll("'[^']+'", "''");
+        }
+        StringTokenizer tokenizer = new StringTokenizer(s);
+        while (tokenizer.hasMoreTokens()) {
+            if (tokenizer.nextToken().equalsIgnoreCase("UNION")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/TextSearch.java b/src/net/argius/stew/ui/window/TextSearch.java
new file mode 100644 (file)
index 0000000..fee5579
--- /dev/null
@@ -0,0 +1,156 @@
+package net.argius.stew.ui.window;
+
+import java.awt.*;
+import java.util.regex.*;
+
+import javax.swing.text.*;
+import javax.swing.text.Highlighter.HighlightPainter;
+
+/**
+ * The interface for text search.
+ */
+public interface TextSearch {
+
+    /**
+     * Searches a text.
+     * @param matcher
+     * @return true if matched, otherwise false
+     */
+    boolean search(Matcher matcher);
+
+    /**
+     * Resets searching.
+     */
+    void reset();
+
+    /**
+     * The Matcher for TextSearch.
+     */
+    public final class Matcher {
+
+        private Pattern pattern;
+        private String string;
+        private boolean useRegularExpression;
+        private boolean ignoreCase;
+        private boolean backward;
+        private boolean continuously;
+        private int start;
+        private int end;
+
+        /**
+         * A constructor.
+         * @param target
+         * @param useRegularExpression
+         * @param ignoreCase
+         */
+        public Matcher(String target, boolean useRegularExpression, boolean ignoreCase) {
+            if (useRegularExpression) {
+                int option = (ignoreCase) ? Pattern.CASE_INSENSITIVE : 0;
+                this.pattern = Pattern.compile(target, option);
+                this.string = target;
+            } else if (ignoreCase) {
+                this.string = target.toUpperCase();
+            } else {
+                this.string = target;
+            }
+            this.useRegularExpression = useRegularExpression;
+            this.ignoreCase = ignoreCase;
+        }
+
+        /**
+         * Returns HighlightPainter.
+         * @return
+         */
+        public static HighlightPainter getHighlightPainter() {
+            return new DefaultHighlighter.DefaultHighlightPainter(Color.decode("#33dd66"));
+        }
+
+        /**
+         * Returns backword or not.
+         * @return
+         */
+        public boolean isBackward() {
+            return backward;
+        }
+
+        /**
+         * Sets backword or not.
+         * @param backward
+         */
+        public void setBackward(boolean backward) {
+            this.backward = backward;
+        }
+
+        /**
+         * Returns continuously or not.
+         * @return
+         */
+        public boolean isContinuously() {
+            return continuously;
+        }
+
+        /**
+         * Sets continuously or not.
+         * @param continuously
+         */
+        public void setContinuously(boolean continuously) {
+            this.continuously = continuously;
+        }
+
+        /**
+         * Finds the text.
+         * @param value
+         * @return true if text was found, otherwise false
+         */
+        public boolean find(String value) {
+            return find(value, 0);
+        }
+
+        /**
+         * Finds the text.
+         * @param value
+         * @param start
+         * @return true if text was found, otherwise false
+         */
+        public boolean find(String value, int start) {
+            this.start = -1;
+            this.end = -1;
+            boolean found;
+            if (useRegularExpression) {
+                java.util.regex.Matcher m = pattern.matcher(value);
+                found = m.find(start);
+                if (found) {
+                    this.start = m.start();
+                    this.end = m.end();
+                }
+            } else {
+                String v = (ignoreCase) ? value.toUpperCase() : value;
+                int index = v.indexOf(string, start);
+                found = index >= start;
+                if (found) {
+                    this.start = index;
+                    this.end = index + string.length();
+                }
+            }
+            return found;
+        }
+
+        /**
+         * Returns this start position.
+         * @return
+         */
+        public int getStart() {
+            return start;
+        }
+
+        /**
+         * Returns this end position.
+         * @return
+         */
+        public int getEnd() {
+            return end;
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/TextSearchPanel.java b/src/net/argius/stew/ui/window/TextSearchPanel.java
new file mode 100644 (file)
index 0000000..8623cea
--- /dev/null
@@ -0,0 +1,210 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.KeyEvent.VK_ENTER;
+import static java.awt.event.KeyEvent.VK_ESCAPE;
+import static javax.swing.JOptionPane.WARNING_MESSAGE;
+import static javax.swing.JOptionPane.showMessageDialog;
+import static javax.swing.KeyStroke.getKeyStroke;
+import static net.argius.stew.ui.window.TextSearchPanel.ActionKey.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+
+import net.argius.stew.*;
+import net.argius.stew.ui.window.TextSearch.Matcher;
+
+/**
+ * This is a panel that contains input controls for text-search.
+ */
+final class TextSearchPanel extends JPanel implements AnyActionListener {
+
+    enum ActionKey {
+        close, search, forward, backward
+    }
+
+    private static final ResourceManager res = ResourceManager.getInstance(TextSearchPanel.class);
+
+    private final List<TextSearch> targets;
+    private final JTextField text;
+    private final JCheckBox useRegexCheck;
+    private final JCheckBox ignoreCaseCheck;
+
+    private TextSearch currentTarget;
+    private boolean searchBackward;
+
+    TextSearchPanel(JFrame frame) {
+        // [Init Instances]
+        this.targets = new ArrayList<TextSearch>();
+        this.text = new JTextField(16);
+        this.useRegexCheck = new JCheckBox(res.get("useregex"));
+        this.ignoreCaseCheck = new JCheckBox(res.get("ignorecase"));
+        setLayout(new FlowLayout(FlowLayout.LEFT, 1, 1));
+        setVisible(false);
+        final JButton closeButton = new JButton(Utilities.getImageIcon("close.png"));
+        final JTextField text = this.text;
+        final JButton forwardButton = new JButton(res.get("forward"));
+        final JButton backwardButton = new JButton(res.get("backward"));
+        final JCheckBox useRegexCheck = this.useRegexCheck;
+        final JCheckBox ignoreCaseCheck = this.ignoreCaseCheck;
+        // [Layout]
+        closeButton.setToolTipText(res.get("close"));
+        closeButton.setMargin(new Insets(0, 0, 0, 0));
+        text.setMargin(new Insets(1, 2, 1, 2));
+        forwardButton.setMargin(new Insets(0, 7, 0, 7));
+        backwardButton.setMargin(new Insets(0, 7, 0, 7));
+        changeFontSize(useRegexCheck, 0.8f);
+        changeFontSize(ignoreCaseCheck, 0.8f);
+        add(createFiller(2));
+        add(closeButton);
+        add(createFiller(2));
+        add(new JLabel(res.get("label")));
+        add(text);
+        add(createFiller(1));
+        add(forwardButton);
+        add(backwardButton);
+        add(useRegexCheck);
+        add(ignoreCaseCheck);
+        // [Setup Focus Policy]
+        final FocusTraversalPolicy parentPolicy = frame.getFocusTraversalPolicy();
+        frame.setFocusTraversalPolicy(new LayoutFocusTraversalPolicy() {
+            @Override
+            public Component getComponentAfter(Container focusCycleRoot, Component component) {
+                if (component == ignoreCaseCheck) {
+                    return closeButton;
+                }
+                return parentPolicy.getComponentAfter(focusCycleRoot, component);
+            }
+            @Override
+            public Component getComponentBefore(Container focusCycleRoot, Component component) {
+                if (component == closeButton) {
+                    return ignoreCaseCheck;
+                }
+                return parentPolicy.getComponentBefore(focusCycleRoot, component);
+            }
+        });
+        // [Events]
+        // text field
+        ContextMenu.createForText(text);
+        // buttons
+        bindEvent(text, search, getKeyStroke(VK_ENTER, 0));
+        bindEvent(closeButton, close);
+        bindEvent(forwardButton, forward);
+        bindEvent(backwardButton, backward);
+        // ESC key
+        final KeyStroke ksESC = getKeyStroke(VK_ESCAPE, 0);
+        for (JComponent c : new JComponent[]{closeButton, text, forwardButton, backwardButton,
+                                             useRegexCheck, ignoreCaseCheck}) {
+            bindEvent(c, close, ksESC);
+        }
+    }
+
+    private void bindEvent(JButton b, Object key) {
+        b.setActionCommand(String.valueOf(key));
+        b.addActionListener(new AnyAction(this));
+    }
+
+    private void bindEvent(JComponent c, Object key, KeyStroke ks) {
+        AnyAction aa = new AnyAction(c);
+        aa.bind(this, key, ks);
+    }
+
+    @Override
+    protected void processComponentEvent(ComponentEvent e) {
+        if (e.getID() == ComponentEvent.COMPONENT_RESIZED) {
+            validate();
+        }
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        if (ev.isAnyOf(close)) {
+            setVisible(false);
+        } else if (ev.isAnyOf(search)) {
+            startSearch();
+        } else if (ev.isAnyOf(forward)) {
+            setSearchBackward(false);
+            startSearch();
+        } else if (ev.isAnyOf(backward)) {
+            setSearchBackward(true);
+            startSearch();
+        }
+    }
+
+    private static void changeFontSize(Component c, float rate) {
+        Font f = c.getFont();
+        c.setFont(f.deriveFont(Font.PLAIN, f.getSize() * rate));
+    }
+
+    private static Component createFiller(int width) {
+        char[] a = new char[width];
+        Arrays.fill(a, ' ');
+        return new JLabel(String.valueOf(a));
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        super.setVisible(visible);
+        if (visible) {
+            final int length = text.getText().length();
+            if (length > 0) {
+                text.setSelectionStart(0);
+                text.setSelectionEnd(length);
+            }
+            text.requestFocus();
+        } else {
+            for (TextSearch target : targets) {
+                target.reset();
+            }
+            if (currentTarget instanceof Component) {
+                Component c = (Component)currentTarget;
+                c.requestFocus();
+            }
+        }
+    }
+
+    void addTarget(TextSearch target) {
+        targets.add(target);
+    }
+
+    void removeTarget(TextSearch target) {
+        targets.remove(target);
+    }
+
+    void setCurrentTarget(TextSearch currentTarget) {
+        this.currentTarget = currentTarget;
+    }
+
+    void setSearchBackward(boolean searchBackward) {
+        this.searchBackward = searchBackward;
+    }
+
+    void startSearch() {
+        final String s = text.getText();
+        if (s == null || s.length() == 0 || currentTarget == null) {
+            return;
+        }
+        final boolean useRegularExpression = useRegexCheck.isSelected();
+        final boolean ignoreCase = ignoreCaseCheck.isSelected();
+        Matcher matcher = new Matcher(s, useRegularExpression, ignoreCase);
+        matcher.setBackward(searchBackward);
+        matcher.setContinuously(true);
+        for (TextSearch target : targets) {
+            target.reset();
+        }
+        final boolean found = currentTarget.search(matcher);
+        if (!found) {
+            Component parent;
+            if (currentTarget instanceof Component) {
+                parent = ((Component)currentTarget).getParent();
+            } else {
+                parent = getParent();
+            }
+            showMessageDialog(parent, res.get("message.notfound", s), null, WARNING_MESSAGE);
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/TextSearchPanel.u8p b/src/net/argius/stew/ui/window/TextSearchPanel.u8p
new file mode 100644 (file)
index 0000000..3205a09
--- /dev/null
@@ -0,0 +1,8 @@
+
+label=検索
+forward=次を検索
+backward=前を検索
+close=閉じる
+useregex=正規表現
+ignorecase=英字のケース無視
+message.notfound=文字列 "{0}" は見つかりませんでした
diff --git a/src/net/argius/stew/ui/window/Utilities.java b/src/net/argius/stew/ui/window/Utilities.java
new file mode 100644 (file)
index 0000000..691b0d8
--- /dev/null
@@ -0,0 +1,54 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.InputEvent.CTRL_DOWN_MASK;
+import static java.awt.event.InputEvent.CTRL_MASK;
+
+import java.awt.*;
+import java.util.concurrent.*;
+
+import javax.swing.*;
+
+final class Utilities {
+
+    private Utilities() {
+    } // forbidden
+
+    static ImageIcon getImageIcon(String name) {
+        try {
+            return new ImageIcon(Utilities.class.getResource("icon/" + name));
+        } catch (RuntimeException ex) {
+            return new ImageIcon();
+        }
+    }
+
+    static KeyStroke getKeyStroke(String s) {
+        KeyStroke ks = KeyStroke.getKeyStroke(s);
+        if (ks != null && s.matches("(?i).*Ctrl.*")) {
+            return convertShortcutMask(ks, getMenuShortcutKeyMask());
+        }
+        return ks;
+    }
+
+    static KeyStroke convertShortcutMask(KeyStroke ks, int shortcutMask) {
+        final int mod = ks.getModifiers();
+        if ((mod & (CTRL_DOWN_MASK | CTRL_MASK)) != 0) {
+            final int newmod = mod & ~(CTRL_DOWN_MASK | CTRL_MASK) | shortcutMask;
+            return KeyStroke.getKeyStroke(ks.getKeyCode(), newmod);
+        }
+        return ks;
+    }
+
+    static int getMenuShortcutKeyMask() {
+        return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
+    }
+
+    static void sleep(long interval) {
+        try {
+            TimeUnit.MILLISECONDS.sleep(interval);
+        } catch (InterruptedException ex) {
+            // expects no interruption
+            assert false : ex.toString();
+        }
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/ValueTransporter.java b/src/net/argius/stew/ui/window/ValueTransporter.java
new file mode 100644 (file)
index 0000000..c907fda
--- /dev/null
@@ -0,0 +1,53 @@
+package net.argius.stew.ui.window;
+
+import java.sql.*;
+
+import net.argius.stew.*;
+
+/**
+ * It is used for sending and receiving an object.
+ * It called at {@link WindowOutputProcessor#outputResult(ResultSetReference)} to fetch
+ * and ResultSetTableModel#executeSql (update sql) to upload.
+ */
+class ValueTransporter {
+
+    protected ValueTransporter() {
+        // empty
+    }
+
+    static ValueTransporter getInstance(String className) {
+        if (className != null && className.trim().length() > 0) {
+            try {
+                return DynamicLoader.newInstance(className);
+            } catch (DynamicLoadingException ex) {
+                // ignore
+            }
+        }
+        return new ValueTransporter();
+    }
+
+    /**
+     * Returns the object received from ResultSet.
+     * @param rs
+     * @param index
+     * @return
+     * @throws SQLException
+     */
+    @SuppressWarnings("static-method")
+    Object getObject(ResultSet rs, int index) throws SQLException {
+        return rs.getObject(index);
+    }
+
+    /**
+     * Sets the object to Statement.
+     * @param stmt
+     * @param index
+     * @param o
+     * @throws SQLException
+     */
+    @SuppressWarnings("static-method")
+    void setObject(PreparedStatement stmt, int index, Object o) throws SQLException {
+        stmt.setObject(index, o);
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/WindowLauncher.java b/src/net/argius/stew/ui/window/WindowLauncher.java
new file mode 100644 (file)
index 0000000..d4d31ed
--- /dev/null
@@ -0,0 +1,925 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.event.ActionEvent.ACTION_PERFORMED;
+import static javax.swing.JOptionPane.*;
+import static javax.swing.JSplitPane.VERTICAL_SPLIT;
+import static javax.swing.KeyStroke.getKeyStroke;
+import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER;
+import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS;
+import static net.argius.stew.ui.window.AnyActionKey.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.beans.*;
+import java.io.*;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.sql.*;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.List;
+import java.util.Timer;
+import java.util.concurrent.*;
+import java.util.regex.*;
+
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+import javax.swing.text.*;
+
+import net.argius.stew.*;
+import net.argius.stew.ui.*;
+import net.argius.stew.ui.window.Menu.Item;
+
+/**
+ * The Launcher implementation for GUI(Swing).
+ */
+public final class WindowLauncher implements
+                                 Launcher,
+                                 AnyActionListener,
+                                 Runnable,
+                                 UncaughtExceptionHandler {
+
+    private static final Logger log = Logger.getLogger(WindowLauncher.class);
+    private static final ResourceManager res = ResourceManager.getInstance(WindowLauncher.class);
+    private static final List<WindowLauncher> instances = Collections.synchronizedList(new ArrayList<WindowLauncher>());
+
+    private final WindowOutputProcessor op;
+    private final Menu menu;
+    private final JPanel panel1;
+    private final JPanel panel2;
+    private final JSplitPane split1;
+    private final JSplitPane split2;
+    private final ResultSetTable resultSetTable;
+    private final ConsoleTextArea textArea;
+    private final DatabaseInfoTree infoTree;
+    private final TextSearchPanel textSearchPanel;
+    private final JLabel statusBar;
+    private final List<String> historyList;
+    private final ExecutorService executorService;
+
+    private Environment env;
+    private Map<JComponent, TextSearch> textSearchMap;
+    private int historyIndex;
+    private JComponent focused;
+
+    WindowLauncher() {
+        // [Instances]
+        instances.add(this);
+        final JSplitPane split1 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+        final DatabaseInfoTree infoTree = new DatabaseInfoTree(this);
+        final ResultSetTable resultSetTable = new ResultSetTable(this);
+        final JTableHeader resultSetTableHeader = resultSetTable.getTableHeader();
+        final ConsoleTextArea textArea = new ConsoleTextArea(this);
+        this.op = new WindowOutputProcessor(this, resultSetTable, textArea);
+        this.menu = new Menu(this);
+        this.panel1 = new JPanel(new BorderLayout());
+        this.panel2 = new JPanel(new BorderLayout());
+        this.split1 = split1;
+        this.split2 = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+        this.resultSetTable = resultSetTable;
+        this.textArea = textArea;
+        this.infoTree = infoTree;
+        this.textSearchPanel = new TextSearchPanel(op);
+        this.statusBar = new JLabel(" ");
+        this.historyList = new LinkedList<String>();
+        this.historyIndex = 0;
+        this.executorService = Executors.newScheduledThreadPool(3,
+                                                                DaemonThreadFactory.getInstance());
+        // [Components]
+        // OutputProcessor as frame
+        op.setTitle(res.get(".title"));
+        op.setIconImage(Utilities.getImageIcon("stew.png").getImage());
+        op.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+        // splitpane (infotree and sub-splitpane)
+        split1.setResizeWeight(0.6f);
+        split1.setDividerSize(4);
+        // splitpane (table and textarea)
+        split2.setOrientation(VERTICAL_SPLIT);
+        split2.setDividerSize(6);
+        split2.setResizeWeight(0.6f);
+        // text area
+        textArea.setMargin(new Insets(4, 8, 4, 4));
+        textArea.setLineWrap(true);
+        textArea.setWrapStyleWord(false);
+        // text search
+        this.textSearchMap = new LinkedHashMap<JComponent, TextSearch>();
+        final TextSearchPanel textSearchPanel = this.textSearchPanel;
+        final Map<JComponent, TextSearch> textSearchMap = this.textSearchMap;
+        textSearchMap.put(infoTree, infoTree);
+        textSearchMap.put(resultSetTable, resultSetTable);
+        textSearchMap.put(resultSetTableHeader,
+                          new ResultSetTable.TableHeaderTextSearch(resultSetTable,
+                                                                   resultSetTableHeader));
+        textSearchMap.put(textArea, textArea);
+        for (Entry<JComponent, TextSearch> entry : textSearchMap.entrySet()) {
+            final JComponent c = entry.getKey();
+            c.addFocusListener(new FocusAdapter() {
+                @Override
+                public void focusGained(FocusEvent e) {
+                    focused = c;
+                    textSearchPanel.setCurrentTarget(textSearchMap.get(c));
+                }
+            });
+            textSearchPanel.addTarget(entry.getValue());
+        }
+        // status bar
+        statusBar.setForeground(Color.BLUE);
+        // [Layouts]
+        /*
+         * split2 = ResultSetTable + TextArea 
+         * +----------------------------+
+         * | split2                     |
+         * | +------------------------+ |
+         * | | scroll(resultSetTable) | |
+         * | +------------------------+ |
+         * | +------------------------+ |
+         * | | panel2                 | |
+         * | | +--------------------+ | |
+         * | | | scroll(textArea)   | | |
+         * | | +--------------------+ | |
+         * | | | textSearchPanel    | | |
+         * | | +--------------------+ | |
+         * | +------------------------+ |
+         * +----------------------------+
+         * when DatabaseInfoTree is visible
+         * +-----------------------------------+
+         * | panel1                            |
+         * | +-------------------------------+ |
+         * | | split1                        | |
+         * | | +------------+ +------------+ | |
+         * | | | scroll     | | split2     | | |
+         * | | | (infoTree) | |            | | |
+         * | | +------------+ +------------+ | |
+         * | +-------------------------------+ |
+         * +-----------------------------------+
+         * | status bar                        |
+         * +-----------------------------------+
+         * when DatabaseInfoTree is not visible
+         * +-----------------------------------+
+         * | panel1                            |
+         * | +-------------------------------+ |
+         * | | split2                        | |
+         * | +-------------------------------+ |
+         * +-----------------------------------+
+         * | status bar                        |
+         * +-----------------------------------+
+         */
+        panel2.add(new JScrollPane(textArea, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_NEVER),
+                   BorderLayout.CENTER);
+        panel2.add(textSearchPanel, BorderLayout.SOUTH);
+        split2.setTopComponent(new JScrollPane(resultSetTable));
+        split2.setBottomComponent(panel2);
+        op.add(panel1, BorderLayout.CENTER);
+        op.add(statusBar, BorderLayout.PAGE_END);
+        op.setJMenuBar(menu);
+        // [Restores Configs]
+        op.addPropertyChangeListener(menu);
+        infoTree.addPropertyChangeListener(menu);
+        resultSetTable.addPropertyChangeListener(menu);
+        statusBar.addPropertyChangeListener(menu);
+        loadConfiguration();
+        op.removePropertyChangeListener(menu);
+        infoTree.removePropertyChangeListener(menu);
+        resultSetTable.removePropertyChangeListener(menu);
+        // XXX cannot restore config of status-bar at following code
+        // statusBar.removePropertyChangeListener(menu);
+        // [Events]
+        op.addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosing(WindowEvent e) {
+                requestClose();
+            }
+        });
+        ContextMenu.create(infoTree, infoTree);
+        ContextMenu.create(resultSetTable);
+        ContextMenu.create(resultSetTable.getRowHeader(), resultSetTable, "ResultSetTable");
+        ContextMenu.create(resultSetTableHeader, resultSetTable, "ResultSetTableColumnHeader");
+        ContextMenu.createForText(textArea);
+    }
+
+    @Override
+    public void launch(Environment env) {
+        this.env = env;
+        op.setEnvironment(env);
+        op.setVisible(true);
+        op.output(new Prompt(env));
+        textArea.requestFocus();
+    }
+
+    @Override
+    public void run() {
+        Thread.setDefaultUncaughtExceptionHandler(this);
+        invoke(this);
+    }
+
+    @Override
+    public void uncaughtException(Thread t, Throwable e) {
+        log.fatal(e, "%s", t);
+        op.showErrorDialog(e);
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        log.atEnter("anyActionPerformed", ev);
+        ev.validate();
+        try {
+            resultSetTable.editingCanceled(new ChangeEvent(ev.getSource()));
+            final Object source = ev.getSource();
+            if (ev.isAnyOf(newWindow)) {
+                invoke();
+            } else if (ev.isAnyOf(closeWindow)) {
+                requestClose();
+            } else if (ev.isAnyOf(quit)) {
+                requestExit();
+            } else if (ev.isAnyOf(showInfoTree)) {
+                setInfoTreePane(((JCheckBoxMenuItem)source).isSelected());
+                op.validate();
+                op.repaint();
+            } else if (ev.isAnyOf(cut, copy, paste, selectAll)) {
+                if (focused != null) {
+                    final String cmd;
+                    if (ev.isAnyOf(cut)) {
+                        if (focused instanceof JTextComponent) {
+                            cmd = "cut-to-clipboard";
+                        } else {
+                            cmd = "";
+                        }
+                    } else if (ev.isAnyOf(copy)) {
+                        if (focused instanceof JTextComponent) {
+                            cmd = "copy-to-clipboard";
+                        } else {
+                            cmd = ev.getActionCommand();
+                        }
+                    } else if (ev.isAnyOf(paste)) {
+                        if (focused instanceof JTextComponent) {
+                            cmd = "paste-from-clipboard";
+                        } else if (focused instanceof DatabaseInfoTree) {
+                            cmd = "";
+                        } else {
+                            cmd = ev.getActionCommand();
+                        }
+                    } else if (ev.isAnyOf(selectAll)) {
+                        if (focused instanceof JTextComponent) {
+                            cmd = "select-all";
+                        } else {
+                            cmd = ev.getActionCommand();
+                        }
+                    } else {
+                        cmd = "";
+                    }
+                    if (cmd.length() == 0) {
+                        log.debug("no action: %s, cmd=%s",
+                                  focused.getClass(),
+                                  ev.getActionCommand());
+                    } else {
+                        final Action action = focused.getActionMap().get(cmd);
+                        log.debug("convert to plain Action Event: orig=%s", ev);
+                        action.actionPerformed(new ActionEvent(focused, ACTION_PERFORMED, cmd));
+                    }
+                }
+            } else if (ev.isAnyOf(find)) {
+                textSearchPanel.setCurrentTarget(textSearchMap.get(focused));
+                textSearchPanel.setVisible(true);
+            } else if (ev.isAnyOf(toggleFocus)) {
+                if (textArea.isFocusOwner()) {
+                    resultSetTable.requestFocus();
+                } else {
+                    textArea.requestFocus();
+                }
+            } else if (ev.isAnyOf(clearMessage)) {
+                textArea.clear();
+                executeCommand("");
+            } else if (ev.isAnyOf(showStatusBar)) {
+                statusBar.setVisible(((JCheckBoxMenuItem)source).isSelected());
+            } else if (ev.isAnyOf(showColumnNumber)) {
+                resultSetTable.anyActionPerformed(ev);
+            } else if (ev.isAnyOf(refresh)) {
+                refreshResult();
+            } else if (ev.isAnyOf(autoAdjustModeNone,
+                                  autoAdjustModeHeader,
+                                  autoAdjustModeValue,
+                                  autoAdjustModeHeaderAndValue)) {
+                resultSetTable.setAutoAdjustMode(ev.getActionCommand());
+            } else if (ev.isAnyOf(widenColumnWidth, narrowColumnWidth, adjustColumnWidth)) {
+                resultSetTable.anyActionPerformed(ev);
+            } else if (ev.isAnyOf(executeCommand, execute)) {
+                executeCommand(textArea.getEditableText());
+            } else if (ev.isAnyOf(breakCommand)) {
+                env.getOutputProcessor().close();
+                env.setOutputProcessor(new WindowOutputProcessor.Bypass(op));
+                op.output(res.get("i.cancelled"));
+                doPostProcess();
+            } else if (ev.isAnyOf(lastHistory)) {
+                retrieveHistory(-1);
+            } else if (ev.isAnyOf(nextHistory)) {
+                retrieveHistory(+1);
+            } else if (ev.isAnyOf(sendRollback)) {
+                if (confirmCommitable()
+                    && showConfirmDialog(op, res.get("i.confirm-rollback"), null, OK_CANCEL_OPTION) == OK_OPTION) {
+                    executeCommand("rollback");
+                }
+            } else if (ev.isAnyOf(sendCommit)) {
+                if (confirmCommitable()
+                    && showConfirmDialog(op, res.get("i.confirm-commit"), null, OK_CANCEL_OPTION) == OK_OPTION) {
+                    executeCommand("commit");
+                }
+            } else if (ev.isAnyOf(connect)) {
+                env.updateConnectorMap();
+                if (env.getConnectorMap().isEmpty()) {
+                    showMessageDialog(op, res.get("w.no-connector"));
+                    return;
+                }
+                Object[] a = ConnectorEntry.toList(env.getConnectorMap().values()).toArray();
+                final String m = res.get("i.choose-connection");
+                Object value = showInputDialog(op, m, null, PLAIN_MESSAGE, null, a, a[0]);
+                if (value != null) {
+                    ConnectorEntry c = (ConnectorEntry)value;
+                    executeCommand("connect " + c.getId());
+                }
+            } else if (ev.isAnyOf(disconnect)) {
+                executeCommand("disconnect");
+            } else if (ev.isAnyOf(postProcessModeNone,
+                                  postProcessModeFocus,
+                                  postProcessModeShake,
+                                  postProcessModeBlink)) {
+                op.setPostProcessMode(ev.getActionCommand());
+            } else if (ev.isAnyOf(inputEcryptionKey)) {
+                editEncryptionKey();
+            } else if (ev.isAnyOf(editConnectors)) {
+                editConnectorMap();
+            } else if (ev.isAnyOf(sortResult)) {
+                resultSetTable.doSort(resultSetTable.getSelectedColumn());
+            } else if (ev.isAnyOf(importFile, exportFile, showAbout)) {
+                op.anyActionPerformed(ev);
+            } else if (ev.isAnyOf(showHelp)) {
+                showHelp();
+            } else if (ev.isAnyOf(ResultSetTable.ActionKey.findColumnName)) {
+                resultSetTable.getTableHeader().requestFocus();
+                textSearchPanel.setVisible(true);
+            } else if (ev.isAnyOf(ResultSetTable.ActionKey.jumpToColumn)) {
+                resultSetTable.anyActionPerformed(ev);
+            } else if (ev.isAnyOf(ConsoleTextArea.ActionKey.insertText)) {
+                textArea.anyActionPerformed(ev);
+            } else {
+                log.warn("not expected: Event=%s", ev);
+            }
+        } catch (Exception ex) {
+            op.showErrorDialog(ex);
+        }
+        log.atExit("dispatch");
+    }
+
+    /**
+     * Controls visibility of DatabaseInfoTree pane.
+     * @param show
+     */
+    void setInfoTreePane(boolean show) {
+        if (show) {
+            split1.removeAll();
+            split1.setTopComponent(new JScrollPane(infoTree));
+            split1.setBottomComponent(split2);
+            panel1.removeAll();
+            panel1.add(split1, BorderLayout.CENTER);
+            infoTree.setEnabled(true);
+            if (env != null) {
+                try {
+                    infoTree.refreshRoot(env);
+                } catch (SQLException ex) {
+                    log.error(ex);
+                    op.showErrorDialog(ex);
+                }
+            }
+        } else {
+            infoTree.clear();
+            infoTree.setEnabled(false);
+            panel1.removeAll();
+            panel1.add(split2, BorderLayout.CENTER);
+        }
+        SwingUtilities.updateComponentTreeUI(op);
+    }
+
+    private void loadConfiguration() {
+        Configuration cnf = Configuration.load(Bootstrap.getDirectory());
+        op.setSize(cnf.getSize());
+        op.setLocation(cnf.getLocation());
+        split2.setDividerLocation(cnf.getDividerLocation());
+        statusBar.setVisible(cnf.isShowStatusBar());
+        resultSetTable.setShowColumnNumber(cnf.isShowTableColumnNumber());
+        split1.setDividerLocation(cnf.getDividerLocation0());
+        op.setAlwaysOnTop(cnf.isAlwaysOnTop());
+        resultSetTable.setAutoAdjustMode(cnf.getAutoAdjustMode());
+        op.setPostProcessMode(cnf.getPostProcessMode());
+        setInfoTreePane(cnf.isShowInfoTree());
+        changeFont("monospaced", Font.PLAIN, 1.0d);
+    }
+
+    private void saveConfiguration() {
+        Configuration cnf = Configuration.load(Bootstrap.getDirectory());
+        if ((op.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) {
+            // only not maximized
+            cnf.setSize(op.getSize());
+            cnf.setLocation(op.getLocation());
+            cnf.setDividerLocation(split2.getDividerLocation());
+            cnf.setDividerLocation0(split1.getDividerLocation());
+        }
+        cnf.setShowStatusBar(statusBar.isVisible());
+        cnf.setShowTableColumnNumber(resultSetTable.isShowColumnNumber());
+        cnf.setShowInfoTree(infoTree.isEnabled());
+        cnf.setAlwaysOnTop(op.isAlwaysOnTop());
+        cnf.setAutoAdjustMode(resultSetTable.getAutoAdjustMode());
+        cnf.setPostProcessMode(op.getPostProcessMode());
+        cnf.save();
+    }
+
+    /**
+     * Configuration (Bean) for saving and loading.
+     */
+    @SuppressWarnings("all")
+    public static final class Configuration {
+
+        private static final Logger log = Logger.getLogger(Configuration.class);
+
+        private transient File directory;
+
+        private Dimension size;
+        private Point location;
+        private int dividerLocation;
+        private int dividerLocation0;
+        private boolean showStatusBar;
+        private boolean showTableColumnNumber;
+        private boolean showInfoTree;
+        private boolean alwaysOnTop;
+        private String autoAdjustMode;
+        private String postProcessMode;
+
+        public Configuration() {
+            this.size = new Dimension(640, 480);
+            this.location = new Point(200, 200);
+            this.dividerLocation = -1;
+            this.dividerLocation0 = -1;
+            this.showStatusBar = false;
+            this.showTableColumnNumber = false;
+            this.showInfoTree = false;
+            this.alwaysOnTop = false;
+            this.autoAdjustMode = AnyActionKey.autoAdjustMode.toString();
+            this.postProcessMode = AnyActionKey.postProcessMode.toString();
+        }
+
+        void setDirectory(File directory) {
+            this.directory = directory;
+        }
+
+        void save() {
+            final File file = getFile(directory);
+            log.debug("save Configuration to: [%s]", file);
+            try {
+                XMLEncoder encoder = new XMLEncoder(new FileOutputStream(file));
+                try {
+                    encoder.writeObject(this);
+                } finally {
+                    encoder.close();
+                }
+            } catch (Exception ex) {
+                log.warn(ex);
+            }
+        }
+
+        static Configuration load(File directory) {
+            final File file = getFile(directory);
+            log.debug("load Configuration from: [%s]", file);
+            if (file.exists()) {
+                try {
+                    XMLDecoder decoder = new XMLDecoder(new FileInputStream(file));
+                    try {
+                        final Configuration instance = (Configuration)decoder.readObject();
+                        instance.setDirectory(directory);
+                        return instance;
+                    } finally {
+                        decoder.close();
+                    }
+                } catch (Exception ex) {
+                    log.warn(ex);
+                }
+            }
+            final Configuration instance = new Configuration();
+            instance.setDirectory(directory);
+            return instance;
+        }
+
+        private static File getFile(File systemDirectory) {
+            final File file = new File(systemDirectory, Configuration.class.getName() + ".xml");
+            return file.getAbsoluteFile();
+        }
+
+        public Dimension getSize() {
+            return size;
+        }
+
+        public void setSize(Dimension size) {
+            this.size = size;
+        }
+
+        public Point getLocation() {
+            return location;
+        }
+
+        public void setLocation(Point location) {
+            this.location = location;
+        }
+
+        public int getDividerLocation() {
+            return dividerLocation;
+        }
+
+        public void setDividerLocation(int dividerLocation) {
+            this.dividerLocation = dividerLocation;
+        }
+
+        public int getDividerLocation0() {
+            return dividerLocation0;
+        }
+
+        public void setDividerLocation0(int dividerLocation0) {
+            this.dividerLocation0 = dividerLocation0;
+        }
+
+        public boolean isShowStatusBar() {
+            return showStatusBar;
+        }
+
+        public void setShowStatusBar(boolean showStatusBar) {
+            this.showStatusBar = showStatusBar;
+        }
+
+        public boolean isShowTableColumnNumber() {
+            return showTableColumnNumber;
+        }
+
+        public void setShowTableColumnNumber(boolean showTableColumnNumber) {
+            this.showTableColumnNumber = showTableColumnNumber;
+        }
+
+        public boolean isShowInfoTree() {
+            return showInfoTree;
+        }
+
+        public void setShowInfoTree(boolean showInfoTree) {
+            this.showInfoTree = showInfoTree;
+        }
+
+        public boolean isAlwaysOnTop() {
+            return alwaysOnTop;
+        }
+
+        public void setAlwaysOnTop(boolean alwaysOnTop) {
+            this.alwaysOnTop = alwaysOnTop;
+        }
+
+        public String getAutoAdjustMode() {
+            return autoAdjustMode;
+        }
+
+        public void setAutoAdjustMode(String autoAdjustMode) {
+            this.autoAdjustMode = autoAdjustMode;
+        }
+
+        public String getPostProcessMode() {
+            return postProcessMode;
+        }
+
+        public void setPostProcessMode(String postProcessMode) {
+            this.postProcessMode = postProcessMode;
+        }
+
+    }
+
+    private void changeFont(String family, int style, double sizeRate) {
+        FontControlLookAndFeel.change(family, style, sizeRate);
+        SwingUtilities.updateComponentTreeUI(op);
+        Font newfont = textArea.getFont();
+        if (newfont != null) {
+            statusBar.setFont(newfont.deriveFont(newfont.getSize() * 0.8f));
+        }
+    }
+
+    void handleError(Throwable th) {
+        log.error(th);
+        op.showErrorDialog(th);
+    }
+
+    /**
+     * Changes key binds.
+     * @param keyBindInfos key bind infos (info is String value of KeyStroke)
+     */
+    static void changeKeyBinds(List<String> keyBindInfos) {
+        final Pattern p = Pattern.compile("\\s*([^=\\s]+)\\s*=(.*)");
+        List<String> errorInfos = new ArrayList<String>();
+        for (String s : keyBindInfos) {
+            if (s.trim().length() == 0 || s.matches("\\s*#.*")) {
+                continue;
+            }
+            Matcher m = p.matcher(s);
+            if (m.matches()) {
+                try {
+                    Item item = Item.valueOf(m.group(1));
+                    KeyStroke keyStroke = getKeyStroke(m.group(2));
+                    for (WindowLauncher instance : instances) {
+                        instance.menu.setAccelerator(item, keyStroke);
+                    }
+                } catch (Exception ex) {
+                    errorInfos.add(s);
+                }
+            } else {
+                errorInfos.add(s);
+            }
+        }
+        if (!errorInfos.isEmpty()) {
+            throw new IllegalArgumentException(errorInfos.toString());
+        }
+    }
+
+    static void invoke() {
+        invoke(new WindowLauncher());
+    }
+
+    static void invoke(WindowLauncher instance) {
+        final Environment env = new Environment();
+        env.setOutputProcessor(new WindowOutputProcessor.Bypass(instance.op));
+        instance.launch(env);
+        // applies key binds
+        try {
+            File keyBindConf = new File(Bootstrap.getDirectory(), "keybind.conf");
+            if (keyBindConf.exists()) {
+                List<String> a = new ArrayList<String>();
+                Scanner scanner = new Scanner(keyBindConf);
+                try {
+                    while (scanner.hasNextLine()) {
+                        a.add(scanner.nextLine());
+                    }
+                } finally {
+                    scanner.close();
+                }
+                changeKeyBinds(a);
+            }
+        } catch (Exception ex) {
+            instance.handleError(ex);
+        }
+    }
+
+    /**
+     * Starts to exit application.
+     */
+    static void exit() {
+        for (WindowLauncher instance : new ArrayList<WindowLauncher>(instances)) {
+            try {
+                instance.close();
+            } catch (Exception ex) {
+                log.warn(ex, "error occurred when closing all instances");
+            }
+        }
+    }
+
+    /**
+     * Closes this window.
+     */
+    void close() {
+        instances.remove(this);
+        try {
+            env.release();
+            saveConfiguration();
+            executorService.shutdown();
+        } finally {
+            op.dispose();
+        }
+    }
+
+    /**
+     * Confirms whether pressed YES or not at dialog.
+     * @param message
+     * @return true if pressed YES
+     */
+    private boolean confirmYes(String message) {
+        return showConfirmDialog(op, message, "", YES_NO_OPTION) == YES_OPTION;
+    }
+
+    private boolean confirmCommitable() {
+        if (env.getCurrentConnection() == null) {
+            showMessageDialog(op, res.get("w.not-connect"), null, OK_OPTION);
+            return false;
+        }
+        if (env.getCurrentConnector().isReadOnly()) {
+            showMessageDialog(op, res.get("w.connector-readonly"), null, OK_OPTION);
+            return false;
+        }
+        return true;
+    }
+
+    private void retrieveHistory(int value) {
+        if (historyList.isEmpty()) {
+            return;
+        }
+        historyIndex += value;
+        if (historyIndex >= historyList.size()) {
+            historyIndex = 0;
+        } else if (historyIndex < 0) {
+            historyIndex = historyList.size() - 1;
+        }
+        textArea.replace(historyList.get(historyIndex));
+        final int endPosition = textArea.getEndPosition();
+        textArea.setSelectionStart(endPosition);
+        textArea.moveCaretPosition(endPosition);
+        textArea.requestFocus();
+    }
+
+    /**
+     * Requests to close this window.
+     */
+    void requestClose() {
+        if (instances.size() == 1) {
+            requestExit();
+        } else if (env.getCurrentConnection() == null || confirmYes(res.get("i.confirm-close"))) {
+            close();
+        }
+    }
+
+    /**
+     * Requests to exit this application.
+     */
+    void requestExit() {
+        if (confirmYes(res.get("i.confirm-quit"))) {
+            exit();
+        }
+    }
+
+    private void refreshResult() {
+        if (resultSetTable.getModel() instanceof ResultSetTableModel) {
+            ResultSetTableModel m = resultSetTable.getResultSetTableModel();
+            if (m.isSameConnection(env.getCurrentConnection())) {
+                final String s = m.getCommandString();
+                if (s != null && s.length() > 0) {
+                    executeCommand(s);
+                }
+            }
+        }
+    }
+
+    private void editEncryptionKey() {
+        JPasswordField password = new JPasswordField(20);
+        Object[] a = {res.get("i.input-encryption-key"), password};
+        if (showConfirmDialog(op, a, null, OK_CANCEL_OPTION) == OK_OPTION) {
+            CipherPassword.setSecretKey(String.valueOf(password.getPassword()));
+        }
+    }
+
+    private void editConnectorMap() {
+        env.updateConnectorMap();
+        if (env.getCurrentConnector() != null) {
+            showMessageDialog(op, res.get("i.reconnect-after-edited-current-connector"));
+        }
+        ConnectorMapEditDialog dialog = new ConnectorMapEditDialog(op, env);
+        dialog.pack();
+        dialog.setModal(true);
+        dialog.setLocationRelativeTo(op);
+        dialog.setVisible(true);
+        env.updateConnectorMap();
+    }
+
+    private static void showHelp() {
+        final String htmlFileName = "MANUAL_ja.html";
+        if (Desktop.isDesktopSupported()) {
+            Desktop desktop = Desktop.getDesktop();
+            if (desktop.isSupported(Desktop.Action.OPEN)) {
+                File file = new File(htmlFileName);
+                if (file.exists()) {
+                    try {
+                        desktop.open(file);
+                    } catch (IOException ex) {
+                        throw new RuntimeException(ex);
+                    }
+                }
+            }
+        } else {
+            final String msg = res.get("e.cannot-open-help-automatically", htmlFileName);
+            WindowOutputProcessor.showInformationMessageDialog(getRootFrame(), msg, "");
+        }
+    }
+
+    /**
+     * Executes a command.
+     * @param commandString
+     */
+    void executeCommand(String commandString) {
+        assert commandString != null;
+        if (!commandString.equals(textArea.getEditableText())) {
+            textArea.replace(commandString);
+        }
+        op.output("");
+        if (commandString.trim().length() == 0) {
+            doPostProcess();
+        } else {
+            final String cmd = commandString;
+            final Environment env = this.env;
+            final DatabaseInfoTree infoTree = this.infoTree;
+            final JLabel statusBar = this.statusBar;
+            final OutputProcessor opref = env.getOutputProcessor();
+            try {
+                doPreProcess();
+                executorService.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        Connection conn = env.getCurrentConnection();
+                        long time = System.currentTimeMillis();
+                        if (!Command.invoke(env, cmd)) {
+                            exit();
+                        }
+                        if (infoTree.isEnabled()) {
+                            try {
+                                if (env.getCurrentConnection() != conn) {
+                                    infoTree.clear();
+                                    if (env.getCurrentConnection() != null) {
+                                        infoTree.refreshRoot(env);
+                                    }
+                                }
+                            } catch (Throwable th) {
+                                handleError(th);
+                            }
+                        }
+                        if (env.getOutputProcessor() == opref) {
+                            time = System.currentTimeMillis() - time;
+                            statusBar.setText(res.get("i.statusbar-message", time / 1000f, cmd));
+                            AnyAction invoker = new AnyAction(this);
+                            invoker.doLater("callDoPostProcess");
+                        }
+                    }
+                    @SuppressWarnings("unused")
+                    void callDoPostProcess() {
+                        WindowLauncher.this.doPostProcess();
+                    }
+                });
+            } catch (Exception ex) {
+                throw new RuntimeException(ex);
+            } finally {
+                historyIndex = historyList.size();
+            }
+            if (historyList.contains(commandString)) {
+                historyList.remove(commandString);
+            }
+            historyList.add(commandString);
+        }
+        historyIndex = historyList.size();
+    }
+
+    void doPreProcess() {
+        ((Menu)op.getJMenuBar()).setEnabledStates(true);
+        resultSetTable.setEnabled(false);
+        textArea.setEnabled(false);
+        op.repaint();
+    }
+
+    void doPostProcess() {
+        ((Menu)op.getJMenuBar()).setEnabledStates(false);
+        resultSetTable.setEnabled(true);
+        textArea.setEnabled(true);
+        op.output(new Prompt(env));
+        op.doPostProcess();
+    }
+
+    static void wakeup() {
+        for (WindowLauncher instance : new ArrayList<WindowLauncher>(instances)) {
+            try {
+                SwingUtilities.updateComponentTreeUI(instance.op);
+            } catch (Exception ex) {
+                log.warn(ex);
+            }
+        }
+        log.info("wake up");
+    }
+
+    private static final class WakeupTimerTask extends TimerTask {
+        private final AnyAction aa = new AnyAction(this);
+        @Override
+        public void run() {
+            aa.doLater("callWakeup");
+        }
+        @SuppressWarnings("unused")
+        void callWakeup() {
+            wakeup();
+        }
+    }
+
+    /**
+     * (entry point)
+     * @param args
+     */
+    public static void main(String... args) {
+        final int residentCycle = Bootstrap.getPropertyAsInt("net.argius.stew.ui.window.resident",
+                                                             0);
+        if (residentCycle > 0) {
+            final long msec = residentCycle * 60000L;
+            Timer timer = new Timer(true);
+            timer.scheduleAtFixedRate(new WakeupTimerTask(), msec, msec);
+        }
+        EventQueue.invokeLater(new WindowLauncher());
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/WindowOutputProcessor.java b/src/net/argius/stew/ui/window/WindowOutputProcessor.java
new file mode 100644 (file)
index 0000000..a201bc6
--- /dev/null
@@ -0,0 +1,469 @@
+package net.argius.stew.ui.window;
+
+import static java.awt.EventQueue.invokeLater;
+import static javax.swing.JOptionPane.*;
+import static net.argius.stew.Bootstrap.getPropertyAsInt;
+import static net.argius.stew.ui.window.AnyActionKey.*;
+import static net.argius.stew.ui.window.Utilities.getImageIcon;
+import static net.argius.stew.ui.window.Utilities.sleep;
+
+import java.awt.*;
+import java.io.*;
+import java.sql.*;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+import javax.swing.table.*;
+
+import net.argius.stew.*;
+import net.argius.stew.io.*;
+import net.argius.stew.ui.*;
+
+/**
+ * The OutputProcessor Implementation for GUI(Swing).
+ */
+final class WindowOutputProcessor extends JFrame implements OutputProcessor, AnyActionListener {
+
+    private static final Logger log = Logger.getLogger(WindowOutputProcessor.class);
+    private static final ResourceManager res = ResourceManager.getInstance(WindowOutputProcessor.class);
+
+    private final AnyAction invoker;
+
+    private ResultSetTable resultSetTable;
+    private ConsoleTextArea textArea;
+
+    private Environment env;
+    private File currentDirectory;
+    private String postProcessMode;
+
+    WindowOutputProcessor(WindowLauncher launcher,
+                          ResultSetTable resultSetTable,
+                          ConsoleTextArea textArea) {
+        this.resultSetTable = resultSetTable;
+        this.textArea = textArea;
+        this.invoker = new AnyAction(this);
+    }
+
+    @Override
+    public void output(final Object o) {
+        try {
+            if (o instanceof ResultSet) {
+                outputResult(new ResultSetReference((ResultSet)o, ""));
+                return;
+            } else if (o instanceof ResultSetReference) {
+                outputResult((ResultSetReference)o);
+                return;
+            }
+        } catch (SQLException ex) {
+            throw new RuntimeException("WindowOutputProcessor", ex);
+        }
+        final String message;
+        if (o instanceof Prompt) {
+            message = o.toString();
+        } else {
+            message = String.format("%s%n", o);
+        }
+        AnyAction aa4text = new AnyAction(textArea);
+        AnyActionEvent ev = new AnyActionEvent(this,
+                                               ConsoleTextArea.ActionKey.outputMessage,
+                                               replaceEOL(message));
+        aa4text.doLater("anyActionPerformed", ev);
+    }
+
+    @Override
+    public void close() {
+        dispose();
+    }
+
+    @Override
+    public void anyActionPerformed(AnyActionEvent ev) {
+        log.atEnter("anyActionPerformed", ev);
+        try {
+            if (ev.isAnyOf(importFile)) {
+                importIntoCurrentTable();
+            } else if (ev.isAnyOf(exportFile)) {
+                exportTableContent();
+            } else if (ev.isAnyOf(showAbout)) {
+                showVersionInfo();
+            } else {
+                log.warn("not expected: Event=%s", ev);
+            }
+        } catch (Exception ex) {
+            log.error(ex);
+            showErrorDialog(ex);
+        }
+        log.atExit("anyActionPerformed");
+    }
+
+    void setEnvironment(Environment env) {
+        this.env = env;
+        if (currentDirectory == null) {
+            setCurrentDirectory(env.getCurrentDirectory());
+        }
+    }
+
+    /**
+     * Outputs a result.
+     * @param ref
+     * @throws SQLException
+     */
+    void outputResult(ResultSetReference ref) throws SQLException {
+        // NOTICE: This method will be called by non AWT thread.
+        // To access GUI, use "Later".
+        final OutputProcessor opref = env.getOutputProcessor();
+        invoker.doLater("clearResultSetTable");
+        ResultSet rs = ref.getResultSet();
+        ColumnOrder order = ref.getOrder();
+        final boolean needsOrderChange = order.size() > 0;
+        ResultSetMetaData meta = rs.getMetaData();
+        final int columnCount = (needsOrderChange) ? order.size() : meta.getColumnCount();
+        final ResultSetTableModel m = new ResultSetTableModel(ref);
+        Vector<Object> v = new Vector<Object>(columnCount);
+        ValueTransporter transfer = ValueTransporter.getInstance("");
+        final int limit = Bootstrap.getPropertyAsInt("net.argius.stew.rowcount.limit",
+                                                     Integer.MAX_VALUE);
+        int rowCount = 0;
+        while (rs.next()) {
+            if (rowCount >= limit) {
+                invoker.doLater("notifyOverLimit", limit);
+                break;
+            }
+            ++rowCount;
+            v.clear();
+            for (int i = 0; i < columnCount; i++) {
+                final int index = needsOrderChange ? order.getOrder(i) : i + 1;
+                v.add(transfer.getObject(rs, index));
+            }
+            m.addRow((Vector<?>)v.clone());
+            if (env.getOutputProcessor() != opref) {
+                throw new SQLException("interrupted");
+            }
+        }
+        invoker.doLater("showResult", m);
+        ref.setRecordCount(m.getRowCount());
+    }
+
+    @SuppressWarnings("unused")
+    private void clearResultSetTable() {
+        resultSetTable.setVisible(false);
+        resultSetTable.getTableHeader().setVisible(false);
+        ((DefaultTableModel)resultSetTable.getModel()).setRowCount(0);
+    }
+
+    @SuppressWarnings("unused")
+    private void notifyOverLimit(int limit) {
+        output(res.get("w.exceeded-limit", limit));
+    }
+
+    @SuppressWarnings("unused")
+    private void showResult(ResultSetTableModel m) {
+        resultSetTable.setModel(m);
+        Container p = resultSetTable.getParent();
+        if (p != null && p.getParent() instanceof JScrollPane) {
+            JScrollPane scrollPane = (JScrollPane)p.getParent();
+            ImageIcon icon = getImageIcon(String.format("linkable-%s.png", m.isLinkable()));
+            scrollPane.setCorner(ScrollPaneConstants.UPPER_LEFT_CORNER,
+                                 new JLabel(icon, SwingConstants.CENTER));
+        }
+        resultSetTable.anyActionPerformed(new AnyActionEvent(this, AnyActionKey.adjustColumnWidth));
+        resultSetTable.getTableHeader().setVisible(true);
+        resultSetTable.doLayout();
+        resultSetTable.setVisible(true);
+    }
+
+    String getPostProcessMode() {
+        return postProcessMode;
+    }
+
+    void setPostProcessMode(String postProcessMode) {
+        final String oldValue = this.postProcessMode;
+        this.postProcessMode = postProcessMode;
+        firePropertyChange("postProcessMode", oldValue, postProcessMode);
+    }
+
+    void doPostProcess() {
+        final JTextArea textArea = this.textArea;
+        final Runnable focusAction = new Runnable() {
+            @Override
+            public void run() {
+                requestFocus();
+                textArea.requestFocusInWindow();
+            }
+        };
+        if (isActive()) {
+            invokeLater(focusAction);
+            return;
+        }
+        final String prefix = getClass().getName() + ".postprocess.";
+        final int count = getPropertyAsInt(prefix + "count", 32);
+        final int range = getPropertyAsInt(prefix + "range", 2);
+        final long interval = getPropertyAsInt(prefix + "interval", 50);
+        switch (AnyActionKey.of(postProcessMode)) {
+            case postProcessModeNone:
+                break;
+            case postProcessModeFocus:
+                invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        toFront();
+                        focusAction.run();
+                    }
+                });
+                break;
+            case postProcessModeShake:
+                DaemonThreadFactory.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        final Runnable shakeAction = new Runnable() {
+                            int sign = -1;
+                            @Override
+                            public void run() {
+                                sign *= -1;
+                                setLocation(getX() + range * sign, getY());
+                            }
+                        };
+                        invokeLater(new Runnable() {
+                            @Override
+                            public void run() {
+                                textArea.requestFocusInWindow();
+                            }
+                        });
+                        for (int i = 0, n = count >> 1 << 1; i < n; i++) {
+                            invokeLater(shakeAction);
+                            sleep(interval);
+                        }
+                    }
+                });
+                break;
+            case postProcessModeBlink:
+                DaemonThreadFactory.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        final byte[] alpha = {0};
+                        final JPanel p = new JPanel() {
+                            @Override
+                            protected void paintComponent(Graphics g) {
+                                super.paintComponent(g);
+                                g.setColor(new Color(0, 0xE6, 0x2E, alpha[0] & 0xFF));
+                                g.fillRect(0, 0, getWidth(), getHeight());
+                            }
+                        };
+                        invokeLater(new Runnable() {
+                            @Override
+                            public void run() {
+                                textArea.requestFocusInWindow();
+                                setGlassPane(p);
+                                p.setOpaque(false);
+                                p.setVisible(true);
+                            }
+                        });
+                        for (int i = 0, n = (count / 45 + 1) * 45; i < n; i++) {
+                            alpha[0] = (byte)((Math.sin(i * 0.25f) + 1) * 32 * range);
+                            p.repaint();
+                            sleep(interval);
+                        }
+                        invokeLater(new Runnable() {
+                            @Override
+                            public void run() {
+                                p.setVisible(false);
+                                remove(p);
+                            }
+                        });
+                    }
+                });
+                break;
+            default:
+                log.warn("doPostProcess: postProcessMode=%s", postProcessMode);
+        }
+    }
+
+    /**
+     * Imports data into the current table.
+     * @throws IOException
+     * @throws SQLException
+     */
+    void importIntoCurrentTable() throws IOException, SQLException {
+        if (env.getCurrentConnection() == null) {
+            showMessageDialog(this, res.get("w.not-connect"));
+            return;
+        }
+        TableModel tm = resultSetTable.getModel();
+        final boolean importable;
+        if (tm instanceof ResultSetTableModel) {
+            ResultSetTableModel m = (ResultSetTableModel)tm;
+            importable = m.isLinkable() && m.isSameConnection(env.getCurrentConnection());
+        } else {
+            importable = false;
+        }
+        if (!importable) {
+            showMessageDialog(this, res.get("w.import-target-not-available"));
+            return;
+        }
+        ResultSetTableModel m = (ResultSetTableModel)tm;
+        assert currentDirectory != null;
+        JFileChooser fileChooser = new JFileChooser(currentDirectory);
+        fileChooser.setDialogTitle(res.get("Action.import"));
+        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        fileChooser.showOpenDialog(this);
+        final File file = fileChooser.getSelectedFile();
+        if (file == null) {
+            return;
+        }
+        setCurrentDirectory(file);
+        Importer importer = Importer.getImporter(file);
+        try {
+            while (true) {
+                Object[] row = importer.nextRow();
+                if (row.length == 0) {
+                    break;
+                }
+                m.addUnlinkedRow(row);
+                m.linkRow(m.getRowCount() - 1);
+            }
+        } finally {
+            importer.close();
+        }
+    }
+
+    /**
+     * Exports data in this table.
+     * @throws IOException
+     */
+    void exportTableContent() throws IOException {
+        assert currentDirectory != null;
+        JFileChooser fileChooser = new JFileChooser(currentDirectory);
+        fileChooser.setDialogTitle(res.get("Action.export"));
+        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        fileChooser.showSaveDialog(this);
+        final File file = fileChooser.getSelectedFile();
+        if (file == null) {
+            return;
+        }
+        setCurrentDirectory(file);
+        if (file.exists()) {
+            if (showConfirmDialog(this, res.get("i.confirm-overwrite", file), null, YES_NO_OPTION) != YES_OPTION) {
+                return;
+            }
+        }
+        Exporter exporter = Exporter.getExporter(file);
+        try {
+            TableColumnModel columnModel = resultSetTable.getTableHeader().getColumnModel();
+            List<Object> headerValues = new ArrayList<Object>();
+            for (TableColumn column : Collections.list(columnModel.getColumns())) {
+                headerValues.add(column.getHeaderValue());
+            }
+            exporter.addHeader(headerValues.toArray());
+            DefaultTableModel m = (DefaultTableModel)resultSetTable.getModel();
+            @SuppressWarnings("unchecked")
+            Vector<Vector<Object>> rows = m.getDataVector();
+            for (Vector<Object> row : rows) {
+                exporter.addRow(row.toArray());
+            }
+        } finally {
+            exporter.close();
+        }
+        showMessageDialog(this, res.get("i.exported"));
+    }
+
+    private void showVersionInfo() {
+        ImageIcon icon = new ImageIcon();
+        if (getIconImage() != null) {
+            icon.setImage(getIconImage());
+        }
+        final String about = res.get(".about", Bootstrap.getVersion());
+        showMessageDialog(this, about, null, PLAIN_MESSAGE, icon);
+    }
+
+    private void setCurrentDirectory(File file) {
+        final File dir;
+        if (file.isDirectory()) {
+            dir = file;
+        } else {
+            // when the file object is file, uses its parent's dir.
+            dir = file.getParentFile();
+        }
+        assert dir.isDirectory();
+        // I am optimistic about no-sync ...
+        currentDirectory = dir;
+    }
+
+    void showInformationMessageDialog(String message, String title) {
+        showInformationMessageDialog(this, message, title);
+    }
+
+    static void showInformationMessageDialog(final Component parent, String message, String title) {
+        JTextArea textArea = new JTextArea(message, 6, 60);
+        setupReadOnlyTextArea(textArea);
+        showMessageDialog(parent, new JScrollPane(textArea), title, INFORMATION_MESSAGE);
+    }
+
+    void showErrorDialog(Throwable th) {
+        showErrorDialog(this, th);
+    }
+
+    static void showErrorDialog(final Component parent, final Throwable th) {
+        log.atEnter("showErrorDialog");
+        log.warn(th, "");
+        final String s1;
+        final String s2;
+        if (th == null) {
+            s1 = res.get("e.error-no-detail");
+            s2 = "";
+        } else {
+            s1 = th.getMessage();
+            Writer buffer = new StringWriter();
+            PrintWriter out = new PrintWriter(buffer);
+            th.printStackTrace(out);
+            s2 = replaceEOL(buffer.toString());
+        }
+        JPanel p = new JPanel(new BorderLayout());
+        p.add(new JScrollPane(setupReadOnlyTextArea(new JTextArea(s1, 2, 60))), BorderLayout.NORTH);
+        p.add(new JScrollPane(setupReadOnlyTextArea(new JTextArea(s2, 6, 60))), BorderLayout.CENTER);
+        JDialog d = (new JOptionPane(p, ERROR_MESSAGE)).createDialog(parent, res.get("e.error"));
+        d.setResizable(true);
+        d.setVisible(true);
+        d.dispose();
+        log.atExit("showErrorDialog");
+    }
+
+    private static String replaceEOL(String s) {
+        return s.replaceAll("\\\r\\\n?", "\n");
+    }
+
+    static JTextArea setupReadOnlyTextArea(JTextArea textArea) {
+        textArea.setEditable(false);
+        textArea.setWrapStyleWord(false);
+        textArea.setLineWrap(false);
+        textArea.setOpaque(false);
+        textArea.setMargin(new Insets(4, 4, 4, 4));
+        return textArea;
+    }
+
+    /**
+     * Bypass OutputProcessor for breaking command.
+     */
+    static final class Bypass implements OutputProcessor {
+
+        private OutputProcessor op;
+        private volatile boolean closed;
+
+        Bypass(OutputProcessor op) {
+            this.op = op;
+        }
+
+        @Override
+        public void output(Object object) {
+            if (!closed) {
+                op.output(object);
+            }
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+
+    }
+
+}
diff --git a/src/net/argius/stew/ui/window/icon/close.png b/src/net/argius/stew/ui/window/icon/close.png
new file mode 100644 (file)
index 0000000..f2393e3
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/close.png differ
diff --git a/src/net/argius/stew/ui/window/icon/linkable-false.png b/src/net/argius/stew/ui/window/icon/linkable-false.png
new file mode 100644 (file)
index 0000000..fa4a430
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/linkable-false.png differ
diff --git a/src/net/argius/stew/ui/window/icon/linkable-true.png b/src/net/argius/stew/ui/window/icon/linkable-true.png
new file mode 100644 (file)
index 0000000..66028d9
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/linkable-true.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-catalog.png b/src/net/argius/stew/ui/window/icon/node-catalog.png
new file mode 100644 (file)
index 0000000..09f97ed
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-catalog.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-column.png b/src/net/argius/stew/ui/window/icon/node-column.png
new file mode 100644 (file)
index 0000000..0318300
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-column.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-connector.png b/src/net/argius/stew/ui/window/icon/node-connector.png
new file mode 100644 (file)
index 0000000..dba8bca
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-connector.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-schema.png b/src/net/argius/stew/ui/window/icon/node-schema.png
new file mode 100644 (file)
index 0000000..cc39939
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-schema.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-table.png b/src/net/argius/stew/ui/window/icon/node-table.png
new file mode 100644 (file)
index 0000000..b4fa728
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-table.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-tabletype-.png b/src/net/argius/stew/ui/window/icon/node-tabletype-.png
new file mode 100644 (file)
index 0000000..b8bd68c
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-tabletype-.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-tabletype-INDEX.png b/src/net/argius/stew/ui/window/icon/node-tabletype-INDEX.png
new file mode 100644 (file)
index 0000000..e72346a
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-tabletype-INDEX.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-tabletype-SEQUENCE.png b/src/net/argius/stew/ui/window/icon/node-tabletype-SEQUENCE.png
new file mode 100644 (file)
index 0000000..7eedd0e
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-tabletype-SEQUENCE.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-tabletype-TABLE.png b/src/net/argius/stew/ui/window/icon/node-tabletype-TABLE.png
new file mode 100644 (file)
index 0000000..f8499ab
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-tabletype-TABLE.png differ
diff --git a/src/net/argius/stew/ui/window/icon/node-tabletype-VIEW.png b/src/net/argius/stew/ui/window/icon/node-tabletype-VIEW.png
new file mode 100644 (file)
index 0000000..994c7e0
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/node-tabletype-VIEW.png differ
diff --git a/src/net/argius/stew/ui/window/icon/stew.png b/src/net/argius/stew/ui/window/icon/stew.png
new file mode 100644 (file)
index 0000000..c9db609
Binary files /dev/null and b/src/net/argius/stew/ui/window/icon/stew.png differ
diff --git a/src/net/argius/stew/ui/window/messages.u8p b/src/net/argius/stew/ui/window/messages.u8p
new file mode 100644 (file)
index 0000000..a96566f
--- /dev/null
@@ -0,0 +1,53 @@
+i.cancelled=コマンドがキャンセルされました。\n(サーバ側の処理はキャンセルされていません。)
+i.choose-connection=接続を選択
+i.confirm-close=ウィンドウを閉じてよろしいですか?
+i.confirm-commit=コミットします。よろしいですか?
+i.confirm-overwrite=ファイル {0} は存在します。\n上書きしますか?
+i.confirm-quit=終了してよろしいですか?
+i.confirm-remove=削除してよろしいですか?
+i.confirm-rollback=ロールバックします。よろしいですか?
+i.confirm-save=保存してよろしいですか?
+i.confirm-without-register=登録せずに閉じてよろしいですか?
+i.confirm-without-save=保存せずに閉じてよろしいですか?
+i.exported=エクスポートされました。
+i.help-see-release-package=\
+ヘルプ機能は実装されていません。\n\n\
+簡易ヘルプについては、インストールパッケージ内の\n\
+Manual_ja.htmlを参照してください。
+i.input-encryption-key=暗号鍵を入力
+i.input-new-connector-id=新しいコネクタのIDを入力
+i.paren-in-processing=(処理中)
+i.reconnect-after-edited-current-connector=接続中の設定を変更する場合は、変更後に再接続してください。
+i.statusbar-message=\ 実行時間 {0} 秒 ( {1} )
+w.connector-readonly=コネクタは読取専用です。
+w.import-target-not-available=インポート先が無効です。
+w.no-connector=接続設定がありません。
+w.not-connect=接続されていません。
+e.cannot-open-help-automatically=Can't open help({0}) automatically on this platform.
+e.enables-select-just-1-table=テーブルは1つだけ選択できます。
+e.error=エラー
+e.error-no-detail=エラー(詳細不明)
+e.file-not-exists=ファイル {0} は存在しません。
+e.id-already-exists=ID [{0}] は既に存在します。
+
+# common menu items
+item.cut=Cut
+item.cut.mnemonic=T
+item.cut.shortcut=ctrl X
+item.copy=Copy
+item.copy.mnemonic=C
+item.copy.shortcut=ctrl C
+item.paste=Paste
+item.paste.mnemonic=P
+item.paste.shortcut=ctrl V
+item.selectAll=Select All
+item.selectAll.mnemonic=A
+item.selectAll.shortcut=ctrl A
+item.undo=Undo
+item.undo.mnemonic=U
+item.undo.shortcut=ctrl Z
+item.redo=Redo
+item.redo.mnemonic=R
+item.redo.shortcut=ctrl Y
+item.refresh=Refresh
+item.refresh.mnemonic=R
diff --git a/src/net/argius/stew/ui/window/messages_ja.u8p b/src/net/argius/stew/ui/window/messages_ja.u8p
new file mode 100644 (file)
index 0000000..eb94bf4
--- /dev/null
@@ -0,0 +1,40 @@
+i.cancelled=コマンドがキャンセルされました。\n(サーバ側の処理はキャンセルされていません。)
+i.choose-connection=接続を選択
+i.confirm-close=ウィンドウを閉じてよろしいですか?
+i.confirm-commit=コミットします。よろしいですか?
+i.confirm-overwrite=ファイル {0} は存在します。\n上書きしますか?
+i.confirm-quit=終了してよろしいですか?
+i.confirm-remove=削除してよろしいですか?
+i.confirm-rollback=ロールバックします。よろしいですか?
+i.confirm-save=保存してよろしいですか?
+i.confirm-without-register=登録せずに閉じてよろしいですか?
+i.confirm-without-save=保存せずに閉じてよろしいですか?
+i.exported=エクスポートされました。
+i.help-see-release-package=\
+ヘルプ機能は実装されていません。\n\n\
+簡易ヘルプについては、インストールパッケージ内の\n\
+Manual_ja.htmlを参照してください。
+i.input-encryption-key=暗号鍵を入力
+i.input-new-connector-id=新しいコネクタのIDを入力
+i.paren-in-processing=(処理中)
+i.reconnect-after-edited-current-connector=接続中の設定を変更する場合は、変更後に再接続してください。
+i.statusbar-message=\ 実行時間 {0} 秒 ( {1} )
+w.connector-readonly=コネクタは読取専用です。
+w.import-target-not-available=インポート先が無効です。
+w.no-connector=接続設定がありません。
+w.not-connect=接続されていません。
+e.cannot-open-help-automatically=このプラットフォームでは自動でヘルプ({0})を開けません。\r
+e.enables-select-just-1-table=テーブルは1つだけ選択できます。
+e.error-no-detail=エラー(詳細不明)
+e.error=エラー
+e.file-not-exists=ファイル {0} は存在しません。
+e.id-already-exists=ID [{0}] は既に存在します。
+
+# common menu items
+item.cut=切り取り
+item.copy=コピー
+item.paste=貼り付け
+item.selectAll=すべて選択
+item.undo=元に戻す
+item.redo=やり直し
+item.refresh=最新状態に更新
diff --git a/src/net/argius/stew/version b/src/net/argius/stew/version
new file mode 100644 (file)
index 0000000..5b76b8c
--- /dev/null
@@ -0,0 +1 @@
+4.0.0-beta1
\ No newline at end of file