/**
* Kandata
* Copyright (C) 2003. Yoshinori Watanabe
* 
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* 
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
* 
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package kandata.server.http;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import kandata.server.*;
import kandata.KandataServerException;


/**
 * KandataサーバのServlet実装です.
 */
public class ServletServer extends HttpServlet implements KandataServerContext {

	static final boolean TRACE_ENABLE = true;
//	static final void TRACE(String message) { if (TRACE_ENABLE) System.out.println(message); }
//	static final void TRACE(Throwable ex) { if (TRACE_ENABLE) ex.printStackTrace(System.out); }
	void TRACE(String message) { if (TRACE_ENABLE) getServletContext().log(message); }
	void TRACE(String message, Throwable ex) { if (TRACE_ENABLE) getServletContext().log(message, ex); }

	/** 設定ファイル. */
	private static final String CONFIG_FILENAME = "kandata.properties";

	/** アプリケーションサーバ. */
	protected KandataServer server;

	/** サーバアプリのクラス名. */
	protected String serverClassName;

	/** サーバリソースのパス. */
	protected String serverResourcePath;

	/** クライアントアプリのエントリクラス名. */
	protected String clientEntryClassName;

	/** クライアントアプリのクラスパス. */
	protected String clientClassPath;	// 末尾にseparatorをつけない。

	/** クライアントリソースのパス. */
	protected String clientResourcePath;

	/** クライアント・コンテキストを格納する. クライアントIDと関連付けることにする. */
	private HashMap clientContextMap = new HashMap();	// <String, KandataClientContext>

	/** アクションマップ.コマンド名と関連付けられる */
	private HashMap actionMap = new HashMap();	// <String, KandataAction>

	/** サーブレットの初期化処理. */
	public void init() throws ServletException {
		TRACE("<<< init() >>>");
		super.init();

		// 設定ファイルを読み込む
		loadConfiguration(CONFIG_FILENAME);

		// アプリケーションサーバを生成
		try {
			server = (KandataServer) Class.forName(serverClassName).newInstance();
		} catch (Exception e) {
			TRACE("create KandataServer failed:"+serverClassName, e);
		}
		// アプリケーションサーバの初期化
		server.init(this);
	}

	/**
	 * GET要求を処理します.
	 */
	public void doGet(	HttpServletRequest request, HttpServletResponse response)
											throws IOException, ServletException {
		//TRACE("<<< doGet() >>> session.Id="+request.getSession().getId());
	}

	/**
	 * POST要求を処理します.
	 */
	public void doPost(	HttpServletRequest request,
						HttpServletResponse response)
						throws IOException, ServletException {
		//TRACE("<<< doPost() >>> session.Id="+request.getSession().getId());

		try {
			// 現在日時
			long now = System.currentTimeMillis();

			// クライアントID、メソッド名と引数を取得する。
			ObjectInputStream in = new ObjectInputStream(request.getInputStream());
			String id = (String) in.readObject();
			String command = (String) in.readObject();
			int args_len = in.readInt();
			TRACE("  ID="+id+" command=["+command+"] args_len="+args_len+" -------------------");
			Object[] args = new Object[args_len];
			for (int i=0; i<args_len; i++) {
				args[i] = in.readObject();
				TRACE("  args["+i+"]="+args[i]);
			}
			in.close();

			// クライアントコンテキストを探す。
			KandataClientContext context = (KandataClientContext) clientContextMap.get(id);

			// 呼ぶ！
			KandataServerException ex = null;
			Object ret = null;
			try {
				if (command.charAt(0) == '@') {
					if (command.equals("@connect")) {
						if (context != null) throw new KandataServerException("already connected", null);
						// 新しいクライアントコンテキストを生成する.
						context = new KandataClientContext(now);
						clientContextMap.put(context.getId(), context);
					}
					else {
						if (context == null) throw new KandataServerException("not connected", null);
					}
					ret = invokeSystemCommand(context, command, args);
					if (command.equals("@disconnect")) {
						// クライアントコンテキストを削除する.
						clientContextMap.remove(id);
					}
				}
				else {
					if (context == null) throw new KandataServerException("not connected", null);
					ret = invokeCommand(context, command, args);
				}
			}
			catch (KandataServerException e) {
				ex = e;
			}
			TRACE("  ret="+ret);
			// 戻り値と例外をクライアントへ送る。
			ObjectOutputStream out = new ObjectOutputStream(response.getOutputStream());
			out.writeObject(ret);
			out.writeObject(ex);
			out.close();
			// 最終アクセス日時を記憶
			context.setLastAccessTimeMillis(now);
		}
		catch (ClassNotFoundException e) {
			TRACE("serialize failed.", e);
		}
	}

