/*********************************************************************
*  All Rights reserved,Copyright (c) K-Opticom						 *
**********************************************************************
*＜プログラム内容＞
*	システム名			：eo顧客基幹システム
*	モジュール名		：JKKHttpCommunicator
*	ソースファイル名	：JKKHttpCommunicator.java
*	作成者				：富士通　
*	作成日				：2012年08月25日
*＜機能概要＞
*　HTTP通信用ベースアクセスクラス
*＜修正履歴＞
*	バージョン	修正日		修正者		修正内容
*	v4.00.00	2012/08/25   富士通		新規作成
*	v4.00.01	2013/02/27   FJ)江藤	【IT1-2013-0000339】
*	v8.00.00	2014/04/30   FJ)江藤	【ANK-2060-00-00】ＫＤＤＩシステム切替対応(URLログ出力追加)
*	v8.01.00	2014/05/22   FJ)田中	【OM-2014-0001879】NoSuchElementExceptionが発生した場合のハンドリング処理を追加
*********************************************************************/
package eo.business.common;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.NoSuchElementException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.ws.http.HTTPException;

import org.apache.commons.codec.binary.Base64;


import eo.common.constant.JPCBatchMessageConstant;
import eo.common.util.JCCcomEncryptionUtil;
import eo.framework.application.JBSbatBusinessException;
import eo.framework.item.JBSbatCommonItem;
import eo.framework.log.JBSbatLogPrintControl;


/**
 * HTTP通信用のアクセスクラス<p>
 * <br>
 * @author 富士通
 */
public class JKKHttpCommunicator {
	
	/** HTTPSプロトコルを識別する文字列 */
	public static final String S_HTTPS = "HTTPS";

	/** 通信方式パラメータ*/
	protected static final String S_SSL = JKKBatConst.S_SSL;
	/** リクエスト種別パラメータ POST */
	protected static final String S_POST = JKKBatConst.S_POST;
	/** リクエスト種別パラメータ GET  */
	protected static final String S_GET = "GET";

	/** PROXY(HOST) を設定するためプロパティ値 */
	// 20130415 ST1-2013-0000522 http→httpsに変更 MOD START
//	private static final String S_HTTP_PROXY_HOST = "http.proxyHost";
	private static final String S_HTTP_PROXY_HOST = "https.proxyHost";
	// 20130415 ST1-2013-0000522 http→httpsに変更 MOD END

	/** PROXY(PORT) を設定するためプロパティ値 */
	// 20130415 ST1-2013-0000522 http→httpsに変更 MOD START
//	private static final String S_HTTP_PROXY_PORT = "http.proxyPort";
	private static final String S_HTTP_PROXY_PORT = "https.proxyPort";
	// 20130415 ST1-2013-0000522 http→httpsに変更 MOD END
	
	// 20130415 ST1-2013-0000522 https.proxySetを追加 ADD START
	/** PROXY(SET) を設定するためプロパティ値 */
	private static final String S_HTTP_PROXY_SET = "https.proxySet";
	
	/** PROXY(SET) を設定するためプロパティ値の設定値 */
	private static final String S_HTTP_PROXY_SET_TRUE = "true";
	// 20130415 ST1-2013-0000522 https.proxySetを追加 ADD END
	/** 復号キーを取得するキー値 */
	private static final String S_KEY_ENCRYPT_KEY = "ENCRYPT_KEY";
	
	/** ログ用インスタンス */
	protected JBSbatLogPrintControl log = null;
	
	/* 接続URL */
	private URL targetUrl = null;	 
	
	/* リトライ回数 （デフォルト：なし） */
	private int retryCount = 0;	
	
	/* リトライ時の待機時間 （デフォルト：待ちなし）*/
	private int retryInterval = 0;	
	
	/* タイムアウト時間（デフォルト：1000ミリ秒）*/
	private int timeout = 1000;
	
	/** サーバのコードセット */
	private String charCodeset = null;

