STU - 機能拡張マニュアル

for STU ver. 1.00

index

ユーザーマニュアルを開く

このマニュアルについて

STUは、いくつかの機能拡張を可能にしています。このマニュアルでは、機能拡張の具体的な方法について説明しています。

拡張可能な各機能の概要を次に示します。

コマンドの追加
任意のコマンドを追加することができます。追加したコマンドでは、入力は接続中コネクションとコマンドライン文字列が、出力は実行結果出力機能とメッセージ表示機能がそれぞれ提供されます。
入出力システムのカスタマイズ
既定では、入出力システムは標準入出力を使用しますが、「入力機能」「実行結果出力機能」「メッセージ表示機能」の3つの機能について、カスタマイズすることができます。
有限件数SQL編集部品の追加
有限件数SQL編集部品とは、与えられたSQL(SELECT文)に対して件数の上限を指定する記述を追加編集する部品です。

機能拡張

機能拡張の詳細について説明します。

STUのAPIについては、APIドキュメント(Javadoc)をご覧ください。

※クラスを追加する際には、クラスパスに注意してください。

コマンド

任意のコマンドを追加できます。

追加するクラスは、パッケージnet.argius.stu.commandに属するようにします。net.argius.stu.Commandクラスを継承し、execute()メソッドをオーバーライドするだけで実装完了です。

クラス名は、頭文字のみ大文字で、それ以外は小文字にしてください。コマンド名はケースが無視されますので、クラスを検索する際にケースを一意にする必要があるためです。

基本的な実装方法

次のサンプルは、Userコマンドの実装です。

