/*
 * Paraselene
 * Copyright (c) 2009, 2010  Akira Terasaki
 * このファイルは同梱されているLicense.txtに定めた条件に同意できる場合にのみ
 * 利用可能です。
 */
package paraselene.supervisor;


import paraselene.*;
import paraselene.ajax.*;
import paraselene.tag.*;
import paraselene.tag.form.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.zip.*;
import javax.servlet.*;
import javax.servlet.http.*;


/**
 * サイト制御(ファイルアップロード機能なし)。
 */
public abstract class Supervisor extends HttpServlet {
	class ExceptionTooLong extends Exception {
		ExceptionTooLong( Throwable e ) {
			super( e );
		}
	}

	/**
	 * 不正リクエスト。
	 */
	public enum IllegalRequest {
		/**
		 * 処理済みリクエストの再呼び出し。
		 */
		REPEAT_SAME_REQUEST,
		/**
		 * リクエスト内容と履歴内容が一致しない。
		 */
		DISAPPROVAL_REQUEST,
		/**
		 * 呼び出し元ページが履歴に存在しない。
		 */
		NOT_FOUND_HISTORY,
		/**
		 * セッションが存在しないかタイムアウト。
		 */
		SESSION_ERROR;

		private static final long serialVersionUID = 1L;
	}

	/**
	 * 予約済みセッション項目名。
	 */
	static final String SESSION_DATA = "paraselene$data";

	private static final int[] LINK_LEN = new int[] {
		TransactionSequencer.LINK_DEF[0].length(),
		TransactionSequencer.LINK_DEF[1].length(),
		TransactionSequencer.LINK_DEF[2].length()
	};
	private static final String[] LINK_PROT = new String[] {
		"", "http://", "https://"
	};

	private static volatile HashMap<Thread,DataHolder>	data_map_th
		= new HashMap<Thread,DataHolder>();
	private static volatile HashMap<Supervisor,DataHolder>	data_map_sv
		= new HashMap<Supervisor,DataHolder>();

	static {
		SandBox.doStart();
	}

	/**
	 * コンストラクタ。
	 */
	public Supervisor() {
		super();
	}

	private static String resolvePage( ServerInformation holder, Page from, String p, int from_key, int to_key ) {
		try {
			URI	u = new URI( p );
			PageID	from_page = null;
			if ( from != null ) {
				from_page = from.getID();
			}
			PageID	to = null;
			Page	page = holder.getPageFactory().getPage(
				Integer.parseInt( u.getPath().substring( LINK_LEN[0] ) )
			);
			if ( page != null ) {
				to = page.getID();
				to.getPageFactory().returnPage( page );
			}
			if ( from != null ) {
				Page	parent = from.getParentPage();
				if ( from != parent ) {
					if ( from_page == to ) {
						page = parent;
						from_page = to = parent.getID();
					}
					else {
						from_page = parent.getID();
					}
				}
			}
			if ( to != null ) {
				StringBuilder	path = new StringBuilder(
					TransactionSequencer.makeURI(
						( page.isCheckRepeatSameRequest() )?
							holder.getTransactionSequencer(): null,
						from_page, to, from_key, to_key,
						!holder.isSearchEngine()
					)
				);
				String	query = u.getQuery();
				if ( query != null ) {
					path = path.append( "?" );
					path = path.append( query );
				}
				String	fragment = u.getFragment();
				if ( fragment != null ) {
					path = path.append( "#" );
					path = path.append( fragment );
				}
				return path.toString();
			}
			return "PageID_Not_Found(" + p + ")";
		}
		catch( Exception e ) {
			e.printStackTrace();
			return e.getMessage();
		}
	}

	private static String resolveHost( ServerInformation holder, String path, int def_no ) {
		try {
			int	port = Integer.parseInt( path.substring( LINK_LEN[def_no] ) );
			StringBuilder	buf = new StringBuilder( LINK_PROT[def_no] );
			buf = buf.append( holder.getServerName() );
			if ( port > 0 && port < 0xffff ) {
				buf = buf.append( ":" );
				buf = buf.append( port );
			}
			buf = buf.append( holder.getContextPath() );
			return buf.toString();
		}
		catch( Exception e ) {
			e.printStackTrace();
			return e.getMessage();
		}
	}

