/*
 * 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.IOException;
import java.util.Map;
import java.util.LinkedHashMap ;
import java.util.Vector;
import java.util.Hashtable;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;

/**
 * SFTPConnect.java は、共通的に使用される SFTP関連の基本機能を実装した、クラスです。
 *
 * これは、org.apache.commons.net.ftp.FTPClient をベースに開発されています。
 * このクラスの実行には、commons-net-ftp-2.0.jar が必要です。
 *
 * -host=SFTPサーバー -user=ユーザー -passwd=パスワード -remoteFile=SFTP先のファイル名 を必須設定します。
 * -localFile=ローカルのファイル名は、必須ではありませんが、-command=DEL の場合にのみ不要であり、
 * それ以外の command の場合は、必要です。
 *
 * -command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] は、SFTPサーバーに対しての処理の方法を指定します。
 *   GET:SFTPサーバーからローカルにファイル転送します(初期値)。
 *   PUT:ローカルファイルをSFTPサーバーに PUT(STORE、SAVE、UPLOAD、などと同意語)します。
 *   DEL:SFTPサーバーの指定のファイルを削除します。この場合のみ、-localFile 属性の指定は不要です。
 *   GETDIR,PUTDIR,DELDIR:指定のフォルダ以下のファイルを処理します。
 *
 * -mkdirs=[true/false] は、受け側のファイル(GET時:LOCAL、PUT時:SFTPサーバー)に取り込むファイルのディレクトリが
 * 存在しない場合に、作成するかどうかを指定します(初期値:true)。
 * 通常、SFTPサーバーに、フォルダ階層を作成してPUTする場合、動的にフォルダ階層を作成したいケースで便利です。
 * 逆に、フォルダは確定しており、指定フォルダ以外に PUT するのはバグっていると事が分かっている場合には
 * false に設定して、存在しないフォルダにPUT しようとすると、エラーになるようにします。
 *
 * 引数文字列中に空白を含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  SFTPConnect -host=SFTPサーバー -user=ユーザー -passwd=パスワード -remoteFile=SFTP先のファイル名 [-localFile=ローカルのファイル名]
 *                   [-mode=[ASCII/BINARY]  ] [-command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] ] [-passive=[true/false] ]
 *
 *    -host=SFTPサーバー                ：接続先のSFTPサーバーのアドレスまたは、サーバー名
 *    -user=ユーザー                    ：接続するユーザー名
 *    -remoteFile=SFTP先のファイル名    ：接続先のSFTPサーバー側のファイル名。PUT,GET 関係なくSFTP側として指定します。
 *   [-passwd=パスワード]               ：接続するユーザーのパスワード
 *   [-localFile=ローカルのファイル名]  ：ローカルのファイル名。PUT,GET 関係なくローカルファイルを指定します。
 *   [-port=ポート ]                    ：接続するサーバーのポートを指定します。
 *   [-keyFile=秘密キーファイル ]       ：公開キー暗号化方式を利用する場合のキーファイル名を指定します。
 *   [-command=[GET/PUT/DEL] ]          ：SFTPサーバー側での処理の方法を指定します。
 *             [GETDIR/PUTDIR/DELDIR]]          GET:SFTP⇒LOCAL、PUT:LOCAL⇒SFTP への転送です(初期値:GET)
 *                                              DEL:SFTPファイルを削除します。
 *                                              GETDIR,PUTDIR,DELDIR 指定のフォルダ以下のファイルを処理します。
 *   [-mkdirs=[true/false]  ]           ：受け側ファイル(GET時:LOCAL、PUT時:SFTPサーバー)にディレクトリを作成するかどうか(初期値:true)
 *                                              (false:ディレクトリが無ければ、エラーにします。)
 *   [-timeout=タイムアウト[秒] ]       ：Dataタイムアウト(初期値:600 [秒])
 *   [-display=[false/true] ]           ：trueは、検索状況を表示します(初期値:false)
 *   [-debug=[false|true]   ]           ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @og.rev 5.1.6.0 (2010/05/01) 新規追加
 *
 * @version  5.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class SFTPConnect extends AbstractConnect {
	private final JSch jsch;

	private static final int DEF_PORT	= 22;	// ポート

	private boolean	isConnect	;				// コネクト済みかどうか。

	private String	lastRemoteDir	= "/";		// SFTP先の最後に登録したフォルダ名(mkdir の高速化のため)
	private String	keyFile		;				// 公開キー暗号化方式を利用する場合のキーファイル名を指定します。

	private Session		session	;
	private ChannelSftp channel	;

	/**
	 * デフォルトコンストラクター
	 */
	public SFTPConnect() {
		jsch = new JSch();
	}

	/**
	 * SFTPサーバーへの接続、ログインを行います。
	 *
	 * このメソッドは、初期化メソッドです。
	 * SFTPサーバーへの接続、ログインを行いますので、複数ファイルを転送する
	 * ケースでは、最初に１度だけ呼び出すだけです。
	 * 接続先を変更する場合は、もう一度このメソッドをコールする必要があります。
	 * (そのような場合は、通常、オブジェクトを構築しなおす方がよいと思います。)
	 *
	 */
	@Override
	public void connect() {
		if( isDisplay ) { System.out.println( "CONNECT: HOST=" + host + ",USER=" + user + ",PORT=" + port ); }

		// もし、すでに接続されていた場合は、クロース処理を行います。
		if( isConnect ) { disconnect(); }

		// HostKeyチェックを行わない
		final Hashtable<String,String> config = new Hashtable<String,String>();
		config.put( "StrictHostKeyChecking", "no" );
		JSch.setConfig( config );

		// サーバーに対して接続を行います。
		try {
			if( keyFile == null ) {
				// パスワード認証
				session=jsch.getSession( user, host, getPort( DEF_PORT ) );
				session.setPassword( passwd );
			}
			else {
				// 公開キー、秘密キー認証
				jsch.addIdentity( keyFile );
				session=jsch.getSession( user, host, getPort( DEF_PORT ) );
		//		session.setUserInfo(new MyUserInfo());
			}

			session.connect( timeout*1000 );		// タイムアウトの設定

			channel=(ChannelSftp)session.openChannel("sftp");
			channel.connect();
		}
		catch ( JSchException ex ) {
			errAppend( "SFTP server refused connection. " );
			errAppend( "   host    = [" , host	, "]" );
			errAppend( "   user    = [" , user	, "]" );
			errAppend( "   port    = [" , port	, "]" );
			errAppend( ex );
			if( isDebug ) { ex.printStackTrace(); }
			disconnect();
			throw new RuntimeException( getErrMsg(),ex );
		}

		isConnect = true;
	}

	/**
	 * SFTPサーバーとの接続をクローズします。
	 *
	 * ログインされている場合は、ログアウトも行います。
	 * コネクトされている場合は、ディスコネクトします。
	 *
	 */
	@Override
	public void disconnect() {
		if( isDisplay ) { System.out.println( "DISCONNECT:" ); }

		if( isConnect ) {
			isConnect = false;
			try {
				channel.disconnect();
				session.disconnect();
			}
			catch( Throwable th ) {
				errAppend( "disconnect Error." );
				errAppend( th );
				if( isDebug ) { th.printStackTrace(); }
				throw new RuntimeException( getErrMsg(),th );
			}
		}
	}

	/**
	 * command="GET" が指定されたときの処理を行います。
	 *
	 * ローカルファイルを、接続先のSFTPサーバー側にアップロードします。
	 *
	 * @og.rev 6.0.2.5 (2014/10/31) throws で、JSchException,SftpException を返していたのを、IOException に限定します。
	 *
	 * @param	localFile 	ローカルのファイル名
	 * @param	remoteFile	SFTP先のファイル名
	 * @throws  IOException  入出力エラーが発生したとき
	 */
	@Override
