/*
 * $Id: InteractiveCommunicationClient.java,v 0.0 2008/10/25 11:31:41 t-imamura Exp $
 *
 * Copyright (c) 2008 t-imamura, All rights reserved.
 */
package objsend;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

/**
 * オブジェクト双方向通信 クライアント(HttpURLConnection)サイド.
 */
public class InteractiveCommunicationClient {

	/** サーブレットURL */
	private URL servlet;
	/** GZIP圧縮を行うか */
	private boolean compress = true;

	/** ホスト名検証 */
	private HostnameVerifier hostVerifier = null;
	/** SSLソケットの作成 */
	private SSLSocketFactory sslFactory = null;
	/** 信頼マネージャ */
	private TrustManager[] trustManager = null;
	/** 鍵マネージャ */
	private KeyManager[] keyManager = null;

	/**
	 * コンストラクタ.
	 * @param servlet サーブレットURL
	 * @param compress データの圧縮をするか
	 */
	public InteractiveCommunicationClient(URL servlet, boolean compress) {
		this.servlet = servlet;
		this.compress = compress;
	}

	/**
	 * セキュア通信を行うための証明書のロード.<br>
	 * キーストアが<code>null</code>、ロードしない場合は、Javaのデフォルトを使用
	 * @param store キーストアのストリーム
	 * @param storePass キーストアのパスワード
	 */
	public void loadTrusts(InputStream store, String storePass) {
		if(store == null){
			setTrustManagers(null);
			return;
			//throw new IllegalArgumentException("InputStream of KeyStore is null.");
		}
		if(storePass == null){
			throw new IllegalArgumentException("KeyStore password is null.");
		}

		// 信用する証明書のロード
		try{
			// キーストアのインスタンスを作成
			KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() );
			// キーストアをロードする
			ks.load(store, storePass.toCharArray());
			// 信頼マネージャのファクトリを作成 (DefaultAlgorithm=PKIX)
			TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() );
			// 信頼マネージャのファクトリの初期化
			tmf.init(ks);
			// 信頼マネージャをセット
			setTrustManagers(tmf.getTrustManagers());
		} catch (GeneralSecurityException e) {
			throw new RuntimeException("A certificate cannot be loaded.", e);
		} catch (IOException e) {
			throw new RuntimeException("A certificate cannot be loaded.", e);
		}
	}

	/**
	 * 信頼マネージャを設定します。
	 * @param trustManager 信頼マネージャ
	 */
	public void setTrustManagers(TrustManager[] trustManager) {
		this.trustManager = trustManager;
		this.sslFactory = null;
	}

	/**
	 * セキュア通信を行うための鍵のロード.<br>
	 * キーストアが<code>null</code>、ロードしない場合は、Javaのデフォルトを使用
	 * @param store キーストアのストリーム
	 * @param storePass キーストアのパスワード
	 * @param keyAlias 使用する秘密鍵の別名 ※将来拡張用 現状では使用されません。
	 * @param keyPass 鍵のパスワード
	 */
	public void loadKeys(InputStream store, String storePass, String keyAlias, String keyPass) {
		if(store == null){
			setKeyManagers(null);
			return;
			//throw new IllegalArgumentException("InputStream of KeyStore is null.");
		}
		if(storePass == null){
			throw new IllegalArgumentException("KeyStore password is null.");
		}
		if(keyPass == null){
			throw new IllegalArgumentException("Key password is null.");
		}

		// 認証用の鍵のロード
		try {
			// キーストアのインスタンスを作成
			KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() );
			// キーストアをロードする
			ks.load(store, storePass.toCharArray());
			// 鍵マネージャのファクトリを作成 (DefaultAlgorithm=SunX509)
			KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() );
			// 鍵マネージャのファクトリの初期化
			kmf.init(ks, keyPass.toCharArray());
			// 鍵マネージャをセット
			setKeyManagers(kmf.getKeyManagers());
		} catch (GeneralSecurityException e) {
			throw new RuntimeException("A private key cannot be loaded.", e);
		} catch (IOException e) {
			throw new RuntimeException("A private key cannot be loaded.", e);
		}
	}

	/**
	 * 鍵マネージャを設定します。
	 * @param keyManager 鍵マネージャ
	 */
	public void setKeyManagers(KeyManager[] keyManager) {
		this.keyManager = keyManager;
		this.sslFactory = null;
	}

	/**
	 * SSLソケットファクトリーを設定します。
	 * @param sslFactory SSLソケットファクトリー
	 */
	public void setSocketFactory(SSLSocketFactory sslFactory) {
		this.sslFactory = sslFactory;
	}

	/**
	 * SSLソケットの作成を取得します。
	 * @return SSLSocketFactory
	 */
	private SSLSocketFactory getSocketFactory(){
		if(this.sslFactory != null) return this.sslFactory;

		// https接続認証設定処理
		try {
			// SSLプロトコルの SSLContext を作成
			SSLContext sc = SSLContext.getInstance("SSL");
			// 認証キー, 信頼判断, 乱数 のソースをセットし初期化する
			sc.init(this.keyManager, this.trustManager, new SecureRandom());
			// SSLソケットの作成(SSLSocketFactory)
			this.sslFactory = sc.getSocketFactory();
		} catch (NoSuchAlgorithmException e) {
			// SSLContext を指定したプロトコル,プロバイダーで作成できなかった
			throw new RuntimeException("SSLContext cannot be made.", e);
		} catch (KeyManagementException e) {
			// SSLContext 初期化できなかった
			throw new RuntimeException("SSLContext cannot be initialized.", e);
		}
		return this.sslFactory;
	}

	/**
	 * ホスト名検証クラスを設定します。
	 * @param hostVerifier ホスト名検証クラス
	 */
	public void setHostnameVerifier(HostnameVerifier hostVerifier) {
		this.hostVerifier = hostVerifier;
	}

	/**
	 * ホスト名検証クラスを取得します。
	 * @return HostnameVerifier
	 */
	private HostnameVerifier getHostnameVerifier() {
		if(this.hostVerifier == null){
			this.hostVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
		}
		return this.hostVerifier;
	}

	// -----=====-----=====-----=====-----=====-----=====-----=====-----=====-----=====-----

	/**
	 * サーブレットとHTTP/HTTPS通信を行います。
	 * @param reqData 要求オブジェクト
	 * @return 応答オブジェクト
	 * @throws IllegalArgumentException リクエストモデルの指定がなかった場合
	 */
	public Object execute(Object reqData) {
		if( reqData == null ){
			throw new IllegalArgumentException("A request object is not specified.");
		}
		Object resData = null; // 応答オブジェクト

		// サーブレットに接続
		HttpURLConnection conn = null;
		try {
			// コネクションを開く
			conn = (HttpURLConnection)this.servlet.openConnection();
			// 接続の設定
			//conn.setConnectTimeout(0);	// 接続タイムアウト
			//conn.setReadTimeout(0);		// 応答タイムアウト
			conn.setUseCaches(false);
			conn.setDoInput(true);
			conn.setDoOutput(true);
			conn.setRequestProperty("Content-type", "application/octet-stream");
			// データの圧縮をする時は、リクエストヘッダ追加
			if( this.compress ){
				conn.setRequestProperty("Content-Encoding", "gzip");
				conn.setRequestProperty("Accept-Encoding", "gzip");
			}
			// SSLコネクションの場合の追加設定
			if (conn instanceof HttpsURLConnection) {
				HttpsURLConnection sslConn = (HttpsURLConnection)conn;
				sslConn.setSSLSocketFactory(getSocketFactory());
				sslConn.setHostnameVerifier(getHostnameVerifier());
			}
			// 接続
			conn.connect();
		} catch (SocketTimeoutException e) {
			// サーブレット connect タイムアウト
			throw new RuntimeException("Servlet connect timed out.", e);
		} catch (SSLException e) {
			// SSLが利用できない
			throw new RuntimeException("cannot open ssl connection of servlet.", e);
		} catch (IOException e) {
			// サーバのコネクションが開けない
			throw new RuntimeException("cannot open connection of servlet.", e);
		}

		// データ(オブジェクト)の送信
		ObjectOutputStream oos = null;	// 出力ストリーム
		try {
			// オブジェクトストリームに変換して送信
			OutputStream os = conn.getOutputStream();
			if( this.compress ){
				oos = new ObjectOutputStream(new GZIPOutputStream(new BufferedOutputStream(os)));
			}else{
				oos = new ObjectOutputStream(new BufferedOutputStream(os));
			}
			oos.writeObject(reqData);
			oos.flush();
		} catch (SocketTimeoutException e) {
			// Appサーバ connect/read タイムアウト
			throw new RuntimeException("Servlet timed out.", e);
		} catch (ObjectStreamException e) {
			// 応答オブジェクトを直列化してオブジェクトストリームに渡せない
			throw new RuntimeException("a request object of output stream is not serialize.", e);
		} catch (IOException e) {
			// Appサーバ通信エラー
			throw new RuntimeException("Servlet access error.", e);
		} finally {
			try {
				// 開いているストリームを閉じる
				if (oos != null) {
					oos.close();
					oos = null;
				}
			} catch (IOException e) {
				throw new RuntimeException("A object stream cannot be closed.", e);
			}
		}

		// データ(オブジェクト)の送受信
		ObjectInputStream ois = null;	// 入力ストリーム
		try {
			// オブジェクトストリームの受信して復元
			InputStream is = conn.getInputStream();
			String encoding = conn.getRequestProperty("Content-Encoding");
			if( this.compress && "gzip".equals(encoding) ){
				// レスポンスヘッダの "Content-Encoding" が "gzip" だったらGZIP圧縮されている
				ois = new ObjectInputStream(new GZIPInputStream(new BufferedInputStream(is)));
			}else{
				ois = new ObjectInputStream(new BufferedInputStream(is));
			}
			resData = ois.readObject();
		} catch (SocketTimeoutException e) {
			// Appサーバ connect/read タイムアウト
			throw new RuntimeException("Server timed out.", e);
		} catch (ClassNotFoundException e) {
			// オブジェクトストリームから応答オブジェクトが復元できない
			throw new RuntimeException("A response object of input stream is not found.", e);
		} catch (ObjectStreamException e) {
			// オブジェクトストリームから応答オブジェクトが復元できない
			throw new RuntimeException("A response object of input stream is not restorable.", e);
		} catch (IOException e) {
			// Appサーバ通信エラー
			throw new RuntimeException("Server access error.", e);
		} finally {
			// 開いているストリームを閉じる
			try {
				if (ois != null) {
					ois.close();
					ois = null;
				}
			} catch (IOException e) {
				throw new RuntimeException("A object stream cannot be closed.", e);
			}
		}

		return resData;
	}

}
