/*
 * 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.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.ParserConfigurationException;

import java.io.Reader;
import java.io.IOException;
import java.util.Map;

import static org.opengion.fukurou.util.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.util.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * このクラスは、拡張オラクル XDK形式のXMLファイルを処理するハンドラです。
 * オラクルXDK形式のXMLとは、下記のような ROWSET をトップとする ROW の
 * 集まりで１レコードを表し、各ROWには、カラム名をキーとするXMLになっています。
 *
 *   &lt;ROWSET&gt;
 *       &lt;ROW num="1"&gt;
 *           &lt;カラム1&gt;値1&lt;/カラム1&gt;
 *             ･･･
 *           &lt;カラムn&gt;値n&lt;/カラムn&gt;
 *       &lt;/ROW&gt;
 *        ･･･
 *       &lt;ROW num="n"&gt;
 *          ･･･
 *       &lt;/ROW&gt;
 *   &lt;ROWSET&gt;
 *
 * この形式であれば、XDK(Oracle XML Developer's Kit)を利用すれば、非常に簡単に
 * データベースとXMLファイルとの交換が可能です。
 * <a href="http://otn.oracle.co.jp/software/tech/xml/xdk/index.html" target="_blank" >
 * XDK(Oracle XML Developer's Kit)</a>
 *
 * 拡張XDK形式とは、ROW 以外に、SQL処理用タグ(EXEC_SQL)を持つ XML ファイルです。
 * また、登録するテーブル(table)を ROWSETタグの属性情報として付与することができます。
 * (大文字小文字に注意)
 * これは、オラクルXDKで処理する場合、無視されますので、同様に扱うことが出来ます。
 * この、EXEC_SQL は、それそれの XMLデータをデータベースに登録する際に、
 * SQL処理を自動的に流す為の、SQL文を記載します。
 * この処理は、イベント毎に実行される為、その配置順は重要です。
 * このタグは、複数記述することも出来ますが、BODY部には、１つのSQL文のみ記述します。
 *
 *   &lt;ROWSET tableName="XX" &gt;
 *       &lt;EXEC_SQL&gt;                    最初に記載して、初期処理(データクリア等)を実行させる。
 *           delete from GEXX where YYYYY
 *       &lt;/EXEC_SQL&gt;
 *       &lt;MERGE_SQL&gt;                   このSQL文で UPDATEして、結果が０件ならINSERTを行います。
 *           update GEXX set AA=[AA] , BB=[BB] where CC=[CC]
 *       &lt;/MERGE_SQL&gt;
 *       &lt;ROW num="1"&gt;
 *           &lt;カラム1&gt;値1&lt;/カラム1&gt;
 *             ･･･
 *           &lt;カラムn&gt;値n&lt;/カラムn&gt;
 *       &lt;/ROW&gt;
 *        ･･･
 *       &lt;ROW num="n"&gt;
 *          ･･･
 *       &lt;/ROW&gt;
 *       &lt;EXEC_SQL&gt;                    最後に記載して、項目の設定(整合性登録)を行う。
 *           update GEXX set AA='XX' , BB='YY' where CC='ZZ'
 *       &lt;/EXEC_SQL&gt;
 *   &lt;ROWSET&gt;
 *
 * DefaultHandler クラスを拡張している為、通常の処理と同様に、使用できます。
 *
 *      InputSource input = new InputSource( reader );
 *      HybsXMLHandler hndler = new HybsXMLHandler();
 *
 *      SAXParserFactory f = SAXParserFactory.newInstance();
 *      SAXParser parser = f.newSAXParser();
 *      parser.parse( input,hndler );
 *
 * また、上記の処理そのものを簡略化したメソッド:parse( Reader ) を持っているため、
 * 通常そのメソッドを使用します。
 *
 * HybsXMLHandler には、TagElementListener をセットすることができます。
 * これは、ROW 毎に 内部情報を TagElement オブジェクト化し、action( TagElement )
 * が呼び出されます。この Listener を介して、１レコードずつ処理することが
 * 可能です。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class HybsXMLHandler extends DefaultHandler {

	/** このハンドラのトップタグ名 	{@value}	*/
	public static final	String ROWSET		= "ROWSET";
	/** このハンドラで取り扱える ROWSETタグの属性 	*/
	public static final	String ROWSET_TABLE = "tableName";

	/** このハンドラで取り扱えるタグ名 	{@value}	*/
	public static final	String ROW			= "ROW";
	/** このハンドラで取り扱える ROWタグの属性 	{@value}	*/
	public static final	String ROW_NUM		= "num";
	/** このハンドラで取り扱えるタグ名 	{@value}	*/
	public static final	String EXEC_SQL		= "EXEC_SQL";
	/** このハンドラで取り扱えるタグ名 	{@value}	*/
	public static final	String MERGE_SQL	= "MERGE_SQL";

	private Map<String,String>	defaultMap;
	private TagElementListener listener	;
	private TagElement		element		;
	private String			key			;
	private StringBuilder	body		;
	private boolean			bodyIn		;
	private int				level		;

	/**
	 *	パース処理を行います。
	 *	通常のパース処理の簡易メソッドになっています。
	 *
	 * @param reader パース処理用のReaderオブジェクト
	 */
	public void parse( final Reader reader ) {
		try {
			final SAXParserFactory fact = SAXParserFactory.newInstance();
			final SAXParser parser = fact.newSAXParser();

			final InputSource input = new InputSource( reader );

			try {
				parser.parse( input,this );
			}
			catch( SAXException ex ) {
				if( ! "END".equals( ex.getMessage() ) ) {
					String errMsg = "XMLパースエラー key=" + key + CR
								+ "element=" + element + CR
								+ ex.getMessage() ;
					if( body != null ) {
						errMsg = errMsg + CR + body.toString();
					}
					throw new RuntimeException( errMsg,ex );
				}
			}
		}
		catch( ParserConfigurationException ex1 ) {
			final String errMsg = "SAXParser のコンフィグレーションが構築できません。"
						+ "key=" + key + CR + ex1.getMessage();
			throw new RuntimeException( errMsg,ex1 );
		}
		catch( SAXException ex2 ) {
			final String errMsg = "SAXParser が構築できません。"
						+ "key=" + key + CR + ex2.getMessage();
			throw new RuntimeException( errMsg,ex2 );
		}
		catch( IOException ex3 ) {
			final String errMsg = "InputSource の読み取り時にエラーが発生しました。"
						+ "key=" + key + CR + ex3.getMessage();
			throw new RuntimeException( errMsg,ex3 );
		}
	}

	/**
	 * 内部に TagElementListener を登録します。
	 * これは、&lt;ROW&gt; タグの endElement 処理毎に呼び出されます。
	 * つまり、行データを取得都度、TagElement オブジェクトを作成し、
	 * この TagElementListener の action( TagElement ) メソッドを呼び出します。
	 * 何もセットしない、または、null がセットされた場合は、何もしません。
	 *
	 * @param listener TagElementListenerオブジェクト
	 */
	public void setTagElementListener( final TagElementListener listener ) {
		this.listener = listener;
	}

	/**
	 * TagElement オブジェクトを作成する時の 初期カラム/値を設定します。
	 * TagElements オブジェクトは、XMLファイルより作成する為、項目(カラム)も
	 * XMLファイルのROW属性に持っている項目と値で作成されます。
	 * このカラム名を、外部から初期設定することが可能です。
	 * その場合、ここで登録したカラム順(Mapに、LinkedHashMap を使用した場合)
	 * が保持されます。また、ROW属性に存在しないカラムがあれば、値とともに
	 * 初期値として設定しておくことが可能です。
	 * なお、ここでのMapは、直接設定していますので、ご注意ください。
	 *
	 * @param	map	初期カラムマップ
	 */
	public void setDefaultMap( final Map<String,String> map ) {
		defaultMap = map;
	}

	/**
	 * 要素内の文字データの通知を受け取ります。
	 * インタフェース ContentHandler 内の characters メソッドをオーバーライドしています。
	 * 各文字データチャンクに対して特殊なアクション (ノードまたはバッファへのデータの追加、
	 * データのファイルへの出力など) を実行することができます。
	 *
	 * @param	buffer	文字データ配列
	 * @param	start	配列内の開始位置
	 * @param	length	配列から読み取られる文字数
	 * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int )
	 */
	@Override
	public void characters( final char[] buffer, final int start, final int length ) throws SAXException {
		if( ! ROW.equals( key ) && ! ROWSET.equals( key ) && length > 0 ) {
			body.append( buffer,start,length );
			bodyIn = true;
		}
	}

	/**
	 * 要素の開始通知を受け取ります。
	 * インタフェース ContentHandler 内の startElement メソッドをオーバーライドしています。
	 * パーサは XML 文書内の各要素の前でこのメソッドを呼び出します。
	 * 各 startElement イベントには対応する endElement イベントがあります。
	 * これは、要素が空である場合も変わりません。対応する endElement イベントの前に、
	 * 要素のコンテンツ全部が順番に報告されます。
	 * ここでは、タグがレベル３以上の場合は、上位タグの内容として取り扱います。よって、
	 * タグに名前空間が定義されている場合、その属性は削除します。
	 *
	 * @param	namespace	名前空間 ＵＲＩ
	 * @param	localName	前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
	 * @param	qname		前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
	 * @param	attributes	要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
	 * @see org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
	 */
	@Override
	public void startElement(final String namespace, final String localName,
							 final String qname, final Attributes attributes) throws SAXException {
		if( ROWSET.equals( qname ) ) {
			if( listener != null ) {
				element = new TagElement( ROWSET,defaultMap );
				element.put( ROWSET_TABLE,attributes.getValue( ROWSET_TABLE ) );
				listener.actionInit( element );
			}
			element = null;
		}
		else if( ROW.equals( qname ) ) {
			element = new TagElement( ROW,defaultMap );
			final String num = attributes.getValue( ROW_NUM );
			element.setRowNo( num );
		}
		else if( EXEC_SQL.equals( qname ) ) {
			element = new TagElement( EXEC_SQL );
		}
		else if( MERGE_SQL.equals( qname ) ) {
			element = new TagElement( MERGE_SQL );
		}

		if( level <= 2 ) {
			key = qname;
			body = new StringBuilder( BUFFER_MIDDLE );
		}
		else {
			// レベル３ 以上のタグは上位タグの内容として扱います。
			// 6.0.2.5 (2014/10/31) char を append する。
			body.append( '<' ).append( qname );
			final int len = attributes.getLength();
			for( int i=0; i<len; i++ ) {
				// 名前空間の宣言は、削除しておきます。あくまでデータとして取り扱う為です。
				final String attr = attributes.getQName(i);
				if( ! attr.startsWith( "xmlns:" ) ) {
					body.append( ' ' );
					body.append( attr ).append( "=\"" );
					body.append( attributes.getValue(i) ).append( '"' );
				}
			}
			body.append( '>' );
		}

		bodyIn = false;		// 入れ子状のタグのBODY部の有無
		level ++ ;
	}

	/**
	 * 要素の終了通知を受け取ります。
	 * インタフェース ContentHandler 内の endElement メソッドをオーバーライドしています。
	 * SAX パーサは、XML 文書内の各要素の終わりにこのメソッドを呼び出します。
	 * 各 endElement イベントには対応する startElement イベントがあります。
	 * これは、要素が空である場合も変わりません。
	 *
	 * @param	namespace	名前空間 URI
	 * @param	localName	前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
	 * @param	qname	前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列
	 * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String )
	 */
	@Override
	public void endElement(final String namespace, final String localName, final String qname) throws SAXException {
		level -- ;
		if( ROW.equals( qname ) ) {
			if( listener != null ) {
				listener.actionRow( element );
			}
			element = null;
		}
		else if( EXEC_SQL.equals( qname ) ) {
			element.setBody( body.toString().trim() );
			if( listener != null ) {
				listener.actionExecSQL( element );
			}
			element = null;
		}
		else if( MERGE_SQL.equals( qname ) ) {
			element.setBody( body.toString().trim() );
			if( listener != null ) {
				listener.actionMergeSQL( element );
			}
			element = null;
		}
		else if( level <= 2 ) {
			if( element != null ) {
				element.put( key , body.toString().trim() );
			}
		}
		else {
			if( bodyIn ) {
				body.append( "</" ).append( qname ).append( '>' );		// 6.0.2.5 (2014/10/31) char を append する。
			}
			else {
				body.insert( body.length()-1, " /" );		// タグの最後を " />" とする。
			}
		}
	}
}
