/*
 * ProxyConnection.java
 *
 * Created on 2006/12/19, 9:46
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

/*
 Oracle JDBCドライバはJ2SEのAPI規定と異なり、GCによるConnectionやStatementの
 自動クローズを行いません。そのため、これらのJDBCオブジェクトを自動的にクローズ
 する他のJDBC実装用のコードを流用するとリソースリークの原因となります。
 これを回避するには、アプリケーションがすべてのStatementなどのオブジェクトを
 クローズするか、またはミドルウェアなどでアプリケーションが作成したすべての
 JDBCオブジェクトを保持しておき、なんらかのタイミングでクローズするように
 実装しなければなりません。
 
 JDBCのConnectionやStatementはインターフェイスですので、Proxyの適用が可能です。
 このため、簡単なハンドラクラス（Proxyからの呼び出しを処理するクラス）を用意
 することで、後者の方法を実装できます。
 
 この場合、アプリケーションにとってはインターフェイスが透過なため呼び出し
 時の特別な考慮を必要としません。
 
 本記事では、上記のシナリオに沿ってJDBCのインターフェイスに対するProxyを
 生成し、Connectionオブジェクトのクローズ時にそのConnectionオブジェクトが
 生成した未クローズのJDBCオブジェクトを自動的にクローズする、
 Oracle JDBCドライバの制約回避処理の実装方法を示します。
 
 */
package org.itscool.commons.connection;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;

/**
 *
 * @author ios
 */
/*
 はじめに書いたように、OracleのJDBC実装には、生成したJDBCオブジェクトを明示的に
 クローズする必要があるという制限があります。そのため、きちんとcloseメソッドを
 呼ばないとWebアプリケーションのように長期間に渡って実行するアプリケーション
 ではResultSetなどが解放されないまま蓄積されていき、最終的にJVMのヒープが食いつ
 ぶされてしまうという問題があります。
 
「ProxyConnection.java」は、この制限に対応できるように、Connection#closeで、
 そのConnectionから返されたStatement、およびそのStatementから返されたResultSet
 で未クローズのオブジェクトのcloseメソッドを呼び出すクラスです。
 
 このクラスはProxyを利用することでクライアントに対してはConnectionとして振る舞います。
 
 ProxyConnectionは以下のフィールドを持ちます。
 
 Connection connection;
 元のConnectionのインスタンスを保持します。
 
 Set<Statement> openedStatements;
 Connectionから返されたStatementのうち、未クローズのオブジェクトを保持します。
 
 Set<ResultSet> openedResultSets;
 Statementから返されたResultSetのうち、未クローズのオブジェクトを保持します。
 
 Object proxy;
 作成したProxyのインスタンスを保持します。
 コンストラクタ
 ProxyConnectionは、デコレーションの対象となるConnectionのインスタンスを受け取り、
 Connectionインターフェイスに対するProxyを生成します。
 
createProxy
外部へ公開するstaticメソッドです。
ProxyConnectionを利用するプログラムは、たとえば以下のように当メソッドを呼び
 出すことを想定します。
利用例
Connection connection;
...
try {
    connection =
        ProxyConnection.createProxy(dataSource.getConnection());
} catch (SQLException e) {
    log("connectionの取得に失敗", e);
}
 
 これはProxyConnectionクラスそのものを直接クライアントに見せる必要が無いからです。
 なお、一見するとせっかく生成したProxyConnectionのインスタンスが誰からも参照され
 ないため（クライアントへ返しているのはProxyのインスタンスです）すぐにGCされてし
 まいそうですが、実際にはインナークラスのConnectionHandlerのインスタンスがthisを
 参照しているためConnectionHandlerが有効な間はGCされることはありません。
 
 また、ConnectionHandlerの寿命はProxyと一致するため、結局、クライアントが
 Connectionオブジェクト（のプロクシ）の参照を保持している限り有効となります。
 
 なお、本記事でJDBCオブジェクトのクローズのトリガーをConnection#closeにしている
 理由は、ここで意図的にProxyConnectionの呼び出しが必要な以上、Connectionを特別扱
 いするプログラムの存在を前提できるからです。
 
 たとえば、Webアプリケーションであれば、DataSourceのProxyを作り、getConnectionの
 呼び出し時にこのメソッドを実行し、リクエストの終了時に作成したConnectionのclose
 を呼ばせるように実装することが可能です。
 
send
 内部処理用のメソッドです。Proxyから与えられたMethodオブジェクトと引数を利用して、
 内包するConnectionやStatementなどのオブジェクトへ処理をデリゲートします。
 
 ここでキャッチしているInvocationTargetExceptionは、Method#invokeの呼び出し先で
 スローされた例外を保持する例外です。したがって、Connectionオブジェクトなどが
 スローした例外はMethod#invoke内でInvocationTargetExceptionにラップされること
 になります。
 
 クライアントにProxyを意識させないために、ここでInvocationTargetException#getCause
 により元の例外を取得し再スローしています。
 リスト上はgetCauseの内容がnullかチェックしていますが、本来nullになることはありません。
 
 なお、InvocationTargetException以外の例外は、Method#invokeの処理そのものがスロー
 したものとなります。
 このような例外としては、たとえばargsが呼び出し対象のメソッドとマッチしないなどが
 考えられます。
 しかし、クライアントはConnectionインターフェイスのメソッドを呼び出すソースをコ
 ンパイルしたと想定できるため、このような状況はバグ以外にはあり得ません。
 したがって、ここでは特にキャッチせずにそのままクライアントへスローされるようにしています。
 
remove
 登録したResultSetやStatementオブジェクトのclose呼び出し時に利用します。
 closeが呼ばれた場合、該当オブジェクトをそれ以上保持する必要はないため、当メソッドで登録解除します。
 
closeAll
 Connection#closeの呼び出しを受けて、すべての未クローズのStatementおよび
 ResultSetオブジェクトをクローズします。
 なお、この時点でSQLExceptionをキャッチしても処理できたないため単純に無視しています。
 意味的にはファイナライザでのクローズと同様の扱いだからです。
 
ConnectionHandler（InvocationHandler実装）
 Connectionオブジェクトのデコレータとして以下の処理を実行します。
 
close呼び出し時にcloseAllを呼び出して未クローズのリソースを解放する。
メソッド呼び出しを元のConnectionオブジェクトへデリゲートする。
メソッドの結果がStatementのインスタンス（PreparedStatementなどを含みます）であれば、
 未クローズオブジェクトとして登録します。また、新たなProxyを生成します。
 なお、StatementかPreoparedStatement、CallableStatementの区別なくStatement
 インターフェイスおよびその継承インターフェイスをPreparedStatementインター
 フェイスとして、Proxyオブジェクトを生成しています。
 ここは必要に応じて細分化を行うなどしても良いでしょう。
 たとえば、クライアントが何らかの処理で、PreparedStatementインターフェイスか
 Statementインターフェイスかによって処理を分岐するような場合には、
 この実装では正しく処理できないことに注意してください。
 
Delegate（InvocationHandler実装）
 Connectionオブジェクト専用のConnectionHandlerと異なり、Delegateクラスは、
 StatementとResultSetの両方を扱うハンドラです。
 なお、ここでは個々のリソース単位に新たなDelegateのインスタンスを割り当てていま
 すが（ConnectionHandler#invoke内）、InvocationHandler#invokeメソッドの第1引数
 で説明したように、実際には単一のインスタンスで複数のProxyオブジェクトを扱うこ
 となども可能です。
 しかし、ここでは処理の単純さを重視してオブジェクトごとに作成しています。
 
 コンストラクタでは、デリゲート先のオブジェクト（元のオブジェクト）をフィール
 ドへ保持し、Proxyオブジェクトを生成しています。
 ここで生成したProxyオブジェクトは、Delegateのインスタンスの生成と同時にメソッド
 の戻り値としてそのままクライアントへ返します（各InvocationHandlerがDelegate
 オブジェクトを生成している個所を参照）。
 
 invokeメソッドでは、ResultSet、Statement（および派生したPreparedStatement）
 に対するデリゲートを実行します。ただし、closeメソッドが呼び出された場合は
 removeソッドを呼び出して該当するオブジェクトの登録を解除します。
 また戻り値の型がResultSetの場合には新規に対応するDelegateオブジェクトを生成し
 クライアントへはその結果を返します。
 
 */
