/*
 * LOGICAL-PARADOX.ORG
 * Copyright (C)2005 satoshi akabane(akabane@logical-paradox.org)
 * $Id: DiskArrayController.java,v 1.3 2005/09/28 16:14:29 rampil Exp $
 */
package org.logical_paradox.vsan.dac;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.rmi.RemoteException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.logical_paradox.common.session.SessionManager;
import org.logical_paradox.common.util.Cache;
import org.logical_paradox.common.util.StringUtils;
import org.logical_paradox.vsan.domain.DomainController;
import org.logical_paradox.vsan.node.Database;
import org.logical_paradox.vsan.node.DatabaseAccessException;


/**
 * ディスクアレイコントローラー
 * 所定のプロトコルを受信して、データファイルの操作を実施するサーバー<br>
 * ドメインコントローラーとの通信も実施する
 * @author satoshi akabane@logical-paradox.org
 * @version $Revision: 1.3 $
 */
public class DiskArrayController {
	/** ディスクアレイコントローラーの状態 - 初期状態 */
	public static final int E_INIT = 0;
	/** ディスクアレイコントローラーの状態 - クライアント接続待ち */
	public static final int E_WAITING = 1;
	/** ディスクアレイコントローラーの状態 - 終了中 */
	public static final int E_CLOSING = 100;
	/** ディスクアレイコントローラーの状態 - 停止 */
	public static final int E_CLOSED = 255;

	/** デフォルトのリクエスト受け取り待ち時間(1分) */
	public static final int DEFAULT_CONNECTION_WAIT_MSEC = 60000;
	/** サーバーソケットのaccept待ち状態継続時間 */
	public static final int SERVER_SOCKET_TIMEOUT_SEC = 5000;

	/** 現在のサーバーステータス */
	private int serverStatus = E_INIT;

	/** サーバーソケット */
	private ServerSocket serverSocket = null;

	/** ロガー */
	private static final Log log = LogFactory.getLog(DiskArrayController.class);

	/** コンフィグレーション */
	private final DacConfig config;
	/** ノードコンテキスト */
	private final NodeContext context;

	/** 待機中のスレッドリスト */
	private Cache waitingThreads = new Cache();
	/** リスナー */
	private Listener[] listeners;
	/** リクエストプロセッサの空き待ち時間(msec) */
	private int waittime = DEFAULT_CONNECTION_WAIT_MSEC;
	/** サーバーソケットのaccept処理中断タイミング */
	private int serverSocketTimeoutSec = SERVER_SOCKET_TIMEOUT_SEC;

	/** ドメインコントローラー */
	private DomainController domainController = null;

	/** セッションマネージャー */
	private static final SessionManager sessionManager = new SessionManager();