//	protected void actionGET( final String localFile, final String remoteFile ) throws JSchException, SftpException, IOException {
	protected void actionGET( final String localFile, final String remoteFile ) throws IOException {
		if( isDebug ) { System.out.println( "GET: " + remoteFile + " => " + localFile ); }

		// GET(DOWNLOAD)取得時は、ローカルファイルのディレクトリを作成する必要がある。
		if( isMkdirs ) {
			makeLocalDir( localFile );
		}
		try {		// 6.0.2.5 (2014/10/31) IOException に限定
			channel.get( remoteFile,localFile );
		}
		catch( SftpException ex ) {
			final String errMsg = "チャネル(get)でエラーが発生しました。localFile=[" + localFile + "], remoteFile=[" + remoteFile + "]" ;
			throw new IOException( errMsg,ex );
		}
	}

	/**
	 * command="GETDIR" が指定されたときの処理を行います。
	 *
	 * 接続先のSFTPサーバー側のディレクトリ以下をローカルディレクトリに階層構造のままダウンロードします。
	 *
	 * @og.rev 6.0.2.5 (2014/10/31) throws で、JSchException,SftpException を返していたのを、IOException に限定します。
	 *
	 * @param	localDir 	ローカルのディレクトリ名
	 * @param	remoteDir	SFTP先のディレクトリ名
	 * @throws  IOException  入出力エラーが発生したとき
	 */
	@Override