public class ProxyConnection {
    Connection connection;
    LinkedList openedStatements;
    LinkedList openedResultSets;
    Object proxy;
    ProxyConnection(Connection org) {
        connection = org;
        openedStatements = new LinkedList();
        openedResultSets = new LinkedList();
        proxy = Proxy.newProxyInstance
            (Connection.class.getClassLoader(),
            new Class[] { Connection.class },
            new ConnectionHandler());
    }
    /**
     * 指定されたコネクションのプロクシを生成する。
     * @param c 生成対象のコネクション
     * @return PreparedStatementとResultSetのclose検証処理を
     * 含んだコネクション
     */
    public static Connection createProxy(Connection c) {
        ProxyConnection pc = new ProxyConnection(c);
        return (Connection)pc.proxy;
    }
    Object send(Object target, Method m, Object[] args)
    throws Throwable {
        try {
            return m.invoke(target, args);
        } catch (InvocationTargetException e) {
            if (e.getCause() != null) {
                throw e.getCause();
            }
            throw e;
        }
    }
    void remove(Object o) {
        if (o instanceof ResultSet) {
            openedResultSets.remove(o);
        } else if (o instanceof Statement) {
            openedStatements.remove(o);
        } else {
            throw new IllegalArgumentException("bad class:" + o);
        }
    }
    void closeAll() {
        int size = openedResultSets.size();
        for (int i=0; i<size; i ++ ) {
            ResultSet rs = (ResultSet)openedResultSets.get(i);
            try {
                rs.close();
//                System.out.println("ResultSetインスタンスをクローズしました");
            } catch (SQLException e) {
            }
        }
        size = openedStatements.size();
        for (int i=0; i<size; i++ ) {
            Statement ps = (Statement)openedStatements.get(i);
            try {
                ps.close();
//                System.out.println("Statementインスタンスをクローズしました");
            } catch (SQLException e) {
            }
        }
    }
    class ConnectionHandler implements InvocationHandler {
        public Object invoke(Object proxy, Method method,
            Object[] args) throws Throwable {
            if (method.getName().equals("close")) {
//                System.out.println("Connectionインスタンスのクローズ処理が呼ばれました");
                closeAll();
            }
            Object o = send(connection, method, args);
            if (o instanceof Statement) {
                openedStatements.add((Statement)o);
                o = new Delegate(o, PreparedStatement.class).proxy;
            }
            return o;
        }
    }
    class Delegate implements InvocationHandler {
        Object proxy;
        Object original;
        Delegate(Object o, Class c) {
            original = o;
            proxy = Proxy.newProxyInstance(c.getClassLoader(),
                new Class[] { c }, this);
        }
        public Object invoke(Object proxy, Method method,
            Object[] args) throws Throwable {
            if (method.getName().equals("close")) {
                remove(original);
            }
            Object o = send(original, method, args);
            if (o instanceof ResultSet) {
                openedResultSets.add((ResultSet)o);
                o = new Delegate(o, ResultSet.class).proxy;
            }
            return o;
        }
    }
}
