/*
 * 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.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
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.system.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.system.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="https://docs.oracle.com/cd/F19136_01/adxdk/introduction-to-XDK.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 ) を持っているため、
 * 通常そのﾒｿｯﾄﾞを使用します。
 *
 * 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
 *   EXEC_SQL は、『;』で複数SQLを実行できます。
 *   これに、属性 exists="0" があれば、最初のSQLを実行し、結果が 0 の場合のみ、
 *   以下のSQLを実行します。
 *
 *       &lt;EXEC_SQL exists="0"&gt;
 *           select count(*) from user_tables where table_name=upper('BONUS');
 *           CREATE TABLE BONUS ( ････ )
 *       &lt;/EXEC_SQL&gt;
 *
 *   exists="0" があるため、1行目を実行後、結果が一致した場合(=0)は、CREATE TABLE文を実行します。
 *   exists="1" を指定した場合は、(!=0)と同じで、0以外という意味になります。
 *   値の判定は、検索処理後の1行目1列目の値で判定します。
 *
 * 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";
	/** このﾊﾝﾄﾞﾗで取り扱える EXEC_SQLﾀｸﾞの属性 {@value} */
	public static final	String EXEC_EXISTS	= "exists";		// 8.1.0.3 (2022/01/21)

	/** このﾊﾝﾄﾞﾗで取り扱えるﾀｸﾞ名 {@value} */
	public static final	String MERGE_SQL	= "MERGE_SQL";

	/** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え。 */
	private Map<String,String>	defaultMap;
	private TagElementListener listener	;
	private TagElement		element		;
	private String			key			;
	private boolean			bodyIn		;
	private int				level		;

	private final StringBuilder	body = new StringBuilder( BUFFER_MIDDLE );			// 6.4.2.1 (2016/02/05) PMD refactoring.

	/**
	 * ﾃﾞﾌｫﾙﾄｺﾝｽﾄﾗｸﾀｰ
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public HybsXMLHandler() { super(); }		// これも、自動的に呼ばれるが、空のﾒｿｯﾄﾞを作成すると警告されるので、明示的にしておきます。

	/**
	 * ﾊﾟｰｽ処理を行います。
	 * 通常のﾊﾟｰｽ処理の簡易ﾒｿｯﾄﾞになっています。
	 *
	 * @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( final SAXException ex ) {
				if( ! "END".equals( ex.getMessage() ) ) {
					// 6.4.2.1 (2016/02/05) PMD refactoring.
					final String errMsg = "XMLパースエラー key=" + key + CR
								+ "element=" + element + CR
								+ ex.getMessage() + CR
								+ body.toString();
					throw new OgRuntimeException( errMsg,ex );
				}
			}
		}
		catch( final ParserConfigurationException ex1 ) {
			final String errMsg = "SAXParser のコンフィグレーションが構築できません。"
						+ "key=" + key + CR + ex1.getMessage();
			throw new OgRuntimeException( errMsg,ex1 );
		}
		catch( final SAXException ex2 ) {
			final String errMsg = "SAXParser が構築できません。"
						+ "key=" + key + CR + ex2.getMessage();
			throw new OgRuntimeException( errMsg,ex2 );
		}
		catch( final IOException ex3 ) {
			final String errMsg = "InputSource の読み取り時にエラーが発生しました。"
						+ "key=" + key + CR + ex3.getMessage();
			throw new OgRuntimeException( 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 ｲﾍﾞﾝﾄの前に、
	 * 要素のｺﾝﾃﾝﾂ全部が順番に報告されます。
	 * ここでは、ﾀｸﾞがﾚﾍﾞﾙ３以上の場合は、上位ﾀｸﾞの内容として取り扱います。よって、
	 * ﾀｸﾞに名前空間が定義されている場合、その属性は削除します。
	 *
	 * @og.rev 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
	 *
	 * @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 );
			// 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
			element.put( EXEC_EXISTS,attributes.getValue( EXEC_EXISTS ) );
		}
		else if( MERGE_SQL.equals( qname ) ) {
			element = new TagElement( MERGE_SQL );
		}

		if( level <= 2 ) {
			key = qname;
			body.setLength(0);		// StringBuilder の初期化
		}
		else {
			// ﾚﾍﾞﾙ3 以上のﾀｸﾞは上位ﾀｸﾞの内容として扱います。
			// 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( ' ' )
						.append( attr ).append( "=\"" )
						.append( attributes.getValue(i) ).append( '"' );
				}
			}
			body.append( '>' );
		}

		bodyIn = false;		// 入れ子状のﾀｸﾞのBODY部の有無
		level ++ ;
	}

	/**
	 * 要素の終了通知を受け取ります。
	 * ｲﾝﾀﾌｪｰｽ ContentHandler 内の endElement ﾒｿｯﾄﾞをｵｰﾊﾞｰﾗｲﾄﾞしています。
	 * SAX ﾊﾟｰｻは、XML 文書内の各要素の終わりにこのﾒｿｯﾄﾞを呼び出します。
	 * 各 endElement ｲﾍﾞﾝﾄには対応する startElement ｲﾍﾞﾝﾄがあります。
	 * これは、要素が空である場合も変わりません。
	 *
	 * @og.rev 6.4.3.2 (2016/02/19) findBugs. element は、ｺﾝｽﾄﾗｸﾀで初期化されません。
	 * @og.rev 6.9.9.0 (2018/08/20) body の最後の処理の修正。
	 *
	 * @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;
		}
		// 6.4.3.2 (2016/02/19) findBugs. element は、ｺﾝｽﾄﾗｸﾀで初期化されません。
		else if( EXEC_SQL.equals( qname ) && element != null ) {
			element.setBody( body.toString().trim() );
			if( listener != null ) {
				listener.actionExecSQL( element );
			}
			element = null;
		}
		// 6.4.3.2 (2016/02/19) findBugs. element は、ｺﾝｽﾄﾗｸﾀで初期化されません。
		else if( MERGE_SQL.equals( qname ) && element != null ) {
			element.setBody( body.toString().trim() );
			if( listener != null ) {
				listener.actionMergeSQL( element );
			}
			element = null;
		}
		else if( level <= 2 && 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 {
				// 6.9.9.0 (2018/08/20) body の最後の処理の修正。
//				body.insert( body.length()-1, " /" );		// ﾀｸﾞの最後を " />" とする。
				final int len = body.length();
				if( len > 0 && body.charAt( len-1 ) == '>' ) {
					body.insert( len-1, " /" );				// ﾀｸﾞの最後を " />" とする。
				}
			}
		}
	}
}
