/*
 * 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.FileUtil ;			// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
import org.opengion.fukurou.util.Closer ;
import static org.opengion.fukurou.util.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring

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.io.Writer;								// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。

import java.util.Arrays;
import java.util.List;
import java.util.Map;
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ファイルを読み取って、データベースに登録します。
 *
 * これは、Ver5の時は、org.opengion.hayabusa.common.InitFileLoader として
 * 使用されていたクラスを改造したものです。
 * InitFileLoader は、Ver6 では廃止されていますので、ご注意ください。
 *
 * 登録の実行有無の判断は、ファイルの更新時刻より判断します。(useTimeStamp=true の場合)
 * これは、読み取りファイルの更新時刻が、０でない場合、読み取りを行います。
 * 読み取りが完了した場合は、更新時刻を ０ に設定します。
 * 読み取るファイルは、クラスローダーのリソースや、指定のフォルダ以下のファイル、そして、
 * zip 圧縮されたファイルの中から、拡張子が 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) 新規作成(org.opengion.hayabusa.common.InitFileLoader)
 * @og.rev 6.0.0.0 (2014/04/11) パッケージ、クラスファイル変更
 *
 * @version  6.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK7.0,
 */
public final class XMLFileLoader {
	private final Connection connection ;
	private final boolean useTimeStamp ;

	// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
	private Writer log = FileUtil.getLogWriter( "System.out" );

	// 6.0.0.0 (2014/04/11) タイムスタンプのゼロクリア対象のファイルを管理するリスト
	private final List<File> fileList = new ArrayList<>();

	// 6.0.0.0 (2014/04/11) setAfterMap メソッドの対応
	private Map<String,String> afterMap	;

	// 6.0.0.0 (2014/04/11) 追加,更新,削除,実行 の各実行時のカウントの総数
	private final int[] crudCnt = new int[] { 0,0,0,0 };	// CRUD カウントですが、配列の順番は追加,更新,削除,実行

	/** getCRUDCount() で返される カウント数の配列番号 */
	public static final int INS = 0;
	public static final int DEL = 1;
	public static final int UPD = 2;
	public static final int DDL = 3;

	/**
	 * コネクションを引数にする、コンストラクターです。
	 * classPath="resource" で初期化された XMLFileLoader を作成します。
	 * useTimeStamp 属性を true に設定すると、このファイルを読み取る都度
	 * タイムスタンプを、クリアします。
	 * また、タイムスタンプがクリアされたファイルは読み込みませんので、機能的に
	 * 一度しか読み込まないという事になります。
	 *
	 * @param	conn		登録用コネクション
	 * @param useTimeStamp	タイムスタンプの管理を行うかどうか[true:行う/false:行わない]
	 */
	public XMLFileLoader( final Connection conn , final boolean useTimeStamp ) {
		connection			= conn ;
		this.useTimeStamp	= useTimeStamp;
	}

	/**
	 * ログ出力を行う 内部ログ(Writer) を指定します。
	 * 
	 * 内部ログ(Writer) の初期値は、null とします。
	 * 内部ログ(Writer)が null の場合は、なにもしません。
	 *
	 * @og.rev 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
	 *
	 * @param log Writerオブジェクト
	 */
	public void setLogWriter( final Writer log ) {
		this.log = log ;
	}

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

	/**
	 * XMLファイルを登録後の、追加,更新,削除,実行 のカウント配列を返します。
	 *
	 * 簡易的に処理したいために、配列に設定しています。
	 * 順番に、追加,更新,削除,実行 のカウント値になります。
	 *
	 * @og.rev 6.0.0.0 (2014/04/11) 新規追加
	 *
	 * @return 追加,更新,削除,実行 のカウント配列
	 * @og.rtnNotNull
	 */
	public int[] getCRUDCount() {
		// 6.0.2.5 (2014/10/31) refactoring
		return crudCnt.clone();
	}