//	protected void actionGETdir( final String localDir, final String remoteDir )  throws IOException,JSchException,SftpException {
	protected void actionGETdir( final String localDir, final String remoteDir )  throws IOException {
//		Vector<?> list = channel.ls( remoteDir );
		final Vector<?> list;
		try {		// 6.0.2.5 (2014/10/31) IOException に限定
			list = channel.ls( remoteDir );
		}
		catch( SftpException ex ) {
			final String errMsg = "チャネル(ls)でエラーが発生しました。remoteDir=[" + remoteDir + "]" ;
			throw new IOException( errMsg,ex );
		}

		for (int i=0;i<list.size();i++) {
			final LsEntry entry = (LsEntry)list.get(i);
			final String rmt = entry.getFilename();
			if( ".".equals( rmt ) || "..".equals( rmt ) ) { continue; }		// "." で始まるファイルもあるので、equasl 判定
			final SftpATTRS stat = entry.getAttrs();
			if( stat.isDir() ) {
				actionGETdir( addFile( localDir,rmt ),addFile( remoteDir,rmt ) );
			}
			else {
				actionGET( addFile( localDir,rmt ),addFile( remoteDir,rmt ) );
			}
		}
	}

	/**
	 * command="PUT" が指定されたときの処理を行います。
	 *
	 * 接続先のSFTPサーバー側のファイル名をローカルにダウンロードします。
	 *
	 * @og.rev 6.0.2.5 (2014/10/31) throws で、JSchException,SftpException を返していたのを、IOException に限定します。
	 *
	 * @param	localFile 	ローカルのファイル名
	 * @param	remoteFile	SFTP先のファイル名
	 * @throws IOException 処理中に Sftp エラーが発生した場合
	 */
	@Override
