/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.util;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import java.net.URL;
import java.net.URLConnection;

import org.apache.commons.codec.binary.Base64;

/**
 * URLConnect は、指定のURL にアクセスして、情報/データを取得します。
 * URL へのアクセスにより、エンジンでは各種処理を実行させることが可能になります。
 * 例えば、帳票デーモンの起動や、長時間かかる処理の実行などです。
 * なお、URLに引数が付く場合は、ダブルコーテーションで括って下さい。
 * - 付き引数は、指定順番は、関係ありません。- 無し引数(url,user:passwd)は、
 * 順番があります。
 *
 * java org.opengion.fukurou.util.URLConnect [<-info/-data>] <url> [user:passwd]
 *
 *   args[*] : [<-info/-data>]      情報の取得か、データの取得かを指定します(初期値:-data)。
 *   args[*] : [<-post=ファイル名>] POSTメソッドを指定して、ファイルデータを送信します(初期値:-get)。
 *   args[*] : [<-encode=UTF-8>]    エンコードを指定します(通常は接続先のencodeを使用)。
 *   args[*] : [<-out=ファイル名>]  結果を指定されたファイルエンコードでファイルに出力します。
 *   args[A] : <url>                ＵＲＬを指定します。GETの場合、パラメータは ?KEY=VALです。
 *   args[B] : [<user:passwd>]      BASIC認証のエリアへのアクセス時に指定します。
 *
 * ※ プロキシ設定の３つの方法
 * プロキシ設定には、３つの方法があります。
 *   1.
 *     URL url = URL( "http",proxyHost,proxyPort, url );
 *     URLConnection urlConn = url.openConnection();
 *   2.
 *     SocketAddress scaddr = new InetSocketAddress( proxyHost, proxyPort );
 *     Proxy proxy = new Proxy( Proxy.Type.HTTP, scaddr );
 *     URL url = new Url( url );
 *     URLConnection urlConn = url.openConnection( proxy );
 *   3.
 *     System.setProperty( "http.proxyHost",host );
 *     System.setProperty( "http.proxyPort",String.valueOf( port ) );
 *     URL url = new Url( url );
 *     URLConnection urlConn = url.openConnection();
 *     System.clearProperty( "http.proxyHost" );
 *     System.clearProperty( "http.proxyPort" );
 *
 * 1. 、2. の方法は、urlConn.getContentType() を実行すると、エラーになります。(原因不明)
 * 3. の方法では、マルチスレッドで実行する場合に、問題が発生します。
 * 本クラスでは、方法２ を使用しています。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class URLConnect {
	private static final String CR = System.getProperty("line.separator");

//	private static final String	ENCODE	= "UTF-8";

	private final String urlStr ;
	private final String userPass ;

	private int				rpsCode		= -1;
	private String			rpsMethod	= null;
	private String			rpsMessage	= null;
	private String			type		= null;
	private String			charset		= null;
	private String			postData	= null;
	private long			length		= -1;
	private long			date		= -1;
	private long			modified	= -1;
	private boolean			isPost		= false;
	private URLConnection 	conn		= null;
	private Proxy			proxy		= Proxy.NO_PROXY;

	/**
	 * コンストラクター
	 *
	 * @param	url	接続するアドレスを指定します。(http://server:port/dir/file.html)
	 * @param	pass	ユーザー：パスワード(認証接続が必要な場合)
	 */
	public URLConnect( final String url, final String pass ) {
		urlStr = url;
		userPass = pass;
	}

	/**
	 * 指定のURLに対して、コネクトするのに使用するプロキシ設定を行います。
	 * このときに、ヘッダー情報を内部変数に設定しておきます。
	 *
	 * @param	host	接続するプロキシのホスト名
	 * @param	port	接続するプロキシのポート番号
	 */
	public void setProxy( final String host,final int port ) {
		// 方法2.
		SocketAddress scaddr = new InetSocketAddress( host, port );
		proxy = new Proxy( Proxy.Type.HTTP, scaddr );
	}

	/**
	 * 指定のURLに対して、コネクトします。
	 * このときに、ヘッダー情報を内部変数に設定しておきます。
	 *
	 * @og.rev 4.0.1.0 (2007/12/12) Postで複数キーを使えるように修正
	 * @og.rev 5.1.6.0 (2010/05/01) charsetを指定できるようにする
	 * @throws  java.io.IOException
	 */
	public void connect() throws IOException {
		conn = getConnection();

		if( isPost ) {
			conn.setDoOutput( true );			// POST可能にする

			OutputStream os = null;				// POST用のOutputStream
			PrintStream  ps = null;
			try {
				os = conn.getOutputStream();	// POST用のOutputStreamを取得
				// 5.1.6.0 (2010/05/01)
				if( charset != null ) {
					ps = new PrintStream( os, false, charset );
				}
				else {
					ps = new PrintStream( os );
				}
				ps.print( postData );			// 4.1.0.0 (2007/12/22)
			}
			finally {
				Closer.ioClose( ps );		// close 処理時の IOException を無視
				Closer.ioClose( os );		// close 処理時の IOException を無視
			}
		}
		else {
			// GET 時のコネクション接続
			conn.connect();
		}

		setInfo( conn );
	}

	/**
	 * U接続先のデータを取得します。
	 *
	 * この処理の前に、connect() 処理を実行しておく必要があります。
	 * 取得したデータは、指定のURL へのアクセスのみです。
	 * 通常のWebブラウザは、イメージや、JavaScriptファイル、CSSファイルなど、
	 * 各種ファイル毎にHTTP接続を行い、取得して、レンダリングします。
	 * このメソッドでの処理では、それらのファイル内に指定されているURLの
	 * 再帰的な取得は行いません。
	 * よって、フレーム処理なども行いません。
	 * 本来は、Stream のまま処理することで、バイナリデータも扱えますが、ここでは、
	 * テキストデータ(String)に変換して使用できるデータのみ扱えます。
	 *
	 * @return	接続結果
	 * @throws  java.io.IOException
	 */
	public String readData() throws IOException {
		if( conn == null ) {
			String errMsg = "connect() されていません。データ取得前にconnect()してください。";
			throw new RuntimeException( errMsg );
		}

		BufferedReader reader = null;
		StringBuilder buf = new StringBuilder();
		try {
			reader = getReader();

			String line ;
			while( (line = reader.readLine()) != null ) {
				buf.append( line ).append( CR );
			}
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "指定された文字エンコーディングがサポートされていません。" + CR
						+ " url=[" + urlStr + "]"
						+ " charset=[" + charset + "]" ;
			throw new RuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( reader );
			disconnect();
		}

		return buf.toString();
	}

	/**
	 * サーバへのほかの要求が今後発生しそうにないことを示します。
	 *
	 */
	public void disconnect() {
		if( conn instanceof HttpURLConnection ) {
			((HttpURLConnection)conn).disconnect() ;
		}
	}

	/**
	 * URL と ユーザー：パスワードを与えて、URLConnectionを返します。
	 *
	 * ユーザー：パスワード が null でない場合は、BASCI認証エリアへのアクセスの為、
	 * BASE64Encoder を行って、Authorization プロパティーを設定します。
	 * ここで返す URLConnection は、すでに、connect() メソッド実行済みの
	 * リモート接続が完了した状態のオブジェクトです。
	 *
	 * @return  URLConnectionオブジェクト
	 * @throws  IOException
	 */
	// 5.1.5.0 (2010/04/01) SOAP対応により、PROTECTED化
	protected URLConnection getConnection() throws IOException {
//	private URLConnection getConnection() throws IOException {
		final URL url = new URL( urlStr );

		// 方法2.
		URLConnection urlConn = url.openConnection( proxy );

		if( userPass != null ) {
//			byte[] encoded = Base64.encodeBase64( userPass.getBytes() );
//			String userPassEnc = new String( encoded );
			byte[] encoded = Base64.encodeBase64( userPass.getBytes( StringUtil.DEFAULT_CHARSET ) );	// 5.5.2.6 (2012/05/25) findbugs対応
			String userPassEnc = new String( encoded,StringUtil.DEFAULT_CHARSET );		// 5.5.2.6 (2012/05/25) findbugs対応
			urlConn.setRequestProperty( "Authorization","Basic " + userPassEnc );
		}

		return urlConn ;
	}

	/**
	 * 接続先の情報を内部変数に設定します。
	 *
	 * ここでは、タイプ,エンコード,レスポンスコード,レスポンスメッセージ を設定します。
	 * レスポンスコード,レスポンスメッセージは、接続コネクションが、HttpURLConnection の
	 * 場合のみセットされます。
	 * 途中でエラーが発生した場合でも、継続処理できるようにします。これは、プロキシ
	 * 設定の方法により、conn.getContentType()  でエラーが発生する場合があるためです。
	 *
	 * @param   conn 接続先のコネクション
	 */
	private void setInfo( final URLConnection conn ) {
		try {
			charset = conn.getContentEncoding();
			type	= conn.getContentType() ;
			length	= conn.getContentLength();
			date	= conn.getDate();
			modified= conn.getLastModified();

			if( charset == null && type != null ) {
				int adrs = type.indexOf( "charset" );
				int adrs2 = type.indexOf( '=',adrs );
				if( adrs > 0 && adrs2 > adrs ) {
					charset = type.substring( adrs2+1 ).trim();
				}
			}

			if( conn instanceof HttpURLConnection ) {
				HttpURLConnection httpConn = (HttpURLConnection) conn;
				rpsCode		= httpConn.getResponseCode();
				rpsMethod	= httpConn.getRequestMethod();
				rpsMessage	= httpConn.getResponseMessage() + code2Message( rpsCode );
			}
		}
		// 4.0.0.0 (2007/11/29) Exception から、IOException と RuntimeException に変更
		catch( IOException ex ) {
			System.out.println( ex.getMessage() );
		}
		catch( RuntimeException ex ) {
			System.out.println( ex.getMessage() );
		}
	}

	/**
	 * URL 情報を取得します。
	 *
	 * @og.rev 4.3.4.4 (2009/01/01) メソッド名変更
	 *
	 * @return	URL情報
	 */
	public String getUrl() { return urlStr; }

	/**
	 * POSTするデータを設定します。
	 *
	 * POSTする場合は、connect() 処理を行う前に、データを設定しておく必要があります。
	 *
	 * @og.rev 4.1.0.0 (2007/12/22) キーと値のセットを取得するよう変更
	 * @param	data	POSTデータ
	 */
	public void setPostData( final String data ) {
		postData = data;
		if( postData != null && "?".indexOf( postData ) == 0 ) { // 先頭の?を抜く
			postData = postData.substring(1);
		}
		if( postData != null ) { isPost = true; }
	}

	/**
	 * タイプ 情報を取得します。
	 *
	 * @return	タイプ 情報
	 */
	public String getType() { return type; }

	/**
	 * データ量 情報を取得します。
	 *
	 * @return	データ量 情報
	 */
	public long getLength() { return length; }

	/**
	 * 作成日時 情報を取得します。
	 *
	 * @return	作成日時
	 */
	public long getDate() { return date; }

	/**
	 * 更新日時 情報を取得します。
	 *
	 * @return	更新日時
	 */
	public long getModified() { return modified; }

	/**
	 * 結果コード 情報(HttpURLConnection)を取得します。
	 *
	 * @return	結果コード 情報
	 */
	public int getCode() { return rpsCode; }

	/**
	 * メソッド 情報(HttpURLConnection)を取得します。
	 *
	 * @return	メソッド 情報
	 */
	public String getMethod() { return rpsMethod; }

	/**
	 * メッセージ 情報(HttpURLConnection)を取得します。
	 *
	 * @return	メッセージ 情報
	 */
	public String getMessage() { return rpsMessage; }

	/**
	 * キャラクタ 情報を取得します。
	 *
	 * @return	キャラクタ 情報
	 */
	public String getCharset() { return charset; }

	/**
	 * キャラクタ 情報を設定します。
	 *
	 * @param  chset キャラクタ 情報
	 */
	public void setCharset( final String chset ) { charset = chset; }

	/**
	 * 接続先のデータのリーダーを取得します。
	 *
	 * この処理の前に、connect() 処理を実行しておく必要があります。
	 * 取得したデータは、指定のURL へのアクセスのみです。
	 * 通常のWebブラウザは、イメージや、JavaScriptファイル、CSSファイルなど、
	 * 各種ファイル毎にHTTP接続を行い、取得して、レンダリングします。
	 * このメソッドでの処理では、それらのファイル内に指定されているURLの
	 * 再帰的な取得は行いません。
	 * よって、フレーム処理なども行いません。
	 *
	 * @return	接続結果のリーダー
	 * @throws  java.io.IOException
	 */
	public BufferedReader getReader() throws IOException {
		InputStream in = conn.getInputStream();

		final BufferedReader reader ;
		if( charset != null ) {
			reader = new BufferedReader( new InputStreamReader( in,charset ) );
		}
		else {
//			reader = new BufferedReader( new InputStreamReader( in ) );
			reader = new BufferedReader( new InputStreamReader( in,StringUtil.DEFAULT_CHARSET ) );		// 5.5.2.6 (2012/05/25) findbugs対応
		}

		return reader;
	}

	/**
	 * 接続先のデータの入力ストリームを取得します。
	 *
	 * この処理の前に、connect() 処理を実行しておく必要があります。
	 * 取得したデータは、指定のURL へのアクセスのみです。
	 * 通常のWebブラウザは、イメージや、JavaScriptファイル、CSSファイルなど、
	 * 各種ファイル毎にHTTP接続を行い、取得して、レンダリングします。
	 * このメソッドでの処理では、それらのファイル内に指定されているURLの
	 * 再帰的な取得は行いません。
	 * よって、フレーム処理なども行いません。
	 *
	 * @og.rev 5.4.2.0 (2011/12/01) 新規追加
	 *
	 * @return	接続結果の入力を出力します。
	 * @throws  java.io.IOException
	 */
	public InputStream getInputStream() throws IOException {
//		InputStream in = new BufferedInputStream( conn.getInputStream() );
//		return in;
		return new BufferedInputStream( conn.getInputStream() );		// 5.5.2.4 (2012/05/16)
	}

	/**
	 * HttpURLConnection のレスポンスコードに対応するメッセージ文字列を返します。
	 *
	 * HttpURLConnection の getResponseCode() メソッドにより取得された、HTTPレスポンスコード
	 * に対応する文字列を返します。この文字列は、HttpURLConnection で定義された
	 * static 定数のコメントを、定義しています。
	 *
	 * @param	code	HTTPレスポンスコード
	 *
	 * @return	レスポンスコードに対応する文字列
	 * @see HttpURLConnection#HTTP_ACCEPTED
	 */
	public static String code2Message( final int code ) {
		final String msg ;
		switch( code ) {
			case HttpURLConnection.HTTP_OK 					: msg = "200: OK です。"								;	break;
			case HttpURLConnection.HTTP_CREATED 			: msg = "201: 作成されました。"							;	break;
			case HttpURLConnection.HTTP_ACCEPTED			: msg = "202: 許可されました。"							;	break;
			case HttpURLConnection.HTTP_NOT_AUTHORITATIVE 	: msg = "203: 不当な情報です。"							;	break;
			case HttpURLConnection.HTTP_NO_CONTENT 			: msg = "204: コンテンツがありません。"					;	break;
			case HttpURLConnection.HTTP_RESET 				: msg = "205: コンテンツをリセットします。"				;	break;
			case HttpURLConnection.HTTP_PARTIAL 			: msg = "206: 部分的なコンテンツです。"					;	break;
			case HttpURLConnection.HTTP_MULT_CHOICE 		: msg = "300: 複数選択されています。"					;	break;
			case HttpURLConnection.HTTP_MOVED_PERM 			: msg = "301: 永続的に移動されました。"					;	break;
			case HttpURLConnection.HTTP_MOVED_TEMP 			: msg = "302: 一時的に切り替えます。"					;	break;
			case HttpURLConnection.HTTP_SEE_OTHER 			: msg = "303: 他を参照してください。"					;	break;
			case HttpURLConnection.HTTP_NOT_MODIFIED 		: msg = "304: 修正されませんでした。"					;	break;
			case HttpURLConnection.HTTP_USE_PROXY 			: msg = "305: プロキシを使用してください。"				;	break;
			case HttpURLConnection.HTTP_BAD_REQUEST 		: msg = "400: 不当な要求です。"							;	break;
			case HttpURLConnection.HTTP_UNAUTHORIZED 		: msg = "401: 認証されませんでした。"					;	break;
			case HttpURLConnection.HTTP_PAYMENT_REQUIRED 	: msg = "402: 支払いが必要です。"						;	break;
			case HttpURLConnection.HTTP_FORBIDDEN 			: msg = "403: 禁止されています。"						;	break;
			case HttpURLConnection.HTTP_NOT_FOUND 			: msg = "404: 見つかりませんでした。"					;	break;
			case HttpURLConnection.HTTP_BAD_METHOD 			: msg = "405: メソッドは許可されません。"				;	break;
			case HttpURLConnection.HTTP_NOT_ACCEPTABLE 		: msg = "406: 許容されません。"							;	break;
			case HttpURLConnection.HTTP_PROXY_AUTH 			: msg = "407: プロキシの認証が必要です。"				;	break;
			case HttpURLConnection.HTTP_CLIENT_TIMEOUT 		: msg = "408: 要求が時間切れです。"						;	break;
			case HttpURLConnection.HTTP_CONFLICT 			: msg = "409: 重複しています。"							;	break;
			case HttpURLConnection.HTTP_GONE 				: msg = "410: 存在しません。"							;	break;
			case HttpURLConnection.HTTP_LENGTH_REQUIRED 	: msg = "411: 長さが必要です。"							;	break;
			case HttpURLConnection.HTTP_PRECON_FAILED 		: msg = "412: 前提条件が正しくありません。"				;	break;
			case HttpURLConnection.HTTP_ENTITY_TOO_LARGE 	: msg = "413: 要求エンティティが長すぎます。"			;	break;
			case HttpURLConnection.HTTP_REQ_TOO_LONG 		: msg = "414: 要求 URL が長すぎます。"					;	break;
			case HttpURLConnection.HTTP_UNSUPPORTED_TYPE 	: msg = "415: サポートされないメディアタイプです。"		;	break;
			case HttpURLConnection.HTTP_INTERNAL_ERROR 		: msg = "500: 内部サーバエラーです。"					;	break;
			case HttpURLConnection.HTTP_NOT_IMPLEMENTED 	: msg = "501: 実装されていません。"						;	break;
			case HttpURLConnection.HTTP_BAD_GATEWAY 		: msg = "502: 誤ったゲートウェイです。"					;	break;
			case HttpURLConnection.HTTP_UNAVAILABLE 		: msg = "503: サービスが利用できません。"				;	break;
			case HttpURLConnection.HTTP_GATEWAY_TIMEOUT 	: msg = "504: ゲートウェイが時間切れです。"				;	break;
			case HttpURLConnection.HTTP_VERSION 			: msg = "505: HTTP バージョンがサポートされていません。";	break;
			default : msg = "-1: 未定義" ;
		}
		return msg ;
	}

	/**
	 * サンプル実行用のメインメソッド
	 *
	 * java org.opengion.fukurou.util.URLConnect [<-info/-data>] <url> [user:passwd]
	 *
	 *   args[*] : [<-info/-data>]      情報の取得か、データの取得かを指定します(初期値:-data)。
	 *   args[*] : [<-post=ファイル名>] POSTメソッドを指定して、ファイルデータを送信します(初期値:-get)。
	 *   args[*] : [<-encode=UTF-8>]    エンコードを指定します(通常は接続先のencodeを使用)
	 *   args[*] : [<-out=ファイル名>]  結果をファイルに出力します。ファイルエンコードも指定します。
	 *   args[A] : <url>                ＵＲＬを指定します。GETの場合、パラメータは ?KEY=VALです。
	 *   args[B] : [<user:passwd>]      BASIC認証のエリアへのアクセス時に指定します。
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) throws IOException {
		if( args.length < 3 ) {
			LogWriter.log( "Usage: java org.opengion.fukurou.util.URLConnect [<-info/-data>] <url> [user:passwd]"							);
			LogWriter.log( "   args[*] : [<-info/-data>]      情報の取得か、データの取得かを指定します(初期値:-data)。"				);
			LogWriter.log( "   args[*] : [<-post=ファイル名>] POSTメソッドを指定して、ファイルデータを送信します(初期値:-get)。"		);
			LogWriter.log( "   args[*] : [<-encode=UTF-8>]    エンコードを指定します。(通常は接続先のencodeを使用)"					);
			LogWriter.log( "   args[*] : [<-out=ファイル名>]  結果をファイルに出力します。ファイルエンコードも指定します。"			);
			LogWriter.log( "   args[A] : <url>                ＵＲＬを指定します。GETの場合、パラメータは ?KEY=VALです。"				);
			LogWriter.log( "   args[B] : [<user:passwd>]      BASIC認証のエリアへのアクセス時に指定します。"							);
			return;
		}

		boolean isInfo	= false ;
		boolean isPost	= false ;
		String postKey	= null ;
		String postFile	= null ;
		String outFile	= null ;
		String encode	= null ;
		String[] vals	= new String[2];		// url,userPass の順に引数設定

		int adrs = 0;
		for( int i=0; i<args.length; i++ ) {
			String arg = args[i];
			if( arg.equalsIgnoreCase( "-info" ) ) {
				isInfo = true;
			}
			else if( arg.equalsIgnoreCase( "-data" ) ) {
				isInfo = false;
			}
			else if( arg.startsWith( "-post=" ) ) {
				isPost = true;
				int sepAdrs = arg.indexOf( ':',6 );
				postKey  = arg.substring( 6,sepAdrs );
				postFile = arg.substring( sepAdrs+1 );
			}
			else if( arg.startsWith( "-encode=" ) ) {
				encode = arg.substring( 8 );
			}
			else if( arg.startsWith( "-out=" ) ) {
				outFile = arg.substring( 5 );
			}
			else if( arg.startsWith( "-" ) ) {
				System.out.println( "Error Argment:" + arg );
			}
			else {
				vals[adrs++] = arg;
			}
		}

		String urlStr	= vals[0] ;
		String userPass = vals[1] ;

		URLConnect conn = new URLConnect( urlStr,userPass );

		// POST データは、connect() する前に、設定します。
		if( isPost ) {
			FileString file = new FileString();
			file.setFilename( postFile );
			String postData = file.getValue();

			conn.setPostData( XHTMLTag.urlEncode(postKey, postData) );
		}

		conn.connect();
		if( encode != null ) {
			conn.setCharset( encode );		// encode 指定
		}
		else {
			encode = conn.getCharset();		// 指定がなければ、接続先の charset を使用
		}

		final PrintWriter writer ;
		if( outFile != null ) {
			writer = FileUtil.getPrintWriter( new File( outFile ),encode );
		}
		else {
			writer = FileUtil.getLogWriter( "System.out" );
		}

		if( isInfo ) {
			writer.println( "URL    :" + conn.getUrl() );
			writer.println( "Type   :" + conn.getType() );
			writer.println( "Code   :" + conn.getCode() );
			writer.println( "Message:" + conn.getMessage() );
			writer.println( "Charset:" + conn.getCharset() );
		}
		else {
			writer.println( conn.readData() );
		}

		conn.disconnect();

		Closer.ioClose( writer );
	}
}
