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

import java.io.*;
import java.net.*;
import java.util.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import paraselene.*;
import paraselene.mockup.*;
import paraselene.tag.*;
import paraselene.supervisor.*;

class URIEcho implements URIResolver {
	public String resolve( String src ) {
		System.out.print( "URI: " );
		System.out.println( src );
		return "#";
	}
	public boolean isParamURIName( String name ) {
		return false;
	}
}

/**
 * 動的ページ。
 * 既存のHTMLファイル、またはHTMLを示す文字列をパースし、Pageインスタンスを動的に
 * 生成するためのクラスです。完全なHTML(またはXHTML)を使用して下さい。<br>
 * このインスタンスがサーブレットから直接呼ばれる事はありません。
 * 通常のモックアップHTML由来のPageインスタンスと
 * 組み合わせて({@link paraselene.tag.Tag#include}等)使用します。<br>
 * 上記理由により、PageIDを持ちませんし、履歴管理もされません。<br>
 * またHTMLパース処理を伴うため、通常のモックアップHTML由来のPage生成に
 * 比べ、大幅に処理コストがかかります。その点に留意して使用して下さい。<br>
 * name属性値が付いたタグは{@link paraselene.Page#getTag}等でアクセス可能です。
 */
public class DynamicPage extends Page {
	private static final long serialVersionUID = 2L;
	private static String[] simple = new String[]{
		"area", "base", "basefont", "bgsound", "br", "col", "frame", "hr", "img",
		"input", "isindex", "keygen", "link", "meta", "nextid", "param", "spacer",
		"wbr"
	};
	private static HashMap<String, String>	simple_map = new HashMap<String, String>();

	static {
		for ( int i = 0; i < simple.length; i++ ) {
			simple_map.put( simple[i], simple[i] );
		}
	}

	/**
	 * 閉じタグの有無。
	 * @param name タグ名。
	 * @return true:単一タグ、false:閉じタグあり。
	 */
	public static boolean isSimpleTag( String name ) {
		name = name.toLowerCase( Locale.ENGLISH );
		if ( simple_map.get( name ) == null )	return false;
		return true;
	}

	private Page	page = null;
	private URIResolver	resolve = null;
	private GrantTagProvider[]	provider = null;

	/**
	 * コンストラクタ。以下のメソッドの戻り値は次のようになります。
	 * <UL>
	 * <LI>getContentType nullを返す。
	 * <LI>getCharset UTF-8を返す。
	 * </UL>
	 * この動作を変更したい場合は、
	 * このクラスの派生クラスを作成し、上記メソッドをオーバーライドして下さい。
	 * @param ur URI解決。
	 * null を指定した場合、以下の動作を行う URIResolver を生成します。<ul>
	 * <li>isParamURIName は常に false を返す。
	 * <li>resolve は引数の文字列をそのまま返す。
	 * </ul>
	 * @param gtp 派生クラス置換用。不要な場合は null を指定して下さい。
	 * 配列番号の若い方から優先的にクラス解決されます。<br>
	 * また、ここで指定したクラスに加え{@link paraselene.ui.UI}が解析クラスとして
	 * 追加されます。
	 */
	public DynamicPage( URIResolver ur, GrantTagProvider[] gtp ) {
		if ( ur == null ) {
			resolve = new URIResolver() {
				public boolean isParamURIName(String s){return false;}
				public String resolve(String s){return s;}
			};
		}
		else {
			resolve = ur;
		}
		if ( gtp == null ) {
			provider = new GrantTagProvider[]{ new paraselene.ui.UI() };
		}
		else {
			provider = new GrantTagProvider[gtp.length + 1];
			provider[0] = new paraselene.ui.UI();
			for ( int i = 0; i < gtp.length; i++ ) {
				provider[i + 1] = gtp[i];
			}
		}
	}

	/**
	 * コンストラクタ。以下のメソッドの戻り値を、引数のページより取得します。
	 * <UL>
	 * <LI>getContentType
	 * <LI>getCharset
	 * </UL>
	 * @param ur URI解決。
	 * null を指定した場合、以下の動作を行う URIResolver を生成します。<ul>
	 * <li>isParamURIName は常に false を返す。
	 * <li>resolve は引数の文字列をそのまま返す。
	 * </ul>
	 * @param p 参照ページ。
	 * @param gtp 派生クラス置換用。不要な場合は null を指定して下さい。
	 * 配列番号の若い方から優先的にクラス解決されます。<br>
	 * また、ここで指定したクラスに加え{@link paraselene.ui.UI}が解析クラスとして
	 * 追加されます。
	 */
	public DynamicPage( URIResolver ur, Page p, GrantTagProvider[] gtp ) {
		this( ur, gtp );
		page = p;
	}

