/*
このファイルの中身は、NTT オープンソースライセンス　バージョン 1.0（「本
契約」という）の適用を受けます。
本契約を遵守しない限り、このファイルを使用してはなりません。
本契約のコピーは、次のＵＲＬから入手できます。
【配布サイトのURL】 http://www.oss.ecl.ntt.co.jp/lms/

本契約に基づき頒布されるソフトウェアは、現状のまま、明示又は黙示のあらゆ
る種類の保証なしで、頒布されます。本契約に基づく権利及び制限を律する特定
の文言については、本契約を参照してください。

「オリジナルコード」は、 NTT Cyber Space Laboratories Code です。 
「オリジナルコード」の「初期開発者」は、日本電信電話株式会社です。  
日本電信電話株式会社により創作された部分は、Copyright (C) 2004 
日本電信電話株式会社 です。
全ての権利が留保されます。 
「コントリビュータ」：_____________________________________ 


The contents of this file are subject to the NTT Opensource License
Version 1.0 (the License); you may not use this file except in
compliance with the License. You may obtain a copy of the License at
【配布サイトのURL】 http://www.oss.ecl.ntt.co.jp/lms/

Software distributed under the License is distributed on an AS IS
basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
License for the specific language governing rights and limitations
under the License.

The Original Code is NTT Cyber Space Laboratories Code .

The Initial Developer of the Original Code is NIPPON TELEGRAPH AND 
TELEPHONE CORPORATION.
Portions created by the NIPPON TELEGRAPH AND TELEPHONE CORPORATION 
are Copyright (C) 2004 NIPPON TELEGRAPH AND TELEPHONE CORPORATION. 
All Rights Reserved.

Contributor(s) ______________________________________.
*/

//
//	ScormStudyInfoServlet
//	変更履歴
//		2004.02.01	新規作成
//

package jp.co.ntt.lms.lo.scorm;

import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import jp.co.ntt.lms.lo.LOException;
import jp.co.ntt.lms.xmo.Environment;
import jp.co.ntt.lms.xmo.util.DebugLog;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

/**
 * 教材構造定義ならび中断情報を取得するサーブレット。<br>
 * リクエストの指定に従い、教材構造情報、必要があれば中断情報を取得し、
 * クライアントに以下のXML形式で戻します。
 * &lt;result&gt;<br>
 * 		&lt;status&gt;結果値（整数値）&lt;/status&gt;<br>
 * 	 	教材構造定義（&lt;manifest&gt;で始まる）<br>
 * 		中断情報（&lt;resume&gt;で始まる）<br>
 * &lt;/result&gt;<br>
 * 結果値は、成功(0)、認証エラー(1)、その他エラー(2)。
 * <br>
 * @author T.Nishiki
 */
public class ScormStudyInfoServlet extends HttpServlet {

	/**
	 * GETリクエストを処理します。<br>
	 * @param request HTTPリクエスト。
	 * @param response HTTPレスポンス。
	 * @throws IOException リクエストからの入力またはレスポンスへの出力時に
	 * 　　　　　エラーとなった場合。
	 * @throws ServletException 処理中に例外が発生した場合。
	 * @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse)
	 */
	protected void doGet(
		HttpServletRequest request,
		HttpServletResponse response)
		throws IOException, ServletException {
		// メソッド開始ログ
		DebugLog.write(getClass(),
			"HEAD doGet(reqeust,response)",
			DebugLog.ROW);

		doRequest(request, response);

		// メソッド終了ログ
		DebugLog.write(getClass(),
			"TAIL doGet(reqeust,response)",
			DebugLog.ROW);
	}

	/**
	 * POSTリクエストを処理します。<br>
	 * @param request HTTPリクエスト。
	 * @param response HTTPレスポンス。
	 * @throws IOException リクエストからの入力またはレスポンスへの出力時に
	 * 　　　　　エラーとなった場合。
	 * @throws ServletException 処理中に例外が発生した場合。
	 * @see HttpServlet#doPost(HttpServletRequest, HttpServletResponse)
	 */
	protected void doPost(
		HttpServletRequest request,
		HttpServletResponse response)
		throws IOException, ServletException {
		// メソッド開始ログ
		DebugLog.write(getClass(),
			"HEAD doPost(reqeust,response)",
			DebugLog.ROW);

		doRequest(request, response);

		// メソッド終了ログ
		DebugLog.write(getClass(),
			"TAIL doPost(reqeust,response)",
			DebugLog.ROW);
	}

