001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.fukurou.xml;
017
018import org.opengion.fukurou.util.FileUtil ;                     // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
019import org.opengion.fukurou.util.Closer ;
020
021import java.sql.Connection;
022import java.sql.SQLException;
023
024import java.io.Reader;
025import java.io.BufferedReader;
026import java.io.InputStreamReader;
027import java.io.FileInputStream;
028import java.io.InputStream;
029import java.io.IOException;
030import java.io.File;
031import java.io.UnsupportedEncodingException;
032import java.io.Writer;                                                          // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
033
034import java.util.Arrays;
035import java.util.List;
036import java.util.Map;
037import java.util.ArrayList;
038import java.util.Enumeration;
039import java.util.jar.JarFile;
040import java.util.jar.JarEntry;
041import java.net.URL;
042
043/**
044 * ORACLE XDK 形式のXMLファイルを読み取って、データベースに登録します。
045 *
046 * これは、Ver5の時は、org.opengion.hayabusa.common.InitFileLoader として
047 * 使用されていたクラスを改造したものです。
048 * InitFileLoader は、Ver6 では廃止されていますので、ご注意ください。
049 *
050 * 登録の実行有無の判断は、ファイルの更新時刻より判断します。(useTimeStamp=true の場合)
051 * これは、読み取りファイルの更新時刻が、0でない場合、読み取りを行います。
052 * 読み取りが完了した場合は、更新時刻を 0 に設定します。
053 * 読み取るファイルは、クラスローダーのリソースや、指定のフォルダ以下のファイル、そして、
054 * zip 圧縮されたファイルの中から、拡張子が xml で、UTF-8でエンコードされている
055 * 必要があります。通常は、ファイル名がテーブル名と同一にしておく必要がありますが、
056 * ROWSETのtable属性にテーブル名をセットしておくことも可能です。
057 * ファイルの登録順は、原則、クラスローダーの検索順に、見つかった全てのファイルを
058 * 登録します。データそのものは、INSERT のみ対応していますので、原則登録順は無視されます。
059 * ただし、拡張XDK 形式で、EXEC_SQL タグを使用した場合は、登録順が影響する可能性があります。
060 * 例:GE12.xml GE12 テーブルに登録するXMLファイル
061 * 登録時に、既存のデータの破棄が必要な場合は、拡張XDK 形式のXMLファイルを
062 * 作成してください。これは、EXEC_SQL タグに書き込んだSQL文を実行します。
063 * 詳細は、{@link org.opengion.fukurou.xml.HybsXMLHandler HybsXMLHandler} クラスを参照してください。
064 *
065 *   <ROWSET tableName="XX" >
066 *       <EXEC_SQL>                    最初に記載して、初期処理(データクリア等)を実行させる。
067 *           delete from GEXX where YYYYY
068 *       </EXEC_SQL>
069 *       <ROW num="1">
070 *           <カラム1>値1</カラム1>
071 *             ・・・
072 *           <カラムn>値n</カラムn>
073 *       </ROW>
074 *        ・・・
075 *       <ROW num="n">
076 *          ・・・
077 *       </ROW>
078 *       <EXEC_SQL>                    最後に記載して、項目の設定(整合性登録)を行う。
079 *           update GEXX set AA='XX' , BB='XX' where YYYYY
080 *       </EXEC_SQL>
081 *   <ROWSET>
082 *
083 * @og.rev 4.0.0.0 (2004/12/31) 新規作成(org.opengion.hayabusa.common.InitFileLoader)
084 * @og.rev 6.0.0.0 (2014/04/11) パッケージ、クラスファイル変更
085 *
086 * @version  6.0
087 * @author   Kazuhiko Hasegawa
088 * @since    JDK7.0,
089 */
090public final class XMLFileLoader {
091        private final Connection connection ;
092        private final boolean useTimeStamp ;
093
094        /** 6.0.0.0 (2014/04/11) システム依存の改行記号をセットします。    */
095        public static final String CR = System.getProperty("line.separator");
096
097        // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
098        private Writer log = FileUtil.getLogWriter( "System.out" );
099
100        // 6.0.0.0 (2014/04/11) タイムスタンプのゼロクリア対象のファイルを管理するリスト
101        private final List<File> fileList = new ArrayList<File>();
102
103        // 6.0.0.0 (2014/04/11) setAfterMap メソッドの対応
104        private Map<String,String> afterMap = null;
105
106        // 6.0.0.0 (2014/04/11) 追加,更新,削除,実行 の各実行時のカウントの総数
107        private int[] crudCnt = new int[] { 0,0,0,0 };  // CRUD カウントですが、配列の順番は追加,更新,削除,実行
108
109        /** getCRUDCount() で返される カウント数の配列番号 */
110        public static final int INS = 0;
111        public static final int DEL = 1;
112        public static final int UPD = 2;
113        public static final int DDL = 3;
114
115        /**
116         * コネクションを引数にする、コンストラクターです。
117         * classPath="resource" で初期化された XMLFileLoader を作成します。
118         * useTimeStamp 属性を true に設定すると、このファイルを読み取る都度
119         * タイムスタンプを、クリアします。
120         * また、タイムスタンプがクリアされたファイルは読み込みませんので、機能的に
121         * 一度しか読み込まないという事になります。
122         *
123         * @param       conn            登録用コネクション
124         * @param useTimeStamp  タイムスタンプの管理を行うかどうか[true:行う/false:行わない]
125         */
126        public XMLFileLoader( final Connection conn , final boolean useTimeStamp ) {
127                connection               = conn ;
128                this.useTimeStamp= useTimeStamp;
129        }
130
131        /**
132         * ログ出力を行う 内部ログ(Writer) を指定します。
133         * 
134         * 内部ログ(Writer) の初期値は、標準出力(System.out) から作成された Writerです。
135         * 内部ログ(Writer)が null の場合は、なにもしません。
136         *
137         * @og.rev 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
138         *
139         * @param log Writerオブジェクト
140         */
141        public void setLogWriter( final Writer log ) {
142                this.log = log ;
143        }
144
145        /**
146         * XMLファイルを読み取った後で指定するカラムと値のペア(マップ)情報をセットします。
147         *
148         * このカラムと値のペアのマップは、オブジェクト構築後に設定される為、
149         * XMLファイルのキーの存在に関係なく、Mapのキーと値が使用されます。(Map優先)
150         * null を設定した場合は、なにも処理されません。
151         *
152         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
153         *
154         * @param map   後設定するカラムデータマップ
155         */
156        public void setAfterMap( final Map<String,String> map ) {
157                afterMap = map;
158        }
159
160        /**
161         * XMLファイルを登録後の、追加,更新,削除,実行 のカウント配列を返します。
162         *
163         * 簡易的に処理したいために、配列に設定しています。
164         * 順番に、追加,更新,削除,実行 のカウント値になります。
165         *
166         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
167         *
168         * @return 追加,更新,削除,実行 のカウント配列
169         */
170        public int[] getCRUDCount() {
171                return crudCnt ;
172        }
173
174        /**
175         * 対象となるファイル群を ClassLoader の指定パスから、検索します。
176         * 
177         * 対象ファイルは、指定フォルダに テーブル名.xml 形式で格納しておきます。
178         * このフォルダのファイルをピックアップします。
179         * useTimeStamp 属性を true に設定すると、このファイルを読み取る都度
180         * タイムスタンプを、クリアします。
181         * また、タイムスタンプがクリアされたファイルは読み込みませんので、機能的に
182         * 一度しか読み込まないという事になります。
183         *
184         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
185         *
186         * @param path 対象となるファイル群を検索する、クラスパス
187         */
188        public void loadClassPathFiles( final String path ) {
189                try {
190                        ClassLoader loader = Thread.currentThread().getContextClassLoader();
191                        Enumeration<URL> enume = loader.getResources( path );
192                        while( enume.hasMoreElements() ) {
193                                URL url = enume.nextElement();
194                                // jar:file:/実ディレクトリ または、file:/実ディレクトリ
195                                println( "      XMLFileLoader Scan:[ " + url + " ]" );          // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
196                                String dir = url.getFile();
197                                if( "jar".equals( url.getProtocol() ) ) {
198                                        // dir = file:/C:/webapps/gf/WEB-INF/lib/resource2.jar!/resource 形式です。
199                                        String jar = dir.substring(dir.indexOf( ':' )+1,dir.lastIndexOf( '!' ));
200                                        // jar = /C:/webapps/gf/WEB-INF/lib/resource2.jar 形式に切り出します。
201
202                                        // 5.6.6.1 (2013/07/12) jarファイルも、タイムスタンプ管理の対象
203                                        loadJarFile( new File( jar ) );
204                                }
205                                else {
206                                        // 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
207                                        // dir = /C:/webapps/gf/WEB-INF/classes/resource/ 形式です。
208                                        loadXMLDir( new File( dir ) );
209                                }
210                        }
211                        Closer.commit( connection );
212                }
213                catch( SQLException ex ) {
214                        String errMsg = "SQL実行時にエラーが発生しました。"
215                                        + CR + ex.getMessage();
216                        Closer.rollback( connection );
217                        throw new RuntimeException( errMsg,ex );
218                }
219                catch( IOException ex ) {
220                        String errMsg = "XMLファイル読み取り時にエラーが発生しました。"
221                                        + CR + ex.getMessage();
222                        throw new RuntimeException( errMsg,ex );
223                }
224                finally {
225                        Closer.ioClose( log );                  // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理(closeするとflushもされます。)
226                        setZeroTimeStamp();                             // 6.0.0.0 (2014/04/11) タイムスタンプの書き換えをメソッド化
227                }
228        }
229
230        /**
231         * 対象となるファイル群を ファイル単体、フォルダ階層以下、ZIPファイル から、検索します。
232         * 
233         * 対象ファイルは、テーブル名.xml 形式で格納しておきます。
234         * この処理では、ファイル単体(*.xml)、フォルダ階層以下、ZIPファイル(*.jar , *.zip) は混在できません。
235         * 最初に判定した形式で、個々の処理に振り分けています。
236         *
237         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
238         *
239         * @param       fileObj                 読取元のファイルオブジェクト
240         * @see         #loadClassPathFiles( String )
241         */
242        public void loadXMLFiles( final File fileObj ) {
243                try {
244                        // nullでなく、ファイル/フォルダが存在することが前提
245                        if( fileObj != null && fileObj.exists() ) {
246                                println( "      XMLFileLoader Scan:[ " + fileObj + " ]" );      // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
247                                loadXMLDir( fileObj );                  // ファイルかディレクトリ
248                        }
249                        Closer.commit( connection );
250                }
251                catch( SQLException ex ) {
252                        String errMsg = "SQL実行時にエラーが発生しました。"
253                                        + CR + ex.getMessage();
254                        Closer.rollback( connection );
255                        throw new RuntimeException( errMsg,ex );
256                }
257                catch( IOException ex ) {
258                        String errMsg = "XMLファイル読み取り時にエラーが発生しました。"
259                                        + CR + ex.getMessage();
260                        throw new RuntimeException( errMsg,ex );
261                }
262                finally {
263                        Closer.ioClose( log );                  // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理(closeするとflushもされます。)
264                        setZeroTimeStamp();                             // 6.0.0.0 (2014/04/11) タイムスタンプの書き換えをメソッド化
265                }
266        }
267
268        /**
269         * XMLフォルダ/ファイルを読み取り、データベースに追加(INSERT)するメソッドをコールします。
270         *
271         * ここでは、フォルダ階層を下るための再起処理を行っています。
272         * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
273         * XMLファイルをデータベースに登録することが可能です。
274         * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
275         * を参照願います。
276         *
277         * @og.rev 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
278         *
279         * @param       jarObj                  読取元のJarファイルオブジェクト
280         * @throws SQLException,IOException  データベースアクセスエラー、または、データ入出力エラー
281         */
282        private void loadJarFile( final File jarObj ) throws SQLException,IOException {
283                if( ! useTimeStamp || jarObj.lastModified() > 0 ) {
284                        JarFile jarFile = null;
285                        try {
286                                jarFile = new JarFile( jarObj );
287                                Enumeration<JarEntry> flEnum = jarFile.entries() ;              // 4.3.3.6 (2008/11/15) Generics警告対応
288                                while( flEnum.hasMoreElements() ) {
289                                        JarEntry ent = flEnum.nextElement();                            // 4.3.3.6 (2008/11/15) Generics警告対応
290                                        String file = ent.getName();
291                                        if( ! ent.isDirectory() && file.endsWith( ".xml" ) ) {
292                                                // 5.6.6.1 (2013/07/12) jarファイルの中身のタイムスタンプは見ない。
293                                                String table = file.substring( file.lastIndexOf('/')+1,file.lastIndexOf('.') );
294                                                InputStream stream = null;
295                                                try {
296                                                        stream = jarFile.getInputStream( ent ) ;
297                                                        loadXML( stream,table,file );
298                                                }
299                                                finally {
300                                                        Closer.ioClose( stream );
301                                                }
302                                        }
303                                }
304                                fileList.add( jarObj );                 // 5.6.6.1 (2013/07/12) jarファイルも、タイムスタンプ管理の対象
305                        }
306                        finally {
307                                Closer.zipClose( jarFile );             // 5.5.2.6 (2012/05/25) findbugs対応
308                        }
309                }
310        }
311
312        /**
313         * XMLフォルダ/ファイルを読み取り、データベースに追加(INSERT)するメソッドをコールします。
314         *
315         * ここでは、フォルダ階層を下るための再起処理を行っています。
316         * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
317         * XMLファイルをデータベースに登録することが可能です。
318         * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
319         * を参照願います。
320         *
321         * @og.rev 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
322         *
323         * @param       fileObj                 読取元のファイルオブジェクト
324         * @throws SQLException,IOException  データベースアクセスエラー、または、データ入出力エラー
325         */
326        private void loadXMLDir( final File fileObj ) throws SQLException,IOException {
327                if( fileObj.isDirectory() ) {
328                        File[] list = fileObj.listFiles();
329                        Arrays.sort( list );
330                        for( int i=0; i<list.length; i++ ) {
331                                loadXMLDir( list[i] );
332                        }
333                }
334                else if( ! useTimeStamp || fileObj.lastModified() > 0 ) {
335                        String name = fileObj.getName() ;
336                        if( name.endsWith( ".xml" ) ) {
337                                String table = name.substring( name.lastIndexOf('/')+1,name.lastIndexOf('.') );
338                                InputStream stream = null;
339                                try {
340                                        println( "        " + fileObj );                // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
341                                        stream = new FileInputStream( fileObj ) ;
342                                        loadXML( stream,table,fileObj.getPath() );
343                                        fileList.add( fileObj );                                // 正常に処理が終われば、リストに追加します。
344                                }
345                                finally {
346                                        Closer.ioClose( stream );
347                                }
348                        }
349                        else if( name.endsWith( ".zip" ) || name.endsWith( ".jar" ) ) {
350                                loadJarFile( fileObj );
351                        }
352                }
353        }
354
355        /**
356         * XMLファイルを読み取り、データベースに追加(INSERT)します。
357         *
358         * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
359         * XMLファイルをデータベースに登録することが可能です。
360         * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
361         * を参照願います。
362         *
363         * @og.rev 5.6.6.1 (2013/07/12) 更新カウント数も取得します。
364         * @og.rev 5.6.7.0 (2013/07/27) HybsXMLSave の DDL(データ定義言語:Data Definition Language)の処理件数追加
365         * @og.rev 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行しない。
366         *
367         * @param       stream  XMLファイルを読み取るInputStream
368         * @param       table   テーブル名(ROWSETタグのtable属性が未設定時に使用)
369         * @param       file    ログ出力用のファイル名
370         * @see org.opengion.fukurou.xml.HybsXMLSave
371         */
372        private void loadXML( final InputStream stream, final String table, final String file )
373                                                                        throws SQLException,UnsupportedEncodingException {
374
375                // InputStream より、XMLファイルを読み取り、table に追加(INSERT)します。
376                Reader reader = new BufferedReader( new InputStreamReader( stream,"UTF-8" ) );
377                HybsXMLSave save = new HybsXMLSave( connection,table );
378                save.onExecErrException( false );               // 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行しない。
379                save.setAfterMap( afterMap );                   // 6.0.0.0 (2014/04/11) 新規追加
380                save.insertXML( reader );
381
382                int insCnt = save.getInsertCount();
383                int delCnt = save.getDeleteCount();
384                int updCnt = save.getUpdateCount();             // 5.6.6.1 (2013/07/12) 更新カウント数も取得
385                int ddlCnt = save.getDDLCount();                        // 5.6.7.0 (2013/07/27) DDL処理件数追加
386
387                crudCnt[INS] += insCnt ;
388                crudCnt[DEL] += delCnt ;
389                crudCnt[UPD] += updCnt ;
390                crudCnt[DDL] += ddlCnt ;
391
392                String tableName = save.getTableName() ;
393
394                // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
395                println( "          File=[" + file + "] TABLE=[" + tableName + "] DEL=["+ delCnt +"] INS=[" + insCnt + "] UPD=[" + updCnt + "] DDL=[" + ddlCnt + "]" );
396        }
397
398        /**
399         * 指定のリストのファイルのタイムスタンプをゼロに設定します。
400         * useTimeStamp=true の時に、XMLファイルのロードに成功したファイルの
401         * タイムスタンプをゼロに設定することで、2回目の処理が避けられます。
402         *
403         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
404         */
405        private void setZeroTimeStamp() {
406                if( useTimeStamp ) {
407                        for( File file : fileList ) {
408                                if( !file.setLastModified( 0L ) ) {
409                                        String errMsg = "タイムスタンプの書き換えに失敗しました。"
410                                                                        + "file=" + file ;
411                                        System.err.println( errMsg );
412                                }
413                        }
414                }
415        }
416
417        /**
418         * 登録されている ログ(Writer) に、メッセージを書き込みます。
419         * メッセージの最後に改行を挿入します。
420         *
421         * 内部ログ(Writer)が null の場合は、なにもしません。
422         * 内部ログ(Writer) の初期値は、標準出力(System.out) から作成された Writerです。
423         *
424         * @og.rev 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
425         *
426         * @param       msg 書き出すメッセージ
427         */
428        private void println( final String msg ) {
429                if( log != null ) {
430                        try {
431                                log.write( msg );
432                                log.write( CR );
433                        }
434                        catch( IOException ex ) {
435                                // ファイルが書き込めなかった場合
436                                String errMsg = msg + " が、書き込めませんでした。"
437                                                                        + ex.getMessage() ;
438                                System.err.println( errMsg );
439                                ex.printStackTrace();
440                        }
441                }
442        }
443}