/*
 * 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 org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.File;
import java.io.IOException;

import static org.opengion.fukurou.system.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring
import org.opengion.fukurou.system.HybsConst;						// fukurou.util.StringUtil → fukurou.system.HybsConst に変更
import org.opengion.fukurou.system.Closer;							// 6.4.2.0 (2016/01/29) package変更 fukurou.util → fukurou.system
import org.opengion.fukurou.system.LogWriter;						// 6.4.2.0 (2016/01/29) package変更 fukurou.util → fukurou.system
import org.opengion.fukurou.system.DateSet;							// 6.4.2.0 (2016/01/29)

/**
 * Shell は、Runtime.exec の簡易的に実行するクラスです。
 * 複雑な処理は通常の Runtime.exec を使用する必要がありますが，ほとんどの
 * プロセス実行については、このクラスで十分であると考えています。
 *
 * このクラスでは、OS(特にWindows)でのバッチファイルの実行において、
 * OS自動認識を行い、簡易的なコマンドをセットするだけで実行できるように
 * しています。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Shell {
	/** Shell オブジェクトの状態を表します。正常  {@value} */
	public static final int OK      = 0;		// 0:正常
	/** Shell オブジェクトの状態を表します。実行中  {@value} */
	public static final int RUNNING = 1;		// 1:実行中
	/** Shell オブジェクトの状態を表します。取消  {@value} */
	public static final int CANCEL  = 9;		// 9:取消
	/** Shell オブジェクトの状態を表します。異常終了(負)  {@value} */
	public static final int ERROR   = -1;		// -1:異常終了(負)

	// private static final String CMD_95  = "C:\\windows\\command.com /c ";
	private static final String CMD_NT	= "C:\\WINNT\\system32\\cmd.exe /c ";
	private static final String CMD_XP	= "C:\\WINDOWS\\system32\\cmd.exe /c ";
	private static final String OS_NAME	= System.getProperty("os.name");
	private String		command		;
	private File		workDir		;
	private String[]	envp		;
	private boolean 	isWait		= true;		// プロセスの終了を待つかどうか (デフォルト 待つ)
	private Process 	prcs		;
	private ProcessReader pr1		;
	private ProcessReader pr2		;
	private int  	rtnCode			= ERROR;	// 0:正常  1:実行中  9:取消  -1:異常終了(負)

	// 3.6.1.0 (2005/01/05) タイムアウト時間を設定
	private long timeout			;			// 初期値は、タイムアウトなし

	// 3.8.9.2 (2007/07/13) Windows Vista対応
	// 5.6.7.1 (2013/07/09) NTでもunknown時はCMD_XPとする
	private static final String CMD_COM ;
	static {
		if( (OS_NAME.indexOf( "NT" ) >= 0 ||
				OS_NAME.indexOf( "2000" ) >= 0)
			&& OS_NAME.indexOf( "unknown" ) < 0 ) {
				CMD_COM = CMD_NT ;
		}
	//	else if( OS_NAME.indexOf( "XP" ) >= 0 ||
	//			 OS_NAME.indexOf( "2003" ) >= 0
	//			 OS_NAME.indexOf( "Vista" ) >= 0 ) {
	//			CMD_COM = CMD_XP ;
	//	}
		else {
			CMD_COM = CMD_XP ;
		}
	}

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public Shell() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * プロセスを実行する時に引き渡すコマンド
	 * 第２引数には、コマンドがBATかEXEかを指定できます。
	 * true の場合は，バッチコマンドとして処理されます。
	 *
	 * @og.rev 3.3.3.0 (2003/07/09) Windows XP 対応
	 * @og.rev 3.7.0.1 (2005/01/31) Windows 2003 対応, Windows 95 除外
	 * @og.rev 3.8.9.2 (2007/07/13) Windows Vista 対応
	 *
	 * @param	cmd	コマンド
	 * @param	batch	true:バッチファイル/false:EXEファイル
	 */
	public void setCommand( final String cmd,final boolean batch ) {
		if( batch ) {
			command = CMD_COM + cmd;
		}
		else {
			command = cmd ;
		}
	}

	/**
	 * プロセスを実行する時に引き渡すコマンド。
	 *
	 * @param   cmd EXEコマンド
	 */
	public void setCommand( final String cmd ) {
		setCommand( cmd,false );
	}

	/**
	 * プロセスの実行処理の終了を待つかどうか。
	 *
	 * @param	flag	true:待つ(デフォルト)/ false:待たない
	 */
	public void setWait( final boolean flag ) {
		isWait = flag;
	}

	/**
	 * プロセスの実行処理のタイムアウトを設定します。
	 * ゼロ(0) の場合は、割り込みが入るまで待ちつづけます。
	 *
	 * @param	tout	タイムアウト時間(秒) ゼロは、無制限
	 *
	 */
	public void setTimeout( final int tout ) {
		timeout = (long)tout * 1000;
	}

	/**
	 * 作業ディレクトリを指定します。
	 *
	 * シェルを実行する、作業ディレクトリを指定します。
	 * 指定しない場合は、このJava仮想マシンの作業ディレクトリで実行されます。
	 *
	 * @param   dir 作業ディレクトリ
	 */
	public void setWorkDir( final File dir ) {
		workDir = dir;
	}

	/**
	 * 環境変数設定の配列指定します。
	 *
	 * 環境変数を、name=value という形式で、文字列配列で指定します。
	 * null の場合は、現在のプロセスの環境設定を継承します。
	 *
	 * @param   env 文字列の配列(可変長引数)。
	 */
	public void setEnvP( final String... env ) {
		if( env != null && env.length > 0 ) {		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
			final int size = env.length;
			envp = new String[size];
			System.arraycopy( env,0,envp,0,size );
		}
		else {
			envp = null;
		}
	}

	/**
	 * プロセスの実行処理。
	 *
	 * @return  サブプロセスの終了コードを返します。0 は正常終了を示す
	 */
	public int exec() {
		final Runtime rt = Runtime.getRuntime();
		Thread wait = null;
		try {
			prcs = rt.exec( command,envp,workDir );		// 3.3.3.0 (2003/07/09)
			pr1 = new ProcessReader( prcs.getInputStream() );
			pr1.start();
			pr2 = new ProcessReader( prcs.getErrorStream() );
			pr2.start();

			if( isWait ) {
				// 3.6.1.0 (2005/01/05)
				wait = new WaitJoin( timeout,prcs );
				wait.start();
				rtnCode = prcs.waitFor();
				if( rtnCode > OK ) { rtnCode = -rtnCode; }
			}
			else {
				rtnCode = RUNNING;	// プロセスの終了を待たないので、1:処理中 を返します。
			}
		}
		catch( final IOException ex ) {
			final String errMsg = "入出力エラーが発生しました。";
			LogWriter.log( errMsg );
			LogWriter.log( ex );
		}
		catch( final InterruptedException ex ) {
			final String errMsg = "現在のスレッドが待機中にほかのスレッドによって強制終了されました。";
			LogWriter.log( errMsg );
			LogWriter.log( ex );
		}
		finally {
			if( wait != null ) { wait.interrupt(); }
		}

		return rtnCode;
	}

	/**
	 * プロセスの実行時の標準出力を取得します。
	 *
	 * @return 実行時の標準出力文字列
	 */
	public String getStdoutData() {
		final String rtn ;
		if( pr1 == null ) {
			rtn = "\n.......... Process is not Running. ....";
		}
		else if( pr1.isEnd() ) {
			rtn = pr1.getString();
		}
		else {
			rtn = pr1.getString() + "\n......... stdout Process is under execution. ...";
		}
		return rtn ;
	}

	/**
	 * プロセスの実行時のエラー出力を取得します。
	 *
	 * @return 実行時の標準出力文字列
	 */
	public String getStderrData() {
		final String rtn ;
		if( pr2 == null ) {
			rtn = "\n.......... Process is not Running. ....";
		}
		else if( pr2.isEnd() ) {
			rtn = pr2.getString();
		}
		else {
			rtn = pr2.getString() + "\n......... stderr Process is under execution. ...";
		}
		return rtn ;
	}

	/**
	 * プロセスが実際に実行するコマンドを取得します。
	 * バッチコマンドかどうかで、実行されるコマンドが異なりますので、
	 * ここで取得して確認することができます。
	 * 主にデバッグ用途です。
	 *
	 * @return 実行時の標準出力文字列
	 */
	public String getCommand() {
		return command;
	}

	/**
	 * サブプロセスを終了します。
	 * この Process オブジェクトが表すサブプロセスは強制終了されます。
	 *
	 */
	public void destroy() {
		if( prcs != null ) { prcs.destroy() ; }
		rtnCode = CANCEL;
	}

	/**
	 * プロセスが終了しているかどうか[true/false]を確認します。
	 * この Process オブジェクトが表すサブプロセスは強制終了されます。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
	 *
	 * @return	プロセスが終了しているかどうか[true/false]
	 */
	public boolean isEnd() {
		boolean flag = true;
		if( rtnCode == RUNNING ) {
			// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
			if( pr1 == null || pr2 == null ) {
				final String errMsg = "#exec()を先に実行しておいてください。"	+ CR
								+ "   command =" + command  ;
				throw new OgRuntimeException( errMsg );
			}

			flag = pr1.isEnd() && pr2.isEnd() ;
			if( flag ) { rtnCode = OK; }
		}
		return flag ;
	}

	/**
	 * サブプロセスの終了コードを返します。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
	 *
	 * @return この Process オブジェクトが表すサブプロセスの終了コード。0 は正常終了を示す
	 * @throws  IllegalThreadStateException この Process オブジェクトが表すサブプロセスがまだ終了していない場合
	 */
	public int exitValue() {
		// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
		if( prcs == null ) {
			final String errMsg = "#exec()を先に実行しておいてください。"	+ CR
							+ "   command =" + command  ;
			throw new OgRuntimeException( errMsg );
		}

		if( rtnCode == RUNNING && isEnd() ) {
			rtnCode = prcs.exitValue();
			if( rtnCode > OK ) { rtnCode = -rtnCode ; }
		}
		return rtnCode;
	}

	/**
	 * この Shell のインフォメーション(情報)を出力します。
	 * コマンド、開始時刻、終了時刻、状態(実行中、終了)などの情報を、
	 * 出力します。
	 *
	 * @og.rev 5.5.7.2 (2012/10/09) HybsDateUtil を利用するように修正します。
	 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
	 *
	 * @return	インフォメーション(情報)
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
		if( pr1 == null ) {
			final String errMsg = "#exec()を先に実行しておいてください。"	+ CR
							+ "   command =" + command  ;
			throw new OgRuntimeException( errMsg );
		}

		final boolean isEnd = isEnd() ;
		final String st = DateSet.getDate( pr1.getStartTime() , "yyyy/MM/dd HH:mm:ss" ) ;
		final String ed = isEnd	? DateSet.getDate( pr1.getEndTime() , "yyyy/MM/dd HH:mm:ss" )
							: "----/--/-- --:--:--" ;

		// 6.0.2.5 (2014/10/31) char を append する。
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( "command     = [" ).append( getCommand() ).append( ']' ).append( CR )
			.append( "  isEnd     = [" ).append( isEnd        ).append( ']' ).append( CR )
			.append( "  rtnCode   = [" ).append( exitValue()  ).append( ']' ).append( CR )
			.append( "  startTime = [" ).append( st           ).append( ']' ).append( CR )
			.append( "  endTime   = [" ).append( ed           ).append( ']' ).append( CR );

		return buf.toString();
	}

	/**
	 * stdout と stderr の取得をスレッド化する為のインナークラスです。
	 * これ自身が、Thread の サブクラスになっています。
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) private static final class に変更。
	 *
	 * @version  4.0
	 * @author   Kazuhiko Hasegawa
	 * @since    JDK5.0,
	 */