	/**
	 * コンストラクタ
	 * @param config_ コンフィグレーション
	 * @throws Exception アプリケーション内で処理できなかった例外
	 */
	public DiskArrayController(DacConfig config_) throws DatabaseAccessException {
		config = config_;
		context = new NodeContext(config);
	}
	/**
	 * ディスクアレイコントーラーを初期化する．
	 * @throws DatabaseAccessException データベースの読み込みに失敗
	 */
	protected void init() throws DatabaseAccessException {
		// ノードコンテキストに対してデータベースを設定する
		context.setDatabase(loadDatabase(context));
		String domain = context.getDatabase().getDomain();

		if(config.isJoinOnStartup() == true && StringUtils.isEmpty(domain) == false) {
			// ドメインに自動接続する
			log.info("ドメイン[" + domain + "]に自動接続します");
			log.info("ドメインコントローラー = [" + config.getDomainController() + "]");

			try {
				DomainSynchronizer.sync(context);
				context.join(true);
			} catch (Exception e) {
				log.warn("ドメインコントローラーと通信不能なため，ドメインに参加できませんでした");
				log.warn("ドメインコントローラー起動後，手動でドメインへ参加させてください", e);

				domainController = null;
			}
		}
	}
	/**
	 * セッションマネージャーを返す
	 * @return セッションマネージャー
	 */
	public static final SessionManager getSessionManager() {
		return sessionManager;
	}
	/**
	 * ドメインデータベース情報ファイルをロードする
	 * 所定のディレクトリに何も存在しない場合、空のデータベースを返す
	 * @param context ノードコンテキスト
	 * @return ロードまたは初期化されたデータベースインスタンス
	 */
	protected Database loadDatabase(NodeContext context) throws DatabaseAccessException {
		Database db = new Database(context);

		// 所定のディレクトリに存在するデータファイルを探す
		File dbdir = new File(context.getConfig().getDatabaseDir());
		File[] files = dbdir.listFiles();

		for(int i = 0; i < files.length; i++) {
			File f = files[i];
			String filename = f.getName();
			if(f.isFile() == true && f.getName().endsWith(DacConstant.DBI_EXT) == true) {
				String tokens[] = filename.split("\\.");
				db.setDomain(tokens[tokens.length -2]);
				break;
			}
		}
		if(StringUtils.isEmpty(db.getDomain()) == false) {
			try {
				log.info("既存のドメインデータベースファイル[" + db.getFilename() + "]を復元します");
				db.restore();
				log.info("ドメインデータベースファイルの復元完了");
			} catch (IOException e) {
				log.warn("ドメインデータベースファイル[" + db.getFilename() + "]の復元に失敗:", e);
			}
		}
		return db;
	}
	/**
	 * このディスクアレイコントローラーのコンフィグレーションを返す
	 * @return 現在のコンフィグレーション
	 */
	protected DacConfig getConfig() {
		return config;
	}
	/**
	 * コントーラーを始動する
	 * @throws Exception アプリケーション内で処理できなかった例外
	 */
	public void start() throws Exception {
		log.info("ディスクアレイコントローラーを初期化しています");

		init();

		// 初期化処理中状態へ遷移する
		setServerStatus(E_INIT);

		// 必要数のリスナーを作成してプーリングしておく
		// 作成したリスナーはすぐに待機中リストへ入れる
		listeners = new Listener[config.getConnections()];
		for(int i = 0; i < config.getConnections(); i++) {
			listeners[i] = new Listener(this, context, i+1);
			waitingThreads.add(listeners[i]);
		}
		// リスナーを開始する
		for(int i = 0; i < listeners.length; i++) {
			listeners[i].start();
		}

		log.info("リスナーを[" + listeners.length + "]個生成しました");
		waiting();

		// 終了処理
		shutdown();
	}
	/**
	 * リスニング状態に入る
	 * 停止信号を受信するまでの間，接続とデータ受信を待ち続ける
	 */
	protected void waiting() {
		// 接続待ち状態へ遷移する
		setServerStatus(E_WAITING);
		log.info("ディスクアレイコントローラーはイベント待機中です");

		try {
			serverSocket = new ServerSocket(config.getPort());
			serverSocket.setSoTimeout(serverSocketTimeoutSec);
		} catch (IOException e) {
			log.error("サーバーソケットの生成に失敗: ", e);
			return;
		}
		Socket clientSocket = null;
		while(getServerStatus() == E_WAITING) {
			try {
				clientSocket = serverSocket.accept();
				log.info("新しい接続を受け付けました [" + clientSocket.toString() + "]");
				Listener listener = getListener();

				// 開始
				listener.accept(clientSocket);
			} catch(SocketTimeoutException e) {
				// サーバーソケットの定期点検発生
				// そのまま継続
				continue;
			} catch (IOException e) {
				log.warn("接続中に何らかの入出力エラーが発生しました", e);
			} catch (InterruptedException e) {
				// サーバーbusyのため，接続が受付られなかった
				log.warn("接続数が多すぎて、リスナーの割り当てができませんでした");
				try {
					clientSocket.close();
				} catch (IOException e1) {
					// 強制切断のため無視
				}
			}
		}
		// サーバーソケットを閉じる
		try {
			serverSocket.close();
		} catch(IOException e) {
			log.warn("サーバーソケットのクローズ処理中に何らかのI/Oエラーが発生しました", e);
		}
	}
	/**
	 * ディスクアレイコントローラーをシャットダウンする
	 */
	protected void shutdown() {
		setServerStatus(E_CLOSING);
		log.info("ディスクアレイコントローラーをシャットダウンしています");

		// 全てのリスナーをクローズする
		for(int i = 0; i < listeners.length; i++) {
			listeners[i].tearDown();
		}
		// リスナーの終了待ち
		for(int i = 0; i < listeners.length; i++) {
			try {
				listeners[i].join();
			} catch (InterruptedException e) {
				// 無視
			}
		}
		log.info("全てのリスナーが停止しました");

		// ドメインデータベースファイルを閉じる
		String domain = context.getDatabase().getDomain();

		log.info("ドメインデータベースを停止しています");
		context.getDatabase().close();
		log.info("ドメインデータベースを停止しました");

		if(domainController != null && StringUtils.isEmpty(domain) == false) {
			// ドメインから離脱する
			log.info("ドメイン[" + domain + "]から離脱します");
			try {
				domainController.leave(domain, config.getHost() + ":" + config.getPort());
			} catch (RemoteException e) {
				log.warn("ドメインからの離脱に失敗しました");
				log.warn("必要であれば、手動で離脱させてください");
			}

			domainController = null;
		}

		// 全てのリスナーが無事に停止した場合、ステータス変更
		setServerStatus(E_CLOSED);
	}
	/**
	 * サーバーステータスを返す
	 * @return サーバーステータス
	 */
	public int getServerStatus() {
		return serverStatus;
	}
	/**
	 * サーバーステータスを設定する
	 * @param status サーバーステータス
	 */
	public void setServerStatus(int status) {
		serverStatus = status;
	}
	/**
	 * 待機中スレッドリストを返す
	 * @return 待機中スレッドリスト
	 */
	Cache getWaitingThreads() {
		return waitingThreads;
	}
	/**
	 * 待ち受けリストに追加する
	 * @param listener リスナー
	 */
	void addWaitingThread(Listener listener) {
		log.info("リスナー" + listener.getListenerNo() + "が待機状態に入りました");
		waitingThreads.add(listener);
	}
	/**
	 * 待機中になっているリスナーを取得する
	 * 空いているリスナーがない場合、一定時間待機して、それでも取得できない場合は
	 * 接続BUSYとする
	 * @return 取得できたリスナー
	 * @throws InterruptedException さんざん待ったが空きが発生しなかった
	 */
	protected Listener getListener() throws InterruptedException {
		synchronized(waitingThreads) {
			if(waitingThreads.size() == 0) {
				waitingThreads.wait(waittime);
			}

			return (Listener)waitingThreads.getOne();
		}
	}
	/**
	 * メインメソッド
	 * サーバーを起動するためのブートストラップ
	 * @param args プログラム引数
	 * @throws Exception 処理されなかった例外
	 */
	public static final void main(String[] args) throws Exception {
		new DiskArrayController(new DacConfig(args)).start();
		log.info("ディスクアレイコントローラーは停止しました");
	}
}