	private static String getTarget( Tag tag ) {
		if ( tag == null )	return null;
		String	name = tag.getName();
		String	ret = null;
		if ( "a".equals( name ) || "form".equals( name ) ) {
			ret = tag.getAttributeToString( "target" );
		}
		else if ( "frame".equals( name ) || "iframe".equals( name ) ) {
			ret = tag.getAttributeToString( "name" );
			if ( ret == null ) {
				ret = tag.getAttributeToString( "id" );
			}
		}
		return ret;
	}

	static PageServerInformation getPageServerInformation() {
		Page	page = SandBox.getCurrentPage();
		if ( page != null ) {
			PageServerInformation	psi = page.getPageServerInformation();
			if ( psi != null )	return psi;
		}
		Supervisor	sv = null;
		RequestParameter	req = SandBox.getCurrentRequestParameter();
		if ( req != null ) {
			sv = req.getSupervisor();
		}
		DataHolder	h = getDataHolder( sv );
		if ( h == null )	return null;
		return new PageServerInformation( h );
	}

	private static DataHolder getDataHolder( Supervisor sv ) {
		synchronized( data_map_th ) {
			if ( sv != null ) {
				DataHolder	h = data_map_sv.get( sv );
				if ( h != null )	return h;
			}
			return data_map_th.get( Thread.currentThread() );
		}
	}

	private DataHolder getDataHolder() {
		return getDataHolder( this );
	}

	/**
	 * URIへのセッション情報付加。
	 * @param uri URI。
	 * @param enc エンコード文字コード。
	 * @param tag タグ。
	 * @return セッション情報が追加されたURI。
	 */
	public static String makeWithSessionURI( String uri, String enc, Tag tag ) throws UnsupportedEncodingException, ParaseleneException {
		boolean	xml_f = false;
		Page	page = null;
		if ( tag != null ) {
			page = tag.getAssignedPage();
			xml_f = page.isXML();
		}
		ServerInformation	holder = getDataHolder( null );
		if ( holder == null && page != null )	holder = page.getPageServerInformation();
		if ( holder == null )	return URIValue.encode( uri, enc, xml_f );
		int from_key, to_key;
		if ( page != null ) {
			from_key = page.getHistoryKey();
			to_key = holder.getHistorySet().toInt( getTarget( tag ), from_key );
		}
		else	from_key = to_key = HistorySet.ROOT;
		String[]	part = uri.split( "/" );
		boolean	flag = false;
		String	head = null;
		boolean	alias_f = false;
		for ( int i = 0; i < part.length; i++ ) {
			int	def_no;
			for ( def_no = 0; def_no < TransactionSequencer.LINK_DEF.length; def_no++ ) {
				if ( part[i].length() < LINK_LEN[def_no] )	continue;
				if ( TransactionSequencer.LINK_DEF[def_no].equals( part[i].substring( 0, LINK_LEN[def_no] ) ) ){
					flag = true;
					break;
				}
			}
			switch( def_no ) {
			case 0:
				part[i] = resolvePage( holder, page, part[i], from_key, to_key );
				break;
			case 1:
			case 2:
				head = resolveHost( holder, part[i], def_no );
				part[i] = "";
				break;
			}
		}
		if ( flag ) {
			StringBuilder	buf = new StringBuilder( part[0] );
			for ( int i = 1; i < part.length; i++ ) {
				if ( buf.length() > 0 )	buf = buf.append( "/" );
				buf = buf.append( part[i] );
			}
			uri = buf.toString();
			uri = URIValue.encode( uri, enc, xml_f );
			if ( holder instanceof DataHolder ) {
				uri = ((DataHolder)holder).encodeURL( uri, page );
			}
		}
		else {
			alias_f = holder.getPageFactory().isAlias( uri );
			if ( alias_f && to_key != HistorySet.ROOT ) {
				String[]	dev = uri.split( "/" );
				StringBuilder	buf = new StringBuilder( TransactionSequencer.SELENE[5] );
				buf = buf.append( "." );
				buf = buf.append( Integer.toString( to_key, Character.MAX_RADIX ) );
				buf = buf.append( "." );
				buf = buf.append( TransactionSequencer.SELENE[6] );
				buf = buf.append( "." );
				if ( dev.length == 0 ) {
					buf = buf.append( uri );
					uri = buf.toString();
				}
				else {
					StringBuilder	p = new StringBuilder();
					for ( int i = 0; i < dev.length - 1; i++ ) {
						p = p.append( dev[i] );
						p = p.append( "/" );
					}
					p = p.append( buf );
					p = p.append( dev[dev.length - 1] );
					uri = p.toString();
				}
			}
			uri = URIValue.encode( uri, enc, xml_f );
			if ( !alias_f && isMyHost( uri ) && holder instanceof DataHolder ) {
				uri = ((DataHolder)holder).encodeURL( uri, page );
			}
		}
		if ( head != null ) {
			StringBuilder	buf = new StringBuilder( head );
			buf = buf.append( "/" );
			buf = buf.append( uri );
			uri = buf.toString();
		}
		return uri;
	}

