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