/*
 * 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.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.File;
import java.io.IOException;

/**
 * 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 static final String CR      = System.getProperty("line.separator");
	private String		command		= null;
	private File		workDir		= null;
	private String[]	envp		= null;
	private boolean 	isWait		= true;		// プロセスの終了を待つかどうか (デフォルト 待つ)
	private Process 	prcs		= null;
	private ProcessReader pr1		= null;
	private ProcessReader pr2		= null;
	private int  	rtnCode			= ERROR;	// 0:正常  1:実行中  9:取消  -1:異常終了(負)

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

	// 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 ;
		}
	}

	/**
	 * プロセスを実行する時に引き渡すコマンド
	 * 第２引数には、コマンドが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 文字列の配列。配列の各要素は、name=value という形式で環境変数設定を保持する。
	 */
	public void setEnvP( final String[] env ) {
		if( env != null && env.length > 0 ) {
			int size = env.length;
			envp = new String[size];
			System.arraycopy( env,0,envp,0,size );
		}
		else {
			envp = null;
		}
	}

	/**
	 * プロセスの実行処理
	 *
	 * @return  サブプロセスの終了コードを返します。0 は正常終了を示す
	 */
	public int exec() {
		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(IOException ex) {
			String errMsg = "入出力エラーが発生しました。";
			LogWriter.log( errMsg );
			LogWriter.log( ex );
		}
		catch(InterruptedException ex) {
			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 オブジェクトが表すサブプロセスは強制終了されます。
	 *
	 * @return	プロセスが終了しているかどうか[true/false]
	 */
	public boolean isEnd() {
		boolean flag = true;
		if( rtnCode == RUNNING ) {
			flag = pr1.isEnd() && pr2.isEnd() ;
			if( flag ) { rtnCode = OK; }
		}
		return flag ;
	}

	/**
	 * サブプロセスの終了コードを返します。
	 *
	 * @return この Process オブジェクトが表すサブプロセスの終了コード。0 は正常終了を示す
	 * @throws  IllegalThreadStateException この Process オブジェクトが表すサブプロセスがまだ終了していない場合
	 */
	public int exitValue() {
		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 を利用するように修正します。
	 *
	 * @return	インフォメーション(情報)
	 */
	@Override
	public String toString() {
		boolean isEnd = isEnd() ;
		String st = HybsDateUtil.getDate( pr1.getStartTime() , "yyyy/MM/dd HH:mm:ss" ) ;
		String ed = isEnd	? HybsDateUtil.getDate( pr1.getEndTime() , "yyyy/MM/dd HH:mm:ss" )
							: "----/--/-- --:--:--" ;

		StringBuilder buf = new StringBuilder();
		buf.append( "command     = [" ).append( getCommand() ).append( "]\n" );
		buf.append( "  isEnd     = [" ).append( isEnd        ).append( "]\n" );
		buf.append( "  rtnCode   = [" ).append( exitValue()  ).append( "]\n" );
		buf.append( "  startTime = [" ).append( st           ).append( "]\n" );
		buf.append( "  endTime   = [" ).append( ed           ).append( "]\n" );

		return buf.toString();
	}

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

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

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

		/**
		 * 現在書き込みが行われているストリームを文字列にして返します。
		 *
		 * @return	ストリームの文字列
		 *
		 */
		public String getString() {
			return inStream.toString();
		}

		/**
		 * ストリームからの読取が終了しているか確認します。
		 *
		 * @return	読取終了(true) / 読み取り中(false)
		 *
		 */
		public boolean isEnd() {
			return endFlag;
		}

		/**
		 * ストリーム処理の開始時刻を返します。
		 * 開始していない状態は、-1 を返します。
		 *
		 * @return	開始時刻
		 *
		 */
		public long getStartTime() {
			return startTime;
		}

		/**
		 * ストリーム処理の終了時刻を返します。
		 * 終了していない状態は、-1 を返します。
		 *
		 * @return	終了時刻
		 *
		 */
		public long getEndTime() {
			return endTime;
		}
	}

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

		private final long wait ;
		private final Process prcs;

		/**
		 * コンストラクター
		 *
		 * @param wait long ウェイトする時間(ミリ秒)
		 * @param prcs Process 強制終了(destroy) させるプロセス
		 */
		WaitJoin( final long wait,Process prcs ) {
			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 しないとエラーになる 対応
		 */
		public void run() {
			try {
				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( InterruptedException ex ) {
				LogWriter.log( "終了しました。" );
			}
		}
	}
}