	private static boolean isMyHost( DataHolder holder, String uri ) {
		if ( uri == null )	return false;
		uri = uri.trim();
		if ( uri.isEmpty() )	return false;
		if ( uri.charAt( 0 ) == '#' )	return false;
		try {
			URI	test = new URI( URIValue.encode( uri, "utf-8" ) );
			if ( test.isAbsolute() == false ) {
				return true;
			}
			String	sch = test.getScheme();
			if ( sch != null ) {
				if ( !"https".equals( sch ) && !"http".equals( sch ) )	return false;
			}
			String	host = test.getHost();
			if ( host == null )	return true;
			if ( holder == null )	return false;
			if ( host.equals( holder.getServerName() ) )	return true;
		}
		catch( Exception e ){
		}
		return false;
	}

	static boolean isMyHost( String uri ) {
		DataHolder	holder = getDataHolder( null );
		return isMyHost( holder, uri );
	}

	/**
	 * URIが自ホストであるか？
	 * @param uri 検査する URI。
	 * @return true:自ホスト、false:他のホスト。
	 */
	public boolean isMyHostURI( String uri ) {
		DataHolder	holder = getDataHolder();
		return isMyHost( holder, uri );
	}

	static SessionData getSessionData( Supervisor sv ) {
		DataHolder	holder = getDataHolder( sv );
		if ( holder == null )	return null;
		return holder.data;
	}

	/**
	 * ページファクトリーの取得。
	 * @return ページファクトリー。
	 */
	public abstract PageFactory getPageFactory();

	/**
	 * エラー発生通知。例外が発生すると呼ばれます。
	 * 内容をログへ記録する等、任意の処理を行って下さい。
	 * @param e 発生例外。
	 */
	public abstract void onError( Throwable e );

	private Forward onErrorDebug( Throwable e, HttpServletRequest req, HttpServletResponse res, int status ) {
		Forward fw = new Forward( status );
		Option.debug( e );
		if ( !(e instanceof SocketException) )	onError( e );
		try {
			fw.Action( this, res );
		}
		catch( Throwable e2 ) {
			e2.printStackTrace();
			onError( e2 );
		}
		if ( !(e instanceof SocketException) && status != 413 ) {
			HttpSession	session = req.getSession( false );
			clear_session( session );
		}
		return fw;
	}
	private Forward onErrorDebug( Throwable e, HttpServletRequest req, HttpServletResponse res ) {
		return onErrorDebug( e, req, res, 500 );
	}

	private static void clear_session( HttpSession s ) {
		if ( s != null )	s.invalidate();
		Option.traceWithStack( "session cleared" );
	}

	/**
	 * 不正呼び出しの検出通知。
	 * @param ir 検出内容。
	 * @param req リクエスト。
	 * @param from 遷移元ページ。
	 * @param to 遷移先ページ。
	 * @return 処理の変更指示。nullなら、通常通りに動作します。
	 */
	public abstract Forward onIllegalRequest( IllegalRequest ir, RequestParameter req, Page from, Page to );

	private static final Page	null4alias = new NullPage4Alias();