	/**
	 * 対象となるファイル群を ClassLoader の指定パスから、検索します。
	 * 
	 * 対象ファイルは、指定フォルダに テーブル名.xml 形式で格納しておきます。
	 * このフォルダのファイルをピックアップします。
	 * useTimeStamp 属性を true に設定すると、このファイルを読み取る都度
	 * タイムスタンプを、クリアします。
	 * また、タイムスタンプがクリアされたファイルは読み込みませんので、機能的に
	 * 一度しか読み込まないという事になります。
	 *
	 * @og.rev 6.0.0.0 (2014/04/11) 新規追加
	 * @og.rev 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
	 *
	 * @param path 対象となるファイル群を検索する、クラスパス
	 */
	public void loadClassPathFiles( final String path ) {
		try {
			final ClassLoader loader = Thread.currentThread().getContextClassLoader();
			final Enumeration<URL> enume = loader.getResources( path );
			while( enume.hasMoreElements() ) {
				final URL url = enume.nextElement();
				// jar:file:/実ディレクトリ または、file:/実ディレクトリ
				println( "      XMLFileLoader Scan:[ " + url + " ]" );		// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
				final String dir = url.getFile();
				if( "jar".equals( url.getProtocol() ) ) {
					// dir = file:/C:/webapps/gf/WEB-INF/lib/resource2.jar!/resource 形式です。
					final 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ファイルも、タイムスタンプ管理の対象
					loadJarFile( new File( jar ) );
				}
				else {
					// 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
					// dir = /C:/webapps/gf/WEB-INF/classes/resource/ 形式です。
					loadXMLDir( new File( dir ) );
				}
			}
			Closer.commit( connection );
		}
		catch( SQLException ex ) {
			final String errMsg = "SQL実行時にエラーが発生しました。"
					+ CR + ex.getMessage();
			Closer.rollback( connection );
			throw new RuntimeException( errMsg,ex );
		}
		catch( IOException ex ) {
			final String errMsg = "XMLファイル読み取り時にエラーが発生しました。"
					+ CR + ex.getMessage();
			throw new RuntimeException( errMsg,ex );
		}
		finally {
	 		// 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
//			Closer.ioClose( log );			// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理(closeするとflushもされます。)
			setZeroTimeStamp();				// 6.0.0.0 (2014/04/11) タイムスタンプの書き換えをメソッド化
		}
	}

	/**
	 * 対象となるファイル群を ファイル単体、フォルダ階層以下、ZIPファイル から、検索します。
	 * 
	 * 対象ファイルは、テーブル名.xml 形式で格納しておきます。
	 * この処理では、ファイル単体(*.xml)、フォルダ階層以下、ZIPファイル(*.jar , *.zip) は混在できません。
	 * 最初に判定した形式で、個々の処理に振り分けています。
	 *
	 * @og.rev 6.0.0.0 (2014/04/11) 新規追加
	 * @og.rev 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
	 *
	 * @param	fileObj			読取元のファイルオブジェクト
	 * @see		#loadClassPathFiles( String )
	 */
	public void loadXMLFiles( final File fileObj ) {
		try {
			// nullでなく、ファイル/フォルダが存在することが前提
			if( fileObj != null && fileObj.exists() ) {
				println( "      XMLFileLoader Scan:[ " + fileObj + " ]" );	// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
				loadXMLDir( fileObj );			// ファイルかディレクトリ
			}
			Closer.commit( connection );
		}
		catch( SQLException ex ) {
			final String errMsg = "SQL実行時にエラーが発生しました。"
					+ CR + ex.getMessage();
			Closer.rollback( connection );
			throw new RuntimeException( errMsg,ex );
		}
		catch( IOException ex ) {
			final String errMsg = "XMLファイル読み取り時にエラーが発生しました。"
					+ CR + ex.getMessage();
			throw new RuntimeException( errMsg,ex );
		}
		finally {
	 		// 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
//			Closer.ioClose( log );			// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理(closeするとflushもされます。)
			setZeroTimeStamp();				// 6.0.0.0 (2014/04/11) タイムスタンプの書き換えをメソッド化
		}
	}

	/**
	 * 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	jarObj			読取元のJarファイルオブジェクト
	 * @throws SQLException,IOException  データベースアクセスエラー、または、データ入出力エラー
	 */
	private void loadJarFile( final File jarObj ) throws SQLException,IOException {
		if( ! useTimeStamp || jarObj.lastModified() > 0 ) {
			JarFile jarFile = null;
			try {
				jarFile = new JarFile( jarObj );
				final Enumeration<JarEntry> flEnum = jarFile.entries() ;		// 4.3.3.6 (2008/11/15) Generics警告対応
				while( flEnum.hasMoreElements() ) {
					final JarEntry ent = flEnum.nextElement();				// 4.3.3.6 (2008/11/15) Generics警告対応
					final String file = ent.getName();
					if( ! ent.isDirectory() && file.endsWith( ".xml" ) ) {
						// 5.6.6.1 (2013/07/12) jarファイルの中身のタイムスタンプは見ない。
						final String table = file.substring( file.lastIndexOf('/')+1,file.lastIndexOf('.') );
						InputStream stream = null;
						try {
							stream = jarFile.getInputStream( ent ) ;
							loadXML( stream,table,file );
						}
						finally {
							Closer.ioClose( stream );
						}
					}
				}
				fileList.add( jarObj );			// 5.6.6.1 (2013/07/12) jarファイルも、タイムスタンプ管理の対象
			}
			finally {
				Closer.zipClose( jarFile );		// 5.5.2.6 (2012/05/25) findbugs対応
			}
		}
	}

