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


import java.util.*;
import java.io.*;
import java.net.*;
import javax.servlet.http.*;
import paraselene.tag.*;
import paraselene.css.*;
import paraselene.ui.*;
import paraselene.supervisor.*;
import paraselene.tag.form.*;
import paraselene.ajax.data.*;


/**
 * HTMLページ。
 */
public abstract class Page implements Serializable {
	/**
	 * ページ処理中例外。
	 */
	public class PageException extends Exception {
		private Throwable ex = null;
		private Object	obj = null;
		private Page	page;
		private RequestParameter	request;
		private boolean	input_call;
		/**
		 * コンストラクタ。
		 * 別の例外をラップします。
		 * @param e 元となる例外。
		 */
		public PageException( Throwable e ) {
			super( e );
			while ( true ) {
				if ( !(e instanceof PageException) )	break;
				e = ((PageException)e).ex;
			}
			ex = e;
			page = SandBox.getCurrentPage();
			request = SandBox.getCurrentRequestParameter();
			input_call = SandBox.isCurrentInput();
		}

		/**
		 * コンストラクタ。
		 * 独自の例外。
		 * @param mes メッセージ。
		 */
		public PageException( String mes ) {
			super( mes );
			ex = null;
			page = SandBox.getCurrentPage();
			request = SandBox.getCurrentRequestParameter();
			input_call = SandBox.isCurrentInput();
		}

		/**
		 * コンストラクタ。別の例外をラップします。
		 * 付帯情報を持たせます。
		 * @param e 元となる例外。
		 * @param with 付帯情報。
		 */
		public PageException( Throwable e, Object with ) {
			this( e );
			obj = with;
		}

		/**
		 * コンストラクタ。
		 * 独自の例外。
		 * 付帯情報を持たせます。
		 * @param mes メッセージ。
		 * @param with 付帯情報。
		 */
		public PageException( String mes, Object with ) {
			this( mes );
			obj = with;
		}

		/**
		 * 元となる例外の取得。
		 * @return 例外。
		 */
		public Throwable get() {
			if ( ex == null )	return this;
			if ( ex instanceof PageException ) {
				PageException	pe = (PageException)ex;
				return pe.get();
			}
			return ex;
		}

		/**
		 * 付帯情報の取得。
		 * @return 付帯情報。
		 */
		public Object getWithObject() {
			return obj;
		}

		/**
		 * ページの取得。
		 * @return 例外発生元のページインスタンス。
		 */
		public Page getPage() {
			return page;
		}
		
		/**
		 * リクエストパラメータの取得。
		 * @return 例外発生時のリクエストパラメータ。
		 */
		public RequestParameter getRequestParameter() {
			return request;
		}

		/**
		 * Input処理中であるか。
		 * @return true:input、false:input以外 output や firstOutput。
		 */
		public boolean isInput() {
			return input_call;
		}
	}

	/**
	 * AJAXサポート機能。
	 */
	public enum AjaxSupport {
		/**
		 * 無効。
		 */
		NO,
		/**
		 * ポップアップダイアログ表示。
		 */
		POPUP_DIALOG,
		/**
		 * サーバープッシュ。<br>
		 * ポップアップダイアログ表示も可能です。<br>
		 * これを指定する場合、クライアントはサーバーに HTTP
		 * 接続を維持したままとなります。
		 * クライアント数が多く見込まれるサイトでは注意して下さい。
		 */
		SERVER_PUSH;
		private static final long serialVersionUID = 2L;
	}

	/**
	 * AJAXサポート機能の指定。
	 * @return AJAXサポート。
	 */
	public abstract AjaxSupport getAjaxSupport();

	private volatile int histry_key = 0;
	/**
	 * 画面遷移履歴キーの設定。
	 * @param key 画面遷移履歴キー。
	 */
	public void setHistoryKey( int key ) {
		histry_key = key;
	}
	/**
	 * 画面遷移履歴キーの取得。
	 * @return 画面遷移履歴キー。
	 */
	public int getHistoryKey() {
		return histry_key;
	}

	private static final int ERROR_NO = 403;

	private HashMap<String,Tag>	tag_1_dic = new HashMap<String,Tag>();
	private HashMap<String,HashMap<String,Tag>>	tag_m_dic = new HashMap<String,HashMap<String,Tag>>();
	private HashMap<Tag,String>	name_dic = new HashMap<Tag,String>();

	private ArrayList<PageBypassedDiv>	bypass = new ArrayList<PageBypassedDiv>();
	private ArrayList<PageHooker>	hook = new ArrayList<PageHooker>();
	private ArrayList<Clickable>	clickable = new ArrayList<Clickable>();
	private Page	parent_page = null;

	private int name_seq = 0;
	private Tag	contents = null;
	private boolean init_f = true;

	private boolean xml_f = false;
	private String	doctype_def = null;
	private static final long serialVersionUID = 2L;

	private Popup.Type popup_type = null;

	private static volatile short page_id;
	private static final Object	obj4uniKey = new Object();
	private String uniKey;

	/**
	 * 親ページの取得。
	 * @return 親ページ、もしくは this を返す。
	 */
	public Page getParentPage() {
		if ( parent_page == null )	return this;
		return parent_page.getParentPage();
	}

	/**
	 * 親ページの設定
	 * @param p 親ページ。
	 */
	public void setParentPage( Page p ) {
		parent_page = p;
	}

	/**
	 * 保持している PageBypassedDiv を返します。
	 * @return PageBypassedDivの配列。無ければ 0 個の配列を返します。
	 */
	public PageBypassedDiv[] getPageBypassedDiv() {
		return bypass.toArray( new PageBypassedDiv[0] );
	}

	private static PageHooker[] sort( PageHooker[] ph ) {
		Tag[]	tag = new Tag[ph.length];
		for ( int i = 0; i < ph.length; i++ ) {
			tag[i] = (Tag)ph[i];
		}
		tag = Tag.sort( tag );
		for ( int i = 0; i < ph.length; i++ ) {
			ph[i] = (PageHooker)tag[i];
		}
		return ph;
	}

	/**
	 * 保持している PageHooker を返します。
	 * @return PageHookerの配列。無ければ 0 個の配列を返します。
	 */
	public PageHooker[] getPageHooker() {
		return sort( hook.toArray( new PageHooker[0] ) );
	}