	/**
	 * システムコマンドを処理する.
	 */
	protected Object invokeSystemCommand(KandataClientContext context, String command, Object[] args)
						throws KandataServerException {

		if (command.equals("@getClass")) {
			String classname = (String) args[0];
			String path = getWebInfPath() + File.separator + clientClassPath + File.separator + classname.replace('.', File.separatorChar) + ".class";
			//TRACE("classname="+classname+" path="+path);
			FileInputStream in = null;
			try {
				File file = new File(path);
				byte[] buf = new byte[(int) file.length()];
				in = new FileInputStream(file);
				in.read(buf, 0, (int) file.length());
				return buf;
			} catch (IOException e) {
				throw new KandataServerException("load class failed: classname="+classname+" path="+path, e);
			}
			finally {
				if (in != null) try { in.close(); } catch (IOException e) {}
			}
		}
		else if (command.equals("@getResource")) {
			String resourcename = (String) args[0];
			String path = getWebInfPath() + File.separator + clientResourcePath + resourcename;
			//TRACE("path="+path);
			FileInputStream in = null;
			try {
				File file = new File(path);
				byte[] buf = new byte[(int) file.length()];
				in = new FileInputStream(file);
				in.read(buf, 0, (int) file.length());
				return buf;
			} catch (IOException e) {
				throw new KandataServerException("load resource failed: resourcename="+resourcename+" path="+path, e);
			}
			finally {
				if (in != null) try { in.close(); } catch (IOException e) {}
			}
		}
		else if (command.equals("@connect")) {
			server.connected(context);
			return context.getId();
		}
		else if (command.equals("@disconnect")) {
			server.disconnected(context);
			return null;
		}
		else if (command.equals("@getEntryClassName")) {
			return clientEntryClassName;
		}

		TRACE("未定義のコマンドです command="+command);
		throw new KandataServerException("illegal command:"+command, null);
	}

	protected Object invokeCommand(KandataClientContext context, String command, Object[] args) 
						throws KandataServerException {

		KandataEvent event = new KandataEvent(context, command, args);
		KandataAction action = (KandataAction) actionMap.get(command);
		if (action == null) {
			throw new KandataServerException("illegal command:"+command, null);
		}
		Object result = action.execute(event);
		return result;
	}

	/**
	 * 設定を読み込む.
	 * XMLではなく、Propertiesにしている。JDK1.3ではデフォルトで入っていないので.
	 */
	protected void loadConfiguration(String fname) {
		FileInputStream in = null;
		try {
			in = new FileInputStream(getWebInfPath() + File.separator + fname);
			Properties prop = new Properties();
			prop.load(in);
			String appname       = prop.getProperty("application.name");
			serverClassName      = prop.getProperty("application.serverclassname");
			serverResourcePath   = prop.getProperty("application.serverresourcepath");
			clientEntryClassName = prop.getProperty("application.cliententryclassname");
			clientClassPath      = prop.getProperty("application.clientclasspath");
			clientResourcePath   = prop.getProperty("application.clientresourcepath");
			in.close();
			TRACE("application.name="+appname);
			TRACE("application.serverclassname="+serverClassName);
			TRACE("application.serverresourcepath="+serverResourcePath);
			TRACE("application.cliententryclassname="+clientEntryClassName);
			TRACE("application.clientclasspath="+clientClassPath);
			TRACE("application.clientresourcepath="+clientResourcePath);
		}
		catch (IOException e) {
			TRACE("load configuration failed.", e);
		}
		finally {
			if (in != null) try { in.close(); } catch (IOException e) {}
		}
	}

	/** サーバが終了されます. */
	public void destroy() {
		server.exit();
	}

	public String getResourcePath() {
		return getWebInfPath() + File.separator + serverResourcePath;
	}
	/** %TOMCAT_HOME%/webapps/WEB-INF のディレクトリを返す. */
	private String getWebInfPath() {
		return getServletContext().getRealPath("/WEB-INF");
	}
	/**
	 * アクションを登録する. 
	 */
	public void putAction(String command, KandataAction action) {
		actionMap.put(command, action);

		// TODO:KandataActionを動作中に置き換えることは可能か？
		// システム全体を止めることなく、問題のあったActionのみを止め、
		// 新しいものに置き換える。それを外部から操作できるようにする。
	}
}