Userクラス(User.java

package net.argius.stu.command;

import java.io.*;
import java.sql.*;

import net.argius.stu.*;

public class User extends Command {

    protected void execute(Connection conn, String parameter) 
        throws IllegalArgumentException, IOException, SQLException {

        DatabaseMetaData dbmd = conn.getMetaData();
        String userName = dbmd.getUserName();

        println("INPUT    : " + parameter);
        println("UserName : " + userName);

    }

}

Userの実行結果

 > user
INPUT    : user
UserName : TEST
 > user 1 2 3
INPUT    : user 1 2 3
UserName : TEST
 >

このコマンドでは、DBコネクションのユーザ名を表示します。connは、現在接続中のコネクションです。parameterは、プロンプトから入力されたままの文字列が渡されます。

エラーが発生した場合、Exceptionをメソッド外にスローします。スローされた例外は、システム側で処理されます。以下は、ここで扱う例外についての説明です。

java.lang.IllegalArgumentException
入力された引数が正しくない場合にUSAGEをセットしてスローすると、エラーメッセージが「使い方:User xxx yyy」のように表示されます。
java.io.IOException
通常はJavaの入出力API(java.ioパッケージ)などから報告されるエラーです。例外が報告されないエラーについても、入出力に関連するエラーはこの例外で報告するようにしてください。
java.sql.SQLException
通常はJDBCのAPIから報告されるエラーです。例外が報告されないエラー(例えば、nullが返されたため処理が続行不能になった場合など)についても、DB操作に関連するエラーはこの例外で報告するようにしてください。
java.lang.RuntimeException
上記以外のエラーはこの例外を使用します。

応用的な実装方法

追加するコマンドのクラスは、公開リリースされない限りは、基本的な実装方法以外の制限は特にありません。自由に実装して問題ありません。但し、将来のバージョンアップに備えて、実装済みの機能を使用することを推奨します。

net.argius.stu.Commandクラスは、コマンドが使用する基本的な機能をサブクラスに提供します。また、STUのAPIには、入出力に関するパッケージ(net.argius.stu.io)、JDBCやSQLに関するパッケージ(net.argius.stu.sql)、テキスト編集に関するパッケージ(net.argius.stu.text)があり、これらはシステムに依存しないAPIとなっています。(APIドキュメント参照)

次のサンプルは、Getfunctionsコマンドの実装です。java.sql.DatabaseMetaDataを使用して、DBで使用可能な関数名を取得するコマンドです。

Getfunctionsクラス(Getfunctions.java

package net.argius.stu.command;

import java.io.*;
import java.sql.*;

import net.argius.stu.*;
import net.argius.stu.text.*;

public final class Getfunctions extends Command {

    private static final String USAGE = "[ STRING | NUMBER | NUMERIC | DATE | TIME | SYSTEM ]";
    private static final IllegalArgumentException usageError = new IllegalArgumentException(USAGE);

    protected void execute(Connection conn, String parameter)
        throws IllegalArgumentException, SQLException, IOException {

        StringQueue pq = split(parameter, 3); // Parameter Queue
        pq.draw(); // "GetFunctions"を捨てる
        if (pq.size() == 0) {
            throw usageError;
        }
        String type = pq.draw();

        final String label = "FUNCTIONS : ";
        DatabaseMetaData dbmd = conn.getMetaData();
        if (type.equalsIgnoreCase("STRING")) {
            println(label + dbmd.getStringFunctions());
        } else if (type.equalsIgnoreCase("NUMBER") || type.equalsIgnoreCase("NUMERIC")) {
            println(label + dbmd.getNumericFunctions());
        } else if (type.equalsIgnoreCase("DATE") || type.equalsIgnoreCase("TIME")) {
            println(label + dbmd.getTimeDateFunctions());
        } else if (type.equalsIgnoreCase("SYSTEM")) {
            println(label + dbmd.getSystemFunctions());
        } else {
            throw usageError;
        }

    }

}

Getfunctionsの実行結果(Oracleの場合)

 > getfunctions
使い方:GETFUNCTIONS [ STRING | NUMBER | NUMERIC | DATE | TIME | SYSTEM ]
 > getfunctions string
FUNCTIONS : ASCII,CHAR,CONCAT,LCASE,LENGTH,LTRIM,REPLACE,RTRIM,SOUNDEX,SUBSTRING,UCASE
 > getfunctions date
FUNCTIONS : CURDATE,CURTIME,DAYOFMONTH,HOUR,MINUTE,MONTH,NOW,SECOND,YEAR
 > getfunctions system
FUNCTIONS : USER
 > getfunctions others
使い方:GETFUNCTIONS [ STRING | NUMBER | NUMERIC | DATE | TIME | SYSTEM ]
 > 

次のサンプルは、Groupコマンドの実装です。GROUP BYを含むSQLを自動的に生成して、結果を表示します。結果の表示には、結果出力機能(Command.showResult())を使用しています。

Groupクラス(Group.java

package net.argius.stu.command;

import java.io.*;
import java.sql.*;

import net.argius.stu.*;
import net.argius.stu.text.*;

public class Group extends Command {

    private static final String USAGE = "<table> <group list>";
    private static final IllegalArgumentException usageError = new IllegalArgumentException(USAGE);

    protected void execute(Connection conn, String parameter)
        throws IllegalArgumentException, IOException, SQLException {

        StringQueue pq = split(parameter, -1); // Parameter Queue
        pq.draw(); // "Group"を捨てる
        if (pq.size() <= 0) {
            throw usageError;
        }

        // テーブル名
        String table = pq.draw();
        // グループリスト
        StringBuffer sb = new StringBuffer();
        for (int i = 0; !pq.isEmpty(); i++) {
            if (i != 0) {
                sb.append(", ");
            }
            sb.append(pq.draw());
        }
        String group = sb.toString();

        // SQL作成
        String query = "SELECT " + group + ", count(*) FROM " + table + " GROUP BY " + group;
        println(query);
        writeLog(Log.DEBUG, "query : " + query);

        // SQL発行
        Statement stmt = conn.createStatement();
        try {
            setTimeout(stmt);
            ResultSet rs = stmt.executeQuery(query);
            try {
                // 実行&結果出力
                int recordSize = showResult(rs);
                // メッセージ表示
                if (recordSize > 0) {
                    println(getMessage("selected1", String.valueOf(recordSize)));
                } else {
                    println(getMessage("recordnotfound"));
                }
            } finally {
                rs.close();
            }
        } finally {
            stmt.close();
        }

    }

}

Groupの実行結果

 > group
使い方:GROUP <table> <group list>
 > group dairy_table date
SELECT date, count(*) FROM dairy_table GROUP BY date

DATE     COUNT(*)              
======== ======================
200x0730                      2
200x0731                      3
200x0801                     12
200x0802                      4
200x0803                      7

5 件 ヒットしました。
 > 

推奨事項

入出力システム

STUは、ビューと構造を分離した構造を持っています。

STUでは、入力システムを「スキャナ」、出力システムを「プリンタ」(出力プリンタとエラープリンタ)と呼んでいます。それぞれを入出力システムであるnet.argius.stu.IOManagerの実装サブクラスが統括しています。

入出力システムのカスタマイズは、入力システムのみ、出力システムのみ、エラー出力システムのみ、入出力システムすべての4種類が可能となっています。

※切替えに使用するクラスは、必ずデフォルトコンストラクタだけで初期化ができるように実装してください。

入力のみ変更する

入力は、既定の入出力システムであるnet.argius.stu.DefaultIOManagerの保持しているスキャナによって処理されます。入力された文字列はコマンドに変換されて、コマンドのサブクラスが起動される仕組みになっています。

既定の入力はnet.argius.stu.io.Scannerインターフェイスを実装したnet.argius.stu.io.DefaultScannerとなっています。これを別のクラスに差し替えることができます(ユーザーマニュアル・プロパティ参照)。

次のサンプルは、javax.swing.JFrameを使って入力だけをGUIで行えるようにするGUIOneLinerScannerの実装です。(最低限の実装なので、使い勝手は良くありません。)

GUIOneLinerScannerクラス(GUIOneLinerScanner.java

package net.argius.stu.plugin;

import java.awt.*;
import java.awt.event.*;
import java.io.*;

import javax.swing.*;

import net.argius.stu.io.*;

public final class GUIOneLinerScanner implements Scanner {

    private JFrame frame;
    private JTextField input;
    private JButton button;
    private boolean blockerIsAvailable;
    private boolean blocking;

    // 入力が無いとき何もしないようにブロックする
    private Thread blocker = new Thread() {

        public void run() {
            while (blockerIsAvailable) {
                if (blocking) {
                    getInputMessage();
                }
            }
        }
    };

    // ボタンが押された時のイベントハンドラ
    private ActionListener onSubmit = new ActionListener() {

        public void actionPerformed(ActionEvent e) {
            String s = input.getText();
            if (s != null && s.trim().length() > 0) {
                blocking = false;
                blocker.interrupt();
            }
        }
    };

    public GUIOneLinerScanner() {

        frame = new JFrame("STU - GUIOneLiner");

        input = new JTextField();
        input.setBorder(BorderFactory.createLineBorder(Color.white, 4));

        button = new JButton("実行");
        button.setDefaultCapable(true);
        button.addActionListener(onSubmit);

        JPanel panel = new JPanel(new BorderLayout());
        panel.add(input, BorderLayout.CENTER);
        panel.add(button, BorderLayout.EAST);

        frame.getContentPane().add(panel);
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.show();
        frame.setSize(800, 80);
        frame.validate();

        blockerIsAvailable = true;
        blocking = true;
        blocker.start();

    }

    public String scan() throws IOException {
        String s = getInputMessage();
        blocking = true;
        return s;
    }

    private synchronized String getInputMessage() {
        while (blocking) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        notify();
        return input.getText();
    }

    public void close() {
        blockerIsAvailable = false;
        frame.dispose();
    }
}

出力のみ変更する・エラー出力のみ変更する

出力とエラー出力は、既定の入出力システムであるnet.argius.stu.DefaultIOManagerの保持している出力プリンタとエラープリンタによって処理されます。コマンドは、net.argius.stu.IOManager経由で結果セット(java.sql.ResultSet)の表示や、メッセージの出力を依頼してきます。net.argius.stu.IOManagerのサブクラスであるnet.argius.stu.DefaultIOManagerがこれらの具体的な出力方法を制御します。

既定の出力はnet.argius.stu.io.DefaultOutputPrinter、エラー出力はnet.argius.stu.io.DefaultErrorPrinterとなっています。どちらもnet.argius.stu.io.Printerインターフェイスの実装クラスです。これらを別のクラスに差し替えることができます(ユーザーマニュアル・プロパティ参照)。

次のサンプルは、出力先をカレントディレクトリのテキストファイルに設定するLocalFilePrinterの実装です。

LocalFilePrinterクラス(LocalFilePrinter.java

package net.argius.stu.plugin;

import java.io.*;

import net.argius.stu.io.*;

public final class LocalFilePrinter implements Printer {

    // ファイル名固定
    private static final String FILE_NAME = "./stu.output.log";

    private PrintStream out;

    public LocalFilePrinter() throws IOException {
        out = new PrintStream(new FileOutputStream(FILE_NAME, true), true);
    }

    public void print(String message) {
        out.print(message);
    }

    public void println(String message) {
        out.println(message);
    }

    public void close() {
        out.close();
    }

}

次のサンプルは、javax.swing.JFrameを使って出力先を別ウインドウに設定するJFramePrinterの実装です。

JFramePrinterクラス(JFramePrinter.java

package net.argius.stu.plugin;

import java.awt.Color;
import java.awt.Font;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

import net.argius.stu.io.Printer;
import net.argius.stu.IOManager;

public final class JFramePrinter implements Printer {
    
    private JFrame frame;
    private JTextArea outputArea;
    
    public JFramePrinter() {
        
        frame = new JFrame("STU - OutputPrinter");
        
        outputArea = new JTextArea();
        outputArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
        JScrollPane outputPane = new JScrollPane(outputArea);
        outputPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
        outputPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        outputPane.setViewportBorder(BorderFactory.createLineBorder(Color.white, 4));
        
        frame.getContentPane().add(outputPane);
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.show();
        frame.setSize(800, 600);
        frame.validate();
        
    }
    
    public void print(String message) {
        outputArea.append(message);
    }
    
    public void println(String message) {
        outputArea.append(message);
        outputArea.append(IOManager.getLineSeparator()); // 改行文字
    }
    
    public void close() {
        frame.dispose();
    }
    
}

入出力すべて変更する

冒頭でも説明していますが、STUでは入出力システムであるnet.argius.stu.IOManagerが入出力を統括していて、既定の入出力システムは標準入出力を使用したnet.argius.stu.DefaultIOManagerとなっています。これをnet.argius.stu.IOManager抽象クラスのサブクラスに差し替えることができます(ユーザーマニュアル・プロパティ参照)。

入出力システムを書き換えた場合は、「プリンタ」「スキャナ」を使用する必要はなくなります。その代わりに、入力(scanLineメソッド)、結果出力(showResultメソッド)、メッセージ出力(printMessageメソッド)の3メソッドを実装します。

次のサンプルは、1つのウィンドウに入出力機能をすべて備えたJFrameIOManagerの実装です。このクラスを使えば、GUIのみのプラットフォームでも起動することができます。(これも使い勝手は改善の余地があります。)

JFrameIOManagerクラス(JFrameIOManager.java

package net.argius.stu.plugin;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.sql.*;

import javax.swing.*;

import net.argius.stu.*;
import net.argius.stu.io.*;

public class JFrameIOManager extends IOManager {
    
    private static final Font font = new Font("Monospaced", Font.PLAIN, 12);
    
    private JFrame frame;
    private JTextField commandLine;
    private JButton submit;
    private JTextArea outputArea;
    private JTextArea messageArea;
    
    private boolean blockerIsAvailable;
    private boolean blocking;
    
    // 入力をブロックするスレッド
    private Thread blocker = new Thread() {
        public void run() {
            while (blockerIsAvailable) {
                if (blocking) {
                    waitMessage();
                }
            }
        }
    };
    
    // 実行ボタンを押したときのイベントハンドラ
    private ActionListener onSubmit = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            String s = commandLine.getText();
            if (s.trim().length() == 0) {
                return;
            }
            outputArea.setText("");
            blocking = false;
            blocker.interrupt();
        }
    };
    
    // コンストラクタ
    public JFrameIOManager() {
        
        // フレーム初期化
        frame = new JFrame("STU - JFrameIOManager");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        
        // 入力テキストボックスの定義
        JLabel commandLabel = new JLabel("コマンド");
        commandLine = new JTextField();
        commandLine.setFont(font);
        commandLine.addActionListener(onSubmit);
        
        // 実行ボタンの定義
        submit = new JButton("実行");
        submit.setDefaultCapable(true);
        submit.addActionListener(onSubmit);
        
        // 出力エリアの定義
        outputArea = new JTextArea();
        outputArea.setFont(font);
        JScrollPane outputPane = new JScrollPane(outputArea);
        
        // メッセージエリアの定義
        messageArea = new JTextArea();
        messageArea.setFont(font);
        JScrollPane messagePane = new JScrollPane(messageArea);
        
        // 出力エリアとメッセージエリアのスプリットペイン
        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        splitPane.setTopComponent(outputPane);
        splitPane.setBottomComponent(messagePane);
        splitPane.setResizeWeight(0.7);
        
        // レイアウト
        JPanel p1 = new JPanel(new BorderLayout());
        p1.add(commandLabel, BorderLayout.WEST);
        p1.add(commandLine, BorderLayout.CENTER);
        p1.add(submit, BorderLayout.EAST);
        frame.getContentPane().add(p1, BorderLayout.NORTH);
        frame.getContentPane().add(splitPane, BorderLayout.CENTER);
        
        // 入力待ちスレッドの起動
        blocking = true;
        blockerIsAvailable = true;
        blocker.start();
        
        // 表示
        frame.show();
        frame.setSize(800, 600);
        frame.validate();
        
    }
    
    // 入力スキャンのブロック
    private synchronized void waitMessage() {
        while (blocking) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        notify();
    }
    
    // 入力のスキャン
    public String scanLine() throws IOException {
        waitMessage();
        blocking = true;
        return commandLine.getText();
    }
    
    // 結果出力処理
    public int showResult(ResultSet rs, int limit) throws IOException, SQLException {
        
        // 必須パラメータチェック
        if (rs == null) {
            throw new SQLException("ResultSet is null.");
        }
        // 結果表示の準備
        Printer sp = new StringPrinter();
        TableWriter writer = new NullTableWriter();
        ResultSetMetaData md = rs.getMetaData();
        int cols = md.getColumnCount();
        int[] sizes = new int[cols];
        boolean isFirst = true;
        boolean limited = (limit > 0 ? true : false); 
        
        // メインループ
        int recordSize = 0;
        try {
            while (rs.next()) {
                if (limited && recordSize >= limit) {
                    break;
                }
                ++recordSize;
                if (isFirst) {
                    // 1件目のみ同時にヘッダを生成
                    writer = new PrinterTableWriter(sp, sizes, '-');
                    writer.open();
                    for (int i = 1; i <= cols; i++) {
                        sizes[i-1] = md.getColumnDisplaySize(i);
                        String name = md.getColumnName(i);
                        writer.addColumn(name);
                    }
                    writer.nextRow();
                    isFirst = false;
                }
                // 1レコード出力
                for (int i = 1; i <= cols; i++) {
                    writer.addColumn(rs.getString(i));
                }
                writer.nextRow();
            }
            outputArea.append(sp.toString());
            return recordSize;
        } finally {
            writer.close();
        }
        
    }
    
    // メッセージの表示
    public void printMessage(String message, boolean newLine) throws IOException {
        
        messageArea.append(message);
        if (newLine) {
            messageArea.append(IOManager.getLineSeparator());
        }
        
    }
    
}

有限件数SQL編集部品

STUでは、原則として各DBの実装詳細については個別に対応しないようにしていますが、いくつか例外があります。

SQLには、SELECT文で検索実行する際に取得する件数を制限するキーワードがありますが、これは製品によって異なります。(下図)

PostgreSQL, MySQL など
... SELECT * FROM TEST LIMIT 3

Oracle
... SELECT * FROM TEST WHERE ROWNUM <= 3

Microsoft系 (SQLServer, Access など)
... SELECT TOP 3 * FROM TEST

このキーワードは無駄な検索を避けるために使いたい場合が良くありますが、JDBCのインターフェイスだけでは対応できません。この違いを吸収するために、stu.sql.LimitEditorという編集部品クラスを使用します。

stu.sql.LimitEditorの実装例(stu.sql.PostgreSQLLimitEditor

package stu.sql;

// "stu.sql." + DatabaseMetaData.getDatabaseProductName() + "Editor"
final class PostgreSQLLimitEditor extends LimitEditor {
    
    public String edit(String sql, int limit) {
        StringBuffer sb = new StringBuffer();
        sb.append(sql);
        sb.append(" LIMIT ");
        sb.append(limit);
        return sb.toString();
    }
    
}

stu.sql.LimitEditorの使用例

// conn = java.sql.Connection
LimitEditor editor = LimitEditor.getObject(conn);
String sql = editor.edit("SELECT * FROM TEST", 3);

LimitEditor.getObject(conn)は、DatabaseMetaData.getDatabaseProductName()から返された文字列に対応するLimitEditorのサブクラスを検索し、見つかった場合はそのサブクラスのインスタンスを返します。見つからない場合は、LimitEditor自体のインスタンスを返します。(LimitEditor.edit()は元の文字列をそのまま返すメソッドです。)このため、編集部品が見つからなくても、エラーにはなりません。

以下のクラスについては定義済みです。これら以外のDBに接続する場合、必要であれば追加してください。

PostgreSQLLimitEditor
MySQLLimitEditor
OracleLimitEditor
ACCESSLimitEditor
EXCELLimitEditor
Microsoft_SQL_ServerLimitEditor

デバッグ機能

デバッグ機能について説明します。

トレースログ機能

トレースログは、プロパティを設定することで出力されるようになります。

# プロパティの例
net.argius.stu.system.log.file  = D:/STU/stu.log
net.argius.stu.system.log.level = DEBUG
net.argius.stu.system.log.size  = 4096

net.argius.stu.system.log.file

net.argius.stu.system.log.level

net.argius.stu.system.log.file

ログファイルのパスを指定します。無効なパスの場合は、ログは出力されません。

net.argius.stu.system.log.level

トレースレベルを指定します。

net.argius.stu.system.log.size

トレースログファイルの最大ファイルサイズを指定します。これを超えると、ローテート処理されます。(元のファイルが[ファイル名].bakにリネームされます。)