/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.xml;

import org.opengion.fukurou.util.Closer ;
import org.opengion.fukurou.util.HybsEntry ;
import org.opengion.fukurou.util.FileUtil ;
import org.opengion.fukurou.util.StringUtil ;
import org.opengion.fukurou.util.LogWriter;

import java.io.Reader;
import java.io.Writer;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.stream.StreamResult;

/**
 * XML 入力ファイルに、XSL 入力ファイルを適用して、 XSLT変換を行います。
 * 結果は、XML 出力ファイルにセーブします。
 * 各ファイルの代わりに、Writer,Reader を設定することも可能です。
 *
 * このパーサーでは、内部で実行中の入力ファイル情報を パラメータとして設定できます。
 * useFileInfo( true ) とセットすると、以下の４項目が内部的にセットされます。
 * ただし、この設定が可能なのは、XML 入力ファイルに、Reader ではなく、ファイル名を
 * 渡した場合のみです。ストリームの場合は、各種情報は取れません。
 *
 * 入力ファイル(inXMLのフルパス)     : FILEPATH  (例: G:\webapps\gf\jsp\DOC10\query.jsp)
 * 入力親フォルダ(inXMLの親フォルダ) : ADDRESS   (例: DOC10)
 * 入力ファイル(inXMLのファイル名)   : FILENAME  (例: query.jsp)
 * 入力ファイル(inXMLの更新日付  )   : MODIFIED  (例: yyyyMMddHHmmss形式)
 *
 * xsl ファイルでは、パラメータ は、xsl:param で宣言し、xsl:value-of で取り出します。
 * &lt;xsl:param name="ADDRESS" select="" /&gt; と宣言しておき、必要な箇所で
 * &lt;xsl:value-of select="$ADDRESS"     /&gt; とすれば、取得できます。
 *
 *      String inXSTL  = "inXSLfile.xsl" ;   // 入力ＸＳＬファイル
 *      String outFile = "outXMLfile.xml" ;  // 出力ＸＭＬファイル
 *      String inXML   = "inXMLfile.xml" ;   // 入力ＸＭＬファイル
 *
 *      XSLT xslt = new XSLT();
 *      xslt.setXslFile( inXSTL );
 *      xslt.setOutFile( outFile,false );
 *
 *      xslt.transform( inXML );
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class XSLT {
	private static final String CR = System.getProperty("line.separator") ;
	/** 初期 ENCODE 名	{@value}	*/
	public static final String ENCODE = "UTF-8" ;

	private Transformer transformer = null;

	private String		encode		= ENCODE;
	private String		xmlFile		= null;
	private String		xslFile		= null;
	private String		outFile		= null;
	private Reader		xslReader	= null;
	private Writer		outWriter	= null;
	private HybsEntry[] paramEntry	= null;
	private boolean		isFileInfo	= false;
	private boolean		isErrClose	= true;
	private boolean		isErrXmlIn	= false;	// useErrXmlIn ⇒ isErrXmlIn 変更
	private boolean		isInclude	= true;		// 4.2.3.0 (2008/05/26)
	private StreamResult result = null;

	private DateFormat	formatter	= null;

	private String		jspIncludeData	= null;

	/**
	 * 入力XSLファイルを、指定します。
	 *
	 * @param	file	入力XSLファイル
	 * @see #setXslFile( Reader )
	 */
	public void setXslFile( final String file ) {
		xslFile = file;
		setXslFile( FileUtil.getBufferedReader( new File( xslFile ),encode ) );
	}

	/**
	 * 入力XSLリーダーを、指定します。
	 *
	 * @param	reader	入力XSLリーダー
	 * @see #setXslFile( String )
	 */
	public void setXslFile( final Reader reader ) {
		transformer = null;
		xslReader = reader;
	}

	/**
	 * 結果XML ファイル名と、そのオープン方法を指定します。
	 * 結果XML ファイルを、追記する(append=true)か新規作成する(append=false)か指定します。
	 * なお、結果XML ファイル(outFile) を指定しない(=null)か、特別な名称 "System.out"
	 * 文字列を渡すと、標準出力に 結果を出力します。
	 *
	 * @param	file	出力ファイル名(null または、"System.out" 文字列時は、標準出力)
	 * @param append [true]追記する/false:新規作成する]
	 */
	public void setOutFile( final String file,final boolean append ) {
		outFile = file ;
		setOutFile( FileUtil.getPrintWriter( new File( outFile ),encode,append ) );
	}

	/**
	 * 結果XML データを出力する、Writer を指定します。
	 * ファイル、標準出力、JSPWriter など、必要に応じて Writer を作成してください。
	 * 標準出力(System.out)の場合は、NonClosePrintWriter クラスなどの非close()処理系を、
	 * JSPWriterの場合は、NonFlushPrintWriter クラスなどの非flush()、close()処理系を、
	 * 使用してください。
	 *
	 * @param	writer	出力するWriter
	 */
	public void setOutFile( final Writer writer ) {
		Closer.ioClose( outWriter );
		outWriter = writer ;
		result = new StreamResult( outWriter );
	}

	/**
	 * 結果XML ライターに、指定のデータを書き出します。
	 *
	 * @param	outData	書き出すデータ
	 */
	public void setOutData( final String outData ) {
		if( outData != null && outData.length() > 0 ) {
			try {
				outWriter.write( outData );
				outWriter.write( CR );
			}
			catch( IOException ex ) {
				String errMsg = "ライターにデータ登録を失敗しました。" + CR
								+ ex.getMessage() ;
				close();
				throw new RuntimeException( errMsg,ex );
			}
		}
	}

	/**
	 * XML ファイルをXSLT変換します。
	 * XML 入力ファイルに、XSL 入力ファイルを適用して、 XSLT変換を行います。
	 * 結果は、XML ファイルにセーブします。
	 * 拡張子が『.jsp』の場合、jsp:directive.include 処理を行います。
	 *
	 * @og.rev 4.0.0.2 (2007/12/10) 拡張子が『.jsp』の場合、jsp:directive.include 処理を行います。
	 *
	 * @param	file	入力XMLファイル
	 * @see #transform( Reader )
	 */
	public void transform( final String file ) {
		transform( file, isInclude );
	}

	/**
	 * XML ファイルをXSLT変換します。
	 * XML 入力ファイルに、XSL 入力ファイルを適用して、 XSLT変換を行います。
	 * 結果は、XML ファイルにセーブします。
	 * 引数の isJspInclude によって、jsp:directive.include 処理を行うかどうか判断します。
	 *
	 * @og.rev 4.2.3.0 (2008/05/26) jsp:directive.include 処理の実施可否を引数指定します。
	 * @og.rev 5.2.1.0 (2010/10/01) JspIncludeReader#getString の第３引数を廃止
	 *
	 * @param	file	入力XMLファイル
	 * @param	isJspInclude	jsp:directive.include 処理を行うかどうか。
	 * @see #transform( Reader )
	 */
	public void transform( final String file, final boolean isJspInclude ) {
		xmlFile = file;

		if( xmlFile.endsWith( ".jsp" ) && isJspInclude ) {
//		if( xmlFile.endsWith( ".jsp" ) ) {
//			transform( new JspIncludeReader().getReader( new File( xmlFile ),encode ) );
//			jspIncludeData = new JspIncludeReader().getString( new File( xmlFile ),encode,false );
			jspIncludeData = new JspIncludeReader().getString( new File( xmlFile ),encode );		// 5.2.1.0 (2010/10/01)
			transform( new StringReader( jspIncludeData ) );
		}
		else {
			transform( FileUtil.getBufferedReader( new File( xmlFile ),encode ) );
		}
	}

	/**
	 * XML ファイルをXSLT変換します。
	 * XML 入力リーダーに、XSL 入力リーダーを適用して、 XSLT変換を行います。
	 * 結果は、XML ライターに書き出します。
	 * この処理の終了後に、入力XML リーダー は、close() されます。
	 *
	 * @param	xmlReader	入力XML リーダー
	 * @see #transform( String )
	 */
	public void transform( final Reader xmlReader ) {
		HybsEntry[] entry = null;

		try {
			if( transformer == null ) {
				init();
			}
			else {
				transformer.reset();
			}

			// 入力XMLファイルのファイル情報を設定します。
			if( isFileInfo && xmlFile != null ) {
				entry = getXmlParameter( xmlFile );
				parameterSet( transformer,entry );
			}
			xmlFile = null ;

			// 入力XMLリーダーからStreamSourceを作る
			StreamSource data = new StreamSource( xmlReader );

			transformer.transform( data,result );
		}
		catch( TransformerException ex ) {
			String errMsg = "XML-XSLT 変換に失敗しました。" + CR
								+ ex.getMessage() ;
			if( isErrXmlIn ) { setOutData( toXmlRow( entry, ex ) ); }

			if( isErrClose ) { close(); }
			throw new RuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( xmlReader );
		}
	}

	/**
	 * Transformer オブジェクトに対して、Parameter を設定します。
	 *
	 * 指定されたパラメーターキーは、xsl ファイルでは、xsl:param で宣言し、
	 * xsl:value-of で取り出します。
	 * <xsl:param name="ADDRESS" select="" /> と宣言しておき、必要な箇所で
	 * <xsl:value-of select="$ADDRESS"     /> とすれば、取得できます。
	 *
	 * @param	entry	HybsEntry配列
	 */
	public void setParamEntry( final HybsEntry[] entry ) {
		if( entry != null && entry.length > 0 ) {
			paramEntry = new HybsEntry[entry.length];
			System.arraycopy( entry,0,paramEntry,0,entry.length );
		}
	}

	/**
	 * transform 処理中にエラーが発生した場合に、出力ファイルを閉じるかどうかを指定します。
	 *
	 * 処理途中でエラーが発生した場合に、そこで処理を中断するか、それとも、
	 * 無視して、さらに処理を進めるかを指定することが可能です。
	 * 継続して処理を進めたい場合は、出力ファイルを閉じないため、false を
	 * 設定します。ただし、エラー時には、RuntimeException は throw されます。
	 * 初期値は、true(閉じる)です。
	 *
	 * @param	flag	エラー時クローズ [true:閉じる/false:閉じない]
	 */
	public void errClose( final boolean flag ) {
		isErrClose = flag ;
	}

	/**
	 * transform 処理中エラーを、出力ファイルに、XML形式でエラーを追記するかどうかを指定します。
	 *
	 * 処理途中でエラーが発生した場合に、ログだけではなく、結果XMLファイルに、
	 * エラー内容や、エラーファイルなどを埋め込むと、XMLファイルとしてDB登録や、
	 * その他集計等に使えます。
	 * 今は、GE70 スキーマ形式のファイルしか作成できません。
	 * これは、#errClose( boolean ) メソッドと共に使用すると効果的です。
	 * つまり、errClose = false; にして、エラー時でも出力ファイルを閉じずに、
	 * 処理を続ける事で、エラーメッセージもXMLファイルとして蓄積できます。
	 * 初期値は、false(使用しない)です。
	 *
	 * @param	flag	エラー時XML形式 [false:使用しない/true:使用する]
	 */
	public void useErrXmlIn( final boolean flag ) {
		isErrXmlIn = flag ;
	}

	/**
	 * jsp:directive.include 発見時に、そのファイルを INCLUDE するかを指定するかどうかを指定します(初期値:true:使用する)
	 *
	 * 引数の処理対象ファイル(transformの引数ファイル)が、『.jsp』の場合、
	 * jsp:directive.include 発見時に、そのファイルを INCLUDE するかを指定するか
	 * どうかを指定します。
	 * インクルードされたファイルとあわせて、正規のXML にならないと、パーサー
	 * エラーが発生します。
	 * JSPソース解析を行うには、INCLUDE ファイルも考慮しないと正確な結果を
	 * 得られませんが、INCLUDE 先のファイルまで合わせる必要があるため、
	 * 場合によっては、INCLUDEファイルを無視しなければならないケースがあります。
	 * 初期値は、true(使用する)です。
	 *
	 * @param	flag	エラー時XML形式 [false:使用しない/true:使用する]
	 */
	public void jspInclude( final boolean flag ) {
		isInclude = flag ;
	}

	/**
	 * 入力XSLファイルのストリームを閉じます。
	 *
	 */
	public void close() {
		Closer.ioClose( outWriter );
	}

	/**
	 * XML ファイルをXSLT変換します。
	 * XML 入力ファイルに、XSL 入力ファイルを適用して、 XSLT変換を行います。
	 * 結果は、XML ファイルにセーブします。
	 * なお、結果XML ファイル(outFile) に、特別な名称 "System.out" 文字列を渡すと、
	 * 標準出力に 結果を出力します。
	 */
	private void init() {
		try {
			// xsl属性からStreamSourceを作る
			StreamSource style = new StreamSource( xslReader );

			// Transformerを作り、XMLを変換する
			TransformerFactory tFactory = TransformerFactory.newInstance();
			transformer = tFactory.newTransformer( style );

			parameterSet( transformer,paramEntry );
		}
		catch( TransformerConfigurationException ex ) {
			String errMsg = xslFile + "ファイルの XSLT 解析に失敗しました。" + CR
								+ ex.getMessage() ;
			throw new RuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( xslReader );
			xslReader = null;
		}
	}

	/**
	 * 実行中の入力ファイル名などの属性情報を パラメータとして設定するかどうかを指定します。
	 *
	 * このパーサーでは、内部で実行中の入力ファイル情報を パラメータとして設定できます。
	 * useFileInfo( true ) とセットすると、以下の４項目が内部的にセットされます。
	 *
	 * 入力ファイル(inXMLのフルパス)     : FILEPATH  (例: G:\webapps\gf\jsp\DOC10\query.jsp)
	 * 入力親フォルダ(inXMLの親フォルダ) : ADDRESS   (例: DOC10)
	 * 入力ファイル(inXMLのファイル名)   : FILENAME  (例: query.jsp)
	 * 入力ファイル(inXMLの更新日付  )   : MODIFIED  (例: yyyyMMddHHmmss形式)
	 *
	 * @og.rev 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加。
	 *
	 * xsl ファイルでは、xsl:param で宣言し、xsl:value-of で取り出します。
	 * &lt;xsl:param name="ADDRESS" select="" /&gt; と宣言しておき、必要な箇所で
	 * &lt;xsl:value-of select="$ADDRESS"     /&gt; とすれば、取得できます。
	 *
	 * 初期値は、false(セットしない) です。
	 *
	 * @param	flag	セットする:true/セットしない:false
	 */
	public void useFileInfo( final boolean flag ) {
		isFileInfo = flag;
		if( isFileInfo ) {
			formatter = new SimpleDateFormat( "yyyyMMddHHmmss",Locale.JAPAN );
		}
	}

	/**
	 * ファイル名指定で XML,XSL,OUTファイルを指定する場合のエンコードを指定します。
	 *
	 * 初期値は、UTF-8 です。
	 *
	 * @param	encode	エンコード
	 */
	public void useEncode( final String encode ) {
		this.encode = encode;
	}

	/**
	 * 実行中の入力ファイル名などの属性情報を パラメータとして取得します。
	 *
	 * 入力ファイル(inXMLのフルパス)     : FILEPATH  (例: G:\webapps\gf\jsp\DOC10\query.jsp)
	 * 入力ファイル(inXMLのファイル名)   : FILENAME  (例: query.jsp)
	 * 入力親フォルダ(inXMLの親フォルダ) : ADDRESS   (例: DOC10)
	 * 入力ファイル(inXMLの更新日付  )   : MODIFIED  (例: yyyyMMddHHmmss形式)
	 *
	 * @param	xmlIn	XML入力ファイル
	 *
	 * @return	HybsEntry配列
	 */
	private HybsEntry[] getXmlParameter( final String xmlIn ) {
		HybsEntry[] entry = new HybsEntry[4] ;

		entry[0] = new HybsEntry( "FILEPATH" , xmlIn) ;

		File xmlFile = new File( xmlIn );
		entry[1] = new HybsEntry( "FILENAME" , xmlFile.getName()) ;

		File parentFile = xmlFile.getParentFile() ;
		if( parentFile != null ) {
			entry[2] = new HybsEntry( "ADDRESS"  , parentFile.getName()) ;
		}
		else {
			entry[2] = new HybsEntry( "ADDRESS"  , "" ) ;
		}

		String lastDate = formatter.format( new Date( xmlFile.lastModified() ) ) ;
		entry[3] = new HybsEntry( "MODIFIED" , lastDate ) ;

		return entry ;
	}

	/**
	 * Transformer オブジェクト に、パラメータを設定します。
	 *
	 * 指定されたパラメーターキーは、xsl ファイルでは、xsl:param で宣言し、
	 * xsl:value-of で取り出します。
	 * <xsl:param name="ADDRESS" select="" /> と宣言しておき、必要な箇所で
	 * <xsl:value-of select="$ADDRESS"     /> とすれば、取得できます。
	 *
	 * @param former Transformerオブジェクト
	 * @param entry  パラメータ配列
	 */
	private void parameterSet( final Transformer former,final HybsEntry[] entry ) {
		if( entry != null ) {
			int size   = entry.length;
			for( int i=0; i<size; i++ ) {
				String key = entry[i].getKey() ;
				String val = entry[i].getValue();
				former.setParameter( key , val );
			}
		}
	}

	/**
	 * このオブジェクトの内部文字列表現を返します。
	 *
	 * 接続URL + "," + 接続ユーザー + " (" + 作成日付 + ")" です。
	 *
	 * @return 内部文字列表現
	 */
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();

		buf.append( "XSL File:" ).append( xslFile ).append( CR );
		buf.append( "XML File:" ).append( xmlFile ).append( CR );
		buf.append( "OUT File:" ).append( outFile ).append( CR );

		return buf.toString() ;
	}

	/**
	 * エラー情報の内部XML文字列表現を返します。
	 *
	 * エラー時の情報も、XML化して保存する為の簡易処理。
	 * ここでは、XMLスキーマは、固定で、GF70 の形式になります。
	 *
	 * @og.rev 4.2.3.0 (2008/05/26) エラー発生時のXMLファイルを追加します。
	 * @og.rev 5.2.1.0 (2010/10/01) XML形式を変更します。(TEXT⇒TEXT_DATA)
	 *
	 * @param	entry	HybsEntry配列
	 * @param	ex		エラー情報
	 *
	 * @return XMLの部分文字列
	 */
	private String toXmlRow( final HybsEntry[] entry,final TransformerException ex ) {
		StringBuilder buf = new StringBuilder();

		buf.append( "<ROW>" ).append( CR );
		if( paramEntry != null ) {
			for( int i=0; i<paramEntry.length; i++ ) {
				String key = paramEntry[i].getKey() ;
				String val = paramEntry[i].getValue();
				buf.append( "  <" ).append( key ).append( ">" );
				buf.append( val );
				buf.append( "</" ).append( key ).append( ">" );
				buf.append( CR );
			}
		}

		if( entry != null ) {
			for( int i=0; i<entry.length; i++ ) {
				String key = entry[i].getKey() ;
				String val = entry[i].getValue();
				buf.append( "  <" ).append( key ).append( ">" );
				buf.append( val );
				buf.append( "</" ).append( key ).append( ">" );
				buf.append( CR );
			}
		}

		buf.append( "  <TAGNAME />" ).append( CR );
		buf.append( "  <MSGCD>XML_ERROR</MSGCD>" ).append( CR );
		buf.append( "  <MSGTXT>XML-XSLT 変換に失敗しました。</MSGTXT>" ).append( CR );

		String errMsg = StringUtil.htmlFilter( ex.getMessage() );
		int indx = errMsg.lastIndexOf( "Exception:" );
		if( indx >= 0 ) {
			errMsg = errMsg.substring( indx + "Exception:".length() );
		}
		buf.append( "  <TEXT_DATA>" ).append( errMsg ).append( CR );		// 5.2.1.0 (2010/10/01)
		buf.append( " Location:" ).append( ex.getLocationAsString() ).append( CR );

		// 4.2.3.0 (2008/05/26)
	//	if( jspIncludeData != null ) {
	//		buf.append( StringUtil.htmlFilter( jspIncludeData ) );
	//	}

		buf.append( "</TEXT_DATA>" ).append( CR );		// 5.2.1.0 (2010/10/01)
		buf.append( "</ROW>" ).append( CR );

		return buf.toString() ;

/*
	   <ROW>
		 <SYSTEM_ID>  </SYSTEM_ID>
		 <ADDRESS  >  </ADDRESS>
		 <FILENAME >  </FILENAME>
		 <FILEPATH >  </FILEPATH>
		 <MODIFIED >  </MODIFIED>
		 <TAGNAME  >  </TAGNAME>
		 <MSGCD    >  </MSGCD>
		 <MSGTXT   >  </MSGTXT>
		 <TEXT_DATA>  </TEXT_DATA>
	   </ROW>
*/
	}

	/**
	 * テスト用のメインメソッド
	 *
	 * java XSLT in_xml in_xsl out_xml
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) throws IOException {
		if( args.length != 3 ) {
			LogWriter.log( "Usage: java XSLT in_xml in_xsl out_xml" );
			LogWriter.log( "  XML 入力ファイルに、XSL 入力ファイルを適用して、" );
			LogWriter.log( "  XSLT変換を行います。" );
			LogWriter.log( "  結果は、XML ファイルにセーブします。" );
			LogWriter.log( "  out_xml に System.out を指定すると標準出力に出力します。" );
			return ;
		}

		XSLT xslt = new XSLT();
		xslt.setXslFile( args[1] );
		xslt.setOutFile( args[2],false );
		xslt.transform( args[0] );
		xslt.close();
	}
}