	private void doAction( HttpServletRequest request, HttpServletResponse res ) {
		HttpSession	session = null;
		SessionData	data = null;
		DataHolder	holder = getDataHolder();
		if ( holder != null ) {
			data = holder.data;
		}
		RequestParameter	req = null;
		try {
			if ( data == null ) {
				System.out.println( "SessionData null" );
				ErrorStatus.send( res, 500 );
				return;
			}

			boolean	session_f = false;
			req = holder.request;
			session = req.getSession();
			String	uri = req.getURI().toString();
			PageFactory	fact = getPageFactory();
			PathData	path_data = new PathData();
			Forward	fw = null;
			Page	last_page = null, next_page = null;
			AliasData	alias = fact.getPage( uri );

			if ( alias == null ) {
				path_data = TransactionSequencer.parsePath( data.seq, uri, fact, data.hist );
				if ( TransactionSequencer.isMustSession( uri ) ) {
					if ( holder.isSearchEngine() ) {
						new Forward( 403 ).Action( this, res );
						clear_session( session );
						return;
					}
					if ( holder.session_null && path_data != null ) {
						Option.trace( "%s(%s->%s)", IllegalRequest.SESSION_ERROR, path_data.from.getID(), path_data.to.getID() );
						fw = onIllegalRequest( IllegalRequest.SESSION_ERROR, req, path_data.from, path_data.to );
						if ( fw != null ) {
							session_f = fw.session_clear_f;
							path_data.from = last_page = null;
							path_data.to = next_page = fw.getPage( null );
							if ( next_page == null ) {
								fw.Action( this, res );
								if ( session_f )	clear_session( session );
								return;
							}
						}
					}
				}
			}
			else {
				path_data.from = null4alias;
				path_data.to = alias.page;
				path_data.to_histry = alias.to_histry;
			}
			if ( path_data == null ) {
				fw = new Forward( 403 );
				fw.Action( this, res );
				//clear_session( session );
				return;
			}
			if ( path_data.redirect != null ) {
				if ( !holder.data.write( this, path_data.redirect, holder ) ) {
					fw = new Forward( 403 );
					fw.Action( this, res );
					//clear_session( session );
				}
				return;
			}
			last_page = path_data.from;
			next_page = path_data.to;
			RequestItem	ukey = req.getItem( Form.PAGE_UKEY );
			RequestItem	ajax = req.getItem( A_AJAX_KEY );
			if ( ajax == null )	ajax = req.getItem( Form.FORM_AJAX );
			if ( ajax != null || ukey != null ) {
				String	call_key = ( ajax != null )?	ajax.getValue( 0 ):	ukey.getValue( 0 );
				Option.trace( "call_key=%s", call_key );
				Page	sub = null;
				History	history = data.hist.get( (path_data.from_histry != null)? path_data.from_histry: HistorySet.ROOT );
				if ( history != null ) {
					sub = history.getBrowsingPage( call_key );
				}
				if ( sub != null ) {
					fact.returnPage( last_page );
					last_page = sub;
				}
				else if ( !"-1".equals( call_key ) ) {
					fw = data.redirect( req.getURI() );
					if ( fw != null ) {
						fw.Action( this, res );
						return;
					}
					fw = new Forward( 403 );
					fw.Action( this, res );
					clear_session( session );
					return;
				}
				else	ajax = null;
			}
			Page[]	history_pages = new Page[0];
			if ( ajax == null && last_page != null ) {
				if ( last_page instanceof Null ) {
					System.out.println( "last page is null" );
					fw = new Forward( 403 );
					fw.Action( this, res );
					clear_session( session );
					return;
				}
				if ( last_page.isAllowHistoryAdd() ) {
					Page	sub = null;
					History	history = data.hist.get( (path_data.from_histry != null)? path_data.from_histry: HistorySet.ROOT );
					if ( history != null ) {
						history_pages = history.getAllPage( last_page.getID() );
						sub = history.getPage( last_page.getID() );
					}
					if ( sub != null ) {
						fact.returnPage( last_page );
						last_page = sub;
					}
					else {
						Option.trace( "%s(%s->%s)", IllegalRequest.NOT_FOUND_HISTORY, last_page.getID(), next_page.getID() );
						fw = onIllegalRequest( IllegalRequest.NOT_FOUND_HISTORY, req, last_page, next_page );
						if ( fw != null ) {
							session_f = fw.session_clear_f;
							last_page = fact.getNullPage();
							next_page = fw.getPage( null );
							if ( next_page == null ) {
								fw.Action( this, res );
								if ( session_f )	clear_session( session );
								return;
							}
						}
					}
				}
			}
			else if ( ajax == null ) {
				last_page = fact.getNullPage();
			}
			if ( next_page != null ) {
				if ( next_page instanceof Null ) {
					System.out.println( "next page is null" );
					fw = new Forward( 403 );
					fw.Action( this, res );
					clear_session( session );
					return;
				}
			}
			else {
				fw = new Forward( 403 );
				fw.Action( this, res );
				clear_session( session );
				return;
			}

			if ( path_data.called ) {
				Option.trace( "%s(%s->%s)", IllegalRequest.REPEAT_SAME_REQUEST, last_page.getID(), next_page.getID() );
				fw = onIllegalRequest( IllegalRequest.REPEAT_SAME_REQUEST, req, last_page, next_page );
				if ( fw != null ) {
					session_f = fw.session_clear_f;
					last_page = fact.getNullPage();
					next_page = fw.getPage( null );
					if ( next_page == null ) {
						fw.Action( this, res );
						if ( session_f )	clear_session( session );
						return;
					}
				}
			}
			if ( fw == null ) {
				int	from_key = HistorySet.ROOT;
				int	to_key = HistorySet.ROOT;
				if ( path_data.from_histry != null )	from_key = path_data.from_histry;
				if ( path_data.to_histry != null ){
					to_key = path_data.to_histry;
				}
				Page	do_page = null;
				for ( int i = 0; i < history_pages.length; i++ ) {
					do_page = history_pages[i].reflect( req );
					if ( do_page != null )	break;
				}
				if ( do_page == null ) {
					do_page = last_page.reflect( req );
				}
				if ( do_page == null && !(last_page instanceof NullPage) && req.getItemCount() > 0 ) {
					Option.trace( "%s(%s->%s)", IllegalRequest.DISAPPROVAL_REQUEST, last_page.getID(), next_page.getID() );
					fw = onIllegalRequest( IllegalRequest.DISAPPROVAL_REQUEST, req, last_page, next_page );
					if ( fw != null ) {
						session_f = fw.session_clear_f;
						last_page = fact.getNullPage();
						next_page = fw.getPage( null );
						if ( next_page == null ) {
							fw.Action( this, res );
							if ( session_f )	clear_session( session );
							return;
						}
					}
				}
				else if ( do_page != null && do_page != last_page ) {
					last_page = do_page;
				}
				if ( last_page.getID() != null ) {
					Option.trace( String.format( "reflect -> %s(%s)", last_page.getID().toString(), last_page.getUniqueKey() ) );
				}
				last_page.setRequestParameter( req );
				if ( next_page != null ) {
					if ( from_key != to_key ) {
						fw = new Forward( next_page.getID(), true, false );
					}
					else {
						if ( last_page.getID() == next_page.getID() ) {
							fw = new Feedback( last_page );
						}
						else if ( Closure.isClosure( last_page, next_page, req ) ) {
							if ( last_page.getPopupType() == null ) {
								fw = new Closure( next_page );
							}
							else {
								fw = new Closure( last_page );
							}
						}
						else {
							fw = new Forward( next_page.getID(), true, false );
						}
					}
					fact.returnPage( next_page );
				}
				Option.trace( "%s input(%s) call", last_page.getID(), fw );
				fw = SandBox.input( last_page, req, fw );
				session = req.getSession();
				Option.trace( "%s input return %s", last_page.getID(), fw );
				last_page.setInitialized( false );
				last_page.setRequestParameter( null );
				session_f = fw.session_clear_f;
				if ( fw.getPageID() == Closure.CLOSE ) {
					Forward	old = fw;
					fw = new Feedback( last_page );
					Option.trace( "%s to %s", old, fw );
				}
				if ( (!last_page.isAjax() && fw.isLeave()) || fw.getStatus() != 0 ) {
					fw.Action( this, res );
					if ( session_f )	clear_session( session );
					return;
				}
			}
			int	out_hist_key = HistorySet.ROOT;
			if ( path_data.to_histry != null ) {
				out_hist_key = path_data.to_histry;
			}
			OutputHolder	oh = new OutputHolder( ajax != null && last_page.getParentPage().isAjax(), out_hist_key, last_page, fw, req, data );
			oh.call();
			String	key = last_page.getUniqueKey();
			ajax = req.getItem( A_AJAX_KEY );
			if ( ajax != null ) {
				String	call_key = ajax.getValue( 0 );
				if ( key.equals( call_key ) ) {
					Ajax.remove( session, key );
					Ajax.add( session, key, oh.getPostBack() );
					Page	ajax_page = getPageFactory().getPage( PAGE_ID_JSON );
					ajax_page.output( null, req );
					write( new Page[]{ ajax_page, ajax_page }, session_f, fw, holder );
					Option.trace( "JSON postback %s", key );
					return;
				}
			}
			ajax = req.getItem( Form.FORM_AJAX );
			if ( ajax != null ) {
				String	call_key = ajax.getValue( 0 );
				if ( key.equals( call_key ) ) {
					Ajax.remove( session, key );
					Ajax.add( session, key, oh.getPostBack() );
					Page	ajax_page = getPageFactory().getPage( PAGE_ID_FORM_JSON );
					ajax_page.output( last_page, req );
					if ( isPRG() ) {
						data.redirect( new Page[]{ ajax_page, ajax_page }, fw, req ).Action( this, res );
					}
					else {
						write( new Page[]{ ajax_page, ajax_page }, session_f, fw, holder );
					}
					Option.trace( "FORM postback %s", key );
					return;
				}
			}
			Page[]	out_ret = oh.getPage();
			if ( out_ret == null ) {
				if ( fw.isLeave() ) {
					fw.Action( this, res );
					if ( session_f )	clear_session( session );
					return;
				}
				System.out.println( "output_call false" );
				fw = new Forward( 500 );
				fw.Action( this, res );
				return;
			}
			Option.trace( "no ajax result %s/%s", out_ret[1].getID(), out_ret[1].getUniqueKey() );
			write( out_ret, session_f, fw, holder );
		}
		catch( Throwable e ) {
			onErrorDebug( e, request, res );
		}
	}

