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


import java.util.*;
import java.io.*;
import java.net.*;
import javax.servlet.http.*;

/**
 * リクエスト。
 */
public class RequestParameter implements Serializable {
	private static final long serialVersionUID = 2L;
	/**
	 * 呼び出しリクエストメソッド。
	 */
	public enum Method {
		GET, POST;

		private static final long serialVersionUID = 2L;
	}

	/**
	 * 日本の携帯キャリア。
	 */
	public enum Mobile {
		/**
		 * DoCoMo。
		 */
		DOCOMO,
		/**
		 * au。
		 */
		AU,
		/**
		 * auまたはTU-KAでHDML機。<BR>ParaseleneはHDMLに対応していません。
		 */
		TU_KA,
		/**
		 * SoftBank、Vodafone、J-PHONE。
		 */
		J_PHONE,
		/**
		 * EMOBILE。
		 */
		EMOBILE,
		/**
		 * その他、PC等。
		 */
		NO_MOBILE;

		private static final long serialVersionUID = 1L;
	}

	/**
	 * ユーザーエージェントから、携帯キャリアを判定する。
	 * @param ua ユーザーエージェント。nullなら、NO_MOBILEを返します。
	 * @return 判定結果。
	 */
	public static Mobile judgeMobile( String ua ) {
		if ( ua == null )	return Mobile.NO_MOBILE;
		if ( ua.indexOf( "DoCoMo" ) != -1 )	return Mobile.DOCOMO;
		if ( ua.indexOf( "KDDI" ) != -1 )	return Mobile.AU;
		if ( ua.indexOf( "emobile" ) != -1 )	return Mobile.EMOBILE;
		if ( ua.indexOf( "SoftBank" ) != -1 ||
			ua.indexOf( "Vodafone" ) != -1 ||
			ua.indexOf( "J-PHONE" ) != -1
		) {
			return Mobile.J_PHONE;
		}
		if ( ua.indexOf( "UP.Browser" ) != -1 )	return Mobile.TU_KA;
		return Mobile.NO_MOBILE;
	}

	/**
	 * 検索エンジンクローラー。
	 */
	public enum SearchEngine {
		/**
		 * Google AdSense。
		 * Google AdSense の掲載広告選定用クローラー
		 * (ページの文言を解析して、適した掲載広告を決定します)です。
		 * Google の検索結果自体には影響しません。
		 */
		GOOGLE_ADSENSE( "Mediapartners-Google" ),
		/**
		 * Google。
		 * Google の検索結果自体に反映するためのクローラーです。
		 */
		GOOGLE( "Googlebot" ),
		/**
		 * Yahoo!。
		 */
		YAHOO( "Yahoo!", "Slurp" ),
		/**
		 * Yahoo JAPAN。
		 */
		YAHOO_JAPAN( "Y!J" ),
		/**
		 * Bing。
		 */
		MSN( "msnbot" ),
		/**
		 * 百度。
		 */
		BAIDU( "Baiduspider" ),
		/**
		 * NAVER。
		 */
		NAVER( "Yeti" ),
		/**
		 * Яндекс。
		 */
		YANDEX( "Yandex" ),
		/**
		 * Cuil。
		 */
		CUIL( "Twiceler" ),
		/**
		 * Alexa。
		 */
		ALEXA( "ia_archive" ),
		/**
		 * 検索エンジンではない。
		 */
		NO_SEARCHENGINE();
		private String[]	str = null;
		private SearchEngine( String ... s ) {
			str = new String[s.length];
			for ( int i = 0; i < str.length; i++ ) {
				str[i] = s[i].toLowerCase( Locale.ENGLISH );
			}
		}
		boolean isHit( String ua ) {
			if ( str == null )	return true;
			for ( int i = 0; i < str.length; i++ ) {
				if ( ua.indexOf( str[i] ) == -1 )	return false;
			}
			return true;
		}
	}
	/**
	 * ユーザーエージェントから、検索エンジンクローラーを判定する。
	 * @param ua ユーザーエージェント。nullなら、NO_SEARCHENGINEを返します。
	 * @return 判定結果。
	 */
	public static SearchEngine judgeSearchEngine( String ua ) {
		if ( ua == null )	return SearchEngine.NO_SEARCHENGINE;
		SearchEngine[]	dim = SearchEngine.values();
		ua = ua.toLowerCase( Locale.ENGLISH );
		for ( int i = 0; i < dim.length; i++ ) {
			if ( dim[i].isHit( ua ) )	return dim[i];
		}
		return SearchEngine.NO_SEARCHENGINE;
	}

