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.plugin.io;
017
018import org.opengion.hayabusa.common.HybsSystem;
019import org.opengion.hayabusa.common.HybsSystemException;
020import org.opengion.hayabusa.db.DBTableModelUtil;
021import org.opengion.hayabusa.io.AbstractTableReader;
022import org.opengion.fukurou.util.StringUtil;
023
024import jxl.Workbook;
025import jxl.WorkbookSettings;
026import jxl.Sheet;
027import jxl.Cell;
028import jxl.read.biff.BiffException;
029
030import java.io.File;
031import java.io.BufferedReader;
032import java.io.IOException;
033
034/**
035 * JExcelによるEXCELバイナリファイルを読み取る実装クラスです。
036 *
037 * ファイル名、シート名を指定して、データを読み取ることが可能です。
038 * 第一カラムが # で始まる行は、コメント行なので、読み飛ばします。
039 * カラム名の指定行で、カラム名が null の場合は、その列は読み飛ばします。
040 *
041 * @og.rev 3.5.4.8 (2004/02/23) 新規作成
042 * @og.group ファイル入力
043 *
044 * @version  4.0
045 * @author   Kazuhiko Hasegawa
046 * @since    JDK5.0,
047 */
048public class TableReader_JExcel extends AbstractTableReader {
049        //* このプログラムのVERSION文字列を設定します。   {@value} */
050        private static final String VERSION = "5.5.7.2 (2012/10/09)" ;
051
052        private String  sheetName               = null;         // 3.5.4.2 (2003/12/15)
053        private String  sheetNos                = null;         // 5.5.7.2 (2012/10/09)
054        private String  filename                = null;         // 3.5.4.3 (2004/01/05)
055
056        /**
057         * DBTableModel から 各形式のデータを作成して,BufferedReader より読み取ります。
058         * コメント/空行を除き、最初の行は、必ず項目名が必要です。
059         * それ以降は、コメント/空行を除き、データとして読み込んでいきます。
060         * このメソッドは、EXCEL 読み込み時に使用します。
061         *
062         * @og.rev 4.0.0.0 (2006/09/31) 新規追加
063         * @og.rev 5.1.6.0 (2010/05/01) columns 処理 追加
064         * @og.rev 5.1.6.0 (2010/05/01) skipRowCount , useNumber の追加
065         * @og.rev 5.2.1.0 (2010/10/01) setTableColumnValues メソッドを経由して、テーブルにデータをセットする。
066         * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート
067         *
068         * @see #isExcel()
069         */
070        @Override
071        public void readDBTable() {
072                Workbook wb = null;
073                try {
074                        WorkbookSettings settings = new WorkbookSettings();
075                        // System.gc()「ガベージコレクション」の実行をOFFに設定
076                        settings.setGCDisabled(true);
077                        wb = Workbook.getWorkbook(new File(filename),settings);
078
079//                      Sheet sheet ;
080                        Sheet[] sheets ;                                                                        // 5.5.7.2 (2012/10/09) 配列に変更
081
082                        // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 sheetNos の指定が優先される。
083                        if( sheetNos != null && sheetNos.length() > 0 ) {
084                                String[] sheetList = StringUtil.csv2ArrayExt( sheetNos , wb.getNumberOfSheets()-1 );    // 最大シート番号は、シート数-1
085                                sheets = new Sheet[sheetList.length];
086                                for( int i=0; i<sheetList.length; i++ ) {
087                                        sheets[i] = wb.getSheet( Integer.parseInt( sheetList[i] ) );
088                                }
089                        }
090                        else if( sheetName != null && sheetName.length() > 0 ) {
091                                Sheet sheet = wb.getSheet( sheetName );
092                                if( sheet == null ) {
093                                        String errMsg = "対応するシートが存在しません。 Sheet=[" + sheetName + "]" ;
094                                        throw new HybsSystemException( errMsg );
095                                }
096                                sheets = new Sheet[] { sheet };
097                        }
098                        else {
099                                Sheet sheet = wb.getSheet(0);
100                                sheets = new Sheet[] { sheet };
101                        }
102
103//                      if( sheetName == null || sheetName.length() == 0 ) {
104//                              sheet = wb.getSheet(0);
105//                      }
106//                      else {
107//                              sheet = wb.getSheet( sheetName );
108//                              if( sheet == null ) {
109//                                      String errMsg = "対応するシートが存在しません。 Sheet=[" + sheetName + "]" ;
110//                                      throw new HybsSystemException( errMsg );
111//                              }
112//                      }
113
114                        boolean nameNoSet = true;
115                        table = DBTableModelUtil.newDBTable();
116
117                        int numberOfRows = 0;
118                        JxlHeaderData data = new JxlHeaderData();
119
120                        // 5.1.6.0 (2010/05/01) columns 処理
121                        data.setUseNumber( isUseNumber() );
122                        if( data.setColumns( columns ) ) {
123                                nameNoSet = false;
124                                table.init( data.getColumnSize() );
125                                setTableDBColumn( data.getNames() ) ;
126                        }
127
128                        // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 
129                        for( int i=0; i<sheets.length; i++ ) {                       // 5.5.7.2 (2012/10/09) シート配列を処理します。
130                                Sheet sheet = sheets[i] ;                                       // 5.5.7.2 (2012/10/09)
131                                int rowCnt = sheet.getRows();
132                                int skip = getSkipRowCount();           // 5.1.6.0 (2010/05/01)
133        //                      for( int nIndexRow = 0; nIndexRow < rowCnt; nIndexRow++) {
134                                for( int nIndexRow = skip; nIndexRow < rowCnt; nIndexRow++) {
135                                        Cell[] cells = sheet.getRow( nIndexRow );
136                                        if( data.isSkip( cells ) ) { continue; }
137                                        if( nameNoSet ) {
138                                                nameNoSet = false;
139                                                table.init( data.getColumnSize() );
140                                                setTableDBColumn( data.getNames() ) ;
141                                        }
142
143                                        if( numberOfRows < getMaxRowCount() ) {
144                                                setTableColumnValues( data.toArray( cells ) );          // 5.2.1.0 (2010/10/01)
145        //                                      table.addColumnValues( data.toArray( cells ) );
146                                                numberOfRows ++ ;
147                                        }
148                                        else {
149                                                table.setOverflow( true );
150                                        }
151                                }
152
153                                // 最後まで、#NAME が見つから無かった場合
154                                if( nameNoSet ) {
155                                        String errMsg = "最後まで、#NAME が見つかりませんでした。"
156                                                                        + HybsSystem.CR
157                                                                        + "ファイルが空か、もしくは損傷している可能性があります。"
158                                                                        + HybsSystem.CR ;
159                                        throw new HybsSystemException( errMsg );
160                                }
161                        }
162                }
163                catch (IOException ex) {
164                        String errMsg = "ファイル読込みエラー[" + filename + "]"  ;
165                        throw new HybsSystemException( errMsg,ex );
166                }
167                catch (BiffException ex) {
168                        String errMsg = "ファイル読込みエラー。データ形式が不正です[" + filename + "]"  ;
169                        throw new HybsSystemException( errMsg,ex );
170                }
171                finally {
172                        if( wb != null ) { wb.close(); }
173                }
174        }
175
176        /**
177         * DBTableModel から 各形式のデータを作成して,BufferedReader より読み取ります。
178         * コメント/空行を除き、最初の行は、必ず項目名が必要です。
179         * それ以降は、コメント/空行を除き、データとして読み込んでいきます。
180         *
181         * @og.rev 3.5.4.3 (2004/01/05) 引数に、BufferedReader を受け取る要に変更します。
182         * @og.rev 4.0.0.0 (2006/09/31) UnsupportedOperationException を発行します。
183         *
184         * @param   reader 各形式のデータ(使用していません)
185         */
186        @Override
187        public void readDBTable( final BufferedReader reader ) {
188                String errMsg = "このクラスでは実装されていません。";
189                throw new UnsupportedOperationException( errMsg );
190        }
191
192        /**
193         * DBTableModelのデータとしてEXCELファイルを読み込むときのシート名を設定します。
194         * これにより、複数の形式の異なるデータを順次読み込むことや、シートを指定して
195         * 読み取ることが可能になります。
196         * sheetNos と sheetName が同時に指定された場合は、sheetNos が優先されます。エラーにはならないのでご注意ください。
197         * のでご注意ください。
198         *
199         * @og.rev 3.5.4.2 (2003/12/15) 新規追加
200         *
201         * @param   sheetName シート名
202         */
203        @Override
204        public void setSheetName( final String sheetName ) {
205                this.sheetName = sheetName;
206        }
207
208        /**
209         * EXCELファイルを読み込むときのシート番号を指定します(初期値:0)。
210         *
211         * EXCEL読み込み時に複数シートをマージして取り込みます。
212         * シート番号は、0 から始まる数字で表します。
213         * ヘッダーは、最初のシートのカラム位置に合わせます。(ヘッダータイトルの自動認識はありません。)
214         * よって、指定するシートは、すべて同一レイアウトでないと取り込み時にカラムのずれが発生します。
215         * 
216         * シート番号の指定は、カンマ区切りで、複数指定できます。また、N-M の様にハイフンで繋げることで、
217         * N 番から、M 番のシート範囲を一括指定可能です。また、"*" による、全シート指定が可能です。
218         * これらの組み合わせも可能です。( 0,1,3,5-8,10-* )
219         * ただし、"*" に関しては例外的に、一文字だけで、すべてのシートを表すか、N-* を最後に指定するかの
220         * どちらかです。途中には、"*" は、現れません。
221         * シート番号は、重複(1,1,2,2)、逆転(3,2,1) での指定が可能です。これは、その指定順で、読み込まれます。
222         * sheetNos と sheetName が同時に指定された場合は、sheetNos が優先されます。エラーにはならないのでご注意ください。
223         * このメソッドは、isExcel() == true の場合のみ利用されます。
224         * 
225         * 初期値は、0(第一シート) です。
226         *
227         * ※ このクラスでは実装されていません。
228         *
229         * @og.rev 5.5.7.2 (2012/10/09) 新規追加
230         *
231         * @param   sheetNos EXCELファイルのシート番号(0から始まる)
232         * @see         #setSheetName( String ) 
233         */
234        @Override
235        public void setSheetNos( final String sheetNos ) {
236                this.sheetNos = sheetNos;
237        }
238
239        /**
240         * このクラスが、EXCEL対応機能を持っているかどうかを返します。
241         *
242         * EXCEL対応機能とは、シート名のセット、読み込み元ファイルの
243         * Fileオブジェクト取得などの、特殊機能です。
244         * 本来は、インターフェースを分けるべきと考えますが、taglib クラス等の
245         * 関係があり、問い合わせによる条件分岐で対応します。
246         *
247         * @og.rev 3.5.4.3 (2004/01/05) 新規追加
248         *
249         * @return      EXCEL対応機能を持っているかどうか(常にtrue)
250         */
251        @Override
252        public boolean isExcel() {
253                return true;
254        }
255
256        /**
257         * 読み取り元ファイル名をセットします。(DIR + Filename)
258         * これは、EXCEL追加機能として実装されています。
259         *
260         * @og.rev 3.5.4.3 (2004/01/05) 新規作成
261         *
262         * @param   filename 読み取り元ファイル名
263         */
264        @Override
265        public void setFilename( final String filename ) {
266                this.filename = filename;
267                if( filename == null ) {
268                        String errMsg = "ファイル名が指定されていません。" ;
269                        throw new HybsSystemException( errMsg );
270                }
271        }
272}
273
274/**
275 * EXCEL ネイティブのデータを処理する ローカルクラスです。
276 * このクラスでは、コメント行のスキップ判定、ヘッダー部のカラム名取得、
277 * 行情報(HSSFRow)から、カラムの配列の取得などを行います。
278 *
279 * @og.rev 3.5.4.8 (2004/02/23) 新規追加
280 * @og.group ファイル入力
281 *
282 * @version  4.0
283 * @author   儲
284 * @since    JDK5.0,
285 */
286class JxlHeaderData {
287        private String[] names ;
288        private int[]    index;
289        private int              columnSize = 0;
290        private boolean  nameNoSet = true;
291        private boolean  useNumber = true;
292
293        /**
294         * 行番号情報を使用するかどうか[true/false]を指定します(初期値:true)。
295         *
296         * 初期値は、true(使用する) です。
297         *
298         * @og.rev 5.1.6.0 (2010/05/01) 新規作成
299         *
300         * @param useNumber 行番号情報 [true:使用している/false:していない]
301         */
302        void setUseNumber( final boolean useNumber ) {
303                this.useNumber = useNumber ;
304        }
305
306        /**
307         * カラム名を外部から指定します。
308         * カラム名が、NULL でなければ、#NAME より、こちらが優先されます。
309         * カラム名は、順番に、指定する必要があります。
310         *
311         * @og.rev 5.1.6.0 (2010/05/01) 新規作成
312         *
313         * @param       columns EXCELのカラム列(CSV形式)
314         *
315         * @return true:処理実施/false:無処理
316         */
317        boolean setColumns( final String columns ) {
318                if( columns != null && columns.length() > 0 ) {
319                        names = StringUtil.csv2Array( columns );
320                        columnSize = names.length ;
321                        index = new int[columnSize];
322                        int adrs = (useNumber) ? 1:0 ;  // useNumber =true の場合は、1件目(No)は読み飛ばす。
323                        for( int i=0; i<columnSize; i++ ) { index[i] = adrs++; }
324                        nameNoSet = false;
325
326                        return true;
327                }
328                return false;
329        }
330
331        /**
332         * EXCEL ネイティブのデータを処理する ローカルクラスです。
333         * このクラスでは、コメント行のスキップ判定、ヘッダー部のカラム名取得、
334         * 行情報(HSSFRow)から、カラムの配列の取得などを行います。
335         *
336         * @param cells Cell[] EXCELのセル配列(行)
337         *
338         * @return true:コメント行/false:通常行
339         */
340        boolean isSkip( final Cell[] cells ) {
341                int size = cells.length ;
342                if( size == 0 ) { return true; }
343
344                String strText =  cells[0].getContents();
345                if( strText != null && strText.length() > 0 ) {
346                        if( nameNoSet ) {
347                                if( strText.equalsIgnoreCase( "#Name" ) ) {
348                                        makeNames( cells );
349                                        nameNoSet = false;
350                                        return true;
351                                }
352                                else if( strText.charAt( 0 ) == '#' ) {
353                                        return true;
354                                }
355                                else {
356                                        String errMsg = "#NAME が見つかる前にデータが見つかりました。"
357                                                                        + HybsSystem.CR
358                                                                        + "可能性として、ファイルが、ネイティブExcelでない事が考えられます。"
359                                                                        + HybsSystem.CR ;
360                                        throw new HybsSystemException( errMsg );
361                                }
362                        }
363                        else {
364                                if( strText.charAt( 0 ) == '#' ) {
365                                        return true;
366                                }
367                        }
368                }
369
370                return nameNoSet ;
371        }
372
373        /**
374         * EXCEL ネイティブの行のセル配列からカラム名情報を取得します。
375         *
376         * @og.rev 5.1.6.0 (2010/05/01) useNumber(行番号情報を、使用している(true)/していない(false)を指定)
377         *
378         * @param cells Cell[] EXCELの行のセル配列
379         */
380        private void makeNames( final Cell[] cells ) {
381                int maxCnt = cells.length;
382                String[] names2 = new String[maxCnt];
383                int[]    index2 = new int[maxCnt];
384
385                // 先頭カラムは、#NAME 属性行である。
386//              for( int nIndexCell = 1; nIndexCell < maxCnt; nIndexCell++) {
387                // 先頭カラムは、#NAME 属性行であるかどうかを、useNumber で判定しておく。
388                int nFirstCell = (useNumber) ? 1:0 ;
389                for( int nIndexCell = nFirstCell; nIndexCell < maxCnt; nIndexCell++) {
390                        String strText = cells[nIndexCell].getContents();
391
392                        if( strText != null && strText.length() > 0 ) {
393                                names2[columnSize] = strText;
394                                index2[columnSize] = nIndexCell;
395                                columnSize++;
396                        }
397                }
398
399                // #NAME を使用しない場合:no欄が存在しないケース
400                if( maxCnt == columnSize ) {
401                        names = names2;
402                        index = index2;
403                }
404                else {
405                        names = new String[columnSize];
406                        index = new int[columnSize];
407                        System.arraycopy(names2, 0, names, 0, columnSize);
408                        System.arraycopy(index2, 0, index, 0, columnSize);
409                }
410        }
411
412        /**
413         * カラム名情報を返します。
414         * ここでは、内部配列をそのまま返します。
415         *
416         * @return String[] カラム列配列情報
417         */
418        String[] getNames() {
419                return names;
420        }
421
422        /**
423         * カラムサイズを返します。
424         *
425         * @return      カラムサイズ
426         */
427        int getColumnSize() {
428                return columnSize;
429        }
430
431        /**
432         * カラム名情報を返します。
433         *
434         * @param cells Cell[] EXCELの行のセル配列
435         *
436         * @return String[] カラム列配列情報
437         */
438        String[] toArray( final Cell[] cells ) {
439                if( nameNoSet ) {
440                        String errMsg = "#NAME が見つかる前にデータが見つかりました。";
441                        throw new HybsSystemException( errMsg );
442                }
443
444                int cellSize = cells.length;
445                String[] data = new String[columnSize];
446                for( int i=0;i<columnSize; i++ ) {
447                        int indx = index[i];
448                        if( indx < cellSize ) {
449                                data[i] = cells[indx].getContents();
450                        }
451                        else {
452                                data[i] = null;
453                        }
454                }
455
456                return data;
457        }
458}