	/**
	 * PRGパターン遷移の使用。
	 * @return true:PRGパターンを使用、false:通常のPOST遷移。
	 */
	public abstract boolean isPRG();

	private static final int PAGE_ID_JSON = -1;
	private static final int PAGE_ID_FORM_JSON = -2;
	/**
	 * Aタグからの AJAX 要求。
	 */
	public static final String A_AJAX_KEY = "paraselene_a_ajax";

	HttpSession makeNewSession( HttpSession old ) {
		DataHolder	holder = getDataHolder();
		if ( holder == null )	return old;
		if ( old != null ) {
			if ( holder.data != null )	holder.data.planed_clear = true;
			clear_session( old );
			if ( holder.data != null )	holder.data.planed_clear = false;
		}
		HttpSession	session = holder.getRequest().getSession( true );
		holder.getHistorySet().replaceSession( session.getId() );
		session.setAttribute( SESSION_DATA, holder.data );
		return session;
	}

	private static final String XHTML = "application/xhtml+xml";

	void write( Page[] out_ret, boolean session_f, Forward fw, DataHolder holder ) throws Exception {
		try {
			writeWork( out_ret, session_f, fw, holder );
		}
		catch( Exception e ) {
			String[]	name = getClass().getName().split( "\\." );
			StringBuilder	buf = new StringBuilder();
			int	cnt = name.length - 1;
			for ( int i = 0; i < cnt; i++ ) {
				buf = buf.append( name[i] );
				buf = buf.append( "." );
			}
			String	p_name = buf.toString();
			StackTraceElement[]	st = e.getStackTrace();
			boolean	flag = false;
			for ( int i = 0; i < st.length; i++ ) {
				if ( !st[i].getMethodName().equals( "getInputStream" ) )	continue;
				if ( st[i].getClassName().indexOf( p_name ) != 0 )	continue;
				flag = true;
				break;
			}
			if ( flag )	throw e;
			Option.debug( e );
			Option.trace( "write error %s", e.toString() );
		}
	}