	private void makeKey() {
		long	now = 0;
		synchronized( obj4uniKey ) {
			now = page_id;
			page_id++;
		}
		now = (now << 56) ^ (new Date().getTime() & 0x00ffffffffffffffL);
		now &= 0x7fffffffffffffffL;
		StringBuilder	buf = new StringBuilder( "paraselene_page_key_" );
		buf = buf.append( Long.toString( now, Character.MAX_RADIX ) );
		uniKey = buf.toString();
	}

	/**
	 * ポップアップ状態を返します。
	 * @return ポップアップ状態。
	 * ポップアップでなければ null です。
	 */
	public Popup.Type getPopupType() {
		return popup_type;
	}

	/**
	 * 使用しないで下さい。
	 */
	public void setPopupType( Popup.Type type ) {
		popup_type = type;
	}

	/**
	 * 実行状態であるか？常に true を返します。
	 * @return true:実行中、false:モックアップ解析中。
	 */
	public boolean isRuntime() {
		return true;
	}

	/**
	 * Paraseleneバージョンの記録。
	 * コメントとして、Paraseleneのバージョンを&lt;body&gt;先頭に埋め込みます。
	 * @param parser パースした時のバージョン。
	 * @param date パースした時の時間。
	 */
	protected void addVersionMeta( String parser, String date ) {
		if ( !isRuntime() )	return;
		Tag	head = getFirstTagByType( "head" );
		if ( head == null )	return;
		StringBuilder	buf = new StringBuilder( " Parser[" );
		buf = buf.append( parser );
		buf = buf.append( "," );
		buf = buf.append( date.toString() );
		buf = buf.append( "] Runtime[" );
		buf = buf.append( Version.VERSION );
		buf = buf.append( "," );
		buf = buf.append( new Date().toString() );
		buf = buf.append( "] " );
		Tag	meta = new Tag( "meta", true );
		meta.setAttribute( new Attribute( "name", "generator" ), new Attribute( "content", buf.toString() ) );
		head.addHTMLPart( meta );
	}

	/**
	 * 文書の定義設定。
	 * @param xml true:XHTML, false:HTML。
	 * @param doctype &lt;!DOCTYPE ～ &gt;の文字列。不要であればnull。
	 */
	protected void setDoctype( boolean xml, String doctype ) {
		xml_f = xml;
		doctype_def = doctype;
	}

	/**
	 * XMLであるか？
	 * @return true:XHTML, false:HTML。
	 */
	public boolean isXML() {
		return xml_f;
	}

	/**
	 * DOCTYPEの取得。
	 * @return &lt;!DOCTYPE ～ &gt;の文字列。
	 */
	public String getDoctype() {
		return doctype_def;
	}

	/**
	 * タグの全消去。
	 */
	public void clear() {
		tag_1_dic = new HashMap<String,Tag>();
		tag_m_dic = new HashMap<String,HashMap<String,Tag>>();
		name_dic = new HashMap<Tag,String>();
		name_seq = 0;
		contents = null;
	}

	/**
	 * 初期化。モックアップの状態に復元する。
	 */
	public abstract void init();

	/**
	 * 初期化された事をマークします。
	 * @param flag true:初期化済み、false:変更された。
	 */
	public void setInitialized( boolean flag ) throws PageException {
		init_f = flag;
		if ( flag && beginning_called ) {
			beginning_called = false;
			beginning( now_req );
		}
	}

	/**
	 * 初期化直後状態であるか？次のタイミングで状態が変わります。<br>
	 * init()メソッドが終わった後、trueにマークされます。<br>
	 * HTTPリクエストクエリーの内容が反映されると、falseにマークされます。<br>
	 * input()またはoutput()メソッドが終わった後、falseにマークされます。
	 * @return true:初期化直後、false:変更された。
	 */
	public boolean isInitialized() {
		return init_f;
	}

	/**
	 * 管理下のname属性(インデックス化したもの)の列挙。
	 * @return name属性の配列。
	 */
	public String[] getDefineNameIndex() {
		ArrayList<String>	list = new ArrayList<String>();
		for ( String k:	tag_m_dic.keySet() ) {
			list.add( k );
		}
		return list.toArray( new String[0] );
	}

	/**
	 * 管理下のname属性(完全名)の列挙。
	 * @return name属性の配列。
	 */
	public String[] getDefineName() {
		ArrayList<String>	list = new ArrayList<String>();
		for ( String k: tag_1_dic.keySet() ) {
			list.add( k );
		}
		return list.toArray( new String[0] );
	}

	private ArrayList<Form> getForm( Tag tag, ArrayList<Form> list ) {
		Tag[]	nest = tag.getTagArray();
		for ( int i = 0; i < nest.length; i++ ) {
			list = getForm( nest[i], list );
		}
		if ( tag instanceof Form ) {
			list.add( (Form)tag );
		}
		return list;
	}

	private transient Control[]	modified = new Control[0];

	/**
	 * ブラウザで変更が加えられたコントロールの取得。
	 * 文字列、選択状態の変化を検出し、最後にブラウザへ出力した後に変更されたものを
	 * 返します。<br>
	 * テキストボックス系は、文字列に変化があるとこの戻り値に含まれます。<br>
	 * ボタンは押されている場合にこの戻り値に含まれます。<br>
	 * チェックボックスは、選択/解除の変化があった場合にこの戻り値に加えられます。<br>
	 * ラジオボタン群は、今選択されているもの１つがこの戻り値に加えられます。
	 * ただしそれが、RadioGroup 管理下であると、RadioGroup を代わりに使用します。<br>
	 * リストボックスやコンボボックスは選択状態に変化があると、
	 * この戻り値に加えられます。<br>
	 * ファイルアップロードはファイルが選択されていると、
	 * この戻り値に加えられます。
	 * @return 変化があったコントロール全て。
	 * 何も変更されずにサブミットされていれば、0個の配列を返します。
	 */
	public Control[] getModifiedControl() {
		return modified;
	}

	private Clickable	clicked_tag = null;

	/**
	 * 押下タグの取得。
	 * @return 押下タグ。無ければnull。
	 */
	public Clickable getClickedTag() {
		if ( clicked_tag == null )	return null;
		if ( clicked_tag.isClicked() )	return clicked_tag;
		return null;
	}

	/**
	 * 押下ボタンの取得。
	 * @return 押下ボタン。無ければnull。
	 */
	public Button getClickedButton() {
		Clickable	c = getClickedTag();
		if ( c instanceof Button ) {
			return (Button)c;
		}
		return null;
	}