	private HashMap<String,RequestItem>	item_map = new HashMap<String,RequestItem>();
	private Method	method = Method.GET;
	private URI	uri = null;
	private URI	sub_uri = null;
	private String	context_path = null;
	private transient HttpSession	session = null;
	private transient Cookie[]	in_cookie = null;
	private String	remote_addr = null;
	private HashMap<String,String[]>	header = new HashMap<String,String[]>();
	private transient Supervisor	supervisor = null;
	static final String UA_HEAD = "User-Agent";

	private static final String	AJAX_CALLED = "paraselene$ajax$called";
	/**
	 * フレームワークが使用します。
	 */
	public void markAjaxCalled() {
		if ( session == null )	return;
		session.setAttribute( AJAX_CALLED, new Boolean( true ) );
	}

	/**
	 * クライアントが Ajax 通信を使用したか？
	 * @return true:使用した痕跡がある、false:通信の痕跡がない。
	 */
	public boolean wasUsedAjax() {
		if ( session == null )	return false;
		return session.getAttribute( AJAX_CALLED ) != null;
	}

	/**
	 * リクエスト内容の比較。
	 * @param o 比較対象。
	 * @return true:同一リクエスト、false:異なるリクエスト。
	 */
	public boolean equals( Object o ) {
		if ( o == null )	return false;
		return o.hashCode() == hashCode();
	}

	public int hashCode() {
		int	ret = uri.hashCode();
		for ( String k : item_map.keySet() ) {
			ret += k.hashCode();
			RequestItem	my_item = item_map.get( k );
			String[]	v = my_item.getAllValue();
			if ( v == null )	continue;
			for ( int i = 0; i < v.length; i++ ) {
				ret += v[i].hashCode();
			}
		}
		return ret;
	}

	/**
	 * ユーザーエージェントから、携帯キャリアを判定する。
	 * @return 判定結果。
	 */
	public Mobile judgeMobile() {
		String[]	ua = getHeader( UA_HEAD );
		if ( ua == null )	return Mobile.NO_MOBILE;
		if ( ua.length < 1 )	return Mobile.NO_MOBILE;
		return judgeMobile( ua[0] );
	}

	/**
	 * ユーザーエージェントから、検索エンジンクローラーを判定する。
	 * @return 判定結果。
	 */
	public SearchEngine judgeSearchEngine() {
		String[]	ua = getHeader( UA_HEAD );
		if ( ua == null )	return SearchEngine.NO_SEARCHENGINE;
		if ( ua.length < 1 )	return SearchEngine.NO_SEARCHENGINE;
		return judgeSearchEngine( ua[0] );
	}

	/**
	 * コンストラクタ。
	 */
	public RequestParameter() {
		try {
			uri = new URI( "#" );
		}
		catch( Exception e ) {
			Option.debug( e );
		}
	}

	/**
	 * メソッドの設定。
	 * @param m メソッド。
	 */
	protected void setMethod( Method m ) {
		method = m;
	}

	/**
	 * セッションの設定。
	 * @param s セッション。
	 */
	protected void setSession( HttpSession s ) {
		session = s;
	}

	/**
	 * クッキーの設定。
	 * @param c クッキー。
	 */
	protected void setCookie( Cookie[] c ) {
		in_cookie = c;
	}

	/**
	 * リクエスト元IPアドレスの設定。
	 * @param ip リモートIP。
	 */
	protected void setRemoteAddr( String ip ) {
		remote_addr = ip;
	}

	/**
	 * リクエストURI設定。クエリも含めたフルパスが設定されます。
	 * @param u URI。
	 */
	protected void setURI( URI u ) {
		uri = u;
	}

