/*
 * 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.opengion.fukurou.system.Closer;
import org.opengion.fukurou.system.LogWriter;
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

import java.io.Reader;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Arrays;
import java.util.Locale;

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ParameterMetaData;
import java.sql.SQLException;

/**
 * このクラスは、オラクル XDKの oracle.xml.sql.dml.OracleXMLSave クラスと
 * ほぼ同様の目的で使用できるクラスです。
 * 拡張XDK形式のXMLファイルを読み込み、データベースに INSERT します。
 *
 * 拡張XDK形式の元となる オラクル XDK(Oracle XML Developer's Kit)については、以下の
 * リンクを参照願います。
 * <a href="http://otn.oracle.co.jp/software/tech/xml/xdk/index.html" target="_blank" >
 * XDK(Oracle XML Developer's Kit)</a>
 *
 * このクラスでは、MAP を登録する[ setDefaultMap( Map ) ]ことにより、
 * XMLファイルに存在しないカラムを初期値として設定することが可能になります。
 * 例えば、登録日や、登録者、または、テンプレートより各システムID毎に
 * 登録するなどです。
 * 同様に、読み取った XMLファイルの情報を書き換える機能[ setAfterMap( Map ) ]メソッド
 * により、カラムの値の置き換えも可能です。
 *
 * 拡張XDK形式の元となる オラクル XDK(Oracle XML Developer's Kit)については、以下の
 * リンクを参照願います。
 * <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='XX' where YYYYY
 *       &lt;/EXEC_SQL&gt;
 *   &lt;ROWSET&gt;
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class HybsXMLSave implements TagElementListener {

	private String		tableName		;
	//	private String[]	keyColumns		;	//6.3.9.0 (2015/11/06) 現時点で使われていないため、一旦取り消しておきます。
	private Connection	connection		;
	private PreparedStatement insPstmt	;		// INSERT用の PreparedStatement
	private PreparedStatement updPstmt	;		// UPDATE用の PreparedStatement
	private ParameterMetaData insMeta	;
	private ParameterMetaData updMeta	;
	private int insCnt		;
	private int updCnt		;
	private int delCnt		;
	private int ddlCnt		;					// 5.6.7.0 (2013/07/27) DDL文のカウンター
	/** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え。  */
	private Map<String,String>	defaultMap	;
	/** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え。  */
	private Map<String,String>	afterMap	;
	private List<String>		updClms		;
	private String[]			insClms		;
	private String				lastSQL		;	// 5.6.6.1 (2013/07/12) デバッグ用。最後に使用したSQL文

	private final boolean useParamMetaData	;	// 4.0.0.0 (2007/09/25)

	// UPDATE時の [XXX] を取り出します。\w は、単語構成文字: [a-zA-Z_0-9]と同じ
	private static final Pattern PATTERN = Pattern.compile( "\\[\\w*\\]" );		// 6.4.1.1 (2016/01/16) pattern → PATTERN refactoring

	// 5.6.9.2 (2013/10/18) EXEC_SQL のエラーを無視するかどうかを指定できます。
	private boolean isExecErr	= true;					// 6.0.2.5 (2014/10/31) true は、エラー時に Exception を発行します。

	/**
	 * コネクションを指定して、オブジェクトを構築します。
	 * テーブル名は、拡張XDK形式のROWSETタグのtableName属性に
	 * 記述しておく必要があります。
	 *
	 * @param	conn	データベース接続
	 */
	public HybsXMLSave( final Connection conn ) {
		this( conn,null );
	}

	/**
	 * コネクションとテーブル名を指定して、オブジェクトを構築します。
	 * ここで指定するテーブル名は、デフォルトテーブルという扱いです。
	 * 拡張XDK形式のROWSETタグのtableName属性にテーブル名が記述されている場合は、
	 * そちらが優先されます。
	 *
	 * @og.rev 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加。
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を このクラスで直接取得する。(PostgreSQL対応)
	 *
	 * @param	conn	データベース接続
	 * @param	table	テーブル名(ROWSETタグのtable属性が未設定時に使用)
	 */
	public HybsXMLSave( final Connection conn,final String table ) {
		connection = conn;
		tableName  = table;
		useParamMetaData = useParameterMetaData( connection );		// 5.3.8.0 (2011/08/01)
	}

	/**
	 * EXEC_SQL のエラー時に Exception を発行するかどうかを指定できます(初期値:true)。
	 * true を指定すると、エラー時には、 RuntimeException を throw します。
	 * false にすると、標準エラー出力にのみ、出力します。
	 * このフラグは、EXEC_SQL のみ有効です。それ以外のタブの処理では、エラーが発生すると
	 * その時点で、Exception を発行して、処理を終了します。
	 * 初期値は、true(Exception を発行する) です。
	 *
	 * @og.rev 5.6.9.2 (2013/10/18) 新規追加
	 *
	 * @param flag true:Exception を発行する/false:標準エラー出力に出力する
	 */
	public void onExecErrException( final boolean flag ) {
		isExecErr = flag;						// 6.0.2.5 (2014/10/31) refactoring
	}

	/**
	 * &lt;ROWSET&gt; タグの一番最初に呼び出されます。
	 * ROWSET の属性である、table 属性と、dbid 属性 を、TagElement の
	 * get メソッドで取得できます。
	 * 取得時のキーは、それぞれ、"TABLE" と "DBID" です。
	 *
	 * @param tag タグエレメント
	 * @see org.opengion.fukurou.xml.TagElement
	 * @see HybsXMLHandler#setTagElementListener( TagElementListener )
	 */
	@Override
	public void actionInit( final TagElement tag ) {
		final String table = tag.get( "tableName" );
		if( table != null ) { tableName = table; }
	}

	/**
	 * &lt;ROW&gt; タグの endElement 処理毎に呼び出されます。
	 * この Listener をセットすることにより、行データを取得都度、
	 * TagElement オブジェクトを作成し、このメソッドが呼び出されます。
	 *
	 * @og.rev 4.0.0.0 (2007/05/09) ParameterMetaData を使用したパラメータ設定追加。
	 * @og.rev 4.0.0.0 (2007/09/25) isOracle から useParamMetaData に変更
	 * @og.rev 4.3.7.0 (2009/06/01) HSQLDB対応
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData  setNull 対応(PostgreSQL対応)
	 * @og.rev 5.6.6.1 (2013/07/12) lastSQL 対応。デバッグ用に、最後に使用したSQL文を残します。
	 *
	 * @param tag タグエレメント
	 * @see org.opengion.fukurou.xml.TagElement
	 * @see HybsXMLHandler#setTagElementListener( TagElementListener )
	 */
	@Override
	public void actionRow( final TagElement tag ) {
		tag.setAfterMap( afterMap );

		String[] vals = null;			// 5.6.6.1 (2013/07/12) デバッグ用
		try {
			// 更新SQL(MERGE_SQLタグ)が存在する場合の処理
			int tempCnt = 0;
			if( updPstmt != null ) {
				vals = tag.getValues( updClms );						// 5.6.6.1 (2013/07/12) デバッグ用
				for( int j=0; j<vals.length; j++ ) {
					// 4.3.7.0 (2009/06/01) HSQLDB対応。空文字の場合nullに置換え
					if( vals[j] != null && vals[j].isEmpty() ){
						vals[j] = null;
					}

					// 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加
					if( useParamMetaData ) {
						final int type = updMeta.getParameterType( j+1 );
						// 5.3.8.0 (2011/08/01) setNull 対応
						final String val = vals[j];
						if( val == null || val.isEmpty() ) {
							updPstmt.setNull( j+1, type );
						}
						else {
							updPstmt.setObject( j+1, val, type );
						}
					}
					else {
						updPstmt.setObject( j+1,vals[j] );
					}

				}
				tempCnt = updPstmt.executeUpdate();
				if( tempCnt > 1 ) {
					final String errMsg = "Update キーが重複しています。"
							+ "TABLE=[" + tableName + "] ROW=["
							+ tag.getRowNo() + "]" + CR
							+ " SQL=[" + lastSQL + "]" + CR				// 5.6.6.1 (2013/07/12) デバッグ用
							+ tag.toString() + CR
							+ Arrays.toString( vals ) + CR ;			// 5.6.6.1 (2013/07/12) デバッグ用
					throw new OgRuntimeException( errMsg );
				}
				updCnt += tempCnt;
			}
			// 更新が 0件の場合は、INSERT処理を行います。
			if( tempCnt == 0 ) {
				// 初回INSERT時のタグより、DB登録SQL文を構築します。
				if( insPstmt == null ) {
					insClms = tag.getKeys();
					lastSQL    = insertSQL( insClms,tableName );		// 5.6.6.1 (2013/07/12) デバッグ用
					insPstmt = connection.prepareStatement( lastSQL );
					// 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加
					if( useParamMetaData ) { insMeta = insPstmt.getParameterMetaData(); }
				}
				vals = tag.getValues( insClms );						// 5.6.6.1 (2013/07/12) デバッグ用
				for( int j=0; j<vals.length; j++ ) {
					// 4.3.7.0 (2009/06/01) HSQLDB対応。空文字の場合nullに置換え
					if( vals[j] != null && vals[j].isEmpty() ){
						vals[j] = null;
					}

					// 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加
					if( useParamMetaData ) {
						final int type = insMeta.getParameterType( j+1 );
						// 5.3.8.0 (2011/08/01) setNull 対応
						final String val = vals[j];
						if( val == null || val.isEmpty() ) {
							insPstmt.setNull( j+1, type );
						}
						else {
							insPstmt.setObject( j+1, val, type );
						}
					}
					else {
						insPstmt.setObject( j+1,vals[j] );
					}
				}
				insCnt += insPstmt.executeUpdate();
			}
		}
		catch( SQLException ex ) {
			final String errMsg = "DB登録エラーが発生しました。"
						+ "TABLE=[" + tableName + "] ROW=["
						+ tag.getRowNo() + "]" + CR
						+ " SQL=[" + lastSQL + "]" + CR				// 5.6.6.1 (2013/07/12) デバッグ用
						+ tag.toString() + CR
						+ Arrays.toString( vals ) + CR				// 5.6.6.1 (2013/07/12) デバッグ用
						+ ex.getMessage() + ":" + ex.getSQLState() + CR ;
			throw new OgRuntimeException( errMsg,ex );
		}
	}

	/**
	 * &lt;EXEC_SQL&gt; タグの endElement 処理毎に呼び出されます。
	 * getBody メソッドを使用して、このタグのBODY部の文字列を取得します。
	 * この Listener をセットすることにより、EXEC_SQL データを取得都度、
	 * TagElement オブジェクトを作成し、このメソッドが呼び出されます。
	 * EXEC_SQL タグでは、delete文やupdate文など、特殊な前処理や後処理用の SQLと
	 * DDL（データ定義言語：Data Definition Language）の処理なども記述できます。
	 * ここでは簡易的に、何か実行された場合は、delete 処理と考え、削除カウントを加算し、
	 * 0件で帰ってきた場合に、DDLが実行されたと考え、DDLカウントを＋１します。
	 * ただし、0件 delete も考えられるため、SQL文の先頭文字によるチェックは入れておきます。
	 *
	 * @og.rev 5.6.6.1 (2013/07/12) lastSQL 対応。デバッグ用に、最後に使用したSQL文を残します。
	 * @og.rev 5.6.7.0 (2013/07/27) DDL（データ定義言語：Data Definition Language）の処理件数追加
	 * @og.rev 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行するかどうかを指定
	 * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
	 *
	 * @param tag タグエレメント
	 * @see org.opengion.fukurou.xml.TagElement
	 * @see HybsXMLHandler#setTagElementListener( TagElementListener )
	 */
	@Override
	public void actionExecSQL( final TagElement tag ) {
//		Statement execSQL = null ;
//		try {
//			lastSQL = tag.getBody();		// 5.6.6.1 (2013/07/12) デバッグ用
//			execSQL = connection.createStatement();
		// 6.4.2.1 (2016/02/05) try-with-resources 文
		lastSQL = tag.getBody();			// 5.6.6.1 (2013/07/12) デバッグ用		6.4.2.1 (2016/02/05) try の前に出します。
		try( final Statement execSQL = connection.createStatement() ) {
			lastSQL = tag.getBody();		// 5.6.6.1 (2013/07/12) デバッグ用

			// 5.6.7.0 (2013/07/27) DDL（データ定義言語：Data Definition Language）の処理件数追加
			final int cnt = execSQL.executeUpdate( lastSQL ) ;
			if( cnt > 0 ) { delCnt += cnt; }				// 件数が返れば、DDLでないため、削除数を加算
			else {
				final String sql = lastSQL.trim().toUpperCase( Locale.JAPAN );
				if( !sql.startsWith( "DELETE" ) && !sql.startsWith( "INSERT" ) && !sql.startsWith( "UPDATE" ) ) {
					ddlCnt ++ ;
				}
			}
		}
		catch( SQLException ex ) {
			final String errMsg = "DB登録エラーが発生しました。"
						+ "TABLE=[" + tableName + "] ROW=["
						+ tag.getRowNo() + "]" + CR
						+ " SQL=[" + lastSQL + "]" + CR				// 5.6.6.1 (2013/07/12) デバッグ用
						+ tag.toString() + CR
						+ ex.getMessage() + ":" + ex.getSQLState() + CR ;

			// 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行するかどうかを指定
			if( isExecErr ) {										// 6.0.2.5 (2014/10/31) refactoring
				throw new OgRuntimeException( errMsg,ex );
			}
			else {
				System.err.println( errMsg );
			}
		}
//		finally {
//			Closer.stmtClose( execSQL );
//		}
	}

	/**
	 * &lt;MERGE_SQL&gt; タグの endElement 処理時に呼び出されます。
	 * getBody メソッドを使用して、このタグのBODY部の文字列を取得します。
	 * MERGE_SQLタグは、マージ処理したいデータ部よりも上位に記述しておく
	 * 必要がありますが、中間部に複数回記述しても構いません。
	 * このタグが現れるまでは、INSERT のみ実行されます。このタグ以降は、
	 * 一旦 UPDATE し、結果が ０件の場合は、INSERTする流れになります。
	 * 完全に INSERT のみであるデータを前半に、UPDATE/INSERTを行う
	 * データを後半に、その間に、MERGE_SQL タグを入れることで、無意味な
	 * UPDATE を避けることが可能です。
	 * この Listener をセットすることにより、MERGE_SQL データを取得都度、
	 * TagElement オブジェクトを作成し、このメソッドが呼び出されます。
	 *
	 * @og.rev 4.0.0.0 (2007/05/09) ParameterMetaData を使用したパラメータ設定追加。
	 * @og.rev 4.0.0.0 (2007/09/25) isOracle から useParamMetaData に変更
	 * @og.rev 5.6.6.1 (2013/07/12) lastSQL 対応。デバッグ用に、最後に使用したSQL文を残します。
	 *
	 * @param tag タグエレメント
	 * @see org.opengion.fukurou.xml.TagElement
	 * @see HybsXMLHandler#setTagElementListener( TagElementListener )
	 */
	@Override
	public void actionMergeSQL( final TagElement tag ) {
		if( updPstmt != null ) {
			final String errMsg = "MERGE_SQLタグが、複数回記述されています。"
						+ "TABLE=[" + tableName + "] ROW=["
						+ tag.getRowNo() + "]" + CR
						+ " SQL=[" + lastSQL + "]" + CR				// 5.6.6.1 (2013/07/12) デバッグ用
						+ tag.toString() + CR;
			throw new OgRuntimeException( errMsg );
		}

		final String orgSql = tag.getBody();
		final Matcher matcher = PATTERN.matcher( orgSql );
		updClms = new ArrayList<>();
		while( matcher.find() ) {
			// ここでは、[XXX]にマッチする為、前後の[]を取り除きます。
			updClms.add( orgSql.substring( matcher.start()+1,matcher.end()-1 ) );
		}
		lastSQL = matcher.replaceAll( "?" );		// 5.6.6.1 (2013/07/12) デバッグ用

		try {
			updPstmt = connection.prepareStatement( lastSQL );
			// 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加
			if( useParamMetaData ) { updMeta = updPstmt.getParameterMetaData(); }
		}
		catch( SQLException ex ) {
			final String errMsg = "Statement作成時にエラーが発生しました。"
						+ "TABLE=[" + tableName + "] ROW=["
						+ tag.getRowNo() + "]" + CR
						+ " SQL=[" + lastSQL + "]" + CR				// 5.6.6.1 (2013/07/12) デバッグ用
						+ tag.toString() + CR
						+ ex.getMessage() + ":" + ex.getSQLState() + CR ;
			throw new OgRuntimeException( errMsg,ex );
		}
	}

	//	/**
	//	 * UPDATE,DELETE を行う場合の WHERE 条件になるキー配列
	//	 * このキーの AND 条件でカラムを特定し、UPDATE,DELETE などの処理を
	//	 * 行います。
	//	 *
	//	 * @og.rev 6.3.9.0 (2015/11/06) 現時点で使われていないため、一旦取り消しておきます。
	//	 *
	//	 * @param	keyCols	WHERE条件になるキー配列(可変長引数)
	//	 */
	//	 public void setKeyColumns( final String... keyCols ) {
	//		keyColumns = new String[keyCols.length];
	//		System.arraycopy( keyCols,0,keyColumns,0,keyColumns.length );
	//	 }

	/**
	 * XMLファイルを読み取る前に指定するカラムと値のペア(マップ)情報をセットします。
	 *
	 * このカラムと値のペアのマップは、オブジェクト構築前に設定される為、
	 * XMLファイルにキーが存在している場合は、値が書き変わります。(XML優先)
	 * XMLファイルにキーが存在していない場合は、ここで指定するMapの値が
	 * 初期設定値として使用されます。
	 * ここで指定する Map に LinkedHashMap を使用する場合、カラム順も
	 * 指定することが出来ます。
	 *
	 * @param	map	初期設定するカラムデータマップ
	 * @see #setAfterMap( Map )
	 */
	public void setDefaultMap( final Map<String,String> map ) { defaultMap = map; }

	/**
	 * XMLファイルを読み取った後で指定するカラムと値のペア(マップ)情報をセットします。
	 *
	 * このカラムと値のペアのマップは、オブジェクト構築後に設定される為、
	 * XMLファイルのキーの存在に関係なく、Mapのキーと値が使用されます。(Map優先)
	 * null を設定した場合は、なにも処理されません。
	 *
	 * @param map	後設定するカラムデータマップ
	 * @see #setDefaultMap( Map )
	 */
	public void setAfterMap( final Map<String,String> map ) { afterMap = map; }

	/**
	 * データベースに追加処理(INSERT)を行います。
	 *
	 * 先に指定されたコネクションを用いて、指定のテーブルに INSERT します。
	 * 引数には、XMLファイルを指定したリーダーをセットします。
	 * コネクションは、終了後、コミットされます。(close されません。)
	 * リーダーのクローズは、ここでは行っていません。
	 *
	 * @og.rev 5.1.1.0 (2009/11/11) insMeta , updMeta のクリア(気休め)
	 *
	 * @param	reader	XMLファイルを指定するリーダー
	 */
	public void insertXML( final Reader reader ) {
		try {
			final HybsXMLHandler handler = new HybsXMLHandler();
			handler.setTagElementListener( this );
			handler.setDefaultMap( defaultMap );

			handler.parse( reader );
		}
		finally {
			Closer.stmtClose( insPstmt );
			Closer.stmtClose( updPstmt );
			insPstmt = null;
			updPstmt = null;
			insMeta	= null;		// 5.1.1.0 (2009/11/11)
			updMeta	= null;		// 5.1.1.0 (2009/11/11)
		}
	}

	/**
	 * インサート用のSQL文を作成します。
	 *
	 * @og.rev 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。
	 *
	 * @param	columns	インサートするカラム名
	 * @param	tableName	インサートするテーブル名
	 *
	 * @return	インサート用のSQL文
	 * @og.rtnNotNull
	 */
	private String insertSQL( final String[] columns,final String tableName ) {
		if( tableName == null ) {
			final String errMsg = "tableName がセットされていません。" + CR
						+ "tableName は、コンストラクタで指定するか、ROWSETのtableName属性で"
						+ "指定しておく必要があります" + CR ;
			throw new OgRuntimeException( errMsg );
		}

		// 6.0.2.5 (2014/10/31) char を append する。
		// 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。
		final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE )
			.append( "INSERT INTO " ).append( tableName )
			.append( " ( " )
			.append( String.join( "," , columns ) )	// 6.2.3.0 (2015/05/01)
			.append( " ) VALUES ( ?" );
		for( int i=1; i<columns.length; i++ ) {
			sql.append( ",?" );
		}
		sql.append( " )" );

		return sql.toString();
	}

	/**
	 * データベースに追加した件数を返します。
	 *
	 * @return 登録件数
	 */
	public int getInsertCount() { return insCnt; }

	/**
	 * データベースを更新した件数を返します。
	 * これは、拡張XDK形式で、MERGE_SQL タグを使用した場合の更新処理件数を
	 * 合計した値を返します。
	 *
	 * @return 更新件数
	 */
	public int getUpdateCount() { return updCnt; }

	/**
	 * データベースに変更(更新、削除を含む)した件数を返します。
	 * これは、拡張XDK形式で、EXEC_SQL タグを使用した場合の実行件数を合計した
	 * 値を返します。
	 * よって、更新か、追加か、削除かは、判りませんが、通常 登録前に削除する
	 * ケースで使われることから、deleteCount としています。
	 *
	 * @return 変更件数(主に、削除件数)
	 */
	public int getDeleteCount() { return delCnt; }

	/**
	 * データベースにDDL（データ定義言語：Data Definition Language）処理した件数を返します。
	 * これは、拡張XDK形式で、EXEC_SQL タグを使用した場合の実行件数を合計した
	 * 値を返します。
	 * EXEC_SQL では、登録前に削除する delete 処理も、EXEC_SQL タグを使用して実行しますが
	 * その処理と分けてカウントします。
	 *
	 * @og.rev 5.6.7.0 (2013/07/27) DDL（データ定義言語：Data Definition Language）の処理件数追加
	 *
	 * @return DDL（データ定義言語：Data Definition Language）処理した件数
	 */
	public int getDDLCount() { return ddlCnt; }

	/**
	 * 実際に登録された テーブル名を返します。
	 *
	 * テーブル名は、拡張XDK形式のROWSETタグのtableName属性に
	 * 記述しておくか、コンストラクターで引数として渡します。
	 * 両方指定された場合は、ROWSETタグのtableName属性が優先されます。
	 * ここでの返り値は、実際に使用された テーブル名です。
	 *
	 * @return 変更件数(主に、削除件数)
	 */
	public String getTableName() { return tableName; }

	/**
	 * この接続が、PreparedStatement#getParameterMetaData() を使用するかどうかを判定します。
	 * 本来は、ConnectionFactory#useParameterMetaData(String)を使うべきだが、dbid が無いため、直接取得します。
	 *
	 * ※ 6.1.0.0 (2014/12/26) で、直接取得に変更します。DBUtil 経由で取得する方が、ソースコードレベルでの
	 *    共通化になるので良いのですが、org.opengion.fukurou.db と、org.opengion.fukurou.xml パッケージが
	 *    循環参照(相互参照)になるため、どちらかを切り離す必要があります。
	 *    db パッケージ側では、DBConfig.xml の処理の関係で、org.opengion.fukurou.xml.DomParser を
	 *    使っているため、こちらの処理を、内部処理に変更することで、対応します。
	 *
	 * @og.rev 5.3.8.0 (2011/08/01) 新規作成 ( ApplicationInfo#useParameterMetaData(Connection) からコピー )
	 * @og.rev 5.6.7.0 (2013/07/27) dbProductName は、DBUtil 経由で取得する。
	 * @og.rev 6.1.0.0 (2014/12/26) dbProductName は、DBUtil 経由ではなく、直接取得する。
	 *
	 * @param   conn 接続先(コネクション)
	 *
	 * @return	使用する場合：true / その他:false
	 */
	private static boolean useParameterMetaData( final Connection conn ) {

		String dbName ;
		try {
			dbName = conn.getMetaData().getDatabaseProductName().toLowerCase( Locale.JAPAN );
		}
		catch( SQLException ex ) {
			dbName = "none";
		}

		return "PostgreSQL".equalsIgnoreCase( dbName ) ;
	}

	/**
	 * テスト用のメインメソッド
	 *
	 * Usage: java org.opengion.fukurou.xml.HybsXMLSave USER PASSWD URL TABLE FILE [ENCODE] [DRIVER]
	 *    USER    : DB接続ユーザー(GE)
	 *    PASSWD  : DB接続パスワード(GE)
	 *    URL     : DB接続JDBCドライバURL(jdbc:oracle:thin:@localhost:1521:HYBS
	 *    TABLE   : 登録するテーブルID(GE21)
	 *    FILE    : 登録するORACLE XDK 形式 XMLファイル(GE21.xml)
	 *    [ENCODE]: ファイルのエンコード 初期値:UTF-8
	 *    [DRIVER]: JDBCドライバー 初期値:oracle.jdbc.OracleDriver
	 *
	 * ※ ファイルが存在しなかった場合、FileNotFoundException を RuntimeException に変換して、throw します。
	 * ※ 指定のエンコードが存在しなかった場合、UnsupportedEncodingException を RuntimeException に変換して、throw します。
	 *
	 * @og.rev 5.1.1.0 (2009/12/01) MySQL対応 明示的に、TRANSACTION_READ_COMMITTED を指定する。
	 * @og.rev 5.6.7.0 (2013/07/27) DDL（データ定義言語：Data Definition Language）の処理件数追加
	 * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
	 *
	 * @param	args	コマンド引数配列
	 * @throws ClassNotFoundException クラスを見つけることができなかった場合。
	 * @throws SQLException データベース接続エラーが発生した場合。
	 */
	public static void main( final String[] args )
			throws ClassNotFoundException , SQLException {
		if( args.length < 5 ) {
			LogWriter.log( "Usage: java org.opengion.fukurou.xml.HybsXMLSave USER PASSWD URL TABLE FILE [ENCODE] [DRIVER]" );
			LogWriter.log( "   USER  : DB接続ユーザー(GE)" );
			LogWriter.log( "   PASSWD: DB接続パスワード(GE)" );
			LogWriter.log( "   URL   : DB接続JDBCドライバURL(jdbc:oracle:thin:@localhost:1521:HYBS)" );
			LogWriter.log( "   TABLE : 登録するテーブルID(GE21)" );
			LogWriter.log( "   FILE  : 登録するORACLE XDK 形式 XMLファイル(GE21.xml)" );
			LogWriter.log( " [ ENCODE: ファイルのエンコード 初期値:UTF-8 ]" );
			LogWriter.log( " [ DRIVER: JDBCドライバー 初期値:oracle.jdbc.OracleDriver ]" );
			return ;
		}

		final String user   = args[0] ;
		final String passwd = args[1] ;
		final String url    = args[2] ;
		final String table  = args[3] ;
		final String file   = args[4] ;
		final String encode = ( args.length == 6 ) ? args[5] : "UTF-8"  ;
		final String driver = ( args.length == 7 ) ? args[6] : "oracle.jdbc.OracleDriver"  ;

		Class.forName(driver);

//		final Connection conn = DriverManager.getConnection( url,user,passwd );
//		Reader reader = null;
		int insCnt;
		int updCnt;
		int delCnt;
		int ddlCnt;			// 5.6.7.0 (2013/07/27) DDL処理件数追加
//		try {
		// 6.4.2.1 (2016/02/05) try-with-resources 文
		try( final Connection conn = DriverManager.getConnection( url,user,passwd ) ) {
			conn.setAutoCommit( false );
			conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);  // 5.1.1.0 (2009/12/01)
			final HybsXMLSave save = new HybsXMLSave( conn,table );

			// 6.4.2.1 (2016/02/05) try-with-resources 文
			try( final Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream( file ) ,encode ) ) ) {
//				reader = new BufferedReader(new InputStreamReader(new FileInputStream( file ) ,encode ));
				save.insertXML( reader );
				insCnt = save.getInsertCount();
				updCnt = save.getUpdateCount();
				delCnt = save.getDeleteCount();
				ddlCnt = save.getDDLCount();		// 5.6.7.0 (2013/07/27) DDL処理件数追加
			}
			// FileNotFoundException , UnsupportedEncodingException
			catch( java.io.FileNotFoundException ex ) {
				final String errMsg = "ファイルが存在しません。" + ex.getMessage()
								+ CR + "Table=[" + table + "] File =[" + file + "]" ;
				throw new OgRuntimeException( errMsg,ex );
			}
			catch( java.io.UnsupportedEncodingException ex ) {
				final String errMsg = "指定のエンコードが存在しません。" + ex.getMessage()
								+ CR + "Table=[" + table + "] Encode =[" + encode + "]" ;
				throw new OgRuntimeException( errMsg,ex );
			}
			catch( java.io.IOException ex ) {
				final String errMsg = "ファイル読み込み処理でエラーが発生しました。" + ex.getMessage()
								+ CR + "Table=[" + table + "] File =[" + file + "]" ;
				throw new OgRuntimeException( errMsg,ex );
			}
			Closer.commit( conn );
		}
//		finally {
//			Closer.ioClose( reader );
//			Closer.connClose( conn );
//		}

		System.out.println( "XML File[" + file + "] Into [" + table + "] Table" );
		System.out.println( "   Insert Count : [" + insCnt + "]" );
		System.out.println( "   Update Count : [" + updCnt + "]" );
		System.out.println( "   Delete Count : [" + delCnt + "]" );
		System.out.println( "   DDL    Count : [" + ddlCnt + "]" );
	}
}