	private Page	agency_page = null;
	/**
	 * フレームワークが使用します。
	 */
	public void setAgencyPage( Page page ) {
		if ( page == this ) {
			agency_page = null;
		}
		else {
			agency_page = page;
		}
	}

	/**
	 * ユニークキー一致ページのリターン。
	 * ユニークキーと合致するページを内包していればそれを返します。
	 * @param uid ユニークキー。
	 * @return ユニークキーと一致したページ。this かもしれません。
	 * 一致するページが無ければ null を返します。
	 */
	public Page getBypassPage( String uid ) {
		if ( uid == null )	return null;
		if ( uid.equals( getUniqueKey() ) )	return this;
		PageBypassedDiv[]	div = getPageBypassedDiv();
		for ( int i = 0; i < div.length; i++ ) {
			Page	page = div[i].getBypassPage();
			if ( page == null )	continue;
			page = page.getBypassPage( uid );
			if ( page != null )	return page;
		}
		return null;
	}

	/**
	 * リクエスト内容をフォームに反映する。
	 * input()メソッドを呼ぶ直前にはreflect()がコールされています。
	 * 通常のコーディングであればプログラマはこのメソッドを使う事はありません。
	 * @param req リクエスト。
	 * @return リクエスト内容を反映したページ。
	 */
	public Page reflect( RequestParameter req ) {
		clicked_tag = null;
		int	click_cnt = clickable.size();
		for ( int i = 0; i < click_cnt; i++ ) {
			Clickable	c = clickable.get( i );
			c.setClicked( false );
		}

		if ( agency_page != null ) {
			Page	page = agency_page.reflect( req );
			if ( page != null )	return page;
		}

		PageBypassedDiv[]	div = getPageBypassedDiv();
		for ( int i = 0; i < div.length; i++ ) {
			Page	page = div[i].getBypassPage();
			if ( page != null ) {
				page = page.reflect( req );
				if ( page != null )	return page;
			}
		}
		modified = new Control[0];
		if ( getMainTag() == null )	return null;

		for ( int i = 0; i < click_cnt; i++ ) {
			Clickable	c = clickable.get( i );
			if ( c.isClicked( req ) ) {
				clicked_tag = c;
				c.setClicked( true );
				break;
			}
		}
		if ( clicked_tag instanceof Anchor )	return this;

		ArrayList<Form>	form = getForm( getMainTag(), new ArrayList<Form>() );
		int	form_cnt = form.size();
		String	form_id = "paraselene_form_id";	// 不一致用ダミー
		RequestItem	item = req.getItem( Form.FORM_ID );
		if ( item != null ) {
			form_id = item.getValue();
		}
		for ( int i = form_cnt - 1; i >= 0; i-- ) {
			Form	f = form.get( i );
			if ( !f.getID().equals( form_id ) ) {
				form.remove( i );
			}
		}
		ArrayList<Control>	list = new ArrayList<Control>();
		form_cnt = form.size();
		for ( int i = 0; i < form_cnt; i++ ) {
			list.addAll( form.get( i ).reflect( req ) );
		}
		modified = list.toArray( new Control[0] );
		if ( modified.length > 0 ) {
			try {
				setInitialized( false );
			}
			catch( PageException e ) {
				Option.debug( e );
			}
		}

		if ( getClickedTag() != null )	return this;
		if ( form_cnt > 0 )	return this;
		return null;
	}

	/**
	 * 出力コンテントタイプ。
	 * nullを返すと自動選定されます。選定方法は以下です。
	 * isXML()がtrueを返し、且つ
	 * （リクエストヘッダAcceptに application/xhtml+xml が明示されている、
	 * または携帯からのアクセスである）場合に
	 * application/xhtml+xml を返します。それ以外では text/html を返します。
	 * またその時、getCharset()の戻り値を charset に指定します。
	 * @return &quot;text/html; charset=UTF-8&quot;等
	 */
	public abstract String getContentType();

	/**
	 * 出力文字コード。
	 * @return UTF-8等。
	 */
	public abstract String getCharset();

	/**
	 * URI属性の生成。出力文字コードには、this.getCharset()を使います。
	 * @param name 属性名。
	 * @param uri URI。
	 * @return URI属性。
	 * @exception PageException new URIValueで例外が発生した。
	 */
	public Attribute createURI( String name, String uri ) throws PageException {
		try {
			return new Attribute( name, new URIValue( uri, getCharset() ) );
		}
		catch( Exception e ) {
			throw new PageException( e );
		}
	}

	/**
	 * 自サイト内ページへのURI生成。
	 * URIValue.pageToURI()の戻り値から、this.createURI()を呼んで
	 * インスタンスを返します。
	 * @param name 属性名。
	 * @param pid ページID。
	 * @param fragment フラグメント。#は含めないで下さい。
	 * 不要な場合はnullを指定して下さい。
	 * @param query クエリー。エスケープしないで下さい。
	 * @return URI属性。
	 * @exception PageException new URIValueで例外が発生した。
	 */
	public Attribute createPageToURI( String name, PageID pid, String fragment,
	QueryItem ... query ) throws PageException {
		return createURI( name, URIValue.pageToURI( pid, fragment, query ) );
	}

	/**
	 * 自サイト内ページへのURI生成。ダウンロード用。
	 * URIValue.pageToDownloadURI()の戻り値から、this.createURI()を呼んで
	 * インスタンスを返します。
	 * @param name 属性名。
	 * @param pid ページID。
	 * @param filename ブラウザに認識させるファイル名。
	 * @param query クエリー。エスケープしないで下さい。
	 * @return URI属性。
	 * @exception PageException new URIValueで例外が発生した。
	 */
	public Attribute createPageToDownloadURI( String name, PageID pid,
	String filename, QueryItem ... query ) throws PageException {
		return createURI( name, URIValue.pageToDownloadURI(
			pid, filename, query )
		);
	}