	private void writeWork( Page[] out_ret, boolean session_f, Forward fw, DataHolder holder ) throws Exception {
		SessionData data = holder.data;
		HttpServletResponse	res = holder.response;
		out_ret[1].setResponse( res );
		Page	next_page = out_ret[0];
		RequestParameter	param = holder.request;
		HttpSession	session = param.getSession();
		try {
			session.setAttribute( SESSION_DATA, data );
		}
		catch( Exception e ) {
			session = null;
		}
		if ( session_f ) {
			clear_session( session );
			session = null;
		}
		else if ( session == null ) {
			session = holder.getRequest().getSession( true );
			session.setAttribute( SESSION_DATA, data );
		}
		if ( isPRG() && !(out_ret[1] instanceof Downloadable) && !session_f && param.getMethod() == RequestParameter.Method.POST ) {
			data.redirect( out_ret, fw, param ).Action( this, res );
		}
		else {
			String	mime = out_ret[1].getContentType();
			if ( mime == null ) {
				StringBuilder	buf = null;
				if ( next_page.isXML() ) {
					RequestParameter.HeaderQuality[]	head = param.getHeaderWithQuality( "Accept" );
					if ( head != null ) {
						for ( int i = 0; i < head.length; i++ ) {
							Float	f = head[i].getQuality();
							if ( f != null ) {
								if ( f <= 0.0 )	continue;
							}
							if ( !XHTML.equalsIgnoreCase( head[i].getValue() ) )	continue;
							buf = new StringBuilder( XHTML );
							break;
						}
					}
					if ( buf == null ) {
						RequestParameter.Mobile	mob = param.judgeMobile();
						switch( mob ) {
						case AU: case DOCOMO: case J_PHONE:
							buf = new StringBuilder( XHTML );
							break;
						}
					}
				}
				if ( buf == null )	buf = new StringBuilder( "text/html" );
				buf = buf.append( "; charset=" );
				mime = buf.append( next_page.getCharset() ).toString();
			}
			res.setContentType( mime );
			HTTPDate	date = next_page.getLastModified();
			String[]	mod = param.getHeader( "If-Modified-Since" );
			if ( mod != null && date != null ) {
				try {
					if ( date.compareTo( new HTTPDate( mod[0] ) ) <= 0 ) {
						res.setStatus( 304 );
						res.setContentType( mime );
						data.hist.forceNewNo( next_page.getHistoryKey() );
						data.hist.get( next_page.getHistoryKey() ).add( next_page );
						return;
					}
				}
				catch( java.text.ParseException pe ) {}
			}
			if ( date != null ) {
				res.addHeader( "Last-Modified", date.toString() );
				res.addHeader( "Cache-Control", "public" );
			}
			else if ( next_page.isNoCache() ) {
				res.addHeader( "Pragma", "no-cache" );
				res.addHeader( "Cache-Control", "no-cache, no-store, must-revalidate" );
			}
			if ( fw != null ) {
				if ( fw.out_cookie != null ) {
					int	ck_cnt = fw.out_cookie.size();
					for ( int i = 0; i < ck_cnt; i++ ) {
						res.addCookie( fw.out_cookie.get( i ) );
					}
				}
			}
			if ( gzip( param, holder.getRequest(), res, out_ret[1], data, mime ) ) {
				data.hist.get( next_page.getHistoryKey() ).add( next_page );
				return;
			}
			if ( out_ret[1] instanceof Downloadable ) {
				CacheBin	cb = new CacheBin( mime, false );
				cb.write( (Downloadable)out_ret[1], param, data, res );
			}
			else {
				CacheWriter	w = new CacheWriter( out_ret[1], mime, param, data );
				History	h = data.hist.get( out_ret[1].getHistoryKey() );
				try {
					if ( h != null )	h.lock();
					out_ret[1].write( w );
				}
				finally{
					if ( h != null )	h.unlock();
				}
				w.writeTo( res );
			}
		}
	}