	/**
	 * リクエストを処理します。<br>
	 *
	 * @param request HTTPリクエスト。
	 * @param response HTTPレスポンス。
	 * @throws IOException リクエストからの入力またはレスポンスへの出力時に
	 * 　　　　　エラーとなった場合。
	 * @throws ServletException 処理中に例外が発生した場合。
	 */
	private void doRequest(
		HttpServletRequest request,
		HttpServletResponse response)
		throws IOException, ServletException {

		////////////////////////////////////////////////////////////////////////
		// メソッド開始ログ
		DebugLog.write(getClass(),
			"HEAD doRequest(reqeust,response)",
			DebugLog.ROW);

		ServletOutputStream outputStream = null;
		try {
			////////////////////////////////////////////////////////////////////
			// パラメータの取得
			// ユーザID
			String userID = request.getParameter(
					ScormLoConstant.PARAM_NAME_USERID);
			// LOID
			String loID = request.getParameter(ScormLoConstant.PARAM_NAME_LOID);
			// ダイジェストキー
			String authKey =
				request.getParameter(ScormLoConstant.PARAM_NAME_AUTHKEY);
			// 学習回数
			String studySession = request.getParameter(
				ScormLoConstant.PARAM_NAME_STUDY_SESSION);
			// 学習回数
			String command = request.getParameter(
				ScormLoConstant.PARAM_NAME_COMMAND);
			// =================================================================
			// ログ出力
			DebugLog.write(getClass(),
				"[" + ScormLoConstant.PARAM_NAME_USERID + "][" + userID + "]",
				DebugLog.MID);
			DebugLog.write(getClass(),
				"[" + ScormLoConstant.PARAM_NAME_LOID + "][" + loID + "]",
				DebugLog.MID);
			DebugLog.write(getClass(),
				"[" + ScormLoConstant.PARAM_NAME_AUTHKEY + "][" + authKey + "]",
				DebugLog.MID);
			DebugLog.write(getClass(),
				"[" + ScormLoConstant.PARAM_NAME_STUDY_SESSION + "]"
					+ "[" + studySession + "]",
				DebugLog.MID);
			DebugLog.write(getClass(),
				"[" + ScormLoConstant.PARAM_NAME_COMMAND + "]"
					+ "[" + command + "]", DebugLog.MID);
			////////////////////////////////////////////////////////////////////
			// 最終的にクライアントに返信するXML Dom Dcument
			Document resultDocument = null;
			DocumentBuilderFactory factory =
					DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			resultDocument = builder.newDocument();
			// =================================================================
			// ルート要素の設定
			Element rootElement = resultDocument.createElement(
					ScormLoConstant.STUDYINFO_TAG_NAME_ROOT);
			resultDocument.appendChild(rootElement);
			////////////////////////////////////////////////////////////////////
			// 認証確認
			boolean authStatus = appendAuthenticateElement(
					resultDocument, rootElement, userID, loID, authKey);
			////////////////////////////////////////////////////////////////////
			// 認証に成功したときのみ、情報取得を実施
			if (authStatus == true) {
				String materialID = getMaterialID(loID);
				// =============================================================
				// 教材構造定義の取得・統合
				appendManifestElement(resultDocument, rootElement, materialID);
				// =============================================================
				// 中断情報の取得・統合
				if (command != null
					&& command.equals(
						ScormLoConstant.START_PARAM_COMD_CONT)
					&& studySession != null
					&& studySession.length() > 0) {
					int intStudySession = Integer.parseInt(studySession);
					appendResumeElement(
						resultDocument, rootElement,
						userID, loID, intStudySession);
				}
			}

			ScormLoEnv scormEnv = new ScormLoEnv();
			// レスポンスのエンコードを取得
			String responseEncode = scormEnv.getProperty(
				ScormLoEnv.CHARSET_RESPONSE_STUDY_INFO);
			// レスポンス取得
			response.setContentType(
				"text/xml;charset="	+ responseEncode );
			outputStream = response.getOutputStream();
			Properties outProperties = new Properties();
			outProperties.put(ScormLoUtil.PROPERTY_ENCODING, responseEncode);
			ScormLoUtil.transformNodeToStream(
				resultDocument, outputStream, outProperties);
		}
		catch (Exception e) {
			DebugLog.write(getClass(), e, DebugLog.HIGHT);
			throw new ServletException(e);
		}
		finally {
			if (outputStream != null) {
				outputStream.close();
			}
		}
		////////////////////////////////////////////////////////////////////////
		// メソッド終了ログ
		DebugLog.write(getClass(),
			"TAIL doRequest(reqeust,response)",
			DebugLog.ROW);
	}

