/*
 * Paraselene
 * Copyright (c) 2009  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>
 * スケルトン生成ツールと同様にDTDのキャッシュ機能が働きます。<br>
 * XHTMLは厳密に解釈するため例外が発生する可能性があります。<br>
 * 一方HTMLはルーズな解釈です。このため、意図したデザインと異なる解釈を行う
 * 可能性があります。<br>
 * 用意したHTMLファイルが例外を出さずにパース可能かを検証するために、
 * {@link paraselene.dyna.DynamicPage#main}メソッドを持っています。
 * リリース前検証に利用して下さい。<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解決。
	 * @param gtp 派生クラス置換用。不要な場合は null を指定して下さい。
	 * 配列番号の若い方から優先的にクラス解決されます。
	 */
	public DynamicPage( URIResolver ur, GrantTagProvider[] gtp ) {
		resolve = ur;
		provider = gtp;
	}

	/**
	 * コンストラクタ。以下のメソッドの戻り値を、引数のページより取得します。
	 * <UL>
	 * <LI>getContentType
	 * <LI>getCharset
	 * </UL>
	 * @param ur URI解決。
	 * @param p 参照ページ。
	 * @param gtp 派生クラス置換用。不要な場合は null を指定して下さい。
	 * 配列番号の若い方から優先的にクラス解決されます。
	 */
	public DynamicPage( URIResolver ur, Page p, GrantTagProvider[] gtp ) {
		this( ur, gtp );
		page = p;
	}

	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	UNICODE = "unicode";

	/**
	 * 構築。
	 * @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 ), 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; }

	/**
	 * 解析のテスト。
	 * 本番環境にHTMLファイルを配置してシステムに解析させる前に、解析が可能か？
	 * どのようなURIが含まれるか？どのようなname属性値を持つか？
	 * を検査する事ができます。<br>
	 * 起動パラメータは、<br>
	 * 検査するHTMLファイル名<br>
	 * ファイルの文字コード<br>
	 * の２つを渡して下さい。
	 * @param argv 検査するHTMLファイル名 ファイルの文字コード
	 */
	public static void main( String[] argv ) throws Exception {
		System.out.println( Version.getTitle() );
		System.out.println( Version.getCopyRight() );
		if ( argv.length != 2 ) {
			System.out.println( "HTMLファイル名 ファイルの文字コード" );
			System.out.println( "を指定して下さい。" );
			System.exit( 0 );
		}
		DynamicPage	page = new DynamicPage( new URIEcho(), null );
		page.create( new File( argv[0] ), argv[1] );
		String[]	name = page.getDefineNameIndex();
		for ( int i = 0; i < name.length; i++ ) {
			System.out.print( "name属性: " );
			System.out.println( name[i] );
			Tag[]	tag = page.getAllTag( name[i] );
			for ( int j = 0; j < tag.length; j++ ) {
				System.out.print( " -> " );
				System.out.println( tag[j].getClass().getName() );
			}
		}
		System.out.println( "正常に解析可能です" );
		System.exit( 0 );
	}

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

	public void firstOutput( RequestParameter req ) throws PageException{}
}