	/**
	 * 自サイト内ページへの別名URI生成。
	 * 例えばpidのページのgetAliasURIが'abc'を返し、このメソッドの引数に
	 * '123'を渡すと、'abc123.na'というパスを生成します。
	 * ただし、getAliasURIが完全一致URIを返す場合は、bodyは無視されます。
	 * @param name 属性名。
	 * @param pid ページID。
	 * @param body URIに埋め込む文字列。
	 * @param fragment フラグメント。#は含めないで下さい。
	 * 不要な場合はnullを指定して下さい。
	 * @param query クエリー。エスケープしないで下さい。
	 * @return URI属性。
	 * @exception PageException 別名URIを持たないページを指定した。
	 * new URIValueで例外が発生した。先頭一致URIに対して、bodyにnullを指定した。
	 */
	public Attribute createAliasToURI( String name, PageID pid, String body, String fragment, QueryItem ... query ) throws PageException {
		PageFactory	factory = pid.getPageFactory();
		Page	page  = factory.getPage( pid );
		String	alias = page.getAliasURI();
		factory.returnPage( page );
		if ( alias == null ) {
			throw new PageException( pid.toString() + " aliasURI is null." );
		}
		StringBuilder	buf = new StringBuilder( alias );
		if ( !".na".equals( alias.substring( alias.length() - 3 ) ) ) {
			if ( body == null ) {
				throw new PageException( "body is null." );
			}
			buf = buf.append( body );
			buf = buf.append( ".na" );
		}
		if ( fragment != null ) {
			buf = buf.append( "#" );
			buf = buf.append( fragment );
		}
		try {
			URIValue	uv = new URIValue( buf.toString(), getCharset() );
			if ( query.length > 0 ) {
				uv.setQuery( query );
			}
			Attribute	attr = new Attribute( name, uv );
			return attr;
		}
		catch( Exception e ) {
			throw new PageException( e );
		}
	}

	/**
	 * 別名URI中の部分文字列取得。
	 * 別名URI(接頭辞)と.na(接尾辞)を省いた部分文字列を取得します。
	 * @param req リクエスト。
	 * @return 部分文字列。
	 */
	public String pickAliasURI( RequestParameter req ) {
		String	alias = getAliasURI();
		if ( alias == null ) {
			return null;
		}
		if ( TransactionSequencer.EXTENSION.equals( alias.substring( alias.length() - 3 ) ) ) {
			return null;
		}
		String	uri = req.getURI().getPath();
		String[] path = uri.split( "/" );
		for ( int i = 0; i < path.length; i++ ) {
			if ( path[i].length() < 4 )	continue;
			if ( !TransactionSequencer.EXTENSION.equals( path[i].substring( path[i].length() - 3 ) ) )	continue;
			if ( alias.length() >= path[i].length() )	continue;
			if ( !alias.equals( path[i].substring( 0, alias.length() ) ) ) continue;
			int	start = alias.length();
			int	end = path[i].length() - 3;
			if ( start < end )	return path[i].substring( start, end );
			return "";
		}
		return null;
	}

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

	/**
	 * タグの取得。配列化されたタグは取得できません。
	 * @param name タグのname属性値。
	 * @return タグ。配列化されたタグならnullを返します。
	 */
	public Tag getTag( String name ) {
		return tag_1_dic.get( name );
	}

	/**
	 * タグの取得。配列で返します。HTML先頭からの登場順にソートされています。
	 * @param name タグのname属性値。
	 * @return タグ。
	 */
	public Tag[] getAllTag( String name ) {
		if ( name == null )	return new Tag[0];
		String[]	pre = name.split( "\\$" );
		ArrayList<Tag>	list = new ArrayList<Tag>();
		HashMap<String,Tag>	map = tag_m_dic.get( pre[0] );
		if ( map == null )	return new Tag[0];
		for ( String k : map.keySet() ) {
			list.add( map.get( k ) );
		}
		return Tag.sort( list.toArray( new Tag[0] ) );
	}

	/**
	 * ページのID。
	 * @return ID。
	 */
	public abstract PageID getID();

	/**
	 * ハッシュ値。
	 * @return ハッシュ値。
	 */
	public int hashCode() {
		PageID	id = getID();
		return id != null?	id.getID():	Integer.MIN_VALUE;
	}

	/**
	 * ユニークキー。
	 * @return ユニークキー。
	 */
	public String getUniqueKey() {
		return uniKey;
	}

	/**
	 * 同一ページであるか？
	 * @param p 比較ページ。
	 * @return true:一致、false:不一致。
	 */
	public boolean equals( Object p ) {
		if ( this instanceof NullPage || p instanceof NullPage )	return false;
		if ( p instanceof PageID ) {
			return this.getID() == p;
		}
		if ( p instanceof Page ) {
			return this.hashCode() == p.hashCode();
		}
		return false;
	}

	/**
	 * 名称のインデックス部取得。
	 * @param name name属性値
	 * @return インデックス。
	 */
	public static String getIndexName( String name ) {
		if ( name == null )	return null;
		String[]	idx = name.split("\\$");
		return idx[0];
	}

	private void clearDic( String name ) {
		Tag	tag = tag_1_dic.remove( name );
		name_dic.remove( tag );
		String	index = getIndexName( name );
		HashMap<String,Tag>	map = tag_m_dic.get( index );
		if ( map == null )	return;
		map.remove( name );
		if ( map.size() == 0 ) {
			tag_m_dic.remove( index );
		}
	}

	private String extendName( Tag tag ) {
		String	org_name = tag.getNameAttribute();
		String	index = getIndexName( org_name );
		for ( ; ; name_seq++ ) {
			org_name = String.format( "%s$%d", index, name_seq );
			if ( tag_1_dic.get( org_name ) == null )	break;
		}
		return org_name;
	}

	private void nameTagSingle(Tag tag ) {
		if ( tag == null )	return;
		String	old_name = name_dic.get( tag );
		String	new_name = tag.getNameAttribute();
		if ( old_name == null && new_name == null ) return;
		if ( old_name != null && new_name == null ) {
			clearDic( old_name );
			return;
		}
		if ( old_name != null && new_name != null ) {
			if ( old_name.equals( new_name ) )	return;
			clearDic( old_name );
		}
		if ( tag_1_dic.get( new_name ) != null ) {
			new_name = extendName( tag );
			try {
				tag.setAttribute( new Attribute( "name", new_name ) );
			}
			catch( Exception e ) {}
		}

		tag_1_dic.put( new_name, tag );
		name_dic.put( tag, new_name );
		String	index = getIndexName( new_name );
		HashMap<String,Tag>	map = tag_m_dic.get( index );
		if ( map == null ) {
			map = new HashMap<String,Tag>();
			tag_m_dic.put( index, map );
		}
		map.put( new_name, tag );
	}

