2012年2月6日月曜日

java : sqliteに登録した画像をHttpServerで送信

sqliteに画像を登録して、com.sun.net.httpserver.HttpServerで送信、ブラウザで画像を見る簡単なサンプルプログラムを作りました。 sqliteにはjdbcで接続。 jdbcはこちらで配布しているものを使いました。

以前の投稿をふまえて、

作ったら大体こんな感じになりました。

「test0~2.png」の画像ファイルをclassと同じディレクトリに用意してコマンドプロンプトで実行。 sqliteに画像を登録した後、HttpServerが動きます。 コマンドプロンプトの表示は「なんたらかんたら ... Push enter to stop.」となって待機。 ブラウザで「http://localhost/」にアクセスすると「画像選択アンカー」のついたhtmlが表示されます。 アンカークリックで画像の切り替え。 画像はsqliteからselectして送信です。 uriの受け取り方が適当なので、適当なコンテキストにアクセスすると表示が崩れます。 雑なサンプルコードということでlocalhostから接続されたときしか反応しないようにしました。 コマンドプロンプトでenterキーを押したらサーバの停止です。

コードはこんな感じ。

// MinimumImageServerTest.java
package minimumimageservertest;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;

public class MinimumImageServerTest implements HttpHandler
{
    public final static int SERVER_RESPONSE_OK = 200;
    public final static int SERVER_PORT = 80;
    public final static int SERVER_STOP_DELAY = 10;

    private HttpServer _server = null;
    private DataBase _db = null;
    private Thread _serverThread = null;

    public static void main(String[] args)
    {
        MinimumImageServerTest thisApp = new MinimumImageServerTest();
        thisApp.run();
    }

    public void run()
    {
        _db = new DataBase();
        try
        {
            String dir = Util.getClassDir(this.getClass() );
            _db.connect();
            _db.initialize();
            _db.insertImage(0, Util.readImage(dir + "test0.png"));
            _db.insertImage(1, Util.readImage(dir + "test1.png"));
            _db.insertImage(2, Util.readImage(dir + "test2.png"));

            startServer();
            WaitEnterKey();
            stopServer();
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
        }
        finally
        {
            _db.close();
        }
    }

    public void WaitEnterKey()
    {
        System.out.print("Push enter to stop.\n> ");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try
        {
            br.readLine();
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
        }
    }

    public void startServer()
    {
        System.out.println("Confirm a command : server start");
        _serverThread = new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    _server = HttpServer.create(new InetSocketAddress(SERVER_PORT), 0);
                    _server.createContext("/", MinimumImageServerTest.this);
                    _server.start();
                }
                catch (IOException exc)
                {
                    exc.printStackTrace();
                }
            }
        };
        _serverThread.start();
        System.out.println("Server is started.");
    }

    public void stopServer()
    {
        System.out.println("Confirm a command : server stop");
        System.out.format("Wait %d seconds...\n", SERVER_STOP_DELAY);
        assert _server != null && _serverThread != null;
        _server.stop(SERVER_STOP_DELAY);
        System.out.println("Server is stopped.");
    }

    @Override
    public void handle(HttpExchange ex)  throws IOException
    {
        OutputStream os = ex.getResponseBody();

        if(!ex.getRemoteAddress().getAddress().isLoopbackAddress())
            return;

        try
        {
            String uri = ex.getRequestURI().toString();
            if(uri.endsWith(".png"))
            {
                int i = uri.charAt(uri.length() - "?.png".length() ) - '0';
                byte[] bynary = _db.selectImage(i);

                ex.getResponseHeaders().add("Content-Type", "image/png");
                ex.sendResponseHeaders(SERVER_RESPONSE_OK, bynary.length);
                os.write(bynary);
            }
            else
            {
                int i = (uri.equals("/") ? 0 : uri.charAt(1) - '0');

                ex.getResponseHeaders().add("Content-Type", "text/html");
                String resTxt = String.format("<!doctype html><html><head><meta charset=\"UTF-8\"><title>MinimumImageServerTest</title></head><body><ul><li><a href=\"0\">画像0を選ぶ</a></li><li><a href=\"1\">画像1を選ぶ</a></li><li><a href=\"2\">画像2を選ぶ</a></li></ul><p>↓ただいまの選択画像は「%1$d」(未選択の場合は0)</p><img src=\"test%1$d.png\"/></body></html>", i);
                byte[] resBuff = resTxt.getBytes(Charset.forName("UTF-8"));
                ex.sendResponseHeaders(SERVER_RESPONSE_OK, resBuff.length);
                os.write(resBuff);
            }
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
        }
        finally
        {
            ex.close();
        }
    }
}