	/**
	 * リクエストURI設定。コンテキストパスを含まない、相対パスです。
	 * @param u URI。
	 */
	protected void setRelativeURI( URI u ) {
		sub_uri = u;
	}


	/**
	 * {@link HttpServletRequest#getContextPath()} を返します。
	 * @return コンテキストパス。
	 */
	public String getContextPath() {
		return context_path;
	}

	/**
	 * コンテキストパスの設定。
	 * @param str コンテキストパス。
	 */
	protected void setContextPath( String str ) {
		context_path = str;
	}

	/**
	 * サーブレット設定。
	 * @param sv サーブレット。
	 */
	protected void setSupervisor( Supervisor sv ) {
		supervisor = sv;
	}

	/**
	 * ヘッダ登録。
	 * @param name ヘッダ名。
	 * @param val 値。
	 */
	public void setHeader( String name, String[] val ) {
		header.put( name.toLowerCase( Locale.ENGLISH ), val );
	}

	static final String X_FORWARD = "X-Forwarded-For";

	private String getRemoteAddr( HttpServletRequest req ) {
		String	x = req.getHeader( X_FORWARD );
		if ( x != null )  {
			String[]	tmp = x.split( "[\\.:]" );
			if ( tmp.length >= 4 )	return x;
		}
		return req.getRemoteAddr();
	}

	void setParam( Method m, HttpServletRequest request, Supervisor sv ) {
		setSupervisor( sv );
		setMethod( m );
		setSession( request.getSession( false ) );
		setCookie( request.getCookies() );
		setRemoteAddr( getRemoteAddr( request ) );
		try {
			StringBuffer	buf = request.getRequestURL();
			String	query = request.getQueryString();
			if ( query != null ) {
				buf = buf.append( "?" );
				buf = buf.append( query );
			}
			setURI( new URI( buf.toString() ) );

			String[]	path = getURI().getPath().split( "/" );
			setContextPath( request.getContextPath() );
			String[]	cont = getContextPath().split( "/" );
			String	last = cont[cont.length - 1];
			int	start = 0;
			for ( ; start < path.length; start++ ) {
				if ( last.equals( path[start] ) )	break;
			}
			start++;
			StringBuilder	b = new StringBuilder( path[start] );
			for ( start++; start < path.length; start++ ) {
				b = b.append( "/" );
				b = b.append( path[start] );
			}
			setRelativeURI( new URI( b.toString() ) );
		}
		catch(Exception e){
			Option.debug( e );
		}
		for ( Enumeration<?>	em = request.getHeaderNames(); em.hasMoreElements(); ) {
			String	key = (String)em.nextElement();
			ArrayList<String>	data = new ArrayList<String>();
			for ( Enumeration<?>	e2 = request.getHeaders( key ); e2.hasMoreElements(); ) {
				data.add( (String)e2.nextElement() );
			}
			setHeader( key, data.toArray( new String[0] ) );
		}
	}

	/**
	 * サーブレットインスタンスの取得。
	 * Supervisorの派生クラスである、実行中のGateインスタンスが得られます。
	 * Supervisorは、HttpServletの派生クラスです。
	 * Gateにメソッドを準備する事により、サーブレットコンテナへアクセスできます。
	 * @return サーブレットインスタンス。
	 */
	public Supervisor getSupervisor() {
		return supervisor;
	}

	/**
	 * リクエストヘッダ名の一覧の取得。
	 * getHeader でアクセス可能なヘッダ名の一覧です。
	 * @return リクエストヘッダ名一覧。
	 */
	public String[] getHeaderNames() {
		return header.keySet().toArray( new String[0] );
	}

	/**
	 * リクエストヘッダの取得。複数行存在すれば複数個の配列となります。
	 * 戻り値の配列はカンマで区切りません。行による配列です。
	 * カンマ区切りで扱いたい場合は、getHeaderWithQualityを使用して下さい。
	 * @param name ヘッダ名。
	 * @return ヘッダ。無ければnull。
	 */
	public String[] getHeader( String name ) {
		return header.get( name.toLowerCase( Locale.ENGLISH ) );
	}