	/** ユーザ名 */
	// 20130415 ST1-2013-0000522 MOD START
//	private String authUser = null;
	protected String authUser = null;
	// 20130415 ST1-2013-0000522 MOD END
	/** パスワード */
	private char[] authPasswd = null;

	/** 復号化するためのキー情報 */
	private String encrptKey = null;
	
	/**
	 * デフォルトコンストラクタ
	 */
	public JKKHttpCommunicator()
	{
		setCharCodeset("Shift_JIS");
	}
	
	/**
	 * 全てのプロパティを指定して構築する
	 * 
	 * @param commonItem 
	 */
	public void setLogger(JBSbatCommonItem commonItem) 
	{
		// デバッグログ用オブジェクトの指定
		if (commonItem != null) 
		{
			log = commonItem.getLogPrint();
		}
	}
	
	/**
	 * PROXYを設定する。
	 * 
	 * @param host	PROXYホスト
	 * @param port PROXYポート
	 */
	public void setProxy(String host, String port)
	{
		// PROXYの設定 ホストが設定されている場合だけポートを指定する。
		// （基本両方が設定されている前提ポートだけでは意味がないから）
		if (!isNull(host))
		{
			// 20130415 ST1-2013-0000522 ADD START
			System.setProperty(S_HTTP_PROXY_SET, S_HTTP_PROXY_SET_TRUE);
			// 20130415 ST1-2013-0000522 ADD END
			System.setProperty(S_HTTP_PROXY_HOST, host);
			if (!isNull(port))
			{
				System.setProperty(S_HTTP_PROXY_PORT, port);
			}
		}		
	}
	/**
	 * 文字列が空かどうかを返す。
	 * <br>
	 * @param val 対象文字列変数
	 * @return true : 空文字 false : 文字が存在
	 */
	protected boolean isNull(String val)
	{
		if (val != null && val.length() > 0)
		{
			return false;
		}
		return true;
	}

	/**
	 * 接続先のURLを返す。
	 * <br>
	 * @return 登録されているURL
	 */
	public URL getTargetURL() {
		return targetUrl;
	}

	/**
	 * 接続先のURLを設定する。
	 * <br>
	 * @param targetUrl 接続先を設定する。
	 */
	public void setTargetURL(URL targetUrl) {
		this.targetUrl = targetUrl;
	}

	/**
	 * リトライ回数を返す。
	 * <br>
	 * @return 0以上の数値を返す。0の場合はリトライなしで１回のみ実行
	 */
	public int getRetryCount() {
		return retryCount;
	}

	/**
	 * リトライ回数を指定する。
	 * <br>
	 * @param retryCount 0以上の値を指定する。
	 * @throws IllegalArgumentException マイナスの値が指定された場合の例外
	 */
	public void setRetryCount(int retryCount) {
		if (retryCount < 0)
		{
			throw new IllegalArgumentException();
		}
		this.retryCount = retryCount;
	}

	/**
	 * リトライまでの待機時間を返す。
	 * <br>
	 * @return 0以上の数値を返す。
	 */
	public int getRetryInterval() {
		return retryInterval;
	}

	/**
	 * リトライまでの待機時間を設定する。
	 * <br>
	 * @param retryInterval 待機させたい時間（ミリ秒）を指定する。
	 * @throws IllegalArgumentException マイナスの値が指定された場合の例外
	 */
	public void setRetryInterval(int retryInterval) {
		if (retryInterval < 0)
		{
			throw new IllegalArgumentException();
		}
		this.retryInterval = retryInterval;
	}

	/**
	 * HTTP接続タイムアウト値を返す。
	 * <br>
	 * @return タイムアウトする時間
	 */
	public int getTimeout() {
		return timeout;
	}

	/**
	 * HTTP接続タイムアウト値を設定する。
	 * <br>
	 * @param timeout タイムアウトする時間を指定する。
	 */
	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	/**
	 * コンテンツ受信時のコードセットを返す。
	 * NULLの場合は、システムのデフォルト値で処理する。
	 * <br>
	 * @return コードセットを示す文字列("UTF-8"など）
	 */
	public String getCharCodeset() {
		return charCodeset;
	}
	