	/**
	 * 認証をおこない、その結果をXML要素として追加します。<br>
	 * 指定されたユーザID、LOID、認証キーをもとに認証を実行し、
	 * その結果を包含するDOM Elementを生成し、
	 * 指定した親要素に子要素として登録します。<br>
	 *
	 * @param document 要素を生成するためのDOM Document。
	 * @param parentElement 結果要素を登録する親要素となるDOM Element。
	 * @param userID クライアントから送信されたユーザID。
	 * @param loID クライアントから送信されたLOID。
	 * @param authKey クライアントから送信された認証キー。
	 * @return 認証に成功した場合はtrueを、失敗した場合はfalseを戻します。
	 * @throws LOException 認証処理中にエラーが発生した場合。
	 */
	private boolean appendAuthenticateElement(
			Document document,
			Element parentElement,
			String userID,
			String loID,
			String authKey) throws LOException {
		////////////////////////////////////////////////////////////////////////
		// メソッド開始ログ
		DebugLog.write(getClass(),
			"HEAD appendAuthenticateElement("
				+ document + "," + parentElement + ","
				+ userID + "," + loID + "," + authKey + ")",
			DebugLog.ROW);

		// 認証結果
		boolean authStatus = false;
		try {
			// =================================================================
			// 確認
			authStatus = authenticate(userID, loID, authKey);
			// 認証に失敗した場合、以降の情報取得は省略
			int status = 0;
			if (authStatus == false) {
				status = ScormLoConstant.STUDYINFO_STATUS_FAILURE_AUTH;
			}
			else {
				// ここまで到達した場合、成功とします。
				status = ScormLoConstant.STUDYINFO_STATUS_SUCCESS;
			}
			// =================================================================
			// 結果値用ノード追加
			Element statusElement = document.createElement(
					ScormLoConstant.STUDYINFO_TAG_NAME_STATUS);
			parentElement.appendChild(statusElement);
			Text statusText = document.createTextNode(
					Integer.toString(status));
			statusElement.appendChild(statusText);
		}
		catch (LOException e) {
			throw e;
		}
		catch (Exception e) {
			DebugLog.write(getClass(), e, DebugLog.HIGHT);
			throw new LOException(e.getMessage());
		}
		////////////////////////////////////////////////////////////////////////
		// メソッド終了ログ
		DebugLog.write(getClass(),
		"TAIL appendAuthenticateElement("
			+ document + "," + parentElement + ","
				+ userID + "," + loID + "," + authKey + ")"
				+ (" return " + authStatus),
			DebugLog.ROW);

		return authStatus;
	}

