/*
 * 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.hayabusa.common;

import org.opengion.fukurou.xml.HybsXMLSave;
import org.opengion.fukurou.util.Closer ;

import java.sql.Connection;
import java.sql.SQLException;

import java.io.Reader;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.File;
import java.io.UnsupportedEncodingException;

import java.util.List;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.net.URL;

/**
 * ORACLE XDK 形式のXMLファイルを読み取って、データベースに登録します。
 * 起動(実行)は、コンテキスト読み取り時の初回のみです。GE12パラメータを変更した
 * 場合は、コンテキストのリロードが必要です。
 * 登録の実行有無の判断は、ファイルの更新時刻より判断します。(useTimeStamp=true の場合)
 * これは、読み取りファイルの更新時刻が、０でない場合、読み取りを行います。
 * 読み取りが完了した場合は、更新時刻を ０ に設定します。
 * 読み取るファイルは、クラスローダーのリソースとして取得されますので、クラスパスが
 * 設定されている必要があります。また、ファイルは、拡張子が xml で、UTF-8でエンコード
 * されている必要があります。通常は、ファイル名がテーブル名と同一にしておく必要が
 * ありますが、ROWSETのtable属性にテーブル名をセットしておくことも可能です。
 * ファイルの登録順は、原則、クラスローダーの検索順に、見つかった全てのファイルを
 * 登録します。データそのものは、INSERT のみ対応していますので、原則登録順は無視されます。
 * ただし、拡張XDK 形式で、EXEC_SQL タグを使用した場合は、登録順が影響する可能性があります。
 * 例：GE12.xml GE12 テーブルに登録するXMLファイル
 * 登録時に、既存のデータの破棄が必要な場合は、拡張XDK 形式のXMLファイルを
 * 作成してください。これは、EXEC_SQL タグに書き込んだSQL文を実行します。
 * 詳細は、{@link org.opengion.fukurou.xml.HybsXMLHandler HybsXMLHandler} クラスを参照してください。
 *
 *   &lt;ROWSET tableName="XX" &gt;
 *       &lt;EXEC_SQL&gt;                    最初に記載して、初期処理(データクリア等)を実行させる。
 *           delete from GEXX where YYYYY
 *       &lt;/EXEC_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;
 *
 * @og.rev 4.0.0.0 (2004/12/31) 新規作成
 * @og.group 初期化
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class InitFileLoader {
	private final String CLASSPATH ;
	private final Connection connection ;
	private boolean fileCommit = false;		// ファイル毎にコミット処理を行うかどうか(true:行う/false:行わない)

	/**
	 * コネクションを引数にする、コンストラクターです。
	 * classPath="resource" で初期化された InitFileLoader を作成します。
	 *
	 * @param	conn	登録用コネクション
	 */
	public InitFileLoader( final Connection conn ) {
		this( conn,"resource" );
	}

	/**
	 * コネクションと検索パスを指定して構築する、コンストラクターです。
	 * 対象ファイルは、classPath で指定された場所を、クラスローダーで検索します。
	 * ここで見つかったパス以下の XMLファイル(拡張子は小文字で、.xml )を検索
	 * します。このファイル名は テーブル名.xml 形式で格納しておきます。
	 *
	 * @param	conn	登録用コネクション
	 * @param classPath 対象となるファイル群を検索する、クラスパス
	 */
	public InitFileLoader( final Connection conn,final String classPath ) {
		connection	= conn ;
		CLASSPATH	= classPath;
	}

	/**
	 * ファイル毎にコミット処理を行うかどうか指定します(初期値:false[行わない])。
	 * 対象ファイル毎に、データベースへの登録を完了(commit)するかどうかを指定します。
	 * 通常、Connection は、autoCommit を false に設定し、１件ごとの処理は行いません。
	 * さらに、XMLファイルにも相互関連がある場合があるため、複数ファイルを取り込む場合は、
	 * それらを一群として処理したいケースもあります。また、各ファイルが独立している
	 * 場合は、他のファイル取り込み時にエラーが発生しても、それまでの分は、取り込みたい
	 * ケースがあります。
	 * ここでは、ファイル毎にコミットを発行するかどうかを指定できます。
	 * 初期値は、false[行わない]です。
	 * ※ true に設定した場合でも、途中でエラーが発生した場合は、それ以降の処理は、
	 * 継続しません。それ以前の処理が、登録されているだけです。
	 *
	 * @param fileCmt ファイル毎にコミット処理を行うかどうか [true:行う/false:行わない]
	 */
	public void setFileCommit( final boolean fileCmt ) {
		fileCommit = fileCmt ;
	}

	/**
	 * 対象となるファイル群を検索します。
	 * 対象ファイルは、resource フォルダに テーブル名.xml 形式で格納しておきます。
	 * このフォルダのファイルをピックアップします。
	 * useTimeStamp 属性を true に設定すると、このファイルのタイムスタンプを、
	 * システムパラメータ定義(GE12) にセットします。それ以降、このタイムスタンプと
	 * ファイルを比較して、変更がなければ、登録処理を行いません。
	 *
	 * @og.rev 4.0.0.0 (2007/11/28) メソッドの戻り値をチェックします。
	 * @og.rev 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
	 * @og.rev 5.5.2.6 (2012/05/25) JarFile を、Closer#zipClose( ZipFile ) メソッドを利用して、close します。
	 * @og.rev 5.6.6.1 (2013/07/12) jarファイルの場合も、タイムスタンプ管理の対象とします。
	 *
	 * @param useTimeStamp タイムスタンプの管理を行うかどうか[true:行う/false:行わない]
	 */
	public void loadInitFiles( final boolean useTimeStamp ) {
		List<File> fileList = new ArrayList<File>();

		// 5.5.2.6 (2012/05/25) findbugs対応
		JarFile jarFile = null;
		try {
	//		System.out.println( "    ==========================" );

			ClassLoader loader = Thread.currentThread().getContextClassLoader();
			Enumeration<URL> enume = loader.getResources( CLASSPATH );		// 4.3.3.6 (2008/11/15) Generics警告対応
			while( enume.hasMoreElements() ) {
				URL url = enume.nextElement();								// 4.3.3.6 (2008/11/15) Generics警告対応
				// jar:file:/実ディレクトリ または、file:/実ディレクトリ
				System.out.println( "      InitFileLoader Scan:[ " + url + " ]" );	// 5.6.6.1 (2013/07/12) メッセージ出力
				String dir = url.getFile();
				if( "jar".equals( url.getProtocol() ) ) {
					// dir = file:/C:/webapps/gf/WEB-INF/lib/resource2.jar!/resource 形式です。
					String jar = dir.substring(dir.indexOf( ':' )+1,dir.lastIndexOf( '!' ));
					// jar = /C:/webapps/gf/WEB-INF/lib/resource2.jar 形式に切り出します。

					// 5.6.6.1 (2013/07/12) jarファイルも、タイムスタンプ管理の対象
					File jarObj = new File( jar );
					if( useTimeStamp && jarObj.lastModified() <= 0 ) {		// fileObj は、jarファイルのこと
						continue;
					}

//					JarFile jarFile = new JarFile( jar );
					jarFile = new JarFile( jar );
					Enumeration<JarEntry> flEnum = jarFile.entries() ;		// 4.3.3.6 (2008/11/15) Generics警告対応
					while( flEnum.hasMoreElements() ) {
						JarEntry ent = flEnum.nextElement();				// 4.3.3.6 (2008/11/15) Generics警告対応
						String file = ent.getName();
						if( ! ent.isDirectory() && file.endsWith( ".xml" ) ) {
							// // 5.6.6.1 (2013/07/12) jarファイルの中身のタイムスタンプは見ない。
//							if( ! useTimeStamp || ent.getTime() > 0 ) {
								String table = file.substring( file.lastIndexOf('/')+1,file.lastIndexOf('.') );
								InputStream stream = null;
								try {
//									System.out.println( "      " + url + file );
									System.out.println( "        " + file );	// 5.6.6.1 (2013/07/12) メッセージ変更
									stream = jarFile.getInputStream( ent ) ;
									loadXML( stream,connection,table );
								}
//								catch( IOException ex ) {
//									String errMsg = "jar の XMLファイル読み取り時にエラーが発生しました。"
//											+ HybsSystem.CR + ex.getMessage();
//									throw new RuntimeException( errMsg,ex );
//								}
								finally {
									Closer.ioClose( stream );
								}
								if( fileCommit ) {
									connection.commit();
								}
//							}
						}
					}
					fileList.add( jarObj );			// 5.6.6.1 (2013/07/12) jarファイルも、タイムスタンプ管理の対象
					Closer.zipClose( jarFile );		// 5.5.2.6 (2012/05/25) findbugs対応
					jarFile = null;					// 正常終了時に、close() が２回呼ばれるのを防ぐため。
				}
				else {
//					// dir = /C:/webapps/gf/WEB-INF/classes/resource/ 形式です。
//					File fileObj = new File( dir );
//					File[] list = fileObj.listFiles();
//					for( int i=0; i<list.length; i++ ) {
//						String file = list[i].getName() ;
//						if( list[i].isFile() && file.endsWith( ".xml" ) ) {
//							if( ! useTimeStamp || list[i].lastModified() > 0 ) {
//								String table = file.substring( file.lastIndexOf('/')+1,file.lastIndexOf('.') );
//								InputStream stream = null;
//								try {
//									stream = new FileInputStream( list[i] ) ;
//									System.out.println( "      " + url + file );
//									loadXML( stream,connection,table );
//								}
//								catch( IOException ex ) {
//									String errMsg = "dir の XMLファイル読み取り時にエラーが発生しました。"
//											+ HybsSystem.CR + ex.getMessage();
//									throw new RuntimeException( errMsg,ex );
//								}
//								finally {
//									Closer.ioClose( stream );
//								}
//								if( fileCommit ) {
//									connection.commit();
//									if( !list[i].setLastModified( 0L ) ) {
//										String errMsg = "タイムスタンプの書き換えに失敗しました。"
//												+ "file=" + file ;	// 5.1.8.0 (2010/07/01) errMsg 修正
//										System.out.println( errMsg );
//									}
//								}
//								else {
//									fileList.add( list[i] );
//								}
//							}
//						}
//					}
					// 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
					// dir = /C:/webapps/gf/WEB-INF/classes/resource/ 形式です。
					File fileObj = new File( dir );
					loadXMLDir( fileObj,useTimeStamp,fileList );
				}
			}
			connection.commit();
	//		System.out.println( "    ==========================" );
		}
		catch( SQLException ex ) {
			String errMsg = "SQL実行時にエラーが発生しました。"
					+ HybsSystem.CR + ex.getMessage();
			Closer.rollback( connection );
//			if( connection != null ) {
//				Closer.rollback( connection );
//				try { connection.rollback(); }
//				catch( SQLException ex2 ) { errMsg += ex2.getMessage(); }
//			}
			throw new RuntimeException( errMsg,ex );
		}
		catch( IOException ex ) {
			String errMsg = "XMLファイル読み取り時にエラーが発生しました。"
					+ HybsSystem.CR + ex.getMessage();
			throw new RuntimeException( errMsg,ex );
		}
		finally {
			Closer.zipClose( jarFile );		// 5.5.2.6 (2012/05/25) findbugs対応

		// 5.3.6.0 (2011/06/01) finally 処理で、タイムスタンプの書き換えを行う。

		// commit が成功した場合のみ、ファイルのタイムスタンプの書き換えを行う。
//		if( ! fileCommit ) {
//			File[] files = fileList.toArray( new File[fileList.size()] );
//			for( int i=0; i<files.length; i++ ) {
			// 5.6.6.1 (2013/07/12) useTimeStamp=true の場合のみ、書き換えます。
			if( useTimeStamp ) {
				for( File file : fileList ) {
					if( !file.setLastModified( 0L ) ) {
						String errMsg = "タイムスタンプの書き換えに失敗しました。"
									+ "file=" + file ;	// 5.1.8.0 (2010/07/01) errMsg 修正
						System.out.println( errMsg );
					}
				}
			}
		}
	}

	/**
	 * XMLフォルダ/ファイルを読み取り、データベースに追加(INSERT)するメソッドをコールします。
	 *
	 * ここでは、フォルダ階層を下るための再起処理を行っています。
	 * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
	 * XMLファイルをデータベースに登録することが可能です。
	 * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
	 * を参照願います。
	 *
	 * @og.rev 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
	 *
	 * @param	fileObj			読取元のファイルオブジェクト
	 * @param	useTimeStamp	タイムスタンプの管理を行うかどうか[true:行う/false:行わない]
	 * @param	fileList		処理したファイルを保管しておくListオブジェクト
	 * @throws SQLException,IOException  データベースアクセスエラー、または、データ入出力エラー
	 */
	private void loadXMLDir( final File fileObj , final boolean useTimeStamp , final List<File> fileList )
																				throws SQLException,IOException {
		// dir = /C:/webapps/gf/WEB-INF/classes/resource/ 形式です。
		File[] list = fileObj.listFiles();
	//	Arrays.sort( list );
		for( int i=0; i<list.length; i++ ) {
			String file = list[i].getName() ;
			if( list[i].isFile() && file.endsWith( ".xml" ) ) {
				if( ! useTimeStamp || list[i].lastModified() > 0 ) {
					String table = file.substring( file.lastIndexOf('/')+1,file.lastIndexOf('.') );
					InputStream stream = null;
					try {
						System.out.println( "        " + list[i] );
						stream = new FileInputStream( list[i] ) ;
						loadXML( stream,connection,table );
					}
//					catch( IOException ex ) {
//						String errMsg = "dir の XMLファイル読み取り時にエラーが発生しました。"
//								+ HybsSystem.CR + ex.getMessage();
//						throw new RuntimeException( errMsg,ex );
//					}
					finally {
						Closer.ioClose( stream );
					}
					if( fileCommit ) {
						connection.commit();
					}
					fileList.add( list[i] );
				}
			}
			else {
				loadXMLDir( list[i],useTimeStamp,fileList );
			}
		}
	}

	/**
	 * XMLファイルを読み取り、データベースに追加(INSERT)します。
	 *
	 * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
	 * XMLファイルをデータベースに登録することが可能です。
	 * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
	 * を参照願います。
	 *
	 * @param	stream	読み取るストリーム
	 * @param	conn	DB接続のコネクション
	 * @param	table	追加するテーブル名
	 *
	 * @og.rev 5.6.6.1 (2013/07/12) 更新カウント数も取得します。
	 * @og.rev 5.6.7.0 (2013/07/27) HybsXMLSave の DDL（データ定義言語：Data Definition Language）の処理件数追加
	 * @og.rev 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行しない。
	 *
	 * @return	追加された件数
	 * @see org.opengion.fukurou.xml.HybsXMLSave
	 */
	private int loadXML( final InputStream stream, final Connection conn,final String table )
		throws SQLException,UnsupportedEncodingException {

		// InputStream より、XMLファイルを読み取り、table に追加(INSERT)します。
		Reader reader = new BufferedReader( new InputStreamReader( stream,"UTF-8" ) );
		HybsXMLSave save = new HybsXMLSave( conn,table );
		save.onExecErrException( false );		// 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行しない。
		save.insertXML( reader );
		int insCnt = save.getInsertCount();
		int delCnt = save.getDeleteCount();
		int updCnt = save.getUpdateCount();		// 5.6.6.1 (2013/07/12) 更新カウント数も取得
		int ddlCnt = save.getDDLCount();		// 5.6.7.0 (2013/07/27) DDL処理件数追加
		String tableName = save.getTableName() ;

//		System.out.println( "          TABLE=[" + tableName + "]  DELETE=["+ delCnt +"] INSERT=[" + insCnt + "]" );
//		System.out.println( "          TABLE=[" + tableName + "]  DELETE=["+ delCnt +"] INSERT=[" + insCnt + "] UPDATE=[" + updCnt + "]" );
		System.out.println( "          TABLE=[" + tableName + "]  DELETE=["+ delCnt +"] INSERT=[" + insCnt + "] UPDATE=[" + updCnt + "] DDL=[" + ddlCnt + "]" );
		return insCnt;
	}
}