	/**
	 * タグのname検証。衝突が見つかればnameを変更する。
	 * @param tag 検証対象。
	 */
	public void nameTag( Tag tag ) {
		if ( getMainTag() == null )	return;
		tag.setAssignedPage( this );
		if ( tag instanceof PageHooker ) {
			hook.add( (PageHooker)tag );
		}
		if ( tag instanceof PageBypassedDiv ) {
			bypass.add( (PageBypassedDiv)tag );
		}
		if ( tag instanceof Clickable ) {
			clickable.add( (Clickable)tag );
		}

		String	name = tag.getNameAttribute();
		if ( name != null ) {
			if ( Form.isFixedName( name ) )	return;
		}
		nameTagSingle( tag );
		int	cnt = tag.getHTMLPartCount();
		for ( int i = 0; i < cnt; i++ ) {
			HTMLPart	part = tag.getHTMLPart( i );
			if ( part instanceof Tag ) {
				nameTag( (Tag)part );
			}
		}
		ArrayList<Form>	form = getForm( getMainTag(), new ArrayList<Form>() );
		cnt = form.size();
		for ( int i = 0; i < cnt; i++ ) {
			Form	f = form.get( i );
			if ( f.isInner( tag ) ) {
				f.checkTag( tag );
			}
		}
	}

	/**
	 * タグエントリーの除去。
	 * @param tag 除去対象。
	 */
	public void removeNameEntry( Tag tag ) {
		if ( getMainTag() == null )	return;
		ArrayList<Form>	form = getForm( getMainTag(), new ArrayList<Form>() );
		int cnt = form.size();
		for ( int i = 0; i < cnt; i++ ) {
			Form	f = form.get( i );
			if ( f.isInner( tag ) ) {
				f.checkTag4rm( tag );
			}
		}
		cnt = tag.getHTMLPartCount();
		for ( int i = 0; i < cnt; i++ ) {
			HTMLPart	part = tag.getHTMLPart( i );
			if ( part instanceof Tag ) {
				removeNameEntry( (Tag)part );
			}
		}
		tag.setAssignedPage( null );
		if ( tag instanceof PageHooker ) {
			hook.remove( (PageHooker)tag );
		}
		if ( tag instanceof PageBypassedDiv ) {
			bypass.remove( (PageBypassedDiv)tag );
		}
		if ( tag instanceof Clickable ) {
			clickable.remove( (Clickable)tag );
		}
		String	name = tag.getNameAttribute();
		if ( name == null )	return;
		clearDic( name );
	}

	private void clearAssignedPage( Tag tag ) {
		Tag[]	next = tag.getTagArray();
		if ( next != null ) {
			for ( int i = 0; i < next.length; i++ ) {
				clearAssignedPage( next[i] );
			}
		}
		removeNameEntry( tag );
	}

	/**
	 * HTMLタグの登録。
	 * @param tag HTMLタグ。
	 */
	public void setMainTag( Tag tag ) {
		if ( contents != null ) {
			clearAssignedPage( contents );
		}
		contents = tag;
		if ( tag != null ) {
			nameTag( tag );
		}
	}

	/**
	 * HTMLタグの取得。
	 * @return HTMLタグ。
	 */
	public Tag getMainTag() {
		return contents;
	}

	/**
	 * 内容の移動。&lt;html&gt;配下全ての要素を this に上書きします。
	 * this の元の内容は失われます。src は空になります。
	 * @param src 元のページ。
	 */
	public void moveMainTag( Page src ) {
		Tag	tag = src.getMainTag();
		src.setMainTag( null );
		setMainTag( tag );
	}

	/**
	 * タグの取得。AやTABLEのような、タグの種別で探します。
	 * 再帰的に探し、最初に見つかったインスタンスを返します。
	 * @param name タグの種別。
	 * @return タグ。見つからなければnull。
	 */
	public Tag getFirstTagByType( String name ) {
		Tag	tag = getMainTag();
		if ( tag == null )	return null;
		return tag.getFirstTagByType( name );
	}

	/**
	 * タグの取得。AやTABLEのような、タグの種別で探します。再帰的に探します。
	 * @param name タグの種別。
	 * @return タグの配列。無ければ0個の配列を返す。登場順にソートされてます。
	 * ただし、nameがnullなら、nullを返す。
	 */
	public Tag[] getAllTagByType( String name ) {
		if ( getMainTag() == null )	return new Tag[0];
		Tag	tag = getMainTag();
		if ( tag == null )	return new Tag[0];
		return tag.getAllTagByType( name );
	}

	/**
	 * タグの置換。fromインスタンスをtoインスタンスに置換します。<BR>
	 * fromが削除され、その位置にtoが入ります。<BR>
	 * copy_fをtrueにすると、以下の事を行います。<UL>
	 * <LI>fromのVisible状態をtoが引き継ぎます。
	 * <LI>fromの全ての属性のレプリカがtoに設定されます。
	 * toに予め、同じ属性があった場合、上書きされます。
	 * <LI>fromが持つの全てのHTMLPartのレプリカがtoに追加されます。
	 * toが予めHTMLPartを持っていれば、その末尾から追加されます。
	 * </UL>fromインスタンスがこのページに存在しない場合等には、
	 * NullPointerExceptionが発生する可能性があります。
	 * @param from 置換元インスタンス。
	 * @param to 新規設定インスタンス。
	 * @param copy_f true:内容をコピーする、false:内容のコピーは行わない。
	 */
	public void replaceTag( Tag from, Tag to, boolean copy_f ) {
		if ( getMainTag() == null )	return;
		Tag	tag = getMainTag().getDirectInnerTag( from );
		int	idx = tag.indexOf( from );
		tag.removeHTMLPart( from );
		if ( copy_f ) {
			to.setVisible( from.isVisible() );
			Attribute[]	attr = from.getAllAttribute();
			for ( int i = 0 ; i < attr.length; i++ ) {
				to.setAttribute( attr[i].getReplica() );
			}
			int	cnt = from.getHTMLPartCount();
			for ( int i = 0 ; i < cnt; i++ ) {
				to.addHTMLPart( from.getHTMLPart( i ).getReplica() );
			}
		}
		tag.addHTMLPart( idx, to );
	}

	/**
	 * 文字列化。
	 * @return HTML
	 */
	public String toString() {
		StringBuilder	buf = new StringBuilder();
		if ( isXML() ) {
			buf = buf.append( "<?xml version=\"1.0\" encoding=\"" );
			buf = buf.append( getCharset() );
			buf = buf.append( "\"?>" );
		}
		String	doc = getDoctype();
		if ( doc != null ) {
			buf = buf.append( doc );
		}
		if ( contents != null ) {
			buf = buf.append( contents.toString() );
		}
		return buf.toString();
	}