	/**
	 * 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			読取元のファイルオブジェクト
	 * @throws SQLException,IOException  データベースアクセスエラー、または、データ入出力エラー
	 */
	private void loadXMLDir( final File fileObj ) throws SQLException,IOException {
		if( fileObj.isDirectory() ) {
			final File[] list = fileObj.listFiles();
			// 6.3.9.0 (2015/11/06) null になっている可能性がある(findbugs)
			if( list != null ) {
				Arrays.sort( list );
				for( final File file : list ) {
					loadXMLDir( file );
				}
			}
//			Arrays.sort( list );
//			for( int i=0; i<list.length; i++ ) {
//				loadXMLDir( list[i] );
//			}
		}
		else if( ! useTimeStamp || fileObj.lastModified() > 0 ) {
			final String name = fileObj.getName() ;
			if( name.endsWith( ".xml" ) ) {
				final String table = name.substring( name.lastIndexOf('/')+1,name.lastIndexOf('.') );
				InputStream stream = null;
				try {
					println( "        " + fileObj );		// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
					stream = new FileInputStream( fileObj ) ;
					loadXML( stream,table,fileObj.getPath() );
					fileList.add( fileObj );				// 正常に処理が終われば、リストに追加します。
				}
				finally {
					Closer.ioClose( stream );
				}
			}
			else if( name.endsWith( ".zip" ) || name.endsWith( ".jar" ) ) {
				loadJarFile( fileObj );
			}
		}
	}

	/**
	 * XMLファイルを読み取り、データベースに追加(INSERT)します。
	 *
	 * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
	 * XMLファイルをデータベースに登録することが可能です。
	 * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
	 * を参照願います。
	 *
	 * @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 を発行しない。
	 * @og.rev 6.2.0.0 (2015/02/27) try ～ finally 構文
	 *
	 * @param	stream	XMLファイルを読み取るInputStream
	 * @param	table	テーブル名(ROWSETタグのtable属性が未設定時に使用)
	 * @param	file	ログ出力用のファイル名
	 * @see org.opengion.fukurou.xml.HybsXMLSave
	 */
	private void loadXML( final InputStream stream, final String table, final String file )
									throws SQLException,UnsupportedEncodingException {
		// 6.2.0.0 (2015/02/27) try ～ finally 構文
		Reader reader = null;
		try {
			// InputStream より、XMLファイルを読み取り、table に追加(INSERT)します。
			reader = new BufferedReader( new InputStreamReader( stream,"UTF-8" ) );
			final HybsXMLSave save = new HybsXMLSave( connection,table );
			save.onExecErrException( false );		// 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行しない。
			save.setAfterMap( afterMap );			// 6.0.0.0 (2014/04/11) 新規追加
			save.insertXML( reader );

			final int insCnt = save.getInsertCount();
			final int delCnt = save.getDeleteCount();
			final int updCnt = save.getUpdateCount();		// 5.6.6.1 (2013/07/12) 更新カウント数も取得
			final int ddlCnt = save.getDDLCount();			// 5.6.7.0 (2013/07/27) DDL処理件数追加

			crudCnt[INS] += insCnt ;
			crudCnt[DEL] += delCnt ;
			crudCnt[UPD] += updCnt ;
			crudCnt[DDL] += ddlCnt ;

			final String tableName = save.getTableName() ;

			// 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
			println( "          File=[" + file + "] TABLE=[" + tableName + "] DEL=["+ delCnt +"] INS=[" + insCnt + "] UPD=[" + updCnt + "] DDL=[" + ddlCnt + "]" );
		}
		finally {
			Closer.ioClose( reader );
		}
	}

	/**
	 * 指定のリストのファイルのタイムスタンプをゼロに設定します。
	 * useTimeStamp=true の時に、XMLファイルのロードに成功したファイルの
	 * タイムスタンプをゼロに設定することで、２回目の処理が避けられます。
	 *
	 * @og.rev 6.0.0.0 (2014/04/11) 新規追加
	 */
	private void setZeroTimeStamp() {
		if( useTimeStamp ) {
			for( final File file : fileList ) {
				if( !file.setLastModified( 0L ) ) {
					final String errMsg = "タイムスタンプの書き換えに失敗しました。"
									+ "file=" + file ;
					System.err.println( errMsg );
				}
			}
		}
	}

	/**
	 * 登録されている ログ(Writer) に、メッセージを書き込みます。
	 * メッセージの最後に改行を挿入します。
	 *
	 * 内部ログ(Writer)が null の場合は、なにもしません。
	 * 内部ログ(Writer) の初期値は、標準出力(System.out) から作成された Writerです。
	 *
	 * @og.rev 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
	 *
	 * @param	msg 書き出すメッセージ
	 */
	private void println( final String msg ) {
		if( log != null ) {
			try {
				log.write( msg );
				log.write( CR );
			}
			catch( IOException ex ) {
				// ファイルが書き込めなかった場合
				final String errMsg = msg + " が、書き込めませんでした。"
									+ ex.getMessage() ;
				System.err.println( errMsg );
				ex.printStackTrace();
			}
		}
	}
}