	/**
	 * 品質係数付きヘッダ。
	 */
	public class HeaderQuality {
		String 	val;
		Float	q = null;
		HeaderQuality( String org ) throws NumberFormatException {
			String[]	data = org.split( ";" );
			val = data[0].trim();
			if ( data.length > 1 ) {
				String[]	k = data[1].split( "=" );
				if ( k[0].trim().equals( "q" ) ) {
					if ( k.length > 1 ) {
						q = new Float( data[1] );
					}
				}
			}
		}
		/**
		 * ヘッダ設定値の取得。
		 * en;q=0.7 ならば en を返します。
		 * @return ヘッダ設定値。
		 */
		public String getValue() {
			return val;
		}
		/**
		 * 品質係数の取得。
		 * en;q=0.7 ならば 0.7 を返します。
		 * @return 品質係数。設定されていなければnullです。
		 */
		public Float getQuality() {
			return q;
		}
	}

	/**
	 * リクエストヘッダの取得。品質係数(q)を持つヘッダに使用します。
	 * 戻り値の配列は、カンマで区切られた個数で返します。
	 * @param name ヘッダ名。
	 * @return ヘッダ。無ければnull。
	 *
	 */
	public HeaderQuality[] getHeaderWithQuality( String name ) {
		String[]	org = getHeader( name );
		if ( org == null )	return null;
		ArrayList<HeaderQuality>	head = new ArrayList<HeaderQuality>();
		for ( int i = 0; i < org.length; i++ ) {
			String[]	data = org[i].split( "," );
			for ( int j = 0; j < data.length; j++ ) {
				try {
					head.add( new HeaderQuality( data[j] ) );
				}
				catch( NumberFormatException e ) {}
			}
		}
		return head.toArray( new HeaderQuality[0] );
	}

	/**
	 * リクエスト元のIPアドレスの取得。
	 * @return IPアドレス。
	 */
	public String getRemoteAddr() {
		return remote_addr;
	}

	/**
	 * リクエストされたURIの取得。クエリも含めたフルパスです。
	 * @return URI。
	 */
	public URI getURI() {
		return uri;
	}

	/**
	 * リクエストされたURIの取得。コンテキストパスを含まない、相対パスです。
	 * @return URI。
	 */
	public URI getRelativeURI() {
		return sub_uri;
	}

	/**
	 * クッキーの取得。
	 * @return 全てのクッキー。
	 */
	public Cookie[] getCookie() {
		return in_cookie;
	}

	/**
	 * 指定クッキーの取得。
	 * @param name クッキーの名称。
	 * @return 名称が一致するクッキー。無ければ null。
	 */
	public Cookie getCookie( String name ) {
		if ( name == null )	return null;
		for ( int  i = 0; i < in_cookie.length; i++ ) {
			if ( in_cookie[i].getName().equals( name ) )	return in_cookie[i];
		}
		return null;
	}

	private void setRequestItem( String k, String v, File fb, String mime, boolean f ) {
		RequestItem	item = item_map.get( k );
		if ( item == null ) {
			item = new RequestItem( k );
			item_map.put( k, item );
		}
		item.value.add( v );
		item.file.add( fb );
		item.mime.add( mime );
		item.file_flg.add( f );
	}

	/**
	 * リクエストパラメーターの設定。ファイル付き。<br>
	 * 既に同名のパラメーター名が存在する場合はそこへ追加され、
	 * 複数の値を持つようになります。
	 * @param k パラメーター名。
	 * @param v パラメーター値。
	 * @param f ファイル。
	 * @param type ファイルのコンテントタイプ。
	 */
	public void addItem( String k, String v, File f, String type ) {
		setRequestItem( k, v, f, type, true );
	}

	/**
	 * リクエストパラメーターの設定。ファイル無し。<br>
	 * 既に同名のパラメーター名が存在する場合はそこへ追加され、
	 * 複数の値を持つようになります。
	 * @param k パラメーター名。
	 * @param v パラメーター値。
	 */
	public void addItem( String k, String v ) {
		setRequestItem( k, v, null, null, false );
	}

	/**
	 * パラメーターの削除。
	 * @param k パラメーター名。
	 */
	public void remove( String k ) {
		item_map.remove( k );
	}

	/**
	 * メソッドの取得。
	 * @return メソッド。
	 */
	public Method getMethod() {
		return method;
	}