	/**
	 * ロード時に実行する JavaScript の登録。
	 * このページがブラウザにロードされた時に実行したいスクリプトの指定。
	 * <br>
	 * このメソッドの呼び出しの度にスクリプトが追加されます。
	 * また、このページがブラウザへ出力されると、この登録内容がクリア
	 * されます。
	 * @param script 実行スクリプト。各Stringには改行が付けられます。
	 */
	public void addOnLoadScript( String ... script ) {
		paraselene.ajax.data.JavaScript	js = null;
		if ( getOnloadTag().getHTMLPartCount() == 0 ) {
			js = new paraselene.ajax.data.JavaScript( script );
			getOnloadTag().addHTMLPart( js );
			return;
		}
		js = (paraselene.ajax.data.JavaScript)getOnloadTag().getHTMLPart( 0 );
		js.append( script );
	}

	/**
	 * ロード時に実行する JavaScript を返します。
	 * @return JavaScript。
	 */
	public String getOnLoadScript() {
		return getOnloadTag().getHTMLPart( 0 ).toString();
	}

	/**
	 * ロード時に実行する JavaScript をクリアします。
	 * Paraselene 自身も JavaScript を登録しています。
	 * 不用意にクリアしないで下さい。
	 */
	public void resetOnLoadScript() {
		getOnloadTag().removeHTMLPart();
		getOnloadTag().addHTMLPart( new paraselene.ajax.data.JavaScript() );
		if ( getAjaxSupport() == Page.AjaxSupport.SERVER_PUSH ) {
			StringBuilder	buf = new StringBuilder( "paraselene.comet_start('" );
			buf = buf.append( getUniqueKey() );
			buf = buf.append( "');" );
			addOnLoadScript( buf.toString() );
		}
	}

	/**
	 * 出力。
	 * @param w ライター。
	 */
	public void write( PrintWriter w ) throws UnsupportedEncodingException, URISyntaxException {
		if ( isXML() ) {
			w.print( "<?xml version=\"1.0\" encoding=\"" );
			w.print( getCharset() );
			w.println( "\"?>" );
		}
		String	doc = getDoctype();
		if ( doc != null ) {
			w.println( doc );
		}
		if ( contents != null ) {
			contents.write( w, HTMLPart.StringMode.BODY );
		}
		resetOnLoadScript();
	}

	/**
	 * 更新日時を返す。<br>
	 * nullを返すと更新日なしとなり、ブラウザへのキャッシュを抑制します。<br>
	 * null以外を返すと、ブラウザへ更新日時を返しキャッシュを許可します。<br>
	 * null以外を返した際にブラウザがIf-Modified-Since要求している場合、
	 * この戻り値と比較し、更新されていなければ304(not modified)をブラウザへ返し、
	 * 実データは送信されません。<br>
	 * nullを返すと、常に実データをブラウザへ送信します。
	 * @return 常にnullを返し、キャッシュを抑制します。
	 * 必要に応じてオーバーライドして下さい。<br>
	 * ブログであれば記事の投稿日、画像であれば画像ファイルのタイムスタンプ、
	 * 等を返すとブラウザキャッシュが有効に働き通信量を減らせます。
	 */
	public HTTPDate getLastModified() {
		return null;
	}

	/**
	 * GZIP圧縮応答を許可するか？
	 * ブラウザが対応していれば、GZIP圧縮をかけて応答します。<br>
	 * 出力する内容(既に圧縮されている等)や出力サイズ(サイズが小さい等)によっては
	 * falseを返しGZIP圧縮応答をさせない方が効率が良い場合もあります。
	 * @return 常にfalseを返し、GZIP圧縮応答を禁止する。
	 */
	public boolean isGZIP() {
		return false;
	}

	/**
	 * 履歴追加方法。<br>
	 * History.add()を行う際に呼ばれる。
	 * 過去に同一ページがあれば、直近からそのページまでの履歴をクリアする。
	 * この動作を指定できる。trueならクリア、falseなら維持する。
	 * @return true(履歴クリア)推奨。
	 */
	public abstract boolean isHistoryClear();

	/**
	 * 履歴追加許可。<br>
	 * History.add()を行う際に呼ばれる。
	 * @return true(履歴追加)推奨。
	 * 履歴に追加したくないページであればfalseを返すこと。
	 */
	public abstract boolean isAllowHistoryAdd();

	/**
	 * 入力値の検証を行う。必ずセッションが発生しています。<br>
	 * 入力値のエラーチェックや入力値に即した動作を記述します。<br>
	 * @param req リクエスト内容。
	 * @param fw デフォルト遷移先。
	 * @exception PageException 処理の継続が不可能(ブラウザには500を返す)。
	 */
	public abstract Forward input( RequestParameter req, Forward fw ) throws PageException;

	/**
	 * 出力情報の設定を行う。
	 * @param from 遷移元ページ。初めて表示されている場合nullです。
	 * @param req リクエスト内容。
	 * @return 出力ページ。
	 * nullを返すとthisをリターンしたのと同じ扱いにされます。
	 * @exception PageException 処理の継続が不可能(ブラウザには500を返す)。
	 */
	public abstract Page output( Page from, RequestParameter req ) throws PageException;

	/**
	 * 別名URI設定。nullを返すと別名は設定しません。
	 * @return URI。必ず".na"で終えて下さい。/やディレクトリ名は含めないで下さい。
	 */
	public abstract String getAliasURI();

	/**
	 * アップロードファイルの最大バイト数。
	 * @return 負数なら無制限に受け付けます。
	 */
	public abstract int getUploadMaxBytes();

	/**
	 * 処理済みリクエストの再呼び出しを検出するか？
	 * このページに遷移するためのURIを生成する際のリクエストID付与をコントロール
	 * します。trueを返すとリクエストIDが付与されます。
	 * @return true:検出する、false:検出しない。
	 */
	public abstract boolean isCheckRepeatSameRequest();

	/**
	 * 初回outputの呼び出し直前にコールされます。初期化処理を記述します。
	 * @param req リクエスト内容。
	 * @exception PageException 処理の継続が不可能(ブラウザには500を返す)。
	 */
	public abstract void firstOutput( RequestParameter req ) throws PageException;