//	protected void actionPUT( final String localFile, final String remoteFile ) throws JSchException,SftpException {
	protected void actionPUT( final String localFile, final String remoteFile ) throws IOException {
		if( isDebug ) { System.out.println( "PUT: " + localFile + " => " + remoteFile ); }

		try {		// 6.0.2.5 (2014/10/31) IOException に限定
			// PUT(UPLOAD)登録時は、リモートファイルのディレクトリを作成する必要がある。
			if( isMkdirs ) {
				// 前回のDIRとの比較で、すでに存在していれば、makeDirectory 処理をパスする。
				final int ad = remoteFile.lastIndexOf( '/' ) + 1;			// 区切り文字を＋１する。
				final String tmp = remoteFile.substring( 0,ad );

				if( ad > 0 && !lastRemoteDir.startsWith( tmp ) ) {
					lastRemoteDir = tmp;
//					if( remoteFile.startsWith( "/" ) ) {
					if( StringUtil.startsChar( remoteFile , '/' ) ) {		// 6.2.0.0 (2015/02/27) １文字 String.startsWith
						final String[] fls = remoteFile.split( "/" );
						channel.cd( "/" );
						for( int i=1; i<fls.length-1; i++ ) {
							try {
				//				SftpATTRS stat = channel.lstat(fls[i]);		// 存在しないと、SftpException
								channel.cd( fls[i] );						// 存在しないと、SftpException
								continue;
							} catch (SftpException ex) {
								// ファイルが存在しないとき
								channel.mkdir( fls[i] );
								channel.cd( fls[i] );
							}
						}
					}
				}
			}

			channel.put( localFile,remoteFile );
		}
		catch( SftpException ex ) {
			final String errMsg = "チャネル(put)でエラーが発生しました。localFile=[" + localFile + "], remoteFile=[" + remoteFile + "]" ;
			throw new IOException( errMsg,ex );
		}
	}

	/**
	 * command="DEL" が指定されたときの処理を行います。
	 *
	 * 接続先のSFTPサーバー側のファイル名を削除します。
	 *
	 * @og.rev 6.0.2.5 (2014/10/31) throws で、SftpException を返していたのを、IOException に限定します。
	 *
	 * @param	remoteFile	SFTP先のファイル名
	 * @throws IOException SFTPサーバー側のファイル名の削除に失敗したとき
	 */
	@Override
//	protected void actionDEL( final String remoteFile ) throws SftpException {
	protected void actionDEL( final String remoteFile ) throws IOException {
		if( isDebug ) { System.out.println( "DEL: " + remoteFile ); }

		try {		// 6.0.2.5 (2014/10/31) IOException に限定
			channel.rm( remoteFile );
		}
		catch( SftpException ex ) {
			final String errMsg = "チャネル(rm)でエラーが発生しました。remoteFile=[" + remoteFile + "]" ;
			throw new IOException( errMsg,ex );
		}
	}

	/**
	 * command="DELDIR" が指定されたときの処理を行います。
	 *
	 * 接続先のSFTPサーバー側のディレクトリ名を削除します。
	 *
	 * @og.rev 6.0.2.5 (2014/10/31) throws で、SftpException を返していたのを、IOException に限定します。
	 *
	 * @param	remoteDir	SFTP先のディレクトリ名
	 * @throws IOException SFTPサーバー側のディレクトリ名の削除に失敗したとき
	 */
	@Override