	private boolean gzip( RequestParameter param, HttpServletRequest req, HttpServletResponse res, Page page, SessionData data, String mime )  throws Exception {
		if ( !page.isGZIP() )	return false;
		String[]	pcol = req.getProtocol().split( "\\/" );
		if ( pcol[pcol.length - 1].equals( "1.0" ) )	return false;

		RequestParameter.HeaderQuality[]	encode =
			param.getHeaderWithQuality( "Accept-Encoding" );
		boolean	gzip_f = false;
		if ( encode != null ) {
			for ( int i = 0; i < encode.length; i++ ) {
				if ( "gzip".equalsIgnoreCase( encode[i].getValue() ) ) {
					Float	f = encode[i].getQuality();
					float	q = f == null?	1:	f;
					if ( q > 0 ) {
						gzip_f = true;
						break;
					}
				}
			}
		}
		if ( !gzip_f )	return false;

		res.addHeader( "Content-Encoding", "gzip" );
		if ( page instanceof Downloadable ) {
			CacheBin	cb = new CacheBin( mime, true );
			cb.write( (Downloadable)page, param, data, res );
		}
		else {
			CacheWriter	w = new CacheWriter( page, mime, param, data );
			History	h = data.hist.get( page.getHistoryKey() );
			try {
				if ( h != null )	h.lock();
				page.write( w );
			}
			finally {
				if ( h != null )	h.unlock();
			}
			GZIPOutputStream	gz_out = new GZIPOutputStream( res.getOutputStream() );
			w.writeTo( res, new PrintWriter(
				new OutputStreamWriter(
					gz_out,
					page.getCharset()
				) )
			);
			gz_out.finish();
		}

		return true;
	}

	RequestParameter normalReq( HttpServletRequest req ) {
		RequestParameter	ret = new RequestParameter();
		Enumeration<?>	e = req.getParameterNames();
		while( e.hasMoreElements() ) {
			String	key = (String)e.nextElement();
			String[]	val = req.getParameterValues( key );
			for ( int i = 0; i < val.length; i++ ) {
				ret.addItem( key, val[i] );
			}
		}
		return ret;
	}

	/**
	 * 接続制限数を返す。戻り値を上回ると503を返します。
	 * @return 0以下なら無制限。1以上なら制限する。
	 */
	public abstract int getRequestMaxCount();

