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.util;
017
018import java.io.InputStream;
019import java.io.FileInputStream;
020import java.io.BufferedInputStream;
021import java.io.IOException;
022
023import java.text.DecimalFormat;
024import java.text.NumberFormat;
025import java.util.Locale;
026
027import org.apache.poi.extractor.ExtractorFactory;
028import org.apache.poi.POITextExtractor;
029
030import org.apache.poi.hwpf.HWPFDocument;
031import org.apache.poi.hwpf.usermodel.Range;
032import org.apache.poi.hwpf.usermodel.Section;
033import org.apache.poi.hwpf.usermodel.Paragraph;
034import org.apache.poi.hwpf.usermodel.CharacterRun;
035// import org.apache.poi.hwpf.usermodel.HeaderStories;
036
037import org.apache.poi.hslf.HSLFSlideShow;
038import org.apache.poi.hslf.usermodel.SlideShow;
039import org.apache.poi.hslf.model.Slide;
040import org.apache.poi.hslf.model.TextRun;
041
042import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
043
044import org.apache.poi.ss.usermodel.WorkbookFactory;
045import org.apache.poi.ss.usermodel.Workbook;
046import org.apache.poi.ss.usermodel.Sheet;
047import org.apache.poi.ss.usermodel.Row;
048import org.apache.poi.ss.usermodel.Cell;
049
050import org.apache.poi.ss.usermodel.CreationHelper;
051
052import org.apache.poi.ss.usermodel.DateUtil;
053import org.apache.poi.ss.usermodel.RichTextString;
054import org.apache.poi.ss.usermodel.FormulaEvaluator;
055
056import org.opengion.fukurou.util.Closer;
057import org.opengion.fukurou.util.HybsDateUtil;
058
059/**
060 * POI による、Excel/Word/PoworPoint等に対する、ユーティリティクラスです。
061 *
062 * 基本的には、ネイティブファイルを読み取り、テキストを取得する機能が主です。
063 * Excel、Word、PowerPoint、Visio、Publisher からのテキスト取得が可能です。
064 *
065 * @og.rev 6.0.2.0 (2014/08/29) 新規作成
066 * @og.group その他
067 *
068 * @version  6.0
069 * @author   Kazuhiko Hasegawa
070 * @since    JDK7.0,
071 */
072public class POIUtil {
073        /** このプログラムのVERSION文字列を設定します。   {@value} */
074        private static final String VERSION = "6.0.2.0 (2014/08/29)" ;
075
076        private static final String CR = System.getProperty("line.separator");
077
078        /**
079         * 引数ファイルを、POITextExtractor を使用してテキスト化します。
080         *
081         * Excel、Word、PowerPoint、Visio、Publisher からのテキスト取得が可能です。
082         * 拡張子から、ファイルの種類を自動判別します。
083         *
084         * @og.rev 6.0.2.0 (2014/08/29) 新規作成
085         *
086         * @param       fname 入力ファイル名
087         * @return      変換後のテキスト
088         */
089        public static final String getText( final String fname ) {
090                InputStream fis = null;
091                POITextExtractor extractor = null;
092                try {
093                        fis = new BufferedInputStream( new FileInputStream( fname ) );
094                        extractor = ExtractorFactory.createExtractor( fis );
095                        return extractor.getText();
096                }
097                catch( Exception ex ) {
098                        String errMsg = "ファイル処理エラー[" + fname + "]" + CR + ex.getMessage() ;
099                        throw new RuntimeException( errMsg,ex );
100                }
101                finally {
102                        Closer.ioClose( extractor );
103                        Closer.ioClose( fis );
104                }
105        }
106
107        /**
108         * 引数ファイル(Word)を、HWPFDocument を使用してテキスト化します。
109         *
110         * POIEventは、唯一の void readText( String,String int... ) メソッドを持ち、
111         * 処理単位に、このメソッドが呼ばれます。つまり、内部にメモリを溜め込みません。
112         * int...可変長引数は、0:Section番号 1:page番号 2:Paragraph番号 がセットされます。
113         *
114         * POIEventは、データとして設定されているレコードのみCallされます。
115         *
116         * @og.rev 6.0.2.0 (2014/08/29) 新規作成
117         *
118         * @param       fname 入力ファイル名
119         * @param       event イベント処理させるI/F
120         */
121        public static final void wordReader( final String fname , final POIEvent event ) {
122                InputStream fis = null;
123                StringBuilder buf = new StringBuilder(200);
124
125                try {
126                        fis = new BufferedInputStream( new FileInputStream( fname ) );
127                        HWPFDocument doc = new HWPFDocument( fis );
128                        Range r = doc.getRange();
129                        // HeaderStories header = new HeaderStories( doc );
130
131                        int pCnt = 0;           // ページ番号
132                        for (int sno = 0; sno < r.numSections(); sno++) {
133                                Section sec = r.getSection(sno);
134                                String title = null;                                                                    // HeaderStories からうまく取れなかった。
135                                for (int pno = 0; pno < sec.numParagraphs(); pno++) {
136                                        Paragraph para = sec.getParagraph(pno);
137
138                                        if( para.text().indexOf("\f") >= 0 ) {
139                                                pCnt++;
140                        //                      title = header.getHeader( pCnt ).trim();
141                                        }
142
143                                        buf.setLength(0);               // Clearの事
144                                        for (int cno = 0; cno < para.numCharacterRuns(); cno++) {
145                                                CharacterRun run = para.getCharacterRun(cno);
146                                                // Removes any fields (eg macros, page markers etc) from the string
147                                                buf.append( Range.stripFields( run.text() ) );
148                                        }
149                                        String text = buf.toString().trim();
150                                        // データとして設定されているレコードのみイベントを発生させる。
151                                        if( text.length() > 0 ) {
152                                                if( title == null ) { title = text; }           // Section の最初の有効なParagraphをタイトルにする。
153                                                event.readText( text,title,sno,pCnt,pno );      // テキスト タイトル 0:Section番号 1:page番号 2:Paragraph番号
154                                        }
155                                }
156                        }
157                }
158                catch( IOException ex ) {
159                        String errMsg = "ファイル読込みエラー[" + fname + "]" + CR + ex.getMessage() ;
160                        throw new RuntimeException( errMsg,ex );
161                }
162                finally {
163                        Closer.ioClose( fis );
164                }
165        }
166
167        /**
168         * 引数ファイル(PoworPoint)を、HSLFSlideShow を使用してテキスト化します。
169         *
170         * POIEventは、唯一の void readText( String,String int... ) メソッドを持ち、
171         * 処理単位に、このメソッドが呼ばれます。つまり、内部にメモリを溜め込みません。
172         * int...可変長引数は、0:Slide番号 1:TextRun番号 がセットされます。
173         *
174         * POIEventは、データとして設定されているレコードのみCallされます。
175         *
176         * @og.rev 6.0.2.0 (2014/08/29) 新規作成
177         *
178         * @param       fname 入力ファイル名
179         * @param       event イベント処理させるI/F
180         */
181        public static final void pptReader( final String fname , final POIEvent event ) {
182                InputStream fis = null;
183
184                try {
185                        fis = new BufferedInputStream( new FileInputStream( fname ) );
186                        SlideShow ss = new SlideShow( new HSLFSlideShow( fis ) );
187                        Slide[] slides = ss.getSlides();
188                        for (int sno = 0; sno < slides.length; sno++) {
189                                String    title   = slides[sno].getTitle();                             // title は、あまり当てにならない。
190                                TextRun[] textRun = slides[sno].getTextRuns();
191                                for (int tno = 0; tno < textRun.length; tno++) {
192                                        String text = textRun[tno].getText().trim();
193                                        // データとして設定されているレコードのみイベントを発生させる。
194                                        if( text.length() > 0 ) {
195                                                event.readText( text,title,sno,tno );                   // テキスト タイトル 0:Slide番号 1:TextRun番号
196                                        }
197                                }
198                        }
199                }
200                catch( IOException ex ) {
201                        String errMsg = "ファイル読込みエラー[" + fname + "]" + CR + ex.getMessage() ;
202                        throw new RuntimeException( errMsg,ex );
203                }
204                finally {
205                        Closer.ioClose( fis );
206                }
207        }
208
209        /**
210         * 引数ファイル(Excel)を、Workbook を使用してテキスト化します。
211         *
212         * POIEventは、唯一の void readText( String,String int... ) メソッドを持ち、
213         * 処理単位に、このメソッドが呼ばれます。つまり、内部にメモリを溜め込みません。
214         * int...可変長引数は、0:Sheet番号 1:Row番号 2:Cell番号 がセットされます。
215         *
216         * EXCEL については、詳細に処理するための、ExcelModel クラスが別にあります。
217         * この処理は、簡易的に処理する場合に、使用できます。
218         *
219         * POIEventは、データとして設定されているレコードのみCallされます。
220         *
221         * @og.rev 6.0.2.0 (2014/08/29) 新規作成
222         *
223         * @param       fname 入力ファイル名
224         * @param       event イベント処理させるI/F
225         * @see         org.opengion.fukurou.model.ExcelModel
226         * @see         #excelReader( String , POIEvent , boolean )
227         */
228        public static final void excelReader( final String fname , final POIEvent event ) {
229                excelReader( fname,event,false );
230        }
231
232        /**
233         * 引数ファイル(Excel)を、Workbook を使用してテキスト化します。
234         *
235         * POIEventは、唯一の void readText( String,String int... ) メソッドを持ち、
236         * 処理単位に、このメソッドが呼ばれます。つまり、内部にメモリを溜め込みません。
237         * int...可変長引数は、0:Sheet番号 1:Row番号 2:Cell番号 がセットされます。
238         *
239         * EXCEL については、詳細に処理するための、ExcelModel クラスが別にあります。
240         * この処理は、簡易的に処理する場合に、使用できます。
241         *
242         * isFull属性に true を設定すると、すべての行列について、POIEvent が Callされます。
243         * 行が存在しない場合、カラムが存在しない場合は、null が設定されます。
244         * 行が存在しない場合のカラム番号は、-1 が設定されます。
245         *
246         * @og.rev 6.0.2.0 (2014/08/29) 新規作成
247         *
248         * @param       fname 入力ファイル名
249         * @param       event イベント処理させるI/F
250         * @param       isFull すべての行列について、イベント処理する場合は、true
251         * @see         org.opengion.fukurou.model.ExcelModel
252         * @see         #excelReader( String , POIEvent )
253         */
254        public static final void excelReader( final String fname , final POIEvent event , final boolean isFull ) {
255                InputStream fis = null;
256
257                try {
258                        fis = new BufferedInputStream( new FileInputStream( fname ) );
259                        Workbook wkbook = WorkbookFactory.create( fis );
260                        for (int x = 0; x < wkbook.getNumberOfSheets(); x++) {
261                                Sheet sheet = wkbook.getSheetAt( x );
262                                String sName = sheet.getSheetName();
263                                int stR = isFull ? 0 : sheet.getFirstRowNum();                          // fullの場合は、0 から開始
264                                int edR = sheet.getLastRowNum();
265                                for (int y = stR; y <= edR; y++) {                                                      // getLastRowNum は、含む
266                                        Row rowObj = sheet.getRow( y );
267                                        if( rowObj != null ) {
268                                                int stC = isFull ? 0 : rowObj.getFirstCellNum();        // fullの場合は、0 から開始
269                                                int edC = rowObj.getLastCellNum();
270                                                for (int z = stC; z <= edC; z++) {                                      // getLastCellNum は、含む
271                                                        Cell colObj = rowObj.getCell( z );
272                                                        if( colObj != null && colObj.getCellType() != Cell.CELL_TYPE_BLANK ) { 
273                                                                String text = getValue( colObj );
274                                                                event.readText( text,sName,x,y,z );                     // Cellテキスト Sheet名 0:Sheet番号 1:Row番号 2:Cell番号
275                                                        }
276                                                        else if( isFull ) {
277                                                                // カラムが存在しない場合のイベント
278                                                                event.readText( null,sName,x,y,z );                     // Cellテキスト Sheet名 0:Sheet番号 1:Row番号 2:Cell番号
279                                                        }
280                                                }
281                                        }
282                                        else if( isFull ) {
283                                                // 行が存在しない場合のイベント
284                                                event.readText( null,sName,x,y,-1 );                            // Cellテキスト Sheet名 0:Sheet番号 1:Row番号 2:Cell番号
285                                        }
286                                }
287                        }
288                }
289                catch( IOException ex ) {
290                        String errMsg = "ファイル読込みエラー[" + fname + "]" + CR + ex.getMessage() ;
291                        throw new RuntimeException( errMsg,ex );
292                }
293                catch( InvalidFormatException ex ) {
294                        String errMsg = "ファイル形式エラー[" + fname + "]" + CR + ex.getMessage() ;
295                        throw new RuntimeException( errMsg,ex );
296                }
297                finally {
298                        Closer.ioClose( fis );
299                }
300        }
301
302        /**
303         * セルオブジェクト(Cell)から値を取り出します。
304         *
305         * セルオブジェクトが存在しない場合は、null を返します。
306         *
307         * @og.rev 3.8.5.3 (2006/08/07) 取り出し方法を少し修正
308         * @og.rev 5.5.1.2 (2012/04/06) フォーマットセルを実行して、その結果を再帰的に処理する。
309         *
310         * @param       oCell EXCELのセルオブジェクト
311         *
312         * @return      セルの値
313         */
314        public static String getValue( final Cell oCell ) {
315                if( oCell == null ) { return null; }
316
317                String strText = "";
318                int nCellType = oCell.getCellType();
319                switch(nCellType) {
320                        case Cell.CELL_TYPE_NUMERIC:
321                                        strText = getNumericTypeString( oCell );
322                                        break;
323                        case Cell.CELL_TYPE_STRING:
324        // POI3.0               strText = oCell.getStringCellValue();
325                                        RichTextString richText = oCell.getRichStringCellValue();
326                                        if( richText != null ) {
327                                                strText = richText.getString();
328                                        }
329                                        break;
330                        case Cell.CELL_TYPE_FORMULA:
331        // POI3.0               strText = oCell.getStringCellValue();
332                                        // 5.5.1.2 (2012/04/06) フォーマットセルを実行して、その結果を再帰的に処理する。
333                                        Workbook wb = oCell.getSheet().getWorkbook();
334                                        CreationHelper crateHelper = wb.getCreationHelper();
335                                        FormulaEvaluator evaluator = crateHelper.createFormulaEvaluator();
336
337                                        try {
338                                                strText = getValue(evaluator.evaluateInCell(oCell));
339                                        }
340                                        catch ( Throwable th ) {
341                                                String errMsg = "セルフォーマットが解析できません。[" + oCell.getCellFormula() + "]"
342                                                                        + CR + getCellMsg( oCell );
343                                                throw new RuntimeException( errMsg,th );
344                                        }
345                                        break;
346                        case Cell.CELL_TYPE_BOOLEAN:
347                                        strText = String.valueOf(oCell.getBooleanCellValue());
348                                        break;
349                        case Cell.CELL_TYPE_BLANK :
350                        case Cell.CELL_TYPE_ERROR:
351                                        break;
352                        default :
353                                break;
354                }
355                return strText.trim();
356        }
357
358        /**
359         * セル値が数字の場合に、数字か日付かを判断して、対応する文字列を返します。
360         *
361         * @og.rev 3.8.5.3 (2006/08/07) 新規追加
362         * @og.rev 5.5.7.2 (2012/10/09) HybsDateUtil を利用するように修正します。
363         *
364         * @param       oCell EXCELのセルオブジェクト
365         *
366         * @return      数字の場合は、文字列に変換した結果を、日付の場合は、"yyyyMMddHHmmss" 形式で返します。
367         */
368        public static String getNumericTypeString( final Cell oCell ) {
369                final String strText ;
370
371                double dd = oCell.getNumericCellValue() ;
372                if( DateUtil.isCellDateFormatted( oCell ) ) {
373                        strText = HybsDateUtil.getDate( DateUtil.getJavaDate( dd ).getTime() , "yyyyMMddHHmmss" );      // 5.5.7.2 (2012/10/09) HybsDateUtil を利用
374                }
375                else {
376                        NumberFormat numFormat = NumberFormat.getInstance();
377                        if( numFormat instanceof DecimalFormat ) {
378                                ((DecimalFormat)numFormat).applyPattern( "#.####" );
379                        }
380                        strText = numFormat.format( dd );
381                }
382                return strText ;
383        }
384
385        /**
386         * セル情報を返します。
387         *
388         * エラー発生時に、どのセルでエラーが発生したかの情報を取得できるようにします。
389         *
390         * @og.rev 6.0.2.0 (2014/08/29) 新規作成
391         *
392         * @param       oCell EXCELのセルオブジェクト
393         * @return      セル情報の文字列
394         */
395        public static String getCellMsg( final Cell oCell ) {
396                String lastMsg = null;
397
398                if( oCell != null ) {
399                        int rowNo = oCell.getRowIndex();
400                        int celNo = oCell.getColumnIndex();
401                        String shtNm = oCell.getSheet().getSheetName();
402
403                        lastMsg = " Sheet=" + shtNm + ", Row=" + rowNo + ", Cel=" + celNo ;
404                }
405
406                return lastMsg;
407        }
408
409        /**
410         * アプリケーションのサンプルです。
411         *
412         * 入力ファイル名 は必須で、第一引数固定です。
413         * 第二引数は、処理方法で、指定しない場合は、-ALL として処理します。
414         *
415         * Usage: java org.opengion.fukurou.util.POIUtil 入力ファイル名 [-A(LL)/-W(ORD)/-P(PT)/-E(XCEL)/-F(ULL EXCEL)]
416         *   -A(LL)        ・・・ ALL 一括処理(初期値)
417         *   -W(ORD)       ・・・ Word
418         *   -P(PT)        ・・・ PoworPoint
419         *   -E(XCEL)      ・・・ Excel
420         *   -F(ULL EXCEL) ・・・ Full Excel
421         *
422         * @og.rev 6.0.2.0 (2014/08/29) 新規作成
423         *
424         * @param       args    コマンド引数配列
425         */
426        public static void main( final String[] args ) {
427                final String usageMsg = "Usage: java org.opengion.fukurou.util.POIUtil 入力ファイル名 [-A(LL)/-W(ORD)/-P(PT)/-E(XCEL)/-F(ULL EXCEL)]" ;
428                if( args.length == 0 ) {
429                        System.err.println( usageMsg );
430                        return ;
431                }
432
433                String fname = args[0];
434                char   type  = (args.length == 1) ? 'A' : args[1].charAt(1) ;
435
436                switch( type ) {
437                        case 'A' :  System.out.println( POIUtil.getText(fname) );
438                                                break;
439                        case 'W' :  POIUtil.wordReader(
440                                                        fname,
441                                                        new POIEvent() {
442                                                                public void readText( final String text , final String cmnt , final int... args ) {
443                                                                        System.out.println( "S[" + args[0] + "],P[" + fname + "],G[" + args[2] + "],C[" + cmnt + "]=" + text );
444                                                                }
445                                                        }
446                                                );
447                                                break;
448                        case 'P' :  POIUtil.pptReader(
449                                                        fname,
450                                                        new POIEvent() {
451                                                                public void readText( final String text , final String cmnt , final int... args ) {
452                                                //                      System.out.println( "S[" + args[0] + "],T[" + fname + "],C[" + cmnt + "]=" + text );
453                                                                        System.out.println( "S[" + args[0] + "],T[" + fname + "]=" + text );
454                                                                }
455                                                        }
456                                                );
457                                                break;
458                        case 'E' :  POIUtil.excelReader(
459                                                        fname,
460                                                        new POIEvent() {
461                                                                public void readText( final String text , final String cmnt , final int... args ) {
462                                                                        System.out.println( cmnt + ":S[" + args[0] + "],R[" + fname + "],C[" + args[2] + "]=" + text );
463                                                                }
464                                                        }
465                                                );
466                                                break;
467                        case 'F' :  StringBuilder buf = new StringBuilder();
468                                                POIUtil.excelReader(
469                                                        fname,
470                                                        new POIEvent() {
471                                                                int bkSht = -1;         // シートブレーク
472                                                                int bkRow = -1;         // 行ブレーク
473                                                                
474                                                                public void readText( final String text , final String cmnt , final int... args ) {
475                                                                        if( bkSht != args[0] ) {                        // シートブレーク
476                                                                                if( buf.length() > 0 ) {
477                                                                                        System.out.println( buf );
478                                                                                        buf.setLength(0);                       // Clearの事
479                                                                                }
480                                                                                System.out.println( "SheetName=" + cmnt + ":S[" + args[0] + "]" );
481                                                                                bkSht = args[0];
482                                                                                bkRow = 0;
483                                                                        }
484                                                                        if( bkRow != args[1] ) {                        // 行ブレーク
485                                                                                bkRow = args[1];
486                                                                                buf.append( CR );
487                                                                        }
488                                                                        buf.append( "\t" );
489                                                                        if( text != null ) { buf.append( text ); }
490                                                                }
491                                                        },
492                                                        true            // Full 行列処理
493                                                );
494                                                System.out.println( buf );                              // 処理が終了したかどうかはイベントでは判らない。
495                                                break;
496                        default :   System.err.println( usageMsg );
497                                                break;
498                }
499        }
500}