/*
 * Copyright (c) 2003, Morpho Project.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the Morpho Project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package jp.morpho.webapp.servlet;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.logging.Logger;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import jp.morpho.io.OutputStreamFactory;
import jp.morpho.io.UploadFile;
import jp.morpho.util.ConversionMap;
import jp.morpho.webapp.FrameworkContext;
import jp.morpho.webapp.FrameworkException;
import jp.morpho.webapp.PluginKeys;
import jp.morpho.webapp.action.ActionMappingManager;
import jp.morpho.webapp.servlet.http.HttpActionRequest;
import jp.morpho.webapp.servlet.http.impl.HttpActionRequestImpl;

/**
 * <P>
 * サーブレットへの要求からフォームデータを取得します。
 * </P>
 * @author Kumiko Hiroi
 */
public class FormDataParseFilter
	extends AbstractFilter
{
	/** ロガー */
	private Logger log = Logger.getLogger("jp.morpho");

	/** 文字列 "multipart/form-data" */
	private static final String CONST_MULTIPART_FORM_DATA = "multipart/form-data";

	/** 文字列 "boundary=" */
	private static final String CONST_BOUNDARY = "boundary=";

	/** 文字列 "content-disposition:" */
	private static final String CONST_CONTENT_DISPOSITION = "content-disposition:";

	/** 文字列 "name=" */
	private static final String CONST_NAME = "name=";

	/** 文字列 "filename=" */
	private static final String CONST_FILENAME = "filename=";

	/** 文字列 "content-type:" */
	private static final String CONST_CONTENT_TYPE = "content-type:";

	/** アップロードディレクトリ */
	private static final String INITPARAM_UPLOADDIRECTORY = "uploadDirectory";

	/** 文字列エンコーディング */
	private static final String DEFAULT_CHARACTERENCODING = "ISO8859_1";

	/**
	 * <P>
	 * FormDataParseFilter を構築します。
	 * </P>
	 */
	public FormDataParseFilter()
	{
		super();
	}

	/**
	 * <P>
	 * サーブレットへの要求からフォームデータを取得します。
	 * </P>
	 * @param request クライアントがサーブレットに行う要求を含む ServletRequest オブジェクト
	 * @param response サーブレットがクライアントに送信する応答を含む ServletResponse オブジェクト
	 * @param chain フィルタチェーン
	 * @exception IOException フィルタが要求を処理するときに、入出力エラーが検出された場合
	 * @exception ServletException 要求を処理できなかった場合
	 * @see javax.servlet.Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
	 */
	public void doFilter(
		ServletRequest request,
		ServletResponse response,
		FilterChain chain)
		throws IOException, ServletException
	{
		log.fine("doFilter");
		HttpActionRequest actioinRequest = new HttpActionRequestImpl((HttpServletRequest)request);

		String contentType = request.getContentType();
		int contentLength = request.getContentLength();

		try
		{
			if (contentType == null || !contentType.toLowerCase().startsWith(CONST_MULTIPART_FORM_DATA))
			{
				getParameters(actioinRequest);
			}
			else
			{
				parseMultipartFormData(actioinRequest);
			}

			FrameworkContext fc = FrameworkContext.getInstance(this.getServletContext());
			ActionMappingManager amm = (ActionMappingManager)fc.get(PluginKeys.actionMappingManager);

			//ステータスとアクションを取得する
			String status = ((ConversionMap)actioinRequest.getParameterMap()).getString(amm.getStatusParameterName());
			String action = ((ConversionMap)actioinRequest.getParameterMap()).getString(amm.getActionParameterName());

			//ステータスを更新する
			actioinRequest.setStatus(status);

			//HttpServletRequestにアクションを設定する
			actioinRequest.setAction(action);

			//ステータスとアクションをフォーム・パラメータから削除する
			((ConversionMap)actioinRequest.getParameterMap()).remove(amm.getStatusParameterName());
			((ConversionMap)actioinRequest.getParameterMap()).remove(amm.getActionParameterName());
		}
		catch (FrameworkException e)
		{
		}
		catch (Exception e)
		{
		}
		chain.doFilter(request, response);
	}

	/**
	 * <P>
	 * サーブレットへの要求からフォームデータを取得します。
	 * </P>
	 * <P>
	 * getParameterValues(String) メソッドを使用して指定された要求オブジェクトからフォームデータを取得した後、setParameterMap(Map) メソッドを呼び出して要求オブジェクトにフォームデータのマップを格納します。
	 * </P>
	 * @param actioinRequest フォームデータを取得する要求オブジェクト
	 */
	protected void getParameters(HttpActionRequest actioinRequest)
	{
		ConversionMap formValue = new ConversionMap();

		Enumeration paramNames = actioinRequest.getParameterNames();
		while (paramNames.hasMoreElements())
		{
			String paramName = (String)paramNames.nextElement();
			String[] paramValues = actioinRequest.getParameterValues(paramName);
			if (paramValues.length > 1)
			{
				formValue.putObject(paramName, paramValues);
			}
			else
			{
				formValue.putString(paramName, paramValues[0]);
			}
		}

		actioinRequest.setParameterMap(formValue);
	}

	/**
	 * <P>
	 * サーブレットへの要求からフォームデータを取得します。
	 * </P>
	 * <P>
	 * フォームデータのコンテンツ形式が multipart/form-data の場合に、要求オブジェクトから入力ストリームを取得してフォームデータを取得します。
	 * 取得したフォームデータはすべてマップに格納し、setParameterMap(Map) メソッドを呼び出して要求オブジェクトに格納します。
	 * </P>
	 * @param actioinRequest フォームデータを取得する要求オブジェクト
	 * @exception IOException フィルタが要求を処理するときに、入出力エラーが検出された場合
	 */
	protected void parseMultipartFormData(HttpActionRequest actioinRequest) throws IOException
	{
		ConversionMap formValue = new ConversionMap();
		int sPos = 0;
		int ePos = 0;
		byte[] buff = new byte[1024];
		int result = 0;
		long parseStartTime = 0;
		long parseEndTime = 0;
		SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
		OutputStreamFactory osf = new OutputStreamFactory();

		ConversionMap parameters = ConversionMap.valueOf(this.getInitParameterMap());

		//アップロードディレクトリ
		File uploadDirectory = null;
		if (parameters.getString(INITPARAM_UPLOADDIRECTORY).length() == 0)
		{
			uploadDirectory = new File(System.getProperty("java.io.tmpdir"));
		}
		else
		{
			uploadDirectory = new File(parameters.getString(INITPARAM_UPLOADDIRECTORY));
			if (!uploadDirectory.canWrite())
			{
				uploadDirectory = new File(System.getProperty("java.io.tmpdir"));
			}
		}

		//ブラウザチェック
		int ind = actioinRequest.getContentType().indexOf(CONST_BOUNDARY);
		if (ind == -1)
		{
			throw new IOException("Invalid form data");
		}
		String boundary = actioinRequest.getContentType().substring(ind + CONST_BOUNDARY.length());

		ByteArrayOutputStream paramValue = new ByteArrayOutputStream();

		ServletInputStream in = actioinRequest.getInputStream();

		parseStartTime = new Date().getTime();
		while ((result = in.readLine(buff, 0, buff.length)) != -1)
		{
			String text = new String(buff, 0, result);

			//パートの始まり
			if (text.toLowerCase().indexOf(CONST_CONTENT_DISPOSITION) > -1)
			{
				String paramName = "";
				paramValue.reset();
				String paramContentType = "";
				String fileName = "";
				boolean isFile = false;

				//コントロール名取得
				sPos = text.toLowerCase().indexOf(CONST_NAME + "\"");
				if (sPos > -1)
				{
					sPos += (CONST_NAME + "\"").length();
					ePos = text.toLowerCase().indexOf("\"", sPos);
					paramName = text.substring(sPos, ePos);
					isFile = false;
				}

				//ファイル名取得
				sPos = text.toLowerCase().indexOf(CONST_FILENAME + "\"");
				if (sPos > -1)
				{
					sPos += (CONST_FILENAME + "\"").length();
					ePos = text.toLowerCase().indexOf("\"", sPos);
					fileName = text.substring(sPos, ePos);
					isFile = true;
				}

				//コントロール名をチェック
				if (paramName.trim().length() == 0)
				{
					//名称が設定されていなければ、次のパートへ
					continue;
				}

				//空行になるまで読む（区切りを見つける）
				while ((result = in.readLine(buff, 0, buff.length)) != -1)
				{
					text = new String(buff, 0, result);
					if (text.trim().length() == 0)
					{
						break;
					}

					//コンテントタイプ取得
					sPos = text.toLowerCase().indexOf(CONST_CONTENT_TYPE);
					if (sPos > -1)
					{
						sPos += CONST_CONTENT_TYPE.length();
						paramContentType = text.substring(sPos).trim();
					}
				}
				if (result == -1)
				{
					throw new IOException("Reset by peer");
				}

				//データを読み込む
				while ((result = in.readLine(buff, 0, buff.length)) != -1)
				{
					text = new String(buff, 0, result);

					//終了（次のパートへ）
					if (text.toLowerCase().indexOf("--" + boundary) > -1)
					{
						break;
					}

					paramValue.write(buff, 0, result);
				}

				//リクエストパラメータ保存
				if (!isFile)
				{
					//テキスト
					if (formValue.containsKey(paramName))
					{
						if (formValue.isArray(paramName))
						{
							ArrayList arry = new ArrayList(Arrays.asList((Object[])formValue.getObject(paramName)));
							arry.add(new String(paramValue.toByteArray(), 0, paramValue.size() - 2, DEFAULT_CHARACTERENCODING));
							formValue.putObject(paramName, arry.toArray(new String[arry.size()]));
						}
						else
						{
							ArrayList arry = new ArrayList();
							arry.add(formValue.getString(paramName));
							arry.add(new String(paramValue.toByteArray(), 0, paramValue.size() - 2, DEFAULT_CHARACTERENCODING));
							formValue.putObject(paramName, arry.toArray(new String[arry.size()]));
						}
					}
					else
					{
						formValue.putString(paramName, new String(paramValue.toByteArray(), 0, paramValue.size() - 2, DEFAULT_CHARACTERENCODING));
					}
				}
				else
				{
					//画像
					if (fileName.trim().length() > 0)
					{
						File tempFile = File.createTempFile(formatter.format(new Date()), null, uploadDirectory);
						OutputStream outputStream = osf.getOutputStream(tempFile);
						outputStream.write(paramValue.toByteArray(), 0, paramValue.size() - 2);
						outputStream.close();

						UploadFile uploadFile = new UploadFile(tempFile.getAbsolutePath());
						uploadFile.setClientPath(fileName);
						uploadFile.setContentType(paramContentType);

						formValue.putObject(paramName, uploadFile);
					}
					else
					{
						formValue.putObject(paramName, null);
					}
				}

				//最後のパートの場合は終了フラグをオンにする
				if (text.toLowerCase().indexOf("--" + boundary + "--") > -1)
				{
					break;
				}
			}
		}

		//バッファクローズ
		paramValue.close();

		parseEndTime = new Date().getTime();

		actioinRequest.setParameterMap(formValue);
	}

}