	/**
	 * 教材構造定義を取得し、指定された要素に統合します。<br>
	 * 指定された教材IDに合致する教材構造定義をDOM Documentとして取得し、
	 * 指定した親要素に子要素としてインポート登録します。<br>
	 *
	 * @param document 要素を生成するためのDOM Document。
	 * @param parentElement 教材構造を登録する親要素となるDOM Element。
	 * @param materialID 教材ID。
	 * @throws LOException 情報取得中に例外が発生した場合。
	 */
	private void appendManifestElement(
		Document document,
		Element parentElement,
		String materialID) throws LOException {

		////////////////////////////////////////////////////////////////////////
		// メソッド開始ログ
		DebugLog.write(getClass(),
		"HEAD appendManifestElement("
			+ document + "," + parentElement + "," + materialID + ")",
			DebugLog.ROW);

		try {
			// 教材構造定義ファイル
			File materialFile =
					ScormManifestUtil.getManifestPathFile(materialID);
			// 教材構造定義のDOM Document化
			Document manifestDocument =
				ScormLoUtil.transformXmlFileToDocument(materialFile);
			// 教材構造定義Documentを結果Documentに統合
			Node importedManifestNode = document.importNode(
					manifestDocument.getDocumentElement(), true);
			parentElement.appendChild(importedManifestNode);
		}
		catch (LOException e) {
			throw e;
		}
		catch (Exception e) {
			DebugLog.write(getClass(), e, DebugLog.HIGHT);
			throw new LOException(e.getMessage());
		}
		////////////////////////////////////////////////////////////////////////
		// メソッド終了ログ
		DebugLog.write(getClass(),
		"TAIL appendManifestElement("
			+ document + "," + parentElement + "," + materialID + ")",
			DebugLog.ROW);
	}

	/**
	 * 中断情報を取得し、指定された要素に統合します。<br>
	 * 指定された教材IDに合致する教材構造定義をDOM Documentとして取得し、
	 * 指定した親要素に子要素としてインポート登録します。<br>
	 *
	 * @param document 要素を生成するためのDOM Document。
	 * @param parentElement 中断情報を登録する親要素となるDOM Element。
	 * @param userID クライアントから送信されたユーザID。
	 * @param loID クライアントから送信されたLOID。
	 * @param studySession クライアントから送信された学習セッション回数。
	 * @throws LOException 情報取得中に例外が発生した場合。
	 */
	private void appendResumeElement(
		Document document,
		Element parentElement,
		String userID,
		String loID,
		int studySession) throws LOException {

		// 中断情報ファイルを取得
		File resumeFile =
			ScormLoUtil.getResumeFile(
				userID, loID, studySession);
		// 中断情報ファイルのDOM Document化
		Document resumeDocument =
			ScormLoUtil.transformXmlFileToDocument(resumeFile);
		// 中断情報Documentを結果Documentに統合
		Node importedResumeNode = document.importNode(
			resumeDocument.getDocumentElement(), true);
		parentElement.appendChild(importedResumeNode);
	}