//	static class ProcessReader extends Thread {
	private static final class ProcessReader extends Thread {
		private final BufferedReader in ;
		private final StringBuilder inStream = new StringBuilder( BUFFER_MIDDLE );
		private long	startTime	= -1;
		private long	endTime		= -1;
		private boolean	endFlag		;

		/**
		 * コンストラクター。
		 *
		 * ここで、スレッド化したい入力ストリームを引数に、オブジェクトを生成します。
		 *
		 * @og.rev 6.4.2.0 (2016/01/29) fukurou.util.StringUtil → fukurou.system.HybsConst に変更
		 *
		 * @param ins InputStream 入力ストリーム
		 */
		ProcessReader( final InputStream ins ) {
			super();
//			in = new BufferedReader( new InputStreamReader(ins,StringUtil.DEFAULT_CHARSET) );	// 5.5.2.6 (2012/05/25) findbugs対応
			in = new BufferedReader( new InputStreamReader(ins,HybsConst.DEFAULT_CHARSET) );	// 6.4.2.0 (2016/01/29)
			setDaemon( true );		// 3.5.4.6 (2004/01/30)
		}

		/**
		 * Thread が実行された場合に呼び出される、run メソッドです。
		 *
		 * Thread のサブクラスは、このメソッドをオーバーライドしなければなりません。
		 *
		 */
		@Override
		public void run() {
			startTime = System.currentTimeMillis() ;
			String outline;
			try {
				while( (outline = in.readLine()) != null ) {
					inStream.append( outline );
					inStream.append( CR );
				}
			}
			catch( final IOException ex ) {
				final String errMsg = "入出力エラーが発生しました。";
				LogWriter.log( errMsg );
				LogWriter.log( ex );
			}
			finally {
				Closer.ioClose( in );
			}
			endTime = System.currentTimeMillis() ;
			endFlag = true;
		}

		/**
		 * 現在書き込みが行われているストリームを文字列にして返します。
		 *
		 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし(パッケージプライベート)に変更。
		 *
		 * @return	ストリームの文字列
		 * @og.rtnNotNull
		 */
//		public String getString() {
		/* default */ String getString() {
			return inStream.toString();
		}

		/**
		 * ストリームからの読取が終了しているか確認します。
		 *
		 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし(パッケージプライベート)に変更。
		 *
		 * @return	読取終了(true) / 読み取り中(false)
		 *
		 */
//		public boolean isEnd() {
		/* default */ boolean isEnd() {
			return endFlag;
		}

		/**
		 * ストリーム処理の開始時刻を返します。
		 * 開始していない状態は、-1 を返します。
		 *
		 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし(パッケージプライベート)に変更。
		 *
		 * @return	開始時刻
		 */
//		public long getStartTime() {
		/* default */ long getStartTime() {
			return startTime;
		}

		/**
		 * ストリーム処理の終了時刻を返します。
		 * 終了していない状態は、-1 を返します。
		 *
		 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし(パッケージプライベート)に変更。
		 *
		 * @return	終了時刻
		 */
//		public long getEndTime() {
		/* default */ long getEndTime() {
			return endTime;
		}
	}

	/**
	 * スレッドのウェイト処理クラス
	 * 指定のタイムアウト時間が来ると、設定されたプロセスを、強制終了(destroy)します。
	 * 指定のプロセス側は、処理が終了した場合は、このThreadに、割り込み(interrupt)
	 * をかけて、この処理そのものを終了させてください。
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) private static final class に変更。
	 *
	 * @version  4.0
	 * @author   Kazuhiko Hasegawa
	 * @since    JDK5.0,
	 */