	/**
	 * コンストラクタ。下記のコンストラクタ(
	 * {@link #DynamicPage(paraselene.dyna.URIResolver, paraselene.dyna.GrantTagProvider[])}
	 * )と等価です。<br>
	 * new DynamicPage( null, null );
	 */
	public DynamicPage() {
		this( null, null );
	}

	/**
	 * コンストラクタ。下記のコンストラクタ(
	 * {@link #DynamicPage(paraselene.dyna.URIResolver, paraselene.Page, paraselene.dyna.GrantTagProvider[])}
	 * )と等価です。<br>
	 * new DynamicPage( null, p, null );
	 * @param p 参照ページ。
	 */
	public DynamicPage( Page p ) {
		this( null, p, null );
	}

	private static final String	START_XML = "<?xml";
	private static final String START_DOCTYPE = "<!DOCTYPE";
	private static final String START_HTML = "<html";
	private static final String XML_ENCODE = "encoding";
	private String xml_encode = null;

	private void setXmlEncode( String str ) {
		xml_encode = "UTF-8";
		int	idx = str.indexOf( XML_ENCODE );
		if ( idx < 0 )	return;
		str = str.substring( idx + XML_ENCODE.length() );
		idx = str.indexOf( '"' );
		if ( idx < 0 )	return;
		str = str.substring( idx + 1 );
		idx = str.indexOf( '"' );
		if ( idx < 0 )	return;
		xml_encode = str.substring( 0, idx );
	}

	/**
	 * XHTMLであるか検査を行う。
	 * @param in 入力ストリーム。
	 * @return true:XHTML、false:HTML。
	 */
	private boolean isXHTML( InputStream in ) throws IOException {
		BufferedReader	r = new BufferedReader( new InputStreamReader( in ) );
		boolean	xml = false;
		StringBuilder	doc = null;
		StringBuilder	xml_enc = null;
		while( true ) {
			String	line = r.readLine();
			if ( line == null )	break;
			line = line.trim() + " ";
			if ( line.isEmpty() )	continue;
			int	xml_loc = line.indexOf( START_XML );
			if ( xml_loc >= 0 ) {
				xml = true;
				xml_enc = new StringBuilder();
			}
			if ( xml_enc != null ) {
				String	sub = line;
				if ( xml_loc > 0 ) {
					sub = sub.substring( xml_loc );
				}
				int	end = sub.indexOf( '>' );
				if ( end < 0 ) {
					xml_enc = xml_enc.append( sub );
				}
				if ( end >= 0 ) {
					xml_enc = xml_enc.append( sub.substring( 0, end + 1 ) );
					setXmlEncode( xml_enc.toString() );
					xml_enc = null;
				}
			}

			int start = line.indexOf( START_DOCTYPE );
			if ( start >= 0 ) {
				doc = new StringBuilder();
				xml_enc = null;
			}
			if ( doc != null ) {
				if ( start > 0 ) {
					line = line.substring( start );
				}
				int	end = line.indexOf( '>' );
				if ( end < 0 ) {
					doc = doc.append( line );
					continue;
				}
				doc = doc.append( line.substring( 0, end + 1 ) );
				break;
			}
			if ( line.toLowerCase( Locale.ENGLISH ).indexOf( START_HTML ) >= 0 )	break;
		}
		r.close();
		setDoctype( xml, doc == null?	null: doc.toString() );
		return xml;
	}

	private static final String	JAVA_UNICODE = "UTF8";

	/**
	 * 構築。
	 * @param str HTML(XHTML)を表す文字列。
	 */
	public void create( String str ) throws DynamicPageException {
		try {
			if ( isXHTML( new InStream( str ) ) ) {
				createXML( new InStream( str ) );
			}
			else {
				createHTML( new InStream( str ), JAVA_UNICODE );
			}
		}
		catch( DynamicPageException dpe ) { throw dpe; }
		catch( Exception e ) { throw new DynamicPageException( e ); }
	}