	private boolean beginning_called = false;
	/**
	 * フレームワークが使用します。ここからfirstOutputが呼ばれます。
	 */
	public void beginning( RequestParameter req ) throws PageException {
		if ( beginning_called )	return;
		beginning_called = true;
		if ( getMainTag() == null )	return;
		body_tag = getFirstTagByType( "body" );
		firstOutput( req );
		body_tag = getFirstTagByType( "body" );
		if ( this instanceof FramesetPage || this instanceof Downloadable ) {
			ajax_f = false;
			return;
		}
		ajax_f = true;
		if ( getAccessedMobile() == RequestParameter.Mobile.NO_MOBILE && getAccessedSearchEngine() == RequestParameter.SearchEngine.NO_SEARCHENGINE ) {
			AjaxSupport	ajax = getAjaxSupport();
			Tag	head = getFirstTagByType( "head" );
			Tag	body = body_tag;
			if ( head != null ) {
				int	idx = 0;
				int	max = head.getHTMLPartCount();
				for ( ; idx < max; idx++ ) {
					HTMLPart	part = head.getHTMLPart( idx );
					if ( !(part instanceof Tag) )	continue;
					Tag	tag = (Tag)part;
					if ( "link".equals( tag.getName() ) )	break;
				}
				try {
					Tag	tag = new Tag( "link", true );
					tag.setAttribute(
						new Attribute( "rel", new Text( "stylesheet" ) ),
						new Attribute( "href", new URIValue( "css/paraselene.css/paraselene.resource.na" ) ),
						new Attribute( "type", new Text( "text/css" ) )
					);
					head.addHTMLPart( idx, tag );
				}
				catch( Exception e ){
					Option.debug( e );
				}
			}
			else	ajax_f = false;
			if ( head != null && body != null && ajax != AjaxSupport.NO && getPopupType() == null ) {
				Tag	script = new Tag( "script", false );
				try {
					script.setAttribute(
						new Attribute( "type", "text/javascript" ),
						new Attribute( "src", "js/paraselene.js/paraselene.resource.na" )
					);
				}
				catch( Exception e ) {
					Option.debug( e );
				}
				head.addHTMLPart( script );

				Tag[]	div = new Tag[] { new Tag( "div", false ), new Tag( "div", false ) };
				root_id = div[0].makeID();
				div[0].include( body, true );
				body_tag = div[0];
				Tag eclipse = new Tag( "div", false );
				eclipse.setAttribute(
					new Attribute( "id", AJAX_BLACK_ID )
				);
				Tag	iframe = new Tag( "iframe", false );
				iframe.setAttribute(
					new Attribute( "onload", "paraselene.form_comp();" ),
					new Attribute( "id", AJAX_TARGET_ID ),
					new Attribute( "name", AJAX_TARGET_ID ),
					new Attribute( "scrolling", new Text( "no" ) )
				);
				div[1].addHTMLPart( eclipse, iframe );
				div[1].setAttribute(
					new Attribute( "id", AJAX_ADD_ID )
				);
				body.removeHTMLPart();
				body.addHTMLPart( div[0], div[1], getOnloadTag() );
				resetOnLoadScript();
			}
			else	ajax_f = false;
		}
		else	ajax_f = false;
		if ( getPopupType() != null ) {
			ajax_f = true;
			resetOnLoadScript();
		}
	}

	private String	root_id;
	public String getRootID() {
		return root_id;
	}

	private static final String	AJAX_BLACK_ID = "paraseleneEclipse";
	private static final String	AJAX_TARGET_ID = "paraseleneDragonsTail";
	private static final String	AJAX_ADD_ID = "paraseleneDragonsHead";
	private boolean ajax_f = false;
	private RequestParameter	now_req;
	private RequestParameter.Mobile			access_mobile;
	private RequestParameter.SearchEngine	access_se;
	private Tag	onload_tag = null;
	private Tag	body_tag = null;

	/**
	 * body タグを返します。次のように機能します。
<table border="1">
  <tbody>
    <tr>
      <th rowspan="2">メソッド</th>
      <th colspan="2">Ajax</th>
    </tr>
    <tr>
      <th>無効時</th>
      <th>有効時</th>
    </tr>
    <tr>
      <td>init</td>
      <td colspan="2">null</td>
    </tr>
    <tr>
      <td>firstOutput</td>
      <td colspan="2">&lt;body&gt;</td>
    </tr>
    <tr>
      <td>output</td>
      <td rowspan="2">&lt;body&gt;</td>
      <td rowspan="2">&lt;div&gt;</td>
    </tr>
    <tr>
      <td>input</td>
    </tr>
  </tbody>
</table>
	 * Ajax 有効時には、本来の body 内容は、その直下の div に移動します。
	 * <br>body 直下には Paraselene が Ajax 処理を行うためのタグを
	 * 追加・削除させてきますので、body タグ内容は操作しないで下さい。<br>
	 * div へ移動するタイミングは、firstOutput 呼び出し直後です。<br>
	 * そのため、firstOutput 中でのこのメソッドの戻り値は、常に
	 * body タグを返します。
	 * @return body 内容を持っているタグ。
	 */
	public Tag getBodyTag() {
		return body_tag;
	}

	private Tag getOnloadTag() {
		if ( onload_tag != null )	return onload_tag;
		onload_tag = Tag.makeScript();
		onload_tag.setModifyEnable( false );
		return onload_tag;
	}

	/**
	 * Ajax機能が利用可能であるか？以下の全てを満たす場合に true を返します。
	 * <ul>
	 * <li>{@link #getAjaxSupport()}で、AjaxSupport.NO 以外を返した場合。
	 * <li>携帯アクセスではない。
	 * <li>検索エンジンのクローラーではない。
	 * </ul>
	 * @return true:利用可能、false:利用不可。
	 */
	public boolean isAjax() { return ajax_f; }
	
	/**
	 * フレームワークが使用します。
	 */
	public void setRequestParameter( RequestParameter req ) {
		now_req = req;
		if ( req != null ) {
			access_mobile = req.judgeMobile();
			access_se = req.judgeSearchEngine();
		}
	}

	/**
	 * RequestParameterから得られたRequestParameter.Mobileを返します。
	 * @return 携帯判定結果。
	 */
	public RequestParameter.Mobile getAccessedMobile() {
		return access_mobile;
	}
	/**
	 * RequestParameterから得られたRequestParameter.SearchEngineを返します。
	 * @return 検索エンジン判定結果。
	 */
	public RequestParameter.SearchEngine getAccessedSearchEngine() {
		return access_se;
	}

	public Tag[] getModifiedTag() {
		Tag	tag = getFirstTagByType( "body" );
		if ( tag == null )	return new Tag[0];
		return tag.getModifiedTag();
	}