	/**
	 * コンテンツ受信時のコードセットを設定する。
	 * <br>
	 * @param charCodeset コードセットを示す文字列
	 */
	public void setCharCodeset(String charCodeset) {
		this.charCodeset = charCodeset;
	}
	

	/**
	 * メッセージを送信する。
	 * <br>
	 * @param conn	HTTPコネクションのインスタンス
	 * @throws IOException	入出力例外
	 */
	protected void sendMessage(HttpURLConnection conn) throws IOException
	{
		conn.setRequestMethod(S_GET);
		conn.setDoOutput(false);
		conn.setInstanceFollowRedirects(true);
		
		// 20130416 ST1-2013-0000522 ADD START
		if(!isNull(authUser))
		{
			// 認証が必要な場合
			conn.setRequestProperty("Authorization", getAutorizationValue());
		}
		// 20130416 ST1-2013-0000522 ADD END
		
		conn.connect(); 
	}
	
	/**
	 * コンテンツを受信する。
	 * <br>
	 * @param conn HTTPコネクションのインスタンス
	 */
	protected void receiveContent(HttpURLConnection conn) throws IOException 
	{
		BufferedReader reader = null;
		try
		{
			// リクエスト結果読み出し
			InputStream is = conn.getInputStream();
//			if (getCharCodeset() == null)
//			{
//				reader = new BufferedReader(new InputStreamReader(is));
//			} 
//			else
//			{
			reader = new BufferedReader(new InputStreamReader(is, getCharCodeset()));
//			}
			
			String s = null;
			for(s = reader.readLine() ; null != s ; s = reader.readLine())
			{
				receiveContentAtLine(s);
			}
		}
		finally 
		{
			if (reader != null)
			{
				reader.close();
			}
			
		}
	}
	
	/**
	 * 1行受信データを処理する。
	 * @param val 読み込んだ１行データ
	 * @throws IOException IOエラー発生時の例外
	 */
	protected void receiveContentAtLine(String val) throws IOException
	{
		
	}
	