	/**
	 * 構築。
	 * @param f HTML(XHTML)ファイル。
	 * @param enc ファイルの文字コード。
	 * 内容がXHTMLであった場合無視されます。
	 * 予測できない場合は、null を指定して下さい。
	 */
	public void create( File f, String enc ) throws DynamicPageException {
		try {
			if ( isXHTML( new InStream( f ) ) ) {
				createXML( new InStream( f ) );
			}
			else {
				createHTML( new InStream( f ), enc );
			}
		}
		catch( DynamicPageException dpe ) { throw dpe; }
		catch( Exception e ) { throw new DynamicPageException( e ); }
	}

	private String getEncoding( String type ) {
		if ( type == null )	return null;
		String[]	part = type.split( ";" );
		for ( int i = 0; i < part.length; i++ ) {
			String[]	sub = part[i].split( "=" );
			for ( int j = 0; j < sub.length; j++ ) {
				sub[j] = sub[j].trim();
				if ( "charset".equalsIgnoreCase( sub[j] ) ) {
					if ( j < (sub.length - 1) ) {
						return sub[j + 1].trim();
					}
				}
			}
		}
		return null;
	}

	/**
	 * 構築。
	 * @param url HTML(XHTML)ファイルを示すURL。
	 * @param method 取得メソッド。
	 * @param enc ファイルの文字コード。
	 * 内容がXHTMLであった場合無視されます。
	 * 予測できない場合は、null を指定して下さい。
	 */
	public void create( URL url, RequestParameter.Method method, String enc ) throws DynamicPageException {
		InputStream	in = null;
		try {
			HttpURLConnection	con = (HttpURLConnection)url.openConnection();
			con.setInstanceFollowRedirects( true );
			con.setRequestMethod( method == RequestParameter.Method.GET?	"GET": "POST" );
			con.connect();
			int	retcd = con.getResponseCode();
			if ( (retcd / 100) != 2 )	{
				StringBuilder	buf = new StringBuilder( Integer.toString( retcd ) );
				buf = buf.append( " " );
				buf = buf.append( con.getResponseMessage() );
				throw new DynamicPageException( buf.toString() );
			}
			if ( enc == null ) {
				enc = getEncoding( con.getContentType() );
			}
			in = new BufferedInputStream( con.getInputStream() );
			ByteArrayOutputStream	buf = new ByteArrayOutputStream();
			byte[]	data = new byte[4096];
			while ( true ) {
				int	len = in.read( data );
				if ( len <= 0 )	break;
				buf.write( data, 0, len );
			}
			in.close();

			data = buf.toByteArray();
			if ( isXHTML( new InStream( data ) ) ) {
				createXML( new InStream( data ) );
			}
			else {
				createHTML( new InStream( data ), enc );
			}
		}
		catch( DynamicPageException dpe ) { throw dpe; }
		catch( Exception e ) { throw new DynamicPageException( e ); }
		finally {
			try {
				if ( in != null )	in.close();
			}
			catch( Exception e ){}
		}
	}

	/**
	 * XML構築。
	 * @param in 入力元。処理終了時にclose()されます。
	 * @exception DynamicPageException 処理中に例外が発生した。
	 */
	private void createXML( InStream in ) throws DynamicPageException {
		try {
			createHTML( in, null );
		}
		catch( Exception e ) {
			throw new DynamicPageException( e );
		}
		finally {
			try {
				in.close();
			}
			catch( Exception ee ) {}
		}
	}

	String	parse_encode = null;

	/**
	 * HTML解析時文字コードの取得。
	 * 解析した際に使用した文字コードを返します。
	 * @return 文字コード名。ただし、文字コードを判別出来なかった時はnull。
	 */
	public String getParsingCharset() {
		return xml_encode == null?	parse_encode: xml_encode;
	}