	/**
	 * 認証処理を実施します。<br>
	 * @param userID リクエストに含まれるユーザID。
	 * @param loID リクエストに含まれるLOID。
	 * @param requestAuthKey リクエストに含まれるダイジェストキー。
	 * @return 認証に成功した場合はtrueを、
	 *          失敗した場合はfalseを戻します。<br>
	 *          引数のうち１つがnullの場合、失敗としてfalseを戻します。
	 * @throws LOException 認証処理の算出に失敗した場合。
	 */
	private boolean authenticate(
		String userID, String loID, String requestAuthKey)
			throws LOException {
		// メソッド開始ログ
		DebugLog.write(getClass(),
			"HEAD authenticate("
				+ userID + "," + loID + "," + requestAuthKey + ")",
			DebugLog.ROW);

		// このメソッドの戻り値。初期値：認証失敗。
		boolean status = false;
		try {
			if (userID != null && loID != null && requestAuthKey != null) {
				// マネージャ・サーバーホスト名称を取得
				Environment environment = new Environment();
				String hostName = environment.getServerName();
				// 切り捨てる時間幅を取得する。
				ScormLoEnv scormEnv = new ScormLoEnv();
				int minuteSpan = scormEnv.getAuthenticMinuteSpan();
				// 現在日時を取得し、所定の時間幅に切り捨てる。
				Calendar currentTime = Calendar.getInstance();
				ScormLoUtil.truncateMinute(currentTime, minuteSpan);
				// 算出した認証キー
				String authKey = null;
				// =============================================================
				// 順次、現在の時間帯、直前の時間帯、直後の時間帯の
				// ダイジェストキーを計算し、
				// 渡されたダイジェストキーと一致するか判定する。
				// 算出した時間帯から、ダイジェストキーを算出する。
				Date thisTime = currentTime.getTime();
				authKey = ScormLoUtil.makeAuthenticKey(
						userID, loID, hostName, thisTime);

				DebugLog.write(getClass(),
					"[" + thisTime + "]"
					+ "->Digest[" + authKey + "]", DebugLog.MID);
				DebugLog.write(getClass(),
					"compare Auth-Keys[" + authKey + "]"
						+ "[" + requestAuthKey + "]", DebugLog.MID);
				// ダイジェストキーが一致すれば、この時点で成功。
				status = authKey.equals(requestAuthKey);
				// =============================================================
				// 失敗した場合は直前の時間帯
				if (status == false) {
					Calendar previousCalendar = (Calendar)currentTime.clone();
					previousCalendar.add(Calendar.MINUTE, -minuteSpan);
					Date previousTime = previousCalendar.getTime();
					authKey = ScormLoUtil.makeAuthenticKey(
							userID, loID, hostName, previousTime);

					DebugLog.write(getClass(),
						"[" + previousTime + "]"
						+ "->Digest[" + authKey + "]", DebugLog.MID);
					DebugLog.write(getClass(),
						"compare Auth-Keys[" + authKey + "]"
							+ "[" + requestAuthKey + "]", DebugLog.MID);
					// ダイジェストキーが一致すれば、この時点で成功。
					status = authKey.equals(requestAuthKey);
				}
				// =============================================================
				// 失敗した場合は直後の時間帯
				if (status == false) {
					Calendar nextCalendar = (Calendar)currentTime.clone();
					nextCalendar.add(Calendar.MINUTE, minuteSpan);
					Date nextTime = nextCalendar.getTime();
					authKey = ScormLoUtil.makeAuthenticKey(
							userID, loID, hostName, nextTime);

					DebugLog.write(getClass(),
						"[" + nextTime + "]"
						+ "->Digest[" + authKey + "]", DebugLog.MID);
					DebugLog.write(getClass(),
						"compare Auth-Keys[" + authKey + "]"
							+ "[" + requestAuthKey + "]", DebugLog.MID);
					// ダイジェストキーが一致すれば、この時点で成功。
					status = authKey.equals(requestAuthKey);
				}
			}
		}
		catch (LOException e) {
			throw e;
		}
		catch (Exception e) {
			DebugLog.write(getClass(), e, DebugLog.HIGHT);
			throw new LOException(e.getMessage());
		}
		// メソッド終了ログ
		DebugLog.write(getClass(),
			"TAIL authenticate("
				+ userID + "," + loID + "," + requestAuthKey + ")"
				+ (" return " + status),
			DebugLog.ROW);
		return status;
	}

	/**
	 * 教材IDを取得します。
	 * @param loID LOID。
	 * @return 指定されたLOIDに該当する教材の教材ID。
	 * @throws LOException 処理中に例外が発生した場合。
	 */
	private String getMaterialID(String loID)
			throws LOException {
		////////////////////////////////////////////////////////////////////////
		// メソッド開始ログ
		DebugLog.write(getClass(),
			"HEAD getMaterialID(" + loID + ")", DebugLog.ROW);

		String materialID = null;
		try {
			Environment environment = new Environment();
			String serverName = environment.getLoServerName();
			ScormLoClient scormLoClient = new ScormLoClient(serverName);
			boolean result = scormLoClient.getData(loID);
			if ( result == false ) {
				throw new LOException(scormLoClient.getMessageData());
			}
			ScormLoData loData = (ScormLoData)scormLoClient.getDataInfo(0);
			// 教材IDの取得
			materialID = loData.getMaterialID();
		}
		catch (LOException e) {
			throw e;
		}
		catch (Exception e) {
			DebugLog.write(getClass(), e, DebugLog.HIGHT);
			throw new LOException(e.getMessage());
		}
		////////////////////////////////////////////////////////////////////////
		// メソッド終了ログ
		DebugLog.write(getClass(),
			"TAIL getMaterialID(" + loID + ")" + ( " return " + materialID ),
			DebugLog.ROW);

		return materialID;
	}
}