//	static class WaitJoin extends Thread {
	private static final class WaitJoin extends Thread {
		private static final long MAX_WAIT = 3600 * 1000 ;	// １時間に設定

		private final long wait ;
		private final Process prcs;

		/**
		 * コンストラクター
		 *
		 * @og.rev 6.4.1.1 (2016/01/16) PMD refactoring. It is a good practice to call super() in a constructor
		 *
		 * @param wait long ウェイトする時間(ミリ秒)
		 * @param prcs Process 強制終了(destroy) させるプロセス
		 */
		WaitJoin( final long wait,final Process prcs ) {
			super();
			this.wait = wait > 0L ? wait : MAX_WAIT ;
			this.prcs = prcs;
		}

		/**
		 * Thread の run() メソッド
		 * コンストラクタで指定のミリ秒だけウェイトし、それが経過すると、
		 * 指定のプロセスを強制終了(destroy)させます。
		 * 外部より割り込み(interrupt)があると、ウェイト状態から復帰します。
		 * 先に割り込みが入っている場合は、wait せずに抜けます。
		 *
		 * @og.rev 5.4.2.2 (2011/12/14) Threadでwaitをかける場合、synchronized しないとエラーになる 対応
		 */
		@Override
		public void run() {
			try {
				final long startTime = System.currentTimeMillis() ;
				boolean waitFlag = true;
				synchronized( this ) {
					while( ! isInterrupted() && waitFlag ) {
						wait( wait );
						waitFlag = ( startTime + wait ) > System.currentTimeMillis() ;
					}
				}
				prcs.destroy() ;
				System.out.println( "タイムアウトにより強制終了しました。" );
			}
			catch( final InterruptedException ex ) {
				LogWriter.log( "終了しました。" );
			}
		}
	}
}