	/**
	 * HTML構築。
	 * @param is 入力元。処理終了時にclose()されます。
	 * @param enc エンコード指定。
	 * @exception DynamicPageException 処理中に例外が発生した。
	 */
	private void createHTML( InStream is, String enc ) throws DynamicPageException {
		try {
			org.cyberneko.html.parsers.SAXParser	parser = new org.cyberneko.html.parsers.SAXParser();
			if ( enc != null ) {
				parser.setProperty(
					"http://cyberneko.org/html/properties/default-encoding", enc
				);
			}
			InputSource	in = new InputSource();
			in.setByteStream( is );
			parser.setContentHandler( new Handler( this ) );
			parser.parse( in );
			if ( enc == null ) {
				enc = getEncoding( parse_encode );
				if ( enc != null ) {
					is.reload();
					clear();
					createHTML( is, enc );
				}
			}
			parse_encode = enc;
			addVersionMeta( Version.getTitle(), new Date().toString() );
		}
		catch( Exception e ) {
			throw new DynamicPageException( e );
		}
		finally {
			try {
				is.close();
			}
			catch( Exception ee ) {}
		}
	}

	/**
	 * インスタンスの生成。
	 * @return 新しいインスタンス。
	 */
	protected DynamicPage makePage() {
		return new DynamicPage( resolve, this, provider );
	}

	/**
	 * タグの生成。
	 * @param name タグ名。
	 * @param attr 属性。
	 * @return タグ。
	 * @exception Exception タグの生成に失敗した場合。
	 */
	protected Tag makeTag( String name, ArrayList<Attribute> attr ) throws Exception {
		return TagMap.makeTag( name, attr, getCharset(), resolve, provider );
	}

	/**
	 * テキストの生成。
	 * @param str テキスト。
	 * @return テキスト。
	 */
	protected Text makeText( String str ) {
		return new Text( str );
	}

	public void init() {}
	public String getContentType() {
		if ( page != null )	return page.getContentType();
		return null;
	}
	public String getCharset() {
		if ( page != null )	return page.getCharset();
		return "UTF-8";
	}
	/**
	 * ページのID。常にnullを返します。
	 * @return null。
	 */
	public PageID getID() { return null; }
	public boolean isHistoryClear() { return false; }
	public boolean isAllowHistoryAdd() { return false; }
	public Forward input( RequestParameter req, Forward fw ) throws PageException {
		return fw;
	}
	public Page output( Page from, RequestParameter req ) throws PageException {
		return this;
	}
	public String getAliasURI() { return null; }
	public int getUploadMaxBytes() { return -1; }
	public boolean isCheckRepeatSameRequest() { return false; }

	public AjaxSupport getAjaxSupport() { return AjaxSupport.NO; }

	public void firstOutput( RequestParameter req ) throws PageException{}

	/**
	 * name属性値検証。<br>
	 * define で指定された name 属性値が page に登場しているか検査します。<br>
	 * define で指定したうち、検査対象から外したいものがあれば、exclude に
	 * 除外する name 属性値を指定する事ができます。<br>
	 * 登場しない name は戻り値で返されます。
	 * 全ての指定 name が存在すれば null を返します。<br>
	 * define にクラス指定があれば、クラスが一致しなければエラーとなります。
	 * @param page 検証対象ページ。
	 * @param define 検証する name 属性値。
	 * @param exclude 検証から除外する name 属性値。
	 * @return エラーがあった name 属性値。全て登場していれば null。
	 */
	public static NameDefine[] inspectName( Page page, NameDefine[] define, String ... exclude ) {
		if ( define == null )	return null;
		ArrayList<NameDefine>	err = new ArrayList<NameDefine>();
		for ( int i = 0; i < define.length; i++ ) {
			define[i].setError( "OK." );
			boolean	flag = false;
			String	name = define[i].getName();
			for ( int j = 0; j < exclude.length; j++ ) {
				if ( name.equals( exclude[j] ) ) {
					define[i].setError( "It is excluded." );
					flag = true;
					break;
				}
			}
			if ( flag )	continue;

			Tag[]	tag = page.getAllTag( name );
			if ( tag == null || tag.length == 0 ) {
				define[i].setError( "Name is undefined." );
				err.add( define[i] );
				continue;
			}
			Class<?>	cls = define[i].getDefineClass();
			if ( cls == null )	continue;
			flag = false;
			for ( int j = 0; j < tag.length; j++ ) {
				if ( cls.equals( tag[j].getClass() ) ) {
					flag = true;
					break;
				}
			}
			if ( !flag ) {
				define[i].setError( "Class is different." );
				err.add( define[i] );
			}
		}
		if ( err.size() == 0 )	return null;
		return err.toArray( new NameDefine[0] );
	}

}