	private Forward preset(
		RequestParameter.Method method,
		HttpServletRequest request,
		HttpServletResponse res
	) throws
		UnsupportedEncodingException,
		IOException,
		ParaseleneException
	{
		if ( !SandBox.isSafeFreeMemory() )	return new Forward( 503 );
		PageFactory	fact = getPageFactory();
		if ( fact.teapot_f )	return new Forward( 418 );
		DataHolder	holder = new DataHolder( this, request );
		HttpSession	session = request.getSession( false );
		SessionData	data = null;
		if ( session == null ) {
			data = new SessionData( request );
			holder.session_null = true;
		}
		else {
			data = (SessionData)session.getAttribute( SESSION_DATA );
			holder.session_null = false;
			if ( data == null ) {
				data = new SessionData( request );
				session.setAttribute( SESSION_DATA, data );
				holder.session_null = true;
			}
			else if ( !data.isSame( request ) ) {
				clear_session( session );
				session = null;
				data = new SessionData( request );
				holder.session_null = true;
			}
		}
		holder.response = res;
		holder.data = data;

		PathData	p = TransactionSequencer.parsePath( null, request.getRequestURI() , fact, null );
		if ( p != null ) {
			if ( p.from != null ) {
				request.setCharacterEncoding( p.from.getCharset() );
			}
		}
		RequestParameter	req = null;
		try {
			req = parseRequest( request, p );
		}
		catch( ExceptionTooLong tl ) {
			return onErrorDebug( tl.getCause(), request, res, 413 );
		}
		catch( Exception e ){
			return onErrorDebug( e, request, res, 422 );
		}
		req.setParam( method, request, this );

		String[]	header = req.getHeaderNames();
		ArrayList<String>	log = new ArrayList<String>();
		StringBuilder	buf = new StringBuilder( "client access\n" );
		buf = buf.append( req.getMethod().toString() );
		buf = buf.append( " " );
		buf = buf.append( req.getURI() );
		buf = buf.append( "\n" );
		log.add( buf.toString() );
		for ( int i = 0; i < header.length; i++ ) {
			String[]	val = req.getHeader( header[i] );
			for ( int  j = 0; j < val.length; j++ ) {
				buf = new StringBuilder( header[i] );
				buf = buf.append( " : " );
				buf = buf.append( val[j] );
				buf = buf.append( "\n" );
				log.add( buf.toString() );
			}
		}
		Option.appendTrace( log.toArray( new String[0] ) );


		holder.setSearchEngine( req.judgeSearchEngine() != RequestParameter.SearchEngine.NO_SEARCHENGINE );
		holder.request = req;

		int	cnt = getRequestMaxCount();
		synchronized( data_map_th ) {
			if ( cnt > 0 ) {
				if ( cnt < data_map_th.size() )	return new Forward( 503 );
			}
			data_map_th.put( Thread.currentThread(), holder );
			data_map_sv.put( this, holder );
		}
		return null;
	}

	RequestParameter parseRequest( HttpServletRequest request, PathData p ) throws Exception {
		String	head = request.getHeader( "Content-Type" );
		if ( head != null ) {
			head = head.split( ";" )[0];
			if ( paraselene.tag.form.Form.MULTI_PART.equals( head ) ) {
				throw new Exception( "Gate class must extend UploadableSupervisor!" );
			}
		}
		return normalReq( request );
	}

	protected final void doGet( HttpServletRequest req, HttpServletResponse res )  {
		try {
			Forward	fw = preset( RequestParameter.Method.GET, req, res );
			if ( fw != null ) {
				fw.Action( this, res );
				return;
			}
			doAction( req, res );
		}
		catch( Throwable e ) {
			onErrorDebug( e, req, res );
		}
		finally {
			synchronized( data_map_th ) {
				data_map_th.remove( Thread.currentThread() );
				data_map_sv.remove( this );
			}
		}
	}

	protected final void doPost( HttpServletRequest req, HttpServletResponse res )  {
		try {
			Forward	fw = preset( RequestParameter.Method.POST, req, res );
			if ( fw != null ) {
				fw.Action( this, res );
				return;
			}
			doAction( req, res );
		}
		catch( Throwable e ) {
			onErrorDebug( e, req, res );
		}
		finally {
			synchronized( data_map_th ) {
				data_map_th.remove( Thread.currentThread() );
				data_map_sv.remove( this );
			}
		}
	}

}