	/**
	 * 実行する。
	 * <br>
	 * @return 実行結果をbooleanで返す。
	 * @throws IOException 入出力例外
	 */
	public boolean execute() throws IOException
	{
		// デバック出力
		printDebugLog(getClass().getSimpleName() + "#execute start");
		
		// 20140430 ANK-2060-00-00 ADD START
		printBusinessErrorLog(JPCBatchMessageConstant.EKKB1200AI, new String[]{ "Host:"+getTargetURL().getHost()+" Path:"+getTargetURL().getPath() });
		// 20140430 ANK-2060-00-00 ADD END
		
		for(int i = 0 ; i <= getRetryCount() ; i++)
		{
			HttpURLConnection conn = null;
			try
			{
				conn = (HttpURLConnection)getTargetURL().openConnection();
				// タイムアウト設定
				// ※URLConnectionのタイムアウト規定値は、0（無限）なのでタイムアウト値を設定する
				conn.setReadTimeout(getTimeout());
				// 要求の送信
				sendMessage(conn);
				
				// 接続終了ログ
				printDebugLog(getClass().getSimpleName() + "#connect end");
				
				// HTTPのステータスコードを取得
				int statusCode = 500;
				statusCode = conn.getResponseCode();
				printDebugLog(getClass().getSimpleName() + "#statusCode:" + statusCode);
				
				// 本来はステータスコードを見てから処理すべき
				switch (statusCode) 
				{
				case HttpsURLConnection.HTTP_OK:	// HTTP OK
					// 入力データの取得
					receiveContent(conn);
					return true;
				default:
					// IT1-2013-0000339 エラー時の解析のため、業務ログを出力する ADD START
					if (!sleepForRetryAt(i))
					{
						printBusinessErrorLog(JPCBatchMessageConstant.EKKB0990CE, new String[] {"HTTPステータスコード:" + statusCode});
					}
					// IT1-2013-0000339 エラー時の解析のため、業務ログを出力する ADD END
				}
			}
			catch(HTTPException he)
			{
				if (!sleepForRetryAt(i))
				{
					// IT1-2013-0000339 エラー時の解析のため、HTTPのステータスコードを業務ログを出力する ADD START
					printBusinessErrorLog(JPCBatchMessageConstant.EKKB0990CE, new String[] {"HTTPException HTTPステータスコード:" + he.getStatusCode()});
					// IT1-2013-0000339 エラー時の解析のため、HTTPのステータスコードを業務ログを出力する ADD END
					throw he;
				}
			}
			catch(IOException ie)
			{
				if (!sleepForRetryAt(i))
				{
					// IT1-2013-0000339 エラー時の解析のため、HTTPのステータスコードを業務ログを出力する ADD START
					printBusinessErrorLog(JPCBatchMessageConstant.EKKB0990CE, new String[] {"IOException 詳細メッセージ:" + ie.getMessage()});
					// IT1-2013-0000339 エラー時の解析のため、HTTPのステータスコードを業務ログを出力する ADD END
					throw ie;
				}
			}
			// OM-2014-0001879 田中 NoSuchElementException画発生した場合の例外処理を追加 ADD START
			catch(NoSuchElementException nsee)
			{
				if (!sleepForRetryAt(i))
				{
					// エラー時の解析のため、HTTPのステータスコードを業務ログを出力する
					printBusinessErrorLog(JPCBatchMessageConstant.EKKB0990CE, new String[] {"NoSuchElementException 詳細メッセージ:" + nsee.getMessage()});
					throw nsee;
				}
			}
			// OM-2014-0001879 田中 NoSuchElementExceptionの例外処理を追加 ADD END
			
			finally
			{
				if (conn != null)
				{
					conn.disconnect();
				}
				printDebugLog(getClass().getSimpleName() + "#execute end");
			}
		}
		// HTTPステータスが401の場合は、ここに入る
		return false;
	}
	
	/**
	 * デバッグログを出力する。
	 * 
	 * @param logMessage	出力するログメッセージ
	 */
	protected void printDebugLog(String logMessage)
	{
		if (log != null)
		{
			log.printDebugLog(logMessage);
		}
	}
	// IT1-2013-0000339 エラー時の解析のため、業務ログを出力する ADD START
	/**
	 * 業務ログを出力する。
	 * @param msgid 出力するメッセージＩＤ
	 * @param repChar 置換文字列（配列で複数指定可能）
	 */
	protected void printBusinessErrorLog(String msgid, String[] repChar)
	{
		if (log != null)
		{
			log.printBusinessErrorLog(msgid, repChar);
		}
	}
	// IT1-2013-0000339 エラー時の解析のため、業務ログを出力する ADD END
	/**
	 * リトライ用のスリープ処理
	 * 
	 * @param cnt	リトライ回数
	 * @return	リトライする場合は true リトライしない場合はfalseを返す。
	 */
	private boolean sleepForRetryAt(int cnt) 
	{
		if (getRetryCount() > cnt)
		{
			// 指定時間スリープし、リトライする。
			printDebugLog(getClass().getSimpleName() + "#(IOException)sleep start");
			try {
				Thread.sleep(getRetryInterval());
			} catch (InterruptedException e) {
				// この場合ログに出力し、処理は続行
				printDebugLog("sleepに失敗");
			}
			printDebugLog(getClass().getSimpleName() + "#(IOException)sleep end");
			printDebugLog(getClass().getSimpleName() + "#(IOException)retryCount=" + cnt);
			return true;
		}
		return false;
	}

