/*
 * 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.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.LinkedHashMap ;
import java.net.MalformedURLException ;

import jcifs.smb.SmbFile;

/**
 * SMBConnect.java は、共通的に使用される Smb関連の基本機能を実装した、クラスです。
 *
 * これは、jcifs.smb パッケージをベースに開発されています。
 * このクラスの実行には、jcifs-1.3.14.jar が必要です。
 *
 * 接続先のURL schemeは、以下の形式をしています。
 *     smb://[[[domain;]username[:password]@]server[:port]/[[share/[dir/]file]]][?[param=value[param2=value2[...]]]
 * このURLは、内部的に自動作成します。/[[share/[dir/]file]] 部分が、remoteFile として指定する部分になります。 *
 *
 * -host=Smbサーバー -user=ユーザー -passwd=パスワード -remoteFile=Smb先のファイル名 を必須設定します。
 * -localFile=ローカルのファイル名は、必須ではありませんが、-command=DEL の場合にのみ不要であり、
 * それ以外の command の場合は、必要です。
 *
 * -command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] は、SFTPサーバーに対しての処理の方法を指定します。
 *   GET:Smbサーバーからローカルにファイル転送します(初期値)
 *   PUT:ローカルファイルをSmbサーバーに PUT(STORE、SAVE、UPLOAD、などと同意語)します。
 *   DEL:Smbサーバーの指定のファイルを削除します。この場合のみ、-localFile 属性の指定は不要です。
 *   GETDIR,PUTDIR,DELDIR:指定のフォルダ以下のファイルを処理します。
 *
 * -mkdirs=[true/false] は、受け側のファイル(GET時:LOCAL、PUT時:Smbサーバー)に取り込むファイルのディレクトリが
 * 存在しない場合に、作成するかどうかを指定します(初期値:true)。
 * 通常、Smbサーバーに、フォルダ階層を作成してPUTする場合、動的にフォルダ階層を作成したいケースで便利です。
 * 逆に、フォルダは確定しており、指定フォルダ以外に PUT するのはバグっていると事が分かっている場合には
 * false に設定して、存在しないフォルダにPUT しようとすると、エラーになるようにします。
 *
 * 引数文字列中に空白を含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  SMBConnect -host=Smbサーバー -user=ユーザー -passwd=パスワード -remoteFile=Smb先のファイル名 [-localFile=ローカルのファイル名]
 *                   [-command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] ] ]
 *
 *    -host=Smbサーバー                 ：接続先のSmbサーバーのアドレスまたは、サーバー名
 *    -user=ユーザー                    ：接続するユーザー名
 *    -passwd=パスワード                ：接続するユーザーのパスワード
 *    -remoteFile=Smb先のファイル名     ：接続先のSmbサーバー側のファイル名。PUT,GET 関係なくSmb側として指定します。
 *   [-localFile=ローカルのファイル名]  ：ローカルのファイル名。PUT,GET 関係なくローカルファイルを指定します。
 *   [-domain=ドメイン ]                ：接続するサーバーのドメインを指定します。
 *   [-port=ポート ]                    ：接続するサーバーのポートを指定します。
 *   [-command=[GET/PUT/DEL] ]          ：Smbサーバー側での処理の方法を指定します。
 *             [GETDIR/PUTDIR/DELDIR]]          GET:Smb⇒LOCAL、PUT:LOCAL⇒Smb への転送です(初期値:GET)
 *                                              DEL:Smbファイルを削除します。
 *                                              GETDIR,PUTDIR,DELDIR 指定のフォルダ以下のファイルを処理します。
 *   [-mkdirs=[true/false]  ]           ：受け側ファイル(GET時:LOCAL、PUT時:Smbサーバー)にディレクトリを作成するかどうか(初期値:true)
 *                                              (false:ディレクトリが無ければ、エラーにします。)
 *   [-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 SMBConnect extends AbstractConnect {
	private String	domain 	;			// ドメイン
	private String	connURI	;			// Smb接続する場合のURI文字列(の先頭)

	/**
	 * Smbサーバーへの接続、ログインを行います。
	 *
	 * このメソッドは、初期化メソッドです。
	 * Smbサーバーへの接続、ログインを行いますので、複数ファイルを転送する
	 * ケースでは、最初に１度だけ呼び出すだけです。
	 * 接続先を変更する場合は、もう一度このメソッドをコールする必要があります。
	 * (そのような場合は、通常、オブジェクトを構築しなおす方がよいと思います。)
	 * 接続時のアドレスは、下記の形式になりますが、ファイル名の箇所は、action 時に指定するため、
	 * ここでは、先頭部分を先に用意しておきます。
	 *     smb://[[[domain;]username[:password]@]server[:port]/[[share/[dir/]file]]][?[param=value[param2=value2[...]]]
	 *
	 */
	@Override
	public void connect() {
		if( isDisplay ) { System.out.println( "CONNECT: HOST=" + host + ",USER=" + user + ",PORT=" + port ); }

		if( host == null ) {
			errAppend( "host は、必須です。" );
			throw new RuntimeException( getErrMsg() );
		}

		// smb://[[[domain;]username[:password]@]server[:port] ここまで作成
		connURI = "smb://"
				+ ((domain == null) ? "" : domain + ";" )
				+ ((user   == null) ? "" : user )
				+ ((passwd   == null) ? "" : ":" + passwd )
				+ ((user   == null) ? "" : "@" )
				+ host
				+ ((port   == null) ? "" : ":" + port ) ;

		if( isDebug ) { System.out.println( "connURI=" + connURI ); }
	}

	/**
	 * Smbサーバーとの接続をクローズします。
	 *
	 * ここでは、何も処理を行いません。
	 *
	 */
	@Override
	public void disconnect() {
		if( isDisplay ) { System.out.println( "DISCONNECT:" ); }
	}

	/**
	 * command="GET" が指定されたときの処理を行います。
	 *
	 * 接続先のSmbサーバー側のファイル名をローカルにダウンロードします。
	 *
	 * @param	localFile 	ローカルのファイル名
	 * @param	remoteFile	Smb先のファイル名
	 * @throws IOException 入出力エラーが発生したとき
	 */
	@Override
	protected void actionGET( final String localFile, final String remoteFile ) throws IOException {
		if( isDebug ) { System.out.println( "GET: " + remoteFile + " => " + localFile ); }

		final SmbFile rmtFile = makeSmbURI( remoteFile );

		// GET(DOWNLOAD)取得時は、ローカルファイルのディレクトリを作成する必要がある。
		if( isMkdirs ) {
			makeLocalDir( localFile );
		}

		InputStream 	input  = null;
		OutputStream	output = null;
		try {
			input  = new BufferedInputStream( rmtFile.getInputStream() );
			output = new FileOutputStream( localFile );
			FileUtil.copy( input,output );
		}
		finally {
			Closer.ioClose( input );
			Closer.ioClose( output );
		}
	}

	/**
	 * command="GETDIR" が指定されたときの処理を行います。
	 *
	 * 接続先のSmbサーバー側のディレクトリ以下をローカルディレクトリに階層構造のままダウンロードします。
	 *
	 * @param	localDir 	ローカルのディレクトリ名
	 * @param	remoteDir	Smb先のディレクトリ名
	 * @throws IOException 入出力エラーが発生したとき
	 */
	@Override
	protected void actionGETdir( final String localDir, final String remoteDir ) throws IOException {
		final SmbFile rmtFile = makeSmbURI( remoteDir );

		final SmbFile[] rmtFiles = rmtFile.listFiles();
		for( int i=0; i<rmtFiles.length; i++ ) {
			final String rmt = rmtFiles[i].getName();
			if( rmtFiles[i].isDirectory() ) {
				actionGETdir( addFile( localDir,rmt ),addFile( remoteDir,rmt ) );
			}
			else {
				actionGET( addFile( localDir,rmt ),addFile( remoteDir,rmt ) );
			}
		}
	}

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

		final SmbFile rmtFile = makeSmbURI( remoteFile );

		// 存在チェック：すでに存在している場合は、先に削除します。
		if( rmtFile.exists() ) { rmtFile.delete() ; }
		else {
			// PUT(UPLOAD)登録時は、リモートファイルのディレクトリを作成する必要がある。
			if( isMkdirs ) {
				final String tmp = rmtFile.getParent();
				final SmbFile dir = new SmbFile( tmp );
				if( !dir.exists() ) { dir.mkdirs() ; }
			}
		}

		InputStream 	input  = null;
		OutputStream	output = null;
		try {
			input  = new FileInputStream( localFile );
			output = new BufferedOutputStream( rmtFile.getOutputStream() );

			FileUtil.copy( input,output );
		}
		finally {
			Closer.ioClose( input );
			Closer.ioClose( output );
		}
	}

	/**
	 * command="DEL" が指定されたときの処理を行います。
	 *
	 * 接続先のSmbサーバー側のファイル名を削除します。
	 *
	 * @param	remoteFile	Smb先のファイル名
	 * @throws IOException 入出力エラーが発生したとき
	 */
	@Override
	protected void actionDEL( final String remoteFile ) throws IOException {
		if( isDebug ) { System.out.println( "DEL: " + remoteFile ); }

		final SmbFile rmtFile = makeSmbURI( remoteFile );
		rmtFile.delete() ;
	}

	/**
	 * command="DELDIR" が指定されたときの処理を行います。
	 *
	 * 接続先のSmbサーバー側のディレクトリ以下をローカルディレクトリに階層構造のままダウンロードします。
	 *
	 * @param	remoteDir	Smb先のディレクトリ名
	 * @throws IOException 入出力エラーが発生したとき
	 */
	@Override
	protected void actionDELdir( final String remoteDir ) throws IOException {
		final SmbFile rmtFile = makeSmbURI( remoteDir );
		final SmbFile[] rmtFiles = rmtFile.listFiles();
		for( int i=0; i<rmtFiles.length; i++ ) {
			final String rmt = addFile( remoteDir,rmtFiles[i].getName() );
			if( rmtFiles[i].isDirectory() ) {
				actionDELdir( rmt );
			}
			else {
				actionDEL( rmt );
			}
		}
		rmtFile.delete();
	}

	/**
	 * SMB形式のURLを作成します。
	 *
	 * 処理的には、内部で先に作成済みの connURI と、引数のファイルパスを文字列連結します。
	 * connURI の最後には、"/" を付加していませんので、引数のファイルパスに、
	 * "/" を含まない場合は、"/" を付加します。
	 *
	 *     smb://[[[domain;]username[:password]@]server[:port]/[[share/[dir/]file]]]
	 *
	 * @param	remoteFile	Smb先のファイル名
	 *
	 * @return	SMB形式のURL
	 * @og.rtnNotNull
	 */
	private SmbFile makeSmbURI( final String remoteFile ) throws MalformedURLException {
		final String smbFile ;

		if( remoteFile.startsWith( "smb://" ) ) {
			smbFile = remoteFile;
		}
		else if( StringUtil.startsChar( remoteFile , '/' ) ) {			// 6.2.0.0 (2015/02/27) １文字 String.startsWith
			smbFile = connURI + remoteFile ;
		}
		else {
			smbFile = connURI + "/" + remoteFile ;
		}

		return new SmbFile( smbFile );
	}

	/**
	 * 接続先にログインするドメインを設定します。
	 *
	 * @param	domain	接続先にログインするドメイン
	 */
	public void setDomain( final String domain ) {
		this.domain = domain ;
	}

	/**
	 * このクラスの動作確認用の、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",		"接続先のSmbサーバーのアドレスまたは、サーバー名(必須)" );
		mustProparty.put( "user",		"接続するユーザー名(必須)" );
		mustProparty.put( "passwd",		"接続するユーザーのパスワード(必須)" );
		mustProparty.put( "command",	"Smbサーバー側での処理の方法(GET/PUT/DEL/GETDIR/PUTDIR/DELDIR)を指定します。(必須)" );
		mustProparty.put( "remoteFile",	"接続先のSmbサーバー側のファイル名(必須)" );

		usableProparty = new LinkedHashMap<String,String>();
		usableProparty.put( "localFile",	"ローカルのファイル名" );
		usableProparty.put( "domain",		"接続先にログインするドメインを指定します。" );
		usableProparty.put( "port",			"接続に利用するポート番号を設定します。" );
		usableProparty.put( "mkdirs",		"受け側ファイル(GET時:LOCAL、PUT時:Smbサーバー)にディレクトリを作成するかどうか(初期値:true)" );
		usableProparty.put( "display",		"[false/true]:trueは、検索状況を表示します(初期値:false)" );
		usableProparty.put( "debug",		"デバッグ情報を標準出力に表示する(true)かしない(false)か" +
											CR + "(初期値:false:表示しない)" );

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

		final SMBConnect smb = new SMBConnect();

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

		smb.setHostUserPass( host,user,passwd );

		smb.setDomain(	arg.getProparty( "domain"	) );					// 接続先にログインするドメインを指定します。
		smb.setPort(	arg.getProparty( "port"		) );					// 接続に利用するポート番号を設定します。
		smb.setMkdirs(	arg.getProparty( "mkdirs"	,true		) );		// 受け側ファイルにディレクトリを作成するかどうか
		smb.setDisplay(	arg.getProparty( "display"	,false		) );		// trueは、検索状況を表示します(初期値:false)
		smb.setDebug(	arg.getProparty( "debug"	,false		) );		// デバッグ情報を標準出力に表示する(true)かしない(false)か

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

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

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