//	protected void actionDELdir( final String remoteDir ) throws SftpException {
	protected void actionDELdir( final String remoteDir ) throws IOException {

//		Vector<?> list = channel.ls( remoteDir );
		final Vector<?> list;
		try {		// 6.0.2.5 (2014/10/31) IOException に限定
			list = channel.ls( remoteDir );
		}
		catch( SftpException ex ) {
			final String errMsg = "チャネル(ls)でエラーが発生しました。remoteDir=[" + remoteDir + "]" ;
			throw new IOException( errMsg,ex );
		}

		for (int i=0;i<list.size();i++) {
			final LsEntry entry = (LsEntry)list.get(i);
			final String rmt = entry.getFilename();
			if( ".".equals( rmt ) || "..".equals( rmt ) ) { continue; }		// "." で始まるファイルもあるので、equasl 判定
			final SftpATTRS stat = entry.getAttrs();
			if( stat.isDir() ) {
				actionDELdir( addFile( remoteDir,rmt ) );
			}
			else {
				actionDEL( addFile( remoteDir,rmt ) );
			}
		}
		try {		// 6.0.2.5 (2014/10/31) IOException に限定
			channel.rmdir( remoteDir );
		}
		catch( SftpException ex ) {
			final String errMsg = "チャネル(rmdir)でエラーが発生しました。remoteDir=[" + remoteDir + "]" ;
			throw new IOException( errMsg,ex );
		}
	}

	/**
	 * 公開キー暗号化方式を利用する場合のキーファイル名を指定します。
	 *
	 * @param	keyFile	秘密キーファイル名
	 */
	public void setKeyFile( final String keyFile ) {
		if( keyFile != null ) {
			this.keyFile = keyFile ;
		}
	}

	/**
	 * このクラスの動作確認用の、main メソッドです。
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {

		final String[] CMD_LST  = new String[] { "GET","PUT","DEL","GETDIR","PUTDIR","DELDIR" };

		final Map<String,String> mustProparty   ;		// ［プロパティ］必須チェック用 Map
		final Map<String,String> usableProparty ;		// ［プロパティ］整合性チェック Map

		mustProparty = new LinkedHashMap<String,String>();
		mustProparty.put( "host",		"接続先のSFTPサーバーのアドレスまたは、サーバー名(必須)" );
		mustProparty.put( "user",		"接続するユーザー名(必須)" );
		mustProparty.put( "remoteFile",	"接続先のSFTPサーバー側のファイル名(必須)" );

		usableProparty = new LinkedHashMap<String,String>();
		usableProparty.put( "passwd",		"接続するユーザーのパスワード" );
		usableProparty.put( "localFile",	"ローカルのファイル名" );
		usableProparty.put( "port",			"接続に利用するポート番号を設定します。" );
		usableProparty.put( "keyFile",		"公開キー暗号化方式を利用する場合のキーファイル名を指定します。" );
		usableProparty.put( "command",		"SFTPサーバー側での処理の方法(GET/PUT/DEL)を指定します(初期値:GET)" );
		usableProparty.put( "mkdirs",		"受け側ファイル(GET時:LOCAL、PUT時:SFTPサーバー)にディレクトリを作成するかどうか(初期値:true)" );
		usableProparty.put( "timeout",		"Dataタイムアウト(初期値:600 [秒])" );
		usableProparty.put( "display",		"[false/true]:trueは、検索状況を表示します(初期値:false)" );
		usableProparty.put( "debug",		"デバッグ情報を標準出力に表示する(true)かしない(false)か" +
											CR + "(初期値:false:表示しない)" );

		// ******************************************************************************************************* //
		//       以下、単独で使用する場合の main処理
		// ******************************************************************************************************* //
		final Argument arg = new Argument( "org.opengion.fukurou.util.SFTPConnect" );
		arg.setMustProparty( mustProparty );
		arg.setUsableProparty( usableProparty );
		arg.setArgument( args );

		final SFTPConnect sftp = new SFTPConnect();

		final String host   = arg.getProparty( "host");			// SFTPサーバー
		final String user   = arg.getProparty( "user" );			// ユーザー
		final String passwd = arg.getProparty( "passwd" );		// パスワード

		sftp.setHostUserPass( host , user , passwd );

		sftp.setPort(		arg.getProparty( "port"					) );	// 接続に利用するポート番号を設定します。
		sftp.setKeyFile(	arg.getProparty( "keyFile"				) );	// 公開キー暗号化方式を利用する場合のキーファイル名を指定します。
		sftp.setMkdirs(		arg.getProparty( "mkdirs"	,true		) );	// 受け側ファイルにディレクトリを作成するかどうか
		sftp.setTimeout(	arg.getProparty( "timeout"	,TIMEOUT	) );	// Dataタイムアウト(初期値:600 [秒])
		sftp.setDisplay(	arg.getProparty( "display"	,false		) );	// trueは、検索状況を表示します(初期値:false)
		sftp.setDebug(		arg.getProparty( "debug"	,false		) );	// デバッグ情報を標準出力に表示する(true)かしない(false)か

		try {
			// コネクトします。
			sftp.connect();

			final String command		= arg.getProparty( "command" ,"GET" ,CMD_LST  );	// SFTP処理の方法を指定します。
			final String localFile	= arg.getProparty( "localFile"  );					// ローカルのファイル名
			final String remoteFile	= arg.getProparty( "remoteFile" );					// SFTP先のファイル名

			// command , localFile , remoteFile を元に、SFTP処理を行います。
			sftp.action( command,localFile,remoteFile );
		}
		catch( RuntimeException ex ) {
			System.err.println( sftp.getErrMsg() );
		}
		finally {
			// ホストとの接続を終了します。
			sftp.disconnect();
		}
	}
}