	/**
	 * Ajax 使用時の Z インデックスのベースを返します。
	 * 必要に応じてオーバーライドして下さい。
	 * @return 常に 1 を返します。
	 */
	public int getZindex() {
		return 1;
	}

	/**
	 * ポップアップの背景スタイルを指定します。
	 * 必要に応じてオーバーライドして下さい。
	 * @return 常に lavender 指定を返します。
	 */
	public CSSValuable[] getPopupBackGround() {
		return new CSSValuable[] { new Color( WebColor.LAVENDER ) };
	}

	/**
	 * キャッシュ禁止指定。常に false を返します。<br>
	 * ただし、getLastModified に null 以外を返すと、この戻り値は無視されます。
	 * 必要に応じてオーバーライドして下さい。
	 * ただし、特別な理由がない限り、false を返して下さい。
	 * @return true:ブラウザキャッシュを禁止します、
	 * false:ブラウザキャッシュを許可します。
	 */
	public boolean isNoCache() { return false; }

	/**
	 * titleタグ設定値の取得。
	 * @return タイトル。無ければ空文字列を返します。
	 */
	public String getTitleString() {
		Tag	tag = getFirstTagByType( "title" );
		if ( tag == null )	return "";
		String	title = tag.getValueString();
		if ( title == null )	return "";
		return title;
	}

	/**
	 * ページタイトルラベル。
	 * この戻り値がポップアップダイアログのタイトル部等に使用されます。<br>
	 * これをオーバーライドする事で、その表示内容を変更する事ができます。
	 * @return getTitleString()の戻り値から Text を生成して返します。
	 */
	public HTMLPart[] getTitleLabel() {
		String	str = getTitleString();
		if ( str.isEmpty() )	str = "　";
		return new HTMLPart[]{ new Text( str ) };
	}

	private PageServerInformation	server_info = null;
	/**
	 * サーバー情報の取得。
	 * @return サーバー情報。
	 */
	public PageServerInformation getPageServerInformation() {
		return server_info;
	}
	/**
	 * サーバー情報の設定。
	 * @param psi サーバー情報。
	 */
	public void setPageServerInformation( PageServerInformation psi ) {
		server_info = psi;
	}

	private transient ArrayList<Cookie>	cookie = new ArrayList<Cookie>();

	/**
	 * クッキーの追加。クライアントに返したいクッキーを設定します。<br>
	 * このインスタンスが output からリターンされた時にクッキーを返します。
	 * <br>クライアントへクッキーを返すとクッキーはクリアされます。
	 * @param c クッキー。
	 */
	public void addCookie( Cookie c ) {
		if ( cookie == null )	cookie = new ArrayList<Cookie>();
		cookie.add( c );
	}

	/**
	 * フレームワークが使用します。
	 */
	public void setResponse( HttpServletResponse res ) {
		if ( cookie == null )	return;
		int	cnt = cookie.size();
		for ( int i = 0; i < cnt; i++ ) {
			res.addCookie( cookie.get( i ) );
		}
		cookie = new ArrayList<Cookie>();
	}

	/**
	 * レイアウトパーツページ。
	 */
	public class LayoutParts {
		Tag	div;
		Page	page;
		Page	base;
		/**
		 * コンストラクタ。
		 * @param p 埋め込むページ。
		 * @param tag 埋め込み先のタグ。
		 */
		public LayoutParts( Page p, Tag tag ) throws Page.PageException {
			page = p;
			div = tag;
			if ( page == null || div == null ) {
				throw new Page.PageException( "doLayout: null を渡しています。" );
			}
			base = div.getAssignedPage();
		}
		void checkSame( Page layout ) throws Page.PageException {
			if ( base != layout ) {
				throw new Page.PageException( "doLayout: レイアウト適用先のページインスタンスが統一されていません" );
			}
		}
		void set() {
			if ( div instanceof PageBypassedDiv ) {
				PageBypassedDiv	pbd = (PageBypassedDiv)div;
				pbd.setBypassPage( base.getPageServerInformation().getHistorySet().get( base.getHistoryKey() ), page );
			}
			else {
				div.include( page, true );
			}
		}
	}

	/**
	 * レイアウトの適用。<br>
	 * this とはレイアウトを示す別ページインスタンスに this と parts の示す内容を
	 * 設定し、this の出力内容をそのレイアウトに変更します。<br>
	 * また、ページタイトルがあれば引き継ぎます。
	 * 
	 * @param include_me this の埋め込み先。
	 * @param parts this 以外の埋め込みページ。
	 */
	public void doLayout( Tag include_me, LayoutParts ... parts ) throws PageException {
		if ( include_me == null ) {
			throw new PageException( "doLayout: include_me が null です" );
		}
		Page	base = include_me.getAssignedPage();
		if ( base == null ) {
			throw new PageException( "doLayout: include_me がどのページにも所属していません" );
		}
		if ( base == this ) {
			throw new PageException( "doLayout: レイアウト適用先のページインスタンスが this です" );
		}
		if ( include_me instanceof PageBypassedDiv ) {
			throw new PageException( "doLayout: this の設定先が PageBypassedDiv です" );
		}
		for ( int i = 0; i < parts.length; i++ ) {
			parts[i].checkSame( base );
		}

		Tag	title = getFirstTagByType( "title" );
		if ( title != null ) {
			Tag	lo_title = base.getFirstTagByType( "title" );
			if ( lo_title == null ) {
				Tag	head = base.getFirstTagByType( "head" );
				if ( head != null ) {
					head.addHTMLPart( title.getReplica() );
				}
			}
			else {
				lo_title.setValueString( title.getValueString() );
			}
		}
		include_me.include( this, true );
		moveMainTag( base );
		for ( int i = 0; i < parts.length; i++ ) {
			parts[i].set();
		}
	}

	/**
	 * セッションID付きURLへの置換。<br>
	 * これが true を返すと、ブラウザのクッキー対応・非対応に応じてURLを書き換え
	 * クッキー非対応ブラウザでは URL 置換が行われます。<br>
	 * これが false を返すと、クッキー対応・非対応に関わらずURL書き換えを
	 * 行いません。
	 * ただし、アクセス元が携帯であるとこの戻り値を無視して URL 置換を行います。
	 * @return 常に false を返します。
	 * 必要に応じてオーバーライドして下さい。
	 */
	public boolean isEncodeURL() {
		return false;
	}
}