	/**
	 * セッションの取得。
	 * @return セッション。セッションが発生していない場合はnull。
	 */
	public HttpSession getSession() {
		return session;
	}

	/**
	 * セッションの再作成。
	 * 既存セッションを破棄し、新しくセッションを開始します。
	 * Paraseleneが管理用に持つセッション情報は、Page#output
	 * からリターンした後に再設定されます。
	 * 履歴等、必要な情報はmakeNewSessionする前に取得して下さい。
	 * @return 新しいセッション。
	 */
	public HttpSession makeNewSession() {
		if ( supervisor != null ) {
			session = supervisor.makeNewSession( session );
		}
		return session;
	}

	/**
	 * 履歴キーの問い合わせ。
	 * 指定された name 属性を持つウィンドウ(フレーム)に割り当てられた
	 * 画面遷移履歴キーを返します。<br>
	 * 画面遷移履歴キーは正数です。
	 * @param name ウィンドウ(フレーム)の name 属性。
	 * @return 画面遷移履歴キー。name が未登録であれば -1。
	 */
	public int getHistoryKey( String name ) {
		HistorySet	set = getHistorySet();
		if ( set == null )	return -1;
		return set.getKey( name );
	}

	/**
	 * 履歴の取得。
	 * @param key 画面遷移履歴キー。
	 * @return 履歴。
	 */
	public History getHistory( int key ) {
		HistorySet	set = getHistorySet();
		if ( set == null )	return null;
		return set.get( key );
	}

	/**
	 * 履歴セットの取得。
	 * @return 履歴セット。
	 */
	public HistorySet getHistorySet() {
		HttpSession	session = getSession();
		SessionData	data = null;
		if ( session != null ) {
			data = (SessionData)session.getAttribute( Supervisor.SESSION_DATA );
		}
		if ( data == null )	data = Supervisor.getSessionData( getSupervisor() );
		if ( data == null )	return null;
		return data.hist;
	}

	/**
	 * 履歴の取得。
	 * @return 履歴。セッションが発生していない場合はnull。
	 */
	public History getHistory() {
		return getHistory( SandBox.getCurrentPage().getHistoryKey() );
	}

	/**
	 * リクエスト項目の取得。
	 * @param key リクエスト項目名。
	 * @return リクエスト項目。
	 */
	public RequestItem getItem( String key ) {
		if ( key == null )	return null;
		return item_map.get( key );
	}

	/**
	 * リクエスト項目名の列挙。
	 * @return 項目名の配列。
	 */
	public String[] getAllKey() {
		int	cnt = getItemCount();
		String[]	ret = new String[cnt];
		int	i = 0;
		for ( String k: item_map.keySet() ) {
			ret[i] = k;
			i++;
		}
		return ret;
	}

	/**
	 * リクエスト項目数の取得。
	 * @return リクエスト項目数。
	 */
	public int getItemCount() {
		return item_map.size();
	}

	/**
	 * リクエストクエリーの有無。
	 * @return true:クエリーがある、false:クエリーがない。
	 */
	public boolean isExistRequestItem() {
		return item_map.size() > 0;
	}

	String makeCacheKey() {
		if ( getMethod() != Method.GET )	return null;
		String[]	key = getAllKey();
		int	code = 0;
		for ( int i = 0; i < key.length; i++ ) {
			code += key.hashCode();
			RequestItem	item = getItem( key[i] );
			String[]	val = item.getAllValue();
			for ( int j = 0; j < val.length; j++ ) {
				code += val[j].hashCode();
			}
		}
		code ^= getRelativeURI().hashCode();
		return Integer.toString( code, Character.MAX_RADIX );
	}

	/**
	 * リクエスト情報の文字列化。
	 */
	public String toString() {
		StringBuilder	buf = new StringBuilder();
		String[]	header = getHeaderNames();
		buf = buf.append( getMethod().toString() );
		buf = buf.append( " " );
		buf = buf.append( getURI() );
		buf = buf.append( "\n" );
		for ( int i = 0; i < header.length; i++ ) {
			String[]	val = 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" );
			}
		}
		return buf.toString();
	}
}