HttpHandlerをメソッド1つで済ませるとか、相変わらず手抜きです。 読み返すと、uriの受け取り方やっぱり酷いな...

// DataBase.java
package minimumimageservertest;
import java.sql.*;

public class DataBase
{
    private static final String DB_CONNECTION_STRING = "jdbc:sqlite:d:/testmain";
    private Connection _con;

    public void connect() throws SQLException, ClassNotFoundException
    {
        System.out.println("Confirm a command : database connect");

        Class.forName("org.sqlite.JDBC");
        _con = DriverManager.getConnection(DB_CONNECTION_STRING);

        System.out.println("Connected to the database.");
    }

    public void initialize() throws SQLException
    {
        System.out.println("Confirm a command : database initialize");

        Statement s = _con.createStatement();
        s.executeUpdate("drop table if exists ImageTest");
        s.executeUpdate("create table ImageTest (id integer, binary blob)");

        System.out.println("Initialized the database.");
    }

    public void insertImage(int index, byte[] binary) throws SQLException
    {
        PreparedStatement s = _con.prepareStatement("insert into ImageTest values(?,?)");
        s.setInt(1, index);
        s.setBytes(2, binary);
        s.executeUpdate();
    }

    public byte[] selectImage(int index) throws SQLException
    {
        Statement s = _con.createStatement();
        ResultSet rs = s.executeQuery("select binary from ImageTest where id = " + index);
        rs.next();
        byte[] binary = rs.getBytes(1);
        rs.close();

        return binary;
    }

    public void close()
    {
        System.out.println("Confirm a command : database close");
        try
        {
            if(_con != null)
            {
                _con.close();
                _con = null;
            }
            System.out.println("Database is closed.");
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
            System.out.println("Fail to close the database.");
        }
    }
}

そういえば「_con.prepareStatementを毎回作るのは非効率的だからコンストラクタに移さなくちゃなぁ」とかって考えてたんでした。 直すの忘れてた。 でも面倒なのでそのまま投稿です。

jdbcではConnectionは1スレッドに1つ作るものらしいです。 というわけで複雑なプログラムでは「Class.forName("org.sqlite.JDBC")」と「DriverManager.getConnection」を1つのメソッドでやるのはダメかもしれません。 「Class.forName("org.sqlite.JDBC")」の方はstaticなloadClassメソッドを作ってそちらで、「DriverManager.getConnection」の方はインスタンスごとにconnectメソッドを作ってそちらで、とかかな? でもsqliteはマルチスレッド向きじゃないようなので1Connectionだけで頑張る方がいいのかも?

// Util.java
package minimumimageservertest;
import java.io.*;

public class Util
{
    public static String getClassDir(Class appClass)
    {
        String className = appClass.getSimpleName() + ".class";
        String path = appClass.getResource(className).getFile();

        int jarIndex = path.toLowerCase().indexOf(".jar!/");
        if(0 <= jarIndex)
            path = path.substring(0, jarIndex);

        path = path.substring(0, path.lastIndexOf('/') + 1);

        if(path.startsWith("file:"))
            path = path.substring("file:".length() );

        if (path.matches("^/[a-zA-Z]:/.*"))
            path = path.substring(1);

        return path;
    }

    public static byte[] readImage(String filePath)
    {
        FileInputStream in = null;
        try
        {
            File file = new File(filePath);
            in = new FileInputStream(file);
            byte[] res = new byte[(int)file.length()];
            in.read(res);
            return res;
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
            return null;
        }
        finally
        {
            if(in != null)
            {
                try
                {
                    in.close();
                }catch(Exception exc){}
            }
        }
    }
}

ローカルで画像の受け渡しをしているだけなのに意外と重いです。 たった3つの画像でコレってことはオーバーヘッドですよね? まぁ、簡易サーバーなら仕方が無いかな? 今回は画像3つだけなのでこんなつくりですが、多くの画像を扱うならアプリケーション側でメモリにキャッシュしたりブラウザのキャッシュ機能の設定をしたりしなければならないかも?

手抜きで、例外をExceptionだけで済ませるとかLoggerを使わずにprintStackTraceするとか、色々お行儀の悪いところがありますなぁ。 「Confirm a command : database connect」みたいなメッセージもホントはLoggerにはかないと。 というか英語が怪しいので日本語で書いた方が浅い傷ですむかなぁとか...

そのうち何とかしないといけませんね。

---

ローカルに落として使っているjavadocにはcom.sun.net.httpserver.HttpServerの項目は含まれてませんでした。 ここを見て作っています。