	/**
	 * キーストアに登録されていないSSL証明書を無視するためSSLSocketFactoryに登録する
	 * このメソッドは、VMで1度実行されるだけでよい。
	 * 
	 * この方法はあまり薦められない
	 * 本来は証明書を登録するか、承認局に登録されているものを利用すべき
	 * 
	 * @throws NoSuchAlgorithmException	アルゴリズムが存在しない例外
	 * @throws KeyManagementException		キーマネージャの例外
	 */
	public static void initializeSSL() throws NoSuchAlgorithmException, KeyManagementException   
	{
		KeyManager[] km = null;
		TrustManager[] tm = { new X509TrustManager() {

			/**
			 * X509Certificateスタブメソッド
			 * @return null
			 */
			public java.security.cert.X509Certificate[] getAcceptedIssuers() 
			{
				return null;
			}

			/**
			 * checkClientTrustedスタブメソッド
			 * @param arg0
			 * @param arg1
			 * @throws CertificateException
			 */
			public void checkClientTrusted(
					java.security.cert.X509Certificate[] arg0, String arg1)
					throws CertificateException 
			{
				// 処理不要
			}

			/**
			 * checkServerTrustedスタブメソッド
			 * @param arg0
			 * @param arg1
			 * @throws CertificateException
			 */
			public void checkServerTrusted(
					java.security.cert.X509Certificate[] arg0, String arg1)
					throws CertificateException 
			{
				// 処理不要
			}
		} };
		SSLContext sslcontext = SSLContext.getInstance(S_SSL);
		sslcontext.init(km, tm, new SecureRandom());
		HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
			
			/**
			 * verifyスタブメソッド
			 * @param aHostname		ホスト名
			 * @param aSession		セッション
			 * @return 強制的に確認状態を返す
			 */
			public boolean verify(String aHostname, SSLSession aSession) 
			{
				return true;
			}
		});
		HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
		
	}
		
	/**
	 * 認証情報を初期化する。
	 * <br>
	 * @param user		ユーザ名
	 * @param passwd	パスワード
	 */
	public void initializeAuthentication(String user, String passwd)
	{
		// 保存内容の設定
		authUser = user;
		authPasswd = passwd.toCharArray();
		
		/* 認証情報 */
		Authenticator auth = new Authenticator() 
		{
			/**
			 * 登録されているユーザ情報を返す。
			 */
			@Override
			protected PasswordAuthentication getPasswordAuthentication() {
				return new PasswordAuthentication(authUser, authPasswd);
			}
		};
		
		Authenticator.setDefault(auth);
		
	}
	
	/**
	 * アプリケーション定数を取得する。
	 * <br>
	 * @param keyword	キーワード
	 * @param encrypt	復号
	 * @return 取得した値を返す
	 * @throws Exception キー取得、復号時の例外
	 */
	public String getApplicationConst(String keyword, boolean encrypt) throws Exception
	{
		String value = JCCBatCommon.getApplicationConst(keyword);
		if (encrypt)
		{
			if (encrptKey == null)
			{
				encrptKey = JCCBatCommon.getApplicationConst(S_KEY_ENCRYPT_KEY);
				if (encrptKey == null)
				{
					throw new JBSbatBusinessException(JPCBatchMessageConstant.EKKB0130CE, new String[] {"復号キー"});
				}				
			}
			value = JCCcomEncryptionUtil.decrypt(encrptKey, value);
		}
		return value;
	}
	// 20130416 ST1-2013-0000522 ADD START
	/**
	 * BASIC認証の文字列を作成する
	 * <br>
	 * @return "Basic " + [ユーザ]:[パスワード]をBASE64エンコードした文字列
	 */
	protected String getAutorizationValue()
	{
		String userAndPasswd = authUser + ":" + String.valueOf(authPasswd); 
		
		byte[] encodedBytes = Base64.encodeBase64(userAndPasswd.getBytes());
		
		String encodedString = new String(encodedBytes);
		
		return "Basic " + encodedString;
	}
	// 20130416 ST1-2013-0000522 ADD END
}
