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.model;
017
018import java.io.InputStream;
019import java.io.FileInputStream;
020import java.io.BufferedReader;                                                                                                  // 6.2.2.0 (2015/03/27)
021import java.io.BufferedInputStream;
022import java.io.FileNotFoundException;
023import java.io.File;
024import java.io.IOException;
025import java.nio.file.Files;                                                                                                             // 6.2.2.0 (2015/03/27)
026import java.nio.charset.Charset;                                                                                                // 6.2.2.0 (2015/03/27)
027
028import java.util.zip.ZipException;                                                                                              // 8.5.0.0 (2023/04/21)
029import java.util.Set;                                                                                                                   // 6.0.2.3 (2014/10/10)
030import java.util.TreeSet;                                                                                                               // 6.0.2.3 (2014/10/10)
031import java.util.List;                                                                                                                  // 6.4.6.0 (2016/05/27) poi-3.15
032// import java.util.ArrayList;                                                                                                  // 8.0.1.0 (2021/10/29)
033
034// import org.apache.xmlbeans.XmlException;                                                                             // 8.0.0.0 (2021/07/31) Delete
035// import org.apache.poi.POITextExtractor;
036import org.apache.poi.extractor.POITextExtractor;                                                               // 7.0.0.0 (2018/10/01) poi-3.17.jar → poi-4.0.0.jar
037// import org.apache.poi.extractor.ExtractorFactory;
038// import org.apache.poi.ooxml.extractor.ExtractorFactory;                                              // 7.0.0.0 (2018/10/01) poi-ooxml-3.17.jar → poi-ooxml-4.0.0.jar
039import org.apache.poi.ooxml.extractor.POIXMLExtractorFactory;                                   // 8.0.0.0 (2021/07/31) poi-ooxml-4.0.0.jar → poi-ooxml-5.0.0.jar
040import org.apache.poi.hwpf.HWPFDocument;
041import org.apache.poi.hwpf.usermodel.Range;
042import org.apache.poi.hwpf.usermodel.Paragraph;
043import org.apache.poi.hssf.usermodel.HSSFCellStyle;
044import org.apache.poi.hslf.usermodel.HSLFTextParagraph;                                                 // 6.4.6.0 (2016/05/27) poi-3.15
045import org.apache.poi.hslf.usermodel.HSLFSlide;                                                                 // 6.4.6.0 (2016/05/27) poi-3.15
046import org.apache.poi.hslf.usermodel.HSLFSlideShow;                                                             // 6.4.6.0 (2016/05/27) poi-3.15
047
048import org.apache.poi.xwpf.usermodel.XWPFDocument;                                                              // 6.2.0.0 (2015/02/27)
049import org.apache.poi.xwpf.usermodel.XWPFParagraph;                                                             // 6.2.0.0 (2015/02/27)
050import org.apache.poi.xwpf.model.XWPFCommentsDecorator;                                                 // 8.5.0.0 (2023/04/21) Wordのテキスト抜出を改造
051import org.apache.poi.xwpf.usermodel.IBodyElement;                                                              // 8.5.0.0 (2023/04/21)
052import org.apache.poi.xwpf.usermodel.XWPFTable;                                                                 // 8.5.0.0 (2023/04/21)
053import org.apache.poi.xwpf.usermodel.XWPFTableCell;                                                             // 8.5.0.0 (2023/04/21)
054import org.apache.poi.xwpf.usermodel.XWPFTableRow;                                                              // 8.5.0.0 (2023/04/21)
055import org.apache.poi.xwpf.usermodel.XWPFSDT;                                                                   // 8.5.0.0 (2023/04/21)
056
057import org.openxmlformats.schemas.drawingml.x2006.main.CTHyperlink;                             // 8.1.0.1 (2022/01/07)
058import org.apache.poi.openxml4j.opc.PackageRelationshipTypes;                                   // 8.1.0.1 (2022/01/07)
059
060import org.apache.poi.xslf.usermodel.XMLSlideShow;                                                              // 6.2.0.0 (2015/02/27)
061// import org.apache.poi.xslf.extractor.XSLFPowerPointExtractor;                                // 7.0.0.0 (2018/10/01) POI4.0.0 deprecation
062// import org.apache.poi.sl.extractor.SlideShowExtractor;                                               // 7.0.0.0 (2018/10/01) POI4.0.0 XSLFPowerPointExtractor → SlideShowExtractor 8.5.0.0 (2023/04/21) Delete
063import org.apache.poi.xslf.usermodel.XSLFSlide;                                                                 // 8.5.0.0 (2023/04/21)
064import org.apache.poi.xslf.usermodel.XSLFShape;                                                                 // 7.0.0.0 (2018/10/01) POI4.0.0 XSLFPowerPointExtractor → SlideShowExtractor
065import org.apache.poi.xslf.usermodel.XSLFTextParagraph;                                                 // 7.0.0.0 (2018/10/01) POI4.0.0 XSLFPowerPointExtractor → SlideShowExtractor
066import org.apache.poi.sl.usermodel.TableCell;                                                                   // 8.5.0.0 (2023/04/21)
067import org.apache.poi.sl.usermodel.ShapeContainer;                                                              // 8.5.0.0 (2023/04/21)
068import org.apache.poi.sl.usermodel.TableShape;                                                                  // 8.5.0.0 (2023/04/21)
069import org.apache.poi.sl.usermodel.Shape;                                                                               // 8.5.0.0 (2023/04/21)
070import org.apache.poi.sl.usermodel.TextShape;                                                                   // 8.5.0.0 (2023/04/21)
071import org.apache.poi.sl.usermodel.TextRun;                                                                             // 8.5.0.0 (2023/04/21)
072
073import org.apache.pdfbox.pdmodel.PDDocument;                                                                    // 8.5.0.0 (2023/05/12) PDFファイル処理
074// import org.apache.pdfbox.Loader;                                                                                             // 8.5.0.0 (2023/05/12) 3.0.0
075import org.apache.pdfbox.text.PDFTextStripper;                                                                  // 8.5.0.0 (2023/05/12)
076
077// 8.5.0.0 (2023/05/12) pdfboxの警告抑止 … ただし、Log4J に切り替えると、制御できない。
078import java.util.logging.Logger;                                                                                                // 8.5.0.0 (2023/05/12) pdfboxの警告抑止
079import java.util.logging.Level;                                                                                                 // 8.5.0.0 (2023/05/12)
080
081import org.apache.poi.xssf.usermodel.XSSFSimpleShape;                                                   // 8.1.0.1 (2022/01/07) テキスト変換処理
082
083// import org.apache.poi.openxml4j.exceptions.InvalidFormatException;                   // 8.0.0.0 (2021/07/31) Delete
084// import org.apache.poi.openxml4j.exceptions.OpenXML4JException ;                              // 6.1.0.0 (2014/12/26) findBugs 8.0.0.0 (2021/07/31) Delete
085import org.apache.poi.ss.usermodel.WorkbookFactory;
086import org.apache.poi.ss.usermodel.Workbook;
087import org.apache.poi.ss.usermodel.Sheet;
088import org.apache.poi.ss.usermodel.Row;
089import org.apache.poi.ss.usermodel.Cell;
090import org.apache.poi.ss.usermodel.CellStyle;
091import org.apache.poi.ss.usermodel.CreationHelper;                                                              // 8.1.2.3 (2022/05/20) 復活
092import org.apache.poi.ss.usermodel.RichTextString;
093import org.apache.poi.ss.usermodel.DateUtil;
094import org.apache.poi.ss.usermodel.FormulaEvaluator;                                                    // 8.1.2.3 (2022/05/20) 復活
095import org.apache.poi.ss.usermodel.CellValue;                                                                   // 8.1.2.3 (2022/05/20)
096import org.apache.poi.ss.usermodel.Name;                                                                                // 6.0.2.3 (2014/10/10)
097import org.apache.poi.ss.usermodel.CellType;                                                                    // 6.5.0.0 (2016/09/30) poi-3.15
098import org.apache.poi.ss.util.SheetUtil;
099
100import org.opengion.fukurou.system.OgRuntimeException ;                                                 // 6.4.2.0 (2016/01/29)
101import org.opengion.fukurou.util.FileInfo;                                                                              // 6.2.3.0 (2015/05/01)
102// import org.opengion.fukurou.system.ThrowUtil;                                                                // 6.4.2.0 (2016/01/29) 8.5.0.0 (2023/04/21) Delete
103import org.opengion.fukurou.system.Closer;                                                                              // 6.2.0.0 (2015/02/27)
104import static org.opengion.fukurou.system.HybsConst.CR;                                                 // 6.1.0.0 (2014/12/26) refactoring
105import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;                              // 6.4.2.1 (2016/02/05) refactoring
106// import org.apache.poi.sl.usermodel.Slide;                                                                    // 8.5.0.0 (2023/04/21) Delete
107
108/**
109 * POI による、Excel/Word/PoworPoint等に対する、ユーティリティクラスです。
110 *
111 * 基本的には、ネイティブファイルを読み取り、テキストを取得する機能が主です。
112 * Excel、Word、PowerPoint、Visio、Publisher からのテキスト取得が可能です。
113 *
114 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
115 * @og.rev 6.2.0.0 (2015/02/27) パッケージ変更(util → model)
116 * @og.group その他
117 *
118 * @version     6.0
119 * @author      Kazuhiko Hasegawa
120 * @since       JDK7.0,
121 */
122public final class POIUtil {
123        /** このプログラムのVERSION文字列を設定します。 {@value} */
124        private static final String VERSION = "8.5.0.0 (2023/05/12)" ;
125
126        // 6.2.3.0 (2015/05/01)
127        /** POI対象サフィックス {@value} */
128        public static final String POI_SUFIX = "ppt,pptx,doc,docx,xls,xlsx,xlsm" ;
129
130        // 8.5.0.0 (2023/04/21)
131        /** テキスト対象サフィックス {@value} */
132        public static final String TXT_SUFIX = "txt,csv,jsp,java,html,xml,css,js,json,py" ;
133
134        /**
135         * すべてが staticメソッドなので、コンストラクタを呼び出さなくしておきます。
136         *
137         */
138        private POIUtil() {}
139
140        /**
141         * 引数ファイルが、POI関連の拡張子ファイルかどうかを判定します。
142         *
143         * Excel、Word、PowerPoint、Visio、Publisher からのテキスト取得が可能です。
144         * ファイルの拡張子が、{@value #POI_SUFIX} の場合、true を返します。
145         *
146         * @og.rev 6.2.3.0 (2015/05/01) POI関連の拡張子ファイルかどうかを判定
147         *
148         * @param       file    判定するファイル
149         * @return      POI関連の拡張子の場合、true
150         */
151        public static boolean isPOI( final File file ) {
152                return POI_SUFIX.contains( FileInfo.getSUFIX( file ) );
153        }
154
155        /**
156         * 引数ファイルが、処理対象となるテキスト関連の拡張子ファイルかどうかを判定します。
157         *
158         * ファイルの拡張子が、{@value #TXT_SUFIX} の場合、true を返します。
159         *
160         * @og.rev 8.5.0.0 (2023/04/21) 新規追加
161         *
162         * @param       file    判定するファイル
163         * @return      テキスト関連の拡張子の場合、true
164         */
165        public static boolean isText( final File file ) {
166                return TXT_SUFIX.contains( FileInfo.getSUFIX( file ) );
167        }
168
169        /**
170         * 引数ファイルが、textReader の読み取り対象となる拡張子ファイルかどうかを判定します。
171         *
172         * ファイルの拡張子が {@value #POI_SUFIX}、{@value #TXT_SUFIX}、pdf の場合、true を返します。
173         *
174         * @og.rev 8.5.0.0 (2023/05/12) 新規追加
175         *
176         * @param       file    判定するファイル
177         * @return      テキスト関連の拡張子の場合、true
178         */
179        public static boolean isReadText( final File file ) {
180                final String sufix = FileInfo.getSUFIX( file );         // sufix は小文字変換されてくる。
181
182                return POI_SUFIX.contains( sufix )
183                        || TXT_SUFIX.contains( sufix )
184                        || "pdf".equals( sufix ) ;
185        }
186
187        /**
188         * 引数ファイルを、POITextExtractor を使用してテキスト化します。
189         *
190         * Excel、Word、PowerPoint、Visio、Publisher からのテキスト取得が可能です。
191         * 拡張子から、ファイルの種類を自動判別します。
192         *
193         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
194         * @og.rev 6.2.0.0 (2015/02/27) getText → extractor に変更
195         * @og.rev 8.0.0.0 (2021/07/31) ExtractorFactory → POIXMLExtractorFactory に変更
196         *
197         * @param       file    入力ファイル名
198         * @return      変換後のテキスト
199         * @og.rtnNotNull
200         */
201        public static String extractor( final File file ) {
202        //      InputStream fis = null;
203                POITextExtractor extractor = null;
204                try {
205        //              fis = new BufferedInputStream( new FileInputStream( file ) );
206        //              extractor = ExtractorFactory.createExtractor( fis );
207        //              extractor = ExtractorFactory.createExtractor( file );
208                        extractor = new POIXMLExtractorFactory().create( file , null );         // 8.0.0.0 (2021/07/31) poi-ooxml-4.0.0.jar → poi-ooxml-5.0.0.jar
209                        return extractor.getText();
210                }
211                catch( final FileNotFoundException ex ) {
212                        final String errMsg = "ファイルが存在しません[" + file + "]" + CR + ex.getMessage() ;
213                        throw new OgRuntimeException( errMsg,ex );
214                }
215                catch( final IOException ex ) {
216                        final String errMsg = "ファイル処理エラー[" + file + "]" + CR + ex.getMessage() ;
217                        throw new OgRuntimeException( errMsg,ex );
218                }
219                // 8.0.0.0 (2021/07/31) poi-ooxml-4.0.0.jar → poi-ooxml-5.0.0.jar
220//              catch( final InvalidFormatException ex ) {
221//                      final String errMsg = "ファイルフォーマットエラー[" + file + "]" + CR + ex.getMessage() ;
222//                      throw new OgRuntimeException( errMsg,ex );
223//              }
224//              catch( final OpenXML4JException ex ) {
225//                      final String errMsg = "ODF-XML処理エラー[" + file + "]" + CR + ex.getMessage() ;
226//                      throw new OgRuntimeException( errMsg,ex );
227//              }
228//              catch( final XmlException ex ) {
229//                      final String errMsg = "XML処理エラー[" + file + "]" + CR + ex.getMessage() ;
230//                      throw new OgRuntimeException( errMsg,ex );
231//              }
232                finally {
233                        Closer.ioClose( extractor );
234        //              Closer.ioClose( fis );
235                }
236        }
237
238        /**
239         * 引数ファイル(Text)を、テキスト化します。
240         *
241         * ここでは、ファイルとエンコードを指定して、ファイルのテキスト全てを読み取ります。
242         *
243         * @og.rev 6.2.2.0 (2015/03/27) 引数ファイル(Text)を、テキスト化。
244         * @og.rev 6.2.3.0 (2015/05/01) textReader → extractor に変更
245         *
246         * @param       file    入力ファイル
247         * @param       encode  エンコード名
248         * @return      ファイルのテキスト
249         */
250        public static String extractor( final File file , final String encode ) {
251                try {
252                        // 指定のファイルをバイト列として読み込む
253                        final byte[] bytes = Files.readAllBytes( file.toPath() );
254                        // 読み込んだバイト列を エンコードして文字列にする
255                        return new String( bytes, encode );
256                }
257        //      catch( final UnsupportedEncodingException ex ) {
258        //              final String errMsg = "エンコードが不正です[" + file + "] , ENCODE=[" + encode + "]"  ;
259        //              throw new OgRuntimeException( errMsg,ex );
260        //      }
261                catch( final IOException ex ) {
262                        final String errMsg = "ファイル読込みエラー[" + file + "] , ENCODE=[" + encode + "]"  ;
263                        throw new OgRuntimeException( errMsg,ex );
264                }
265        }
266
267        /**
268         * 引数ファイル(Text)を、テキスト化します。
269         *
270         * ここでは、ファイルとエンコードを指定して、ファイルのテキスト全てを読み取ります。
271         *
272         * @og.rev 6.2.2.0 (2015/03/27) 引数ファイル(Text)を、テキスト化。
273         *
274         * @param       file    入力ファイル
275         * @param       conv    イベント処理させるI/F
276         * @param       encode  エンコード名
277         */
278        public static void textReader( final File file , final TextConverter<String,String> conv , final String encode ) {
279                BufferedReader reader = null ;
280
281                int rowNo = 0;          // 6.2.0.0 (2015/02/27) 行番号
282                try {
283                        reader = Files.newBufferedReader( file.toPath() , Charset.forName( encode ) );
284
285                        String line ;
286                        while((line = reader.readLine()) != null) {
287                                conv.change( line,String.valueOf( rowNo++ ) );
288                        }
289                }
290                catch( final IOException ex ) {
291                        final String errMsg = "ファイル読込みエラー[" + file + "] , ENCODE=[" + encode + "] , ROW=[" + rowNo + "]"  ;
292                        throw new OgRuntimeException( errMsg,ex );
293                }
294                finally {
295                        Closer.ioClose( reader );
296                }
297        }
298
299        /**
300         * 引数ファイル(Word,PoworPoint,Excel)を、TableModelHelper を使用してテキスト化します。
301         *
302         * ここでは、ファイル名の拡張子で、処理するメソッドを選別します。
303         * 拡張子が、対象かどうかは、#isPOI( File ) メソッドで判定できます。
304         *
305         * TableModelHelper によるイベント処理できますが、TEXTというカラム名を持つ
306         * 表形式オブジェクトの形で処理されます。
307         * また、内部的に、先頭に、# がある場合や、行データが存在しない場合は、
308         * スキップされます。
309         *
310         * @og.rev 6.2.3.0 (2015/05/01) 新規作成
311         * @og.rev 6.2.5.0 (2015/06/05) xls,xlsxは、それぞれ excelReader1,excelReader2 で処理します。
312         * @og.rev 8.5.0.0 (2023/04/21) txt,csv,jsp,java,xml,css,js は、UTF-8 固定で、textReader を呼び出す。
313         * @og.rev 8.5.0.0 (2023/05/12) pdfReader1 追加
314         *
315         * @param       file    入力ファイル
316         * @param       conv    イベント処理させるI/F
317         */
318        public static void textReader( final File file , final TextConverter<String,String> conv ) {
319                final String sufix = FileInfo.getSUFIX( file );                         // sufix は小文字変換されてくる。
320
321                if( "doc".equalsIgnoreCase( sufix ) ) {
322                        wordReader1( file,conv );
323                }
324                else if( "docx".equalsIgnoreCase( sufix ) ) {
325                        wordReader2( file,conv );
326                }
327                else if( "ppt".equalsIgnoreCase( sufix ) ) {
328                        pptReader1( file,conv );
329                }
330                else if( "pptx".equalsIgnoreCase( sufix ) ) {
331                        pptReader2( file,conv );
332                }
333                else if( "xls".equalsIgnoreCase( sufix ) ) {
334                        excelReader1( file,conv );                                                              // 6.2.5.0 (2015/06/05)
335                }
336                else if( "xlsx".equalsIgnoreCase( sufix ) || "xlsm".equalsIgnoreCase( sufix ) ) {
337                        excelReader2( file,conv );                                                              // 6.2.5.0 (2015/06/05)
338                }
339                else if( "pdf".equalsIgnoreCase( sufix ) ) {
340                        pdfReader1( file,conv );                                                                // 8.5.0.0 (2023/05/12)
341                }
342                // 8.5.0.0 (2023/04/21) txt,csv,jsp,java,xml,css,js は、UTF-8 固定で、textReader を呼び出す。
343                else if( TXT_SUFIX.contains( sufix ) ) {
344                        try {
345                                textReader( file,conv,"UTF-8" );
346                        }
347                        catch( final OgRuntimeException ex ) {                  // ほとんどのケースでencode違い
348                                conv.change( ex.getMessage() , file.getAbsolutePath() );
349
350                                textReader( file,conv,"Windows-31J" );          // Windows-31J で読み直し。
351                        }
352                }
353                else {
354//                      final String errMsg = "拡張子は、" +  POI_SUFIX + " にしてください。[" + file + "]" ;
355//                      throw new OgRuntimeException( errMsg );
356                        try {
357                                final String filename = file.getCanonicalPath();
358                                conv.change( "テキスト化対象外です" , filename );         // text,cmnt の順
359                        }
360                        catch( final IOException ex ) {
361                                final String errMsg = "正規のパス名取得エラー[" + file + "]" ;
362                                throw new OgRuntimeException( errMsg,ex );
363                        }
364                }
365        }
366
367        /**
368         * 引数ファイル(Word)を、HWPFDocument を使用してテキスト化します。
369         *
370         * 拡張子(.doc)のファイルを処理します。
371         * TableModelHelper によるイベント処理できますが、TEXTというカラム名を持つ
372         * 表形式オブジェクトの形で処理されます。
373         * また、内部的に、先頭に、# がある場合や、行データが存在しない場合は、
374         * スキップされます。
375         *
376         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
377         * @og.rev 6.2.0.0 (2015/02/27) POIEvent → TableModelHelper に変更
378         * @og.rev 6.2.4.2 (2015/05/29) 改行以外に、「。」で分割します。
379         *
380         * @param       file    入力ファイル名
381         * @param       conv    イベント処理させるI/F
382         */
383        private static void wordReader1( final File file , final TextConverter<String,String> conv ) {
384                InputStream fis  = null;
385
386                try {
387                        // 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正
388
389                        fis = new BufferedInputStream( new FileInputStream( file ) );           // 6.2.0.0 (2015/02/27)
390                        final HWPFDocument doc = new HWPFDocument( fis );
391
392        //              int rowNo = 0;          // 6.2.0.0 (2015/02/27) 行番号
393
394        //              // WordExtractor を使ったサンプル
395        //              WordExtractor we = new WordExtractor( doc );
396        //              for( String txt : we.getParagraphText() ) {
397        //                      String text = WordExtractor.stripFields( txt )
398        //                                                      .replaceAll( "\\x13[^\\x01]+\\x01\\x14" , "" )
399        //                                                      .replaceAll( "\\x0b" , "\n" ).trim();
400        //                      helper.value( text.trim(),rowNo++,0 );                          // 6.2.0.0 (2015/02/27) イベント変更
401        //              }
402
403                        // Range,Paragraph を使ったサンプル
404                        final Range rng = doc.getRange();
405                        for( int pno=0; pno<rng.numParagraphs(); pno++ ) {
406                                final Paragraph para = rng.getParagraph(pno);
407                                final String text = Range.stripFields( para.text() )
408                                                                .replaceAll( "\\x13[^\\x01]+\\x01\\x14" , "" )
409                                                                .replaceAll( "\\x0b" , "\n" ).trim();
410        //                      conv.change( text, String.valueOf( rowNo++ ) );
411                                if( text.length() > 0 ) {                                                               // 8.5.0.0 (2023/04/21) 存在する場合のみ抜き出す
412                                        conv.change( text, String.valueOf( pno ) );                     // 行番号代わりの数値は飛び番となる
413                                }
414                        }
415
416                        // Range,Paragraph,CharacterRun を使ったサンプル(変な個所で文字が分断される)
417        //              final Range rng = doc.getRange();
418        //              for( int pno = 0; pno < rng.numParagraphs(); pno++ ) {
419        //                      final Paragraph para = rng.getParagraph(pno);
420        //                      for( int cno = 0; cno < para.numCharacterRuns(); cno++ ) {
421        //                              final CharacterRun crun = para.getCharacterRun(cno);
422        //                              String text = Range.stripFields( crun.text() )
423        //                                                              .replaceAll( "\\x13[^\\x01]+\\x01\\x14" , "" )
424        //                                                              .replaceAll( "\\x0b" , "\n" ).trim();
425        //                              helper.value( text,rowNo++,0 );                         // 6.2.0.0 (2015/02/27) イベント変更
426        //                      }
427        //              }
428                }
429                catch( final IOException ex ) {
430                        final String errMsg = "ファイル読込みエラー[" + file + "]" + CR + ex.getMessage() ;
431                        throw new OgRuntimeException( errMsg,ex );
432                }
433                finally {
434                        Closer.ioClose( fis );
435                }
436        }
437
438        /**
439         * 引数ファイル(Word)を、XWPFDocument を使用してテキスト化します。
440         *
441         * 拡張子(.docx)のファイルを処理します。
442         * TableModelHelper によるイベント処理できますが、TEXTというカラム名を持つ
443         * 表形式オブジェクトの形で処理されます。
444         * また、内部的に、先頭に、# がある場合や、行データが存在しない場合は、
445         * スキップされます。
446         *
447         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
448         * @og.rev 6.2.0.0 (2015/02/27) POIEvent → TableModelHelper に変更
449         * @og.rev 6.2.4.2 (2015/05/29) 改行以外に、「。」で分割します。
450         * @og.rev 8.5.0.0 (2023/04/21) XWPFWordExtractor を参考に、テキスト化を詳細化した。
451         *
452         * @param       file    入力ファイル
453         * @param       conv    イベント処理させるI/F
454         */
455        private static void wordReader2( final File file , final TextConverter<String,String> conv ) {
456                InputStream fis  = null;
457
458                try {
459                        // 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正
460
461                        fis = new BufferedInputStream( new FileInputStream( file ) );           // 6.2.0.0 (2015/02/27)
462                        final XWPFDocument doc = new XWPFDocument( fis );
463
464                        int rowNo = 0;          // 6.2.0.0 (2015/02/27) 行番号
465
466//                      for( final XWPFParagraph para : doc.getParagraphs() ) {                         // 8.5.0.0 (2023/04/21) 削除
467//                              final String text = para.getParagraphText().trim();
468//                              conv.change( text, String.valueOf( rowNo++ ) );
469//                      }
470
471                        // 8.5.0.0 (2023/04/21) XWPFWordExtractor を参考に、テキスト化を詳細化した。
472                        // \poi-ooxml\src\main\java\org\apache\poi\xwpf\extractor\XWPFWordExtractor.java
473                        for( final IBodyElement ibody : doc.getBodyElements() ) {
474                                if (ibody instanceof XWPFParagraph) {
475                                        appendParagraphText(conv,rowNo, (XWPFParagraph) ibody);
476                                } else if (ibody instanceof XWPFTable) {
477                                        appendTableText(conv,rowNo, (XWPFTable) ibody);
478                                } else if (ibody instanceof XWPFSDT) {
479                                        final String text = ((XWPFSDT) ibody).getContent().getText().trim();
480                                        if( text.length() > 0 ) {
481                                                conv.change( text, "XWPFSDT " + rowNo );
482                                        }
483                                }
484                                rowNo++ ;
485                        }
486                }
487                catch( final ZipException ex ) {
488                        final String errMsg = "ファイルがすでにオープンされています[" + file + "]" + CR + ex.getMessage() ;
489                        throw new OgRuntimeException( errMsg,ex );
490                }
491                catch( final IOException ex ) {
492                        final String errMsg = "ファイル読込みエラー[" + file + "]" + CR + ex.getMessage() ;
493                        throw new OgRuntimeException( errMsg,ex );
494                }
495                finally {
496                        Closer.ioClose( fis );
497                }
498        }
499
500        /**
501         * wordReader2 から派生した、部分処理
502         *
503         * 引数ファイル(Word)を、XWPFDocument を使用してテキスト化するにあたり
504         * XWPFParagraph のテキスト化を行います。
505         *
506         * @og.rev 8.5.0.0 (2023/04/21) XWPFWordExtractor を参考に、テキスト化を詳細化した。
507         *
508         * @param       conv            イベント処理させるI/F
509         * @param       rowNo           検索における連番
510         * @param       paragraph       XWPFParagraphオブジェクト
511         */
512        private static void appendParagraphText(final TextConverter<String,String> conv, final int rowNo, final XWPFParagraph paragraph) {
513//              final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
514
515//              for (final IRunElement run : paragraph.getIRuns()) {
516//                      if (run instanceof XWPFSDT) {
517//                              buf.append( ((XWPFSDT)run).getContent().getText() );
518//                      }
519//                      else if (run instanceof XWPFRun) {
520//                              buf.append( ((XWPFRun)run).text() );
521//                      }
522//                      else {
523//                              buf.append( String.valueOf( run ) );
524//                      }
525//              }
526//              final String text = buf.toString().trim();              // Paragraph は、1行にまとめる
527
528                final String text = paragraph.getText().trim();
529                if( text.length() > 0 ) {
530                        conv.change( text, "Paragraph " + rowNo );
531                }
532
533                // FootnoteText
534                final String note = paragraph.getFootnoteText().trim();
535                if( note.length() > 0 ) {
536                        conv.change( note, "Footnote " + rowNo );
537                }
538
539                // Add comments
540                final XWPFCommentsDecorator decorator = new XWPFCommentsDecorator(paragraph, null);
541                final String cmnt = decorator.getCommentText().trim();
542                if( cmnt.length() > 0 ) {
543                        conv.change( cmnt, "Comment " + rowNo );
544                }
545        }
546
547        /**
548         * wordReader2 から派生した、部分処理
549         *
550         * 引数ファイル(Word)を、XWPFDocument を使用してテキスト化するにあたり
551         * XWPFTable のテキスト化を行います。
552         *
553         * @og.rev 8.5.0.0 (2023/04/21) XWPFWordExtractor を参考に、テキスト化を詳細化した。
554         *
555         * @param       conv    イベント処理させるI/F
556         * @param       rowNo   検索における連番
557         * @param       table   XWPFTableオブジェクト
558         */
559        private static void appendTableText(final TextConverter<String,String> conv, final int rowNo, final XWPFTable table) {
560                int subNo = 0;
561                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
562
563                //this works recursively to pull embedded tables from tables
564                for (final XWPFTableRow row : table.getRows()) {
565                        // テーブルのセルは、タブで結合する。
566                        for( final XWPFTableCell cell : row.getTableCells() ) {
567                                buf.append( cell.getText().trim() ).append( '\t' );
568                        }
569
570//                      final List<ICell> cells = row.getTableICells();
571//                      // テーブルのセルは、タブで結合する。
572//                      for (int i = 0; i < cells.size(); i++) {
573//                              final ICell cell = cells.get(i);
574//                              if (cell instanceof XWPFTableCell) {
575//                                      final String text = ((XWPFTableCell) cell).getTextRecursively();
576//                                      buf.append( text.trim() ).append( '\t' );
577//                              } else if (cell instanceof XWPFSDTCell) {
578//                                      final String text = ((XWPFSDTCell) cell).getContent().getText();
579//                                      buf.append( text.trim() ).append( '\t' );
580//                              }
581//                      }
582
583                        final String text = buf.toString().trim();      // 先に trim することで、空行を除く
584                        if( text.length() > 0 ) {
585                                conv.change( text , "TableRow " + rowNo + ":" + subNo );
586                        }
587                        buf.setLength(0);               // Clearの事
588                        subNo++ ;
589                }
590        }
591
592        /**
593         * 引数ファイル(PoworPoint)を、HSLFSlideShow を使用してテキスト化します。
594         *
595         * 拡張子(.ppt)のファイルを処理します。
596         * TableModelHelper によるイベント処理できますが、TEXTというカラム名を持つ
597         * 表形式オブジェクトの形で処理されます。
598         * また、内部的に、先頭に、# がある場合や、行データが存在しない場合は、
599         * スキップされます。
600         *
601         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
602         * @og.rev 6.2.0.0 (2015/02/27) POIEvent → TableModelHelper に変更
603         * @og.rev 6.4.6.0 (2016/05/27) poi-3.15 準備
604         * @og.rev 8.5.0.0 (2023/04/21) conv.change の cmnt 修正
605         *
606         * @param       file    入力ファイル
607         * @param       conv    イベント処理させるI/F
608         */
609        private static void pptReader1( final File file , final TextConverter<String,String> conv ) {
610                InputStream fis  = null;
611
612                try {
613                        // 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正
614
615                        fis = new BufferedInputStream( new FileInputStream( file ) );           // 6.2.0.0 (2015/02/27)
616
617        //              6.4.6.0 (2016/05/27) poi-3.15
618                        final HSLFSlideShow ss = new HSLFSlideShow( fis );
619                        final List<HSLFSlide> slides = ss.getSlides();                                          // 6.4.6.0 (2016/05/27) poi-3.15
620                        int rowNo = 0;          // 6.2.0.0 (2015/02/27) 行番号
621                        for( final HSLFSlide  slide : slides ) {                                                        // 6.4.6.0 (2016/05/27) poi-3.15
622                                int subNo = 0;  // 8.5.0.0 (2023/04/21)
623                                for( final List<HSLFTextParagraph> txtList : slide.getTextParagraphs() ) {      // 6.4.6.0 (2016/05/27) poi-3.15
624                                        final String text = HSLFTextParagraph.getText( txtList ).trim();
625                                        if( text.length() > 0 ) {
626//                                              conv.change( text, String.valueOf( rowNo++ ) );
627                                                conv.change( text, "Slide " + rowNo + ":" + subNo );    // 8.5.0.0 (2023/04/21) cmnt 修正
628                                        }
629                                        subNo++;        // 8.5.0.0 (2023/04/21)
630                                }
631                                rowNo++ ;               // 8.5.0.0 (2023/04/21)
632                        }
633
634        //              6.4.6.0 (2016/05/27) poi-3.12
635        //              final SlideShow ss = new SlideShow( new HSLFSlideShow( fis ) );
636        //              final Slide[] slides = ss.getSlides();
637        //              int rowNo = 0;          // 6.2.0.0 (2015/02/27) 行番号
638        //              for( int sno=0; sno<slides.length; sno++ ) {
639        //                      final TextRun[] textRun = slides[sno].getTextRuns();
640        //                      for( int tno=0; tno<textRun.length; tno++ ) {
641        //                              final String text = textRun[tno].getText();
642        //                              // データとして設定されているレコードのみイベントを発生させる。
643        //                              if( text.length() > 0 ) {
644        //                                      conv.change( text, String.valueOf( rowNo++ ) );
645        //                              }
646        //                      }
647        //              }
648                }
649                catch( final IOException ex ) {
650                        final String errMsg = "ファイル読込みエラー[" + file + "]" + CR + ex.getMessage() ;
651                        throw new OgRuntimeException( errMsg,ex );
652                }
653                finally {
654                        Closer.ioClose( fis );
655                }
656        }
657
658        /**
659         * 引数ファイル(PoworPoint)を、XMLSlideShow を使用してテキスト化します。
660         *
661         * 拡張子(.pptx)のファイルを処理します。
662         * TableModelHelper によるイベント処理できますが、TEXTというカラム名を持つ
663         * 表形式オブジェクトの形で処理されます。
664         * また、内部的に、先頭に、# がある場合や、行データが存在しない場合は、
665         * スキップされます。
666         *
667         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
668         * @og.rev 6.2.0.0 (2015/02/27) POIEvent → TableModelHelper に変更
669         * @og.rev 6.2.4.2 (2015/05/29) 行単位に、取り込むようにします。
670         * @og.rev 7.0.0.0 (2018/10/01) 警告:[deprecation] org.apache.poi.xslf.extractorのXSLFPowerPointExtractorは推奨されません (POI4.0.0)
671         * @og.rev 8.5.0.0 (2023/04/21) SlideShowExtractor を参考に、テキスト化を詳細化した。
672         *
673         * @param       file    入力ファイル
674         * @param       conv    イベント処理させるI/F
675         */
676        private static void pptReader2( final File file , final TextConverter<String,String> conv ) {
677                InputStream fis  = null;
678
679                try {
680                        // 6.2.0.0 (2015/02/27) TableModelEvent 変更に伴う修正
681
682                        fis = new BufferedInputStream( new FileInputStream( file ) );           // 6.2.0.0 (2015/02/27)
683                        final XMLSlideShow ss = new XMLSlideShow( fis );
684        //              final SlideShowExtractor<XSLFShape,XSLFTextParagraph> ext = new SlideShowExtractor<>( ss );             // 7.0.0.0 (2018/10/01) poi-4.0.0 Deprecated.
685        //              final String[] vals = ext.getText().split( "\\n" );             // 6.2.4.2 (2015/05/29) 行単位に、取り込むようにします。
686        //              for( int row=0; row<vals.length; row++ ) {
687        //                      conv.change( vals[row], String.valueOf( row ) );
688        //              }
689                        int rowNo = 0;
690                        for (final XSLFSlide slide : ss.getSlides()) {
691                                conv.change( slide.getSlideName(), "Slide " + rowNo );
692                                printShapeText( slide,conv,rowNo );
693                                rowNo ++ ;
694                        }
695                }
696                catch( final IOException ex ) {
697                        final String errMsg = "ファイル読込みエラー[" + file + "]" + CR + ex.getMessage() ;
698                        throw new OgRuntimeException( errMsg,ex );
699                }
700                finally {
701                        Closer.ioClose( fis );
702                }
703        }
704
705        /**
706         * pptReader2 から派生した、部分処理
707         *
708         * 引数ファイル(PowerPoint)を、XSLFSlide を使用してテキスト化します。
709         *
710         * @og.rev 8.5.0.0 (2023/04/21) SlideShowExtractor を参考に、テキスト化を詳細化した。
711         *
712         * @param       container       XSLFShapeを含むShapeContainerオブジェクト
713         * @param       conv            イベント処理させるI/F
714         * @param       rowNo           検索における連番
715         */
716        @SuppressWarnings("unchecked")
717        private static void printShapeText(final ShapeContainer<XSLFShape,XSLFTextParagraph> container, final TextConverter<String,String> conv,final int rowNo ) {
718                int subNo = 0;
719                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
720
721                for (final Shape<XSLFShape,XSLFTextParagraph> shape : container) {
722                        if (shape instanceof TextShape) {
723                                buf.setLength(0);
724                                printTextParagraphs(((TextShape<XSLFShape,XSLFTextParagraph>)shape).getTextParagraphs(),buf);
725                                final String text = buf.toString().trim();
726                                if( text.length() > 0 ) {
727                                        conv.change( text, "Shape " + rowNo + ":" + subNo );
728                                }
729                                subNo++ ;
730                        } else if (shape instanceof TableShape) {
731                                printTableShape((TableShape<XSLFShape,XSLFTextParagraph>)shape,conv,rowNo);
732                        } else if (shape instanceof ShapeContainer) {
733                                printShapeText((ShapeContainer<XSLFShape,XSLFTextParagraph>)shape,conv,rowNo);
734                        }
735                }
736        }
737
738        /**
739         * pptReader2 から派生した、部分処理
740         *
741         * 引数ファイル(PowerPoint)を、XSLFSlide を使用してテキスト化します。
742         *
743         * @og.rev 8.5.0.0 (2023/04/21) SlideShowExtractor を参考に、テキスト化を詳細化した。
744         *
745         * @param       shape   XSLFShapeを含むTableShapeオブジェクト
746         * @param       conv    イベント処理させるI/F
747         * @param       rowNo   検索における連番
748         */
749        private static void printTableShape(final TableShape<XSLFShape,XSLFTextParagraph> shape, final TextConverter<String,String> conv,final int rowNo) {
750                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
751
752                final int nrows = shape.getNumberOfRows();
753                final int ncols = shape.getNumberOfColumns();
754                for (int row = 0; row < nrows; row++) {
755                        for (int col = 0; col < ncols; col++){
756                                final TableCell<XSLFShape,XSLFTextParagraph> cell = shape.getCell(row, col);
757                                //defensive null checks; don't know if they're necessary
758                                if (cell != null) {
759                                        printTextParagraphs( cell.getTextParagraphs(),buf );
760                                        buf.append('\t');
761                                }
762                        }
763                        final String text = buf.toString().trim();
764                        if( text.length() > 0 ) {
765                                conv.change( text, "TableRow " + rowNo + ":" + row );
766                        }
767                        buf.setLength(0);               // Clearの事
768                }
769        }
770
771        /**
772         * pptReader2 から派生した、部分処理
773         *
774         * 引数ファイル(PowerPoint)を、XSLFSlide を使用してテキスト化します。
775         *
776         * @og.rev 8.5.0.0 (2023/04/21) SlideShowExtractor を参考に、テキスト化を詳細化した。
777         *
778         * @param       paras   XSLFTextParagraphオブジェクト のリスト
779         * @param       buf             文字列バッファ
780         */
781        private static void printTextParagraphs( final List<XSLFTextParagraph> paras ,final StringBuilder buf ) {
782                for (final XSLFTextParagraph para : paras) {
783                        for (final TextRun run : para) {
784                                buf.append( run.getRawText().trim() );
785                        }
786                }
787        }
788
789        /**
790         * 引数ファイル(Excel)を、テキスト化します。
791         *
792         * TableModelHelper を与えることで、EXCELデータをテキスト化できます。
793         * ここでは、HSSF(.xls)形式を処理します。
794         * シート名、セル、テキストオブジェクトをテキスト化します。
795         *
796         * @og.rev 6.2.5.0 (2015/06/05) 新規作成
797         *
798         * @param       file    入力ファイル
799         * @param       conv    イベント処理させるI/F
800         * @see         org.opengion.fukurou.model.ExcelModel
801         */
802        public static void excelReader1( final File file , final TextConverter<String,String> conv ) {
803                excelReader2( file , conv );
804        }
805
806        /**
807         * 引数ファイル(Excel)を、テキスト化します。
808         *
809         * TableModelHelper を与えることで、EXCELデータをテキスト化できます。
810         * ここでは、ExcelModelを使用して、(.xlsx , .xlsm)形式を処理します。
811         * シート名、セル、テキストオブジェクトをテキスト化します。
812         *
813         * @og.rev 6.2.5.0 (2015/06/05) 新規作成
814         * @og.rev 6.3.1.0 (2015/06/28) TextConverterに、引数(cmnt)を追加
815         * @og.rev 6.3.9.0 (2015/11/06) Java 8 ラムダ式に変更
816         *
817         * @param       file    入力ファイル
818         * @param       conv    イベント処理させるI/F
819         * @see         org.opengion.fukurou.model.ExcelModel
820         */
821        public static void excelReader2( final File file , final TextConverter<String,String> conv ) {
822                final ExcelModel excel = new ExcelModel( file, true );
823
824                // 6.3.9.0 (2015/11/06) Java 8 ラムダ式に変更
825                // textConverter を使いますが、テキストを読み込むだけで、変換しません。
826                excel.textConverter(
827                        ( val,cmnt ) -> {
828                                conv.change( val,cmnt );        // 変換したくないので、引数の TextConverter を直接渡せない。
829                                return null;                            // nullを返せば、変換しません。
830                        }
831                );
832        }
833
834        /**
835         * 引数ファイル(PDF)を、テキスト化します。
836         *
837         * 引数ファイル(PDF)を、pdfbox を使用してテキスト化します。
838         *
839         * @og.rev 8.5.0.0 (2023/05/12) pdfReader1 追加
840         *
841         * @param       file    入力ファイル
842         * @param       conv    イベント処理させるI/F
843         */
844        public static void pdfReader1( final File file , final TextConverter<String,String> conv ) {
845                Logger.getLogger("org.apache.fontbox").setLevel(Level.OFF);             // 警告抑止
846
847//              // pdfbox-3.0.0-alpha3.jar
848//              try( PDDocument document = Loader.loadPDF( file )) {
849//                      final PDFTextStripper stripper = new PDFTextStripper();
850//                      for( final PDPage page : document.getPages() ) {
851//                              stripper.processPage( page );
852//                              final String text = stripper.getText(document);
853//                              final int no = stripper.getStartPage();
854//                              conv.change( text,String.valueOf( no ) );
855//                      }
856//              }
857//              catch( final IOException ex ) {
858//                      final String errMsg = "ファイル読込みエラー[" + file + "]" + CR + ex.getMessage() ;
859//                      throw new OgRuntimeException( errMsg,ex );
860//              }
861
862                // pdfbox-2.0.28.jar
863                try( final PDDocument document = PDDocument.load(file) ) {
864                        final int psize  = document.getNumberOfPages();                 // 総ページ数
865                        final PDFTextStripper stripper = new PDFTextStripper();
866                        final String sep = stripper.getLineSeparator();
867                        for( int pno=1; pno<=psize; pno++ ) {
868                                stripper.setStartPage(pno);
869                                stripper.setEndPage(pno);
870                                int   lno = 0;
871                                final String text = stripper.getText(document);
872                                for( final String line : text.split( sep ) ) {          // PDFTextStripper を使うと、テキスト行のみ抜き出すので
873                //                      if( line.length() > 0 ) {                                               // 行番号は取れない…感じ。
874                                                final String cmnt = "Page" + pno + " : " + lno ;
875                                                conv.change( line,cmnt );
876                //                      }
877                                        lno++ ;
878                                }
879                        }
880                }
881                catch( final IOException ex ) {
882                        final String errMsg = "ファイル読込みエラー[" + file + "]" + CR + ex.getMessage() ;
883                        throw new OgRuntimeException( errMsg,ex );
884                }
885        }
886
887        /**
888         * Excelの行列記号を、行番号と列番号に分解します。
889         *
890         * Excelの行列記号とは、A1 , B5 , AA23 などの形式を指します。
891         * これを、行番号と列番号に分解します。例えば、A1→0行0列、B5→4行1列、AA23→22行26列 となります。
892         * 分解した結果は、内部変数の、rowNo と colNo にセットされます。
893         * これらは、0 から始まる int型の数字で表します。
894         *
895         *   ①行-列形式
896         *     行列は、EXCELオブジェクトに準拠するため、0から始まる整数です。
897         *     0-0 ⇒ A1 , 1-0 ⇒ A2 , 0-1 ⇒ B1 になります。
898         *   ②EXCEL表記
899         *     EXCEL表記に準拠した、A1,A2,B1 の記述も処理できるように対応します。
900         *     なお、A1,A2,B1 の記述は、必ず、英字1文字+数字 にしてください。(A~Zまで)
901         *   ③EXCELシート名をキーに割り当てるために、"SHEET" という記号に対応します。
902         *     rowNo = -1 をセットします。
903         *
904         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
905         * @og.rev 6.2.6.0 (2015/06/19) 行-列形式と、SHEET文字列判定を採用。
906         *
907         * @param       kigo    Excelの行列記号( A1 , B5 , AA23 など )
908         * @return      行と列の番号を持った配列([0]=行=ROW , [1]=列=COL)
909         * @og.rtnNotNull
910         */
911        public static int[] kigo2rowCol( final String kigo ) {
912                int rowNo = 0;
913                int colNo = -1;                                                                         // +1 して、26 かける処理をしているので、辻褄合わせ
914
915                // 6.2.6.0 (2015/06/19) 行-列形式と、SHEET文字列判定を採用。
916                if( "SHEET".equalsIgnoreCase( kigo ) ) {
917                        rowNo = -1;
918                }
919                else {
920                        final int adrs = kigo.indexOf( '-' );
921                        if( adrs > 0 ) {
922                                rowNo = Integer.parseInt( kigo.substring( 0,adrs ) );
923                                colNo = Integer.parseInt( kigo.substring( adrs+1 ) );
924                        }
925                        else {
926                                for( int i=0; i<kigo.length(); i++ ) {
927                                        final char ch = kigo.charAt(i);
928                                        if( 'A' <= ch && ch <= 'Z' ) { colNo = (colNo+1)*26 + ch-'A'; }
929                                        else {
930                                                // アルファベットでなくなったら、残りは 行番号(ただし、-1する)
931                                                rowNo = Integer.parseInt( kigo.substring( i ) ) -1;
932                                                break;
933                                        }
934                                }
935                        }
936                }
937                return new int[] { rowNo,colNo };
938        }
939
940        /**
941         * セルオブジェクト(Cell)から値を取り出します。
942         *
943         * セルオブジェクトが存在しない場合は、null を返します。
944         * それ以外で、うまく値を取得できなかった場合は、ゼロ文字列を返します。
945         *
946         * @og.rev 3.8.5.3 (2006/08/07) 取り出し方法を少し修正
947         * @og.rev 5.5.1.2 (2012/04/06) フォーマットセルを実行して、その結果を再帰的に処理する。
948         * @og.rev 6.0.3.0 (2014/11/13) セルフォーマットエラー時に、RuntimeException を throw しない。
949         * @og.rev 6.4.2.0 (2016/01/29) StringUtil#ogStackTrace(Throwable) を、ThrowUtil#ogStackTrace(String,Throwable) に置き換え。
950         * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
951         * @og.rev 7.0.0.0 (2018/10/01) 警告:[deprecation] CellのgetCellTypeEnum()は推奨されません (POI4.0.0)
952         * @og.rev 7.3.0.0 (2021/01/06) フォーマットエラー時に、エラーメッセージ取得でもエラーになる。
953         * @og.rev 8.0.0.0 (2021/07/31) FORMULA処理のエラー対応(出来るだけ…)
954         * @og.rev 8.1.2.3 (2022/05/20) 計算式の計算を行う。
955         * @og.rev 8.5.0.0 (2023/04/21) セルフォーマット処理エラー時には、スタックトレースは出さずに計算式を返す。
956         *
957         * @param       oCell   EXCELのセルオブジェクト
958         * @return      セルの値
959         */
960        public static String getValue( final Cell oCell ) {
961                if( oCell == null ) { return null; }
962                String strText = "";
963        //      final int nCellType = oCell.getCellType();                                                                      // 6.5.0.0 (2016/09/30) poi-3.12
964        //      switch(nCellType) {                                                                                                                     // 6.5.0.0 (2016/09/30) poi-3.12
965//              switch( oCell.getCellTypeEnum() ) {                                                                                     // 6.5.0.0 (2016/09/30) poi-3.15
966                switch( oCell.getCellType() ) {                                                                                         // 7.0.0.0 (2018/10/01) poi-4.0.0 Deprecated.
967        //              case Cell.CELL_TYPE_NUMERIC:                                                                                    // 6.5.0.0 (2016/09/30) poi-3.12
968                        case NUMERIC:                                                                                                                   // 6.5.0.0 (2016/09/30) poi-3.15
969                                        strText = getNumericTypeString( oCell );
970                                        break;
971        //              case Cell.CELL_TYPE_STRING:                                                                                             // 6.5.0.0 (2016/09/30) poi-3.12
972                        case STRING:                                                                                                                    // 6.5.0.0 (2016/09/30) poi-3.15
973        // POI3.0               strText = oCell.getStringCellValue();
974                                        final RichTextString richText = oCell.getRichStringCellValue();
975                                        if( richText != null ) {
976                                                strText = richText.getString();
977                                        }
978                                        break;
979        //              case Cell.CELL_TYPE_FORMULA:                                                                                    // 6.5.0.0 (2016/09/30) poi-3.12
980                        case FORMULA:                                                                                                                   // 6.5.0.0 (2016/09/30) poi-3.15
981        // POI3.0               strText = oCell.getStringCellValue();
982                                        // 5.5.1.2 (2012/04/06) フォーマットセルを実行して、その結果を再帰的に処理する。
983                                //      final Workbook wb = oCell.getSheet().getWorkbook();
984                                //      final CreationHelper crateHelper = wb.getCreationHelper();
985                                //      final FormulaEvaluator evaluator = crateHelper.createFormulaEvaluator();
986
987                                        try {
988                                                // 8.1.2.3 (2022/05/20) 計算式の計算を行う。
989                                                final Workbook wb = oCell.getSheet().getWorkbook();
990                                                final CreationHelper crateHelper = wb.getCreationHelper();
991                                                final FormulaEvaluator evaluator = crateHelper.createFormulaEvaluator();
992                                                final CellValue value = evaluator.evaluate(oCell);
993
994                                                switch (value.getCellType()) {
995                                                        case STRING:
996                                                                strText = value.getStringValue();
997                                                                break;
998                                                        case NUMERIC:
999                                                                strText = Double.toString(value.getNumberValue());
1000                                                                break;
1001                                                        case BOOLEAN:
1002                                                                strText = Boolean.toString(value.getBooleanValue());
1003                                                                break;
1004                                                        default:
1005                                                                strText = oCell.getCellFormula();               // 計算式のまま
1006                                                                break;
1007                                                }
1008
1009                //                              strText = oCell.getCellFormula();                               // 8.1.2.3 (2022/05/20) 計算式は返さない
1010                                                // 8.0.0.0 (2021/07/31) FORMULA処理のエラー対応(出来るだけ…)
1011                                //              final Cell fCell = evaluator.evaluateInCell(oCell);
1012                                //              strText = getValue( fCell );
1013                                        }
1014                                        catch( final Throwable th ) {
1015                                                // 7.3.0.0 (2021/01/06) フォーマットエラー時に、エラーメッセージ取得でもエラーになる。
1016                                //              final String errMsg = "セルフォーマットが解析できません。";
1017                                                // 8.5.0.0 (2023/04/21) セルフォーマット処理エラー時には、スタックトレースは出さずに計算式を返す。
1018                                                final String errMsg = "セルフォーマットが解析できません。"
1019                                                                        + CR + "  Formula=[" + oCell.getCellFormula() + "]"
1020                                                                        + CR + getCellMsg( oCell );
1021        //                                      throw new OgRuntimeException( errMsg,th );
1022                                //              System.err.println( ThrowUtil.ogStackTrace( errMsg,th ) );      // 6.4.2.0 (2016/01/29) , 8.5.0.0 (2023/04/21) Delete
1023                                                System.err.println( errMsg );           // 8.5.0.0 (2023/04/21) Add
1024                                                strText = oCell.toString();                     // 8.5.0.0 (2023/04/21) Add
1025                                        }
1026                                        break;
1027        //              case Cell.CELL_TYPE_BOOLEAN:                                                                                    // 6.5.0.0 (2016/09/30) poi-3.12
1028                        case BOOLEAN:                                                                                                                   // 6.5.0.0 (2016/09/30) poi-3.15
1029                                        strText = String.valueOf(oCell.getBooleanCellValue());
1030                                        break;
1031        //              case Cell.CELL_TYPE_BLANK :                                                                                             // 6.5.0.0 (2016/09/30) poi-3.12
1032                        case BLANK :                                                                                                                    // 6.5.0.0 (2016/09/30) poi-3.15
1033                                        break;
1034        //              case Cell.CELL_TYPE_ERROR:                                                                                              // 6.5.0.0 (2016/09/30) poi-3.12
1035                        case ERROR:                                                                                                                             // 6.5.0.0 (2016/09/30) poi-3.15
1036                                        break;
1037                        default :
1038                                final String errMsg = "セルタイプが不明です。CellType=" + oCell.getCellType()
1039                                                                                + CR + getCellMsg( oCell );             // 8.5.0.0 (2023/04/21) Add
1040                                System.err.println( errMsg );                                                   // 8.5.0.0 (2023/04/21) Add
1041                                strText = oCell.toString();                                                             // 8.5.0.0 (2023/04/21) Add
1042                                break;
1043                }
1044                return strText ;
1045        }
1046
1047        /**
1048         * セルオブジェクト(Cell)に、値をセットします。
1049         *
1050         * セルオブジェクトが存在しない場合は、何もしません。
1051         * 引数は、文字列で渡しますが、セルの形式に合わせて、変換します。
1052         * 変換がうまくいかなかった場合は、エラーになりますので、ご注意ください。
1053         *
1054         * @og.rev 6.3.9.0 (2015/11/06) 新規追加
1055         * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
1056         * @og.rev 7.0.0.0 (2018/10/01) 警告:[deprecation] CellのgetCellTypeEnum()は推奨されません (POI4.0.0)
1057         * @og.rev 7.3.0.0 (2021/01/06) setCellType( CellType.BLANK )(Deprecated) → setBlank() (poi-4.1.2)
1058         *
1059         * @param       oCell   EXCELのセルオブジェクト
1060         * @param       val             セットする値
1061         */
1062        public static void setValue( final Cell oCell , final String val ) {
1063                if( oCell == null ) { return ; }
1064        //      if( val == null || val.isEmpty() ) { oCell.setCellType( Cell.CELL_TYPE_BLANK ); }               // 6.5.0.0 (2016/09/30) poi-3.12
1065        //      if( val == null || val.isEmpty() ) { oCell.setCellType( CellType.BLANK ); }                             // 6.5.0.0 (2016/09/30) poi-3.15
1066                if( val == null || val.isEmpty() ) { oCell.setBlank(); }                                                                // 7.3.0.0 (2021/01/06) poi-4.1.2
1067
1068        //      switch( oCell.getCellType() ) {                                                                         // 6.5.0.0 (2016/09/30) poi-3.12
1069//              switch( oCell.getCellTypeEnum() ) {                                                                     // 6.5.0.0 (2016/09/30) poi-3.15
1070                switch( oCell.getCellType() ) {                                                                         // 7.0.0.0 (2018/10/01) poi-4.0.0 Deprecated.
1071        //              case Cell.CELL_TYPE_NUMERIC:                                                                    // 6.5.0.0 (2016/09/30) poi-3.12
1072                        case NUMERIC:                                                                                                   // 6.5.0.0 (2016/09/30) poi-3.15
1073//                                      oCell.setCellValue( Double.valueOf( val ) );
1074                                        oCell.setCellValue( Double.parseDouble( val ) );                // 7.3.0.0 (2021/01/06) SpotBugs 疑わしいプリミティブ値のボクシング
1075                                        break;
1076        //              case Cell.CELL_TYPE_BOOLEAN:                                                                    // 6.5.0.0 (2016/09/30) poi-3.12
1077                        case BOOLEAN:                                                                                                   // 6.5.0.0 (2016/09/30) poi-3.15
1078                                        oCell.setCellValue( "true".equalsIgnoreCase( val ) );
1079                                        break;
1080                        default :
1081                                        oCell.setCellValue( val );
1082                                        break;
1083                }
1084        }
1085
1086        /**
1087         * セル値が数字の場合に、数字か日付かを判断して、対応する文字列を返します。
1088         *
1089         * @og.rev 3.8.5.3 (2006/08/07) 新規追加
1090         * @og.rev 5.5.7.2 (2012/10/09) HybsDateUtil を利用するように修正します。
1091         * @og.rev 6.3.1.0 (2015/06/28) ExcelStyleFormat を使用します。
1092         *
1093         * @param       oCell   EXCELのセルオブジェクト
1094         * @return      数字の場合は、文字列に変換した結果を、日付の場合は、"yyyyMMddHHmmss" 形式で返します。
1095         */
1096        public static String getNumericTypeString( final Cell oCell ) {
1097                final String strText ;
1098
1099                final double dd = oCell.getNumericCellValue() ;
1100                if( DateUtil.isCellDateFormatted( oCell ) ) {
1101        //              strText = DateSet.getDate( DateUtil.getJavaDate( dd ).getTime() , "yyyyMMddHHmmss" );   // 5.5.7.2 (2012/10/09) HybsDateUtil を利用
1102                        strText = ExcelStyleFormat.dateFormat( dd );
1103                }
1104                else {
1105        //              final NumberFormat numFormat = NumberFormat.getInstance();
1106        //              if( numFormat instanceof DecimalFormat ) {
1107        //                      ((DecimalFormat)numFormat).applyPattern( "#.####" );
1108        //              }
1109        //              strText = numFormat.format( dd );
1110                        final String fmrs = oCell.getCellStyle().getDataFormatString();
1111                        strText = ExcelStyleFormat.getNumberValue( fmrs,dd );
1112                }
1113                return strText ;
1114        }
1115
1116        /**
1117         * 全てのSheetに対して、autoSizeColumn設定を行います。
1118         *
1119         * 重たい処理なので、ファイルの書き出し直前に一度だけ実行するのがよいでしょう。
1120         * autoSize設定で、カラム幅が大きすぎる場合、現状では、
1121         * 初期カラム幅のmaxColCount倍を限度に設定します。
1122         * ただし、maxColCount がマイナスの場合は、無制限になります。
1123         *
1124         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
1125         * @og.rev 6.8.2.4 (2017/11/20) rowObj のnull対策(poi-3.17)
1126         *
1127         * @param       wkbook          処理対象のWorkbook
1128         * @param       maxColCount     最大幅を標準セル幅の何倍にするかを指定。マイナスの場合は、無制限
1129         * @param       dataStRow       データ行の開始位置。未設定時は、-1
1130         */
1131        public static void autoCellSize( final Workbook wkbook , final int maxColCount , final int dataStRow ) {
1132                final int shCnt = wkbook.getNumberOfSheets();
1133
1134                for( int shNo=0; shNo<shCnt; shNo++ ) {
1135                        final Sheet sht = wkbook.getSheetAt( shNo );
1136                        final int defW = sht.getDefaultColumnWidth();           // 標準カラムの文字数
1137                        final int maxWidth = defW*256*maxColCount ;                     // Widthは、文字数(文字幅)*256*最大セル数
1138
1139                        int stR = sht.getFirstRowNum();
1140                        final int edR = sht.getLastRowNum();
1141
1142                        // 6.8.2.4 (2017/11/20) rowObj のnull対策(poi-3.17)
1143                        // poi-3.15 でも同じ現象が出ていますが、sht.getRow( stR ) で、Rowオブジェクトにnullが返ってきます。
1144                        // なんとなく、最後の行だけ、返ってきている感じです。
1145                        // 頻繁には使わないと思いますので、最大カラム数=256 として処理します。
1146
1147                        final Row rowObj = sht.getRow( stR );
1148        //              Row rowObj = sht.getRow( stR );
1149        //              if( rowObj == null ) {
1150        //                      for( int i=stR+1; i<edR; i++ ) {
1151        //                              rowObj = sht.getRow( i );
1152        //                              if( rowObj != null ) { break; }
1153        //                      }
1154        //              }
1155
1156                        final int stC = rowObj == null ? 0   : rowObj.getFirstCellNum();        // 6.8.2.4 (2017/11/20) rowObj のnull対策
1157                        final int edC = rowObj == null ? 256 : rowObj.getLastCellNum();         // 含まない (xlsxでは、最大 16,384 列
1158
1159                        // SheetUtil を使用して、計算範囲を指定します。
1160                        if( stR < dataStRow ) { stR = dataStRow; }              // 計算範囲
1161                        for( int colNo=stC; colNo<edC; colNo++ ) {
1162                                final double wpx = SheetUtil.getColumnWidth( sht,colNo,true,stR,edR );
1163                                if( wpx >= 0.0d ) {                                                     // Cellがないと、マイナス値が戻る。
1164                                        int wd = (int)Math.ceil(wpx * 256) ;
1165                                        if( maxWidth >= 0 && wd > maxWidth ) { wd = maxWidth; } // 最大値が有効な場合は、置き換える
1166                                        sht.setColumnWidth( colNo,wd );
1167                                }
1168                        }
1169
1170                        // Sheet#autoSizeColumn(int) を使用して、自動計算させる場合。
1171        //              for( int colNo=stC; colNo<edC; colNo++ ) {
1172        //                      sht.autoSizeColumn( colNo );
1173        //                      if( maxWidth >= 0 ) {                                   // 最大値が有効な場合は、置き換える
1174        //                              int wd = sht.getColumnWidth( colNo );
1175        //                              if( wd > maxWidth ) { sht.setColumnWidth( colNo,maxWidth ); }
1176        //                      }
1177        //              }
1178                }
1179        }
1180
1181//      /**
1182//       * 指定の Workbook の全Sheetを対象に、実際の有効行と有効カラムを取得します。
1183//       *
1184//       * ※ 現在、唯一LibreOfficeでのみ、xslx 変換できますが、有効行とカラムが
1185//       *    シュリンクされず、無駄な行とカラムが存在します。
1186//       *    これは、xsl で出力されたファイルから有効な値を取得して、xslxに適用させるための
1187//       *    機能で、本来きちんとした有効範囲の xslx が生成されれば、不要な処理です。
1188//       *
1189//       * 配列は、[0]=行の最大値(Sheet#getLastRowNum())と、[1]は有効行の中の列の
1190//       * 最大値(Row#getLastCellNum())を、シートごとにListに追加していきます。
1191//       *
1192//       * @og.rev 8.0.1.0 (2021/10/29) 全Sheetを対象に、実際の有効行と有効カラムを取得
1193//       * @og.rev 8.0.3.0 (2021/12/17) 処理が中途半端だったので、廃止します。
1194//       *
1195//       * @param       wkbook          処理対象のWorkbook
1196//       * @return      シートごとの有効行の配列リスト
1197//       * @see         #activeWorkbook( Workbook,List )
1198//       */
1199//      public static List<int[]> getLastRowCellNum( final Workbook wkbook ) {
1200//              final List<int[]> rcList = new ArrayList<>();                                   // シートごとの有効行の配列リスト
1201//
1202//              final int shCnt = wkbook.getNumberOfSheets();
1203//              for( int shNo=0; shNo<shCnt; shNo++ ) {
1204//                      final Sheet sht = wkbook.getSheetAt( shNo );
1205//                      final int stR = sht.getFirstRowNum();
1206//                      final int edR = sht.getLastRowNum();
1207//                      int lastNo = 0;                                                                                         // 行の有効最大値
1208//                      for( int rowNo=edR; rowNo>=stR && rowNo>=0; rowNo-- ) {         // 逆順に処理します。
1209//                              final Row rowObj = sht.getRow( rowNo );
1210//                              if( rowObj != null ) {
1211//                                      final int edC = rowObj.getLastCellNum();                        // 列の有効最大値
1212//                                      if( lastNo < edC ) { lastNo = edC; }                            // シート内での列の最大有効値
1213//                              }
1214//                      }
1215//                      rcList.add( new int[] {edR,lastNo} );                                           // 有効行の配列
1216//              }
1217//              return rcList;
1218//      }
1219
1220        /**
1221         * 指定の Workbook の全Sheetを対象に、空行を取り除き、全体をシュリンクします。
1222         *
1223         * この処理は、#saveFile( String ) の直前に行うのがよいでしょう。
1224         *
1225         * ここでは、Row を逆順にスキャンし、Cellが 存在しない間は、行を削除します。
1226         * 途中の空行の削除ではなく、最終行からの連続した空行の削除です。
1227         *
1228         * isCellDel=true を指定すると、Cellの末尾削除を行います。
1229         * 有効行の最後のCellから空セルを削除していきます。
1230         * 表形式などの場合は、Cellのあるなしで、レイアウトが崩れる場合がありますので
1231         * 処理が不要な場合は、isCellDel=false を指定してください。
1232         *
1233         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
1234         * @og.rev 6.0.2.3 (2014/10/10) CellStyle の有無も判定基準に含めます。
1235         * @og.rev 6.0.2.5 (2014/10/31) Cellの開始、終了番号が、マイナスのケースの対応
1236         * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
1237         * @og.rev 7.0.0.0 (2018/10/01) 警告:[deprecation] CellのgetCellTypeEnum()は推奨されません (POI4.0.0)
1238         * @og.rev 8.0.1.0 (2021/10/29) CellStyle は not null になったための修正
1239         *
1240         * @param       wkbook          処理対象のWorkbook
1241         * @param       isCellDel       Cellの末尾削除を行うかどうか(true:行う/false:行わない)
1242         * @see         #activeWorkbook( Workbook,List )
1243         */
1244        public static void activeWorkbook( final Workbook wkbook , final boolean isCellDel ) {
1245                final int shCnt = wkbook.getNumberOfSheets();
1246                for( int shNo=0; shNo<shCnt; shNo++ ) {
1247                        final Sheet sht = wkbook.getSheetAt( shNo );
1248                        final int stR = sht.getFirstRowNum();
1249                        final int edR = sht.getLastRowNum();
1250
1251                        boolean isRowDel = true;                                                                                        // 行の削除は、Cellが見つかるまで。
1252                        for( int rowNo=edR; rowNo>=stR && rowNo>=0; rowNo-- ) {                         // 逆順に処理します。
1253                                final Row rowObj = sht.getRow( rowNo );
1254                                if( rowObj != null ) {
1255                                        final int stC = rowObj.getFirstCellNum();
1256                                        final int edC = rowObj.getLastCellNum();
1257                                        // 8.0.1.0 (2021/10/29) 各行の最初のセルスタイルをベースとして比較する。
1258
1259                                        if( stC >= 0 && edC >= 0 ) {                                                            // 8.0.3.0 (2021/12/17) 存在しない場合もある。
1260                                                final CellStyle endCellStyle = rowObj.getCell( stC ).getCellStyle();    // nullチェック入れてない…
1261                                                for( int colNo=edC; colNo>=stC && colNo>=0; colNo-- ) {         // 6.0.2.5 (2014/10/31) stC,edC が、マイナスのケースがある。
1262                                                        final Cell colObj = rowObj.getCell( colNo );
1263                                                        if( colObj != null ) {
1264                                                                final String val = getValue( colObj );
1265                                                                if( colObj.getCellType() != CellType.BLANK && val != null && val.length() > 0 ) {                       // 7.0.0.0 (2018/10/01) poi-4.0.0 Deprecated.
1266                                                                        isRowDel = false;                                       // 一つでも現れれば、行の削除は中止
1267                                                                        break;
1268                                                                }
1269                                                                // 6.0.2.3 (2014/10/10) CellStyle の有無も判定基準に含めます。
1270                                                                // 8.0.1.0 (2021/10/29) 各行の最初のセルスタイルをベースとして比較する。
1271                //                                              else if( colObj.getCellStyle() != null ) {
1272                                                                else if( ! endCellStyle.equals(colObj.getCellStyle()) ) {
1273                                                                        isRowDel = false;                                       // 一つでも現れれば、行の削除は中止
1274                                                                        break;
1275                                                                }
1276                                                                else if( isCellDel ) {
1277                                                                        rowObj.removeCell( colObj );            // CELL_TYPE_BLANK の場合は、削除
1278                                                                }
1279                                                        }
1280                                                }
1281                                        }
1282                                        if( isRowDel ) { sht.removeRow( rowObj );       }
1283                                        else if( !isCellDel ) { break; }                                // Cell の末尾削除を行わない場合は、break すればよい。
1284                                }
1285                        }
1286                }
1287        }
1288
1289        /**
1290         * 指定の Workbook の全Sheetを対象に、実際の有効行と有効カラムを元に全体をシュリンクします。
1291         *
1292         * ※ 現在、唯一LibreOfficeでのみ、xslx 変換できますが、有効行とカラムが
1293         *    シュリンクされず、無駄な行とカラムが存在します。
1294         *    これは、xsl で出力されたファイルから有効な値を取得して、xslxに適用させるための
1295         *    機能で、本来きちんとした有効範囲の xslx が生成されれば、不要な処理です。
1296         *
1297         * 引数のListオブジェクトに従って、無条件に処理を行います。
1298         *
1299         * @og.rev 8.0.1.0 (2021/10/29) 全Sheetを対象に、実際の有効行と有効カラムを取得
1300         * @og.rev 8.0.3.0 (2021/12/17) シート毎の行数Listに変更。
1301         *
1302         * @param       wkbook          処理対象のWorkbook
1303//       * @param       rcList          シートごとの有効行の配列リスト
1304         * @param       rowCntList      シートごとの有効行の配列リスト
1305//       * @see         #getLastRowCellNum( Workbook )
1306         * @see         #activeWorkbook( Workbook,boolean )
1307         */
1308//       public static void activeWorkbook( final Workbook wkbook , final List<int[]> rcList ) {
1309         public static void activeWorkbook( final Workbook wkbook , final List<Integer> rowCntList ) {
1310                final int shCnt = wkbook.getNumberOfSheets();
1311                for( int shNo=0; shNo<shCnt; shNo++ ) {
1312                        final Sheet sht = wkbook.getSheetAt( shNo );
1313//                      final int[] rowcol = rcList.get(shNo);                                          // シート内の有効行と列
1314                        final int stR = rowCntList.get(shNo);                                           // シート内の有効行と列
1315
1316//                      final int stR = rowcol[0];
1317                        final int edR = sht.getLastRowNum();                                            // 参考程度
1318                        // edR~stRまでの行は、無条件に削除します。
1319                        for( int rowNo=edR; rowNo>=stR && rowNo>=0; rowNo-- ) {         // 逆順に処理します。
1320                                final Row rowObj = sht.getRow( rowNo );
1321                                if( rowObj != null ) {
1322                                        sht.removeRow( rowObj );
1323                                }
1324                        }
1325
1326                //      カラム列の削除は保留
1327                //      // stR~0までの行は、有効行なので、カラムの処理を考えます。
1328//              //      final int stC = rowcol[1];                                                                      // シートの中での有効カラムの最大値
1329                //      final int stC = 0;
1330                //      for( int rowNo=stR; rowNo>=0; rowNo-- ) {                                       // 逆順に処理します。
1331                //              final Row rowObj = sht.getRow( rowNo );
1332                //              if( rowObj != null ) {
1333                //                      final int edC = rowObj.getLastCellNum();                        // 参考程度
1334                //                      // edC~stCまでのカラムは、無条件に削除します。
1335                //                      for( int colNo=edC; colNo>=stC && colNo>=0; colNo-- ) { // 6.0.2.5 (2014/10/31) stC,edC が、マイナスのケースがある。
1336                //                              final Cell colObj = rowObj.getCell( colNo );
1337                //                              if( colObj != null ) {
1338                //                                      if( colObj.getCellType() == CellType.BLANK ) {
1339                //                                              rowObj.removeCell( colObj );
1340                //                                      }
1341                //                                      else {
1342                //                                              break;
1343                //                                      }
1344                //                              }
1345                //                      }
1346                //              }
1347                //      }
1348                }
1349        }
1350
1351        /**
1352         * ファイルから、Workbookオブジェクトを新規に作成します。
1353         *
1354         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
1355         * @og.rev 6.2.0.0 (2015/02/27) ファイル引数を、String → File に変更
1356         * @og.rev 7.0.0.0 (2018/10/01) poi-4.0.0 例外InvalidFormatExceptionは対応するtry文の本体ではスローされません
1357         *
1358         * @param       file    入力ファイル
1359         * @return      Workbookオブジェクト
1360         * @og.rtnNotNull
1361         */
1362        public static Workbook createWorkbook( final File file ) {
1363                InputStream fis = null;
1364                try {
1365                        // File オブジェクトでcreate すると、ファイルがオープンされたままになってしまう。
1366                        fis = new BufferedInputStream( new FileInputStream( file ) );
1367                        return WorkbookFactory.create( fis );
1368                }
1369                catch( final IOException ex ) {
1370                        final String errMsg = "ファイル読込みエラー[" + file + "]" + CR + ex.getMessage() ;
1371                        throw new OgRuntimeException( errMsg,ex );
1372                }
1373                // 7.0.0.0 (2018/10/01) poi-4.0.0 対応するtry文の本体ではスローされません
1374//              catch( final InvalidFormatException ex ) {
1375//                      final String errMsg = "ファイル形式エラー[" + file + "]" + CR + ex.getMessage() ;
1376//                      throw new OgRuntimeException( errMsg,ex );
1377//              }
1378                finally {
1379                        Closer.ioClose( fis );
1380                }
1381        }
1382
1383        /**
1384         * シート一覧を、Workbook から取得します。
1385         *
1386         * 取得元が、Workbook なので、xls , xlsx どちらの形式でも取り出せます。
1387         *
1388         * EXCEL上のシート名を、配列で返します。
1389         *
1390         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
1391         *
1392         * @param       wkbook  Workbookオブジェクト
1393         * @return      シート名の配列
1394         */
1395        public static String[] getSheetNames( final Workbook wkbook ) {
1396                final int shCnt = wkbook.getNumberOfSheets();
1397
1398                String[] shtNms = new String[shCnt];
1399
1400                for( int i=0; i<shCnt; i++ ) {
1401                        final Sheet sht = wkbook.getSheetAt( i );
1402                        shtNms[i] = sht.getSheetName();
1403                }
1404
1405                return shtNms;
1406        }
1407
1408        /**
1409         * 名前定義一覧を取得します。
1410         *
1411         * EXCEL上に定義された名前を、配列で返します。
1412         * ここでは、名前とFormulaをタブで連結した文字列を配列で返します。
1413         * Name オブジェクトを削除すると、EXCELが開かなくなったりするので、
1414         * 取りあえず一覧を作成して、手動で削除してください。
1415         * なお、名前定義には、非表示というのがありますので、ご注意ください。
1416         *
1417         * ◆ 非表示になっている名前の定義を表示にする EXCEL VBA マクロ
1418         * http://dev.classmethod.jp/tool/excel-delete-name/
1419         *    Sub VisibleNames()
1420         *        Dim name
1421         *        For Each name In ActiveWorkbook.Names
1422         *            If name.Visible = False Then
1423         *                name.Visible = True
1424         *            End If
1425         *        Next
1426         *        MsgBox "すべての名前の定義を表示しました。", vbOKOnly
1427         *    End Sub
1428         *
1429         * ※ EXCEL2010 数式タブ→名前の管理 で、複数選択で、削除できます。
1430         *    ただし、非表示の名前定義は、先に表示しないと、削除できません。
1431         * ◆ 名前の一括削除 EXCEL VBA マクロ
1432         * http://komitsudo.blog70.fc2.com/blog-entry-104.html
1433         *    Sub DeleteNames()
1434         *        Dim name
1435         *        On Error Resume Next
1436         *        For Each name In ActiveWorkbook.Names
1437         *            If Not name.BuiltIn Then
1438         *                name.Delete
1439         *            End If
1440         *        Next
1441         *        MsgBox "すべての名前の定義を削除しました。", vbOKOnly
1442         *    End Sub
1443         *
1444         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
1445         * @og.rev 7.0.0.0 (2018/10/01) 警告:[deprecation] WorkbookのgetNameAt(int)は推奨されません (POI4.0.0)
1446         *
1447         * @param       wkbook  Workbookオブジェクト
1448         * @return      名前定義(名前+TAB+Formula)の配列
1449         * @og.rtnNotNull
1450         */
1451        public static String[] getNames( final Workbook wkbook ) {
1452//              final int cnt = wkbook.getNumberOfNames();
1453
1454                final Set<String> nmSet = new TreeSet<>();
1455
1456                // 7.0.0.0 (2018/10/01) 警告:[deprecation] WorkbookのgetNameAt(int)は推奨されません (POI4.0.0)
1457//              for( int i=0; i<cnt; i++ ) {
1458                for( final Name nm : wkbook.getAllNames() ) {
1459                        String name     = null;
1460                        String ref      = null;
1461
1462//                      final Name nm = wkbook.getNameAt(i);
1463                        try {
1464                                name = nm.getNameName();
1465                                ref  = nm.getRefersToFormula();
1466                        }
1467        //              catch( final Exception ex ) {                                   // 6.1.0.0 (2014/12/26) refactoring
1468                        catch( final RuntimeException ex ) {
1469                                final String errMsg = "POIUtil:RefersToFormula Error! name=[" + name + "]" + ex.getMessage() ;
1470                                System.out.println( errMsg );
1471                                // Excel97形式の場合、getRefersToFormula() でエラーが発生することがある。
1472                        }
1473
1474                        nmSet.add( name + "\t" + ref );
1475
1476                        // 削除するとEXCELが壊れる? なお、削除時には逆順で廻さないとアドレスがずれます。
1477                        // if( nm.isDeleted() ) { wkbook.removeName(i); }
1478                }
1479
1480                return nmSet.toArray( new String[nmSet.size()] );
1481        }
1482
1483        /**
1484         * 書式のスタイル一覧を取得します。
1485         *
1486         * EXCEL上に定義された書式のスタイルを、配列で返します。
1487         * 書式のスタイルの名称は、CellStyle にメソッドが定義されていません。
1488         * 実クラスである HSSFCellStyle にキャストして使用する
1489         * 必要があります。(XSSFCellStyle にも名称を取得するメソッドがありません。)
1490         *
1491         * ※ EXCEL2010 ホームタブ→セルのスタイル は、一つづつしか削除できません。
1492         *    マクロは、開発タブ→Visual Basic で、挿入→標準モジュール を開き
1493         *    テキストを張り付けてください。
1494         *    実行は、開発タブ→マクロ で、マクロ名を選択して、実行します。
1495         *    最後は、削除してください。
1496         *
1497         * ◆ スタイルの一括削除 EXCEL VBA マクロ
1498         * http://komitsudo.blog70.fc2.com/blog-entry-104.html
1499         *    Sub DeleteStyle()
1500         *        Dim styl
1501         *        On Error Resume Next
1502         *        For Each styl In ActiveWorkbook.Styles
1503         *            If Not styl.BuiltIn Then
1504         *                styl.Delete
1505         *            End If
1506         *        Next
1507         *        MsgBox "すべての追加スタイルを削除しました。", vbOKOnly
1508         *    End Sub
1509         *
1510         * ◆ 名前の表示、削除、スタイルの削除の一括実行 EXCEL VBA マクロ
1511         *    Sub AllDelete()
1512         *        Call VisibleNames
1513         *        Call DeleteNames
1514         *        Call DeleteStyle
1515         *        MsgBox "すべての処理を完了しました。", vbOKOnly
1516         *    End Sub
1517         *
1518         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
1519         *
1520         * @param       wkbook  Workbookオブジェクト
1521         * @return      書式のスタイル一覧
1522         * @og.rtnNotNull
1523         */
1524        public static String[] getStyleNames( final Workbook wkbook ) {
1525                final int cnt = wkbook.getNumCellStyles();              // return 値は、short
1526
1527                final Set<String> nmSet = new TreeSet<>();
1528
1529                for( int s=0; s<cnt; s++ ) {
1530                        final CellStyle cs = wkbook.getCellStyleAt( (short)s );
1531                        if( cs instanceof HSSFCellStyle ) {
1532                                final HSSFCellStyle hcs = (HSSFCellStyle)cs;
1533                                final String name = hcs.getUserStyleName();
1534                                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
1535                                if( name == null ) {                                                    // この処理は不要かも。
1536                                        final HSSFCellStyle pst = hcs.getParentStyle();
1537                                        if( pst != null ) {
1538                                                final String pname = pst.getUserStyleName();
1539                                                if( pname != null ) { nmSet.add( pname ); }
1540                                        }
1541                                }
1542                                else {
1543                                        nmSet.add( name );
1544                                }
1545                        }
1546                }
1547
1548                return nmSet.toArray( new String[nmSet.size()] );
1549        }
1550
1551        /**
1552         * セル情報を返します。
1553         *
1554         * エラー発生時に、どのセルでエラーが発生したかの情報を取得できるようにします。
1555         *
1556         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
1557         * @og.rev 6.0.3.0 (2014/11/13) セル情報を作成する時に、値もセットします。
1558         * @og.rev 6.2.2.0 (2015/03/27) celKigo を求めるロジック変更
1559         * @og.rev 6.3.1.0 (2015/06/28) rowNo(行番号)も引数に取るようにします。
1560         *
1561         * @param       oCell   EXCELのセルオブジェクト
1562         * @return      セル情報の文字列
1563         */
1564        public static String getCellMsg( final Cell oCell ) {
1565                String lastMsg = null;
1566
1567                if( oCell != null ) {
1568                        final String shtNm = oCell.getSheet().getSheetName();
1569                        final int  rowNo = oCell.getRowIndex();
1570                        final int  celNo = oCell.getColumnIndex();
1571
1572                        // 6.0.3.0 (2014/11/13) セル情報を作成する時に、値もセットします。
1573                        lastMsg = "  Sheet=" + shtNm + ", Row=" + rowNo + ", Cel=" + celNo
1574                                                 + "(" + getCelKigo(rowNo,celNo) + "), Val=" + oCell.toString() ;
1575                }
1576
1577                return lastMsg;
1578        }
1579
1580        /**
1581         * Excelの行番号,列番号より、セル記号を求めます。
1582         *
1583         * 行番号は、0から始まる数字ですが、記号化する場合は、1から始まります。
1584         * Excelの列記号とは、A,B,C,…,Z,AA,AB,…,ZZ,AAA,AAB,… と続きます。
1585         * つまり、アルファベットだけの、26進数になります。(ゼロの扱いが少し特殊です)
1586         * 列番号は、0から始まる数字で、0=A,1=B,2=C,…,25=Z,26=AA,27=AB,…,701=ZZ,702=AAA,703=AAB,…
1587         * EXCELの行列記号にする場合は、この列記号に、行番号を、+1して付ければよいだけです。
1588         * (※ 列番号に+1するのは、内部では0から始まる列番号ですが、表示上は1から始まります)
1589         *
1590         * @og.rev 6.2.2.0 (2015/03/27) celKigo を求めるロジック変更
1591         * @og.rev 6.3.1.0 (2015/06/28) rowNo(行番号)も引数に取るようにします。
1592         *
1593         * @param       rowNo   行番号(0,1,2,…)
1594         * @param       colNo   列番号(0,1,2,…)
1595         * @return      Excelの列記号(A1,B2,C3,…)
1596         */
1597        public static String getCelKigo( final int rowNo,final int colNo ) {
1598                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
1599                int cnt = colNo;
1600                while( cnt >= 26 ) {
1601                        buf.append( (char)('A'+cnt%26) );
1602                        cnt = cnt/26-1;
1603                }
1604                buf.append( (char)('A'+cnt%26) )
1605                        .reverse()                                                              // append で逆順に付けているので、反転して返します。
1606                        .append( rowNo+1 );
1607
1608                return buf.toString();
1609        }
1610
1611        /**
1612         * XSSFSimpleShapeオブジェクトにリンクを設定します。
1613         *
1614         * 処理の簡素化のために、url引数が null の場合は、何もせず処理を終了します。
1615         *
1616         * @og.rev 8.1.0.1 (2022/01/07) テキストベースのリンク作成
1617         *
1618         * @param       shape   XSSFSimpleShapeオブジェクト
1619         * @param       url             リンク文字列
1620         */
1621        public static void makeShapeLink( final XSSFSimpleShape shape , final String url ) {
1622                if( url == null ) { return; }
1623
1624                final String rid = shape.getDrawing()                   // XSSFDrawing  XSSFShape#getDrawing()
1625                                                        .getPackagePart()                       // PackagePart  POIXMLDocumentPart#getPackagePart()
1626                                                        .addExternalRelationship(       // PackageRelationship  PackagePart#addExternalRelationship(String,String)
1627                                                                        url, PackageRelationshipTypes.HYPERLINK_PART)
1628                                                        .getId();                                       // String               PackageRelationship#getId()
1629
1630                final CTHyperlink hyperlink = CTHyperlink.Factory.newInstance();
1631                hyperlink.setId(rid);
1632
1633                shape.getCTShape()                                                              // CTShape                                      XSSFSimpleShape#getCTShape()
1634                        .getNvSpPr()                                                            // CTShapeNonVisual                     CTShape#getNvSpPr()
1635                        .getCNvPr()                                                                     // CTNonVisualDrawingProps      CTShapeNonVisual#getCNvPr()
1636                        .setHlinkClick( hyperlink );                            // void                                         CTNonVisualDrawingProps#setHlinkClick(CTHyperlink)
1637        }
1638
1639        /**
1640         * XSSFSimpleShapeオブジェクトにカラーを設定します。
1641         *
1642         * 処理の簡素化のために、col引数が null の場合は、何もせず処理を終了します。
1643         * col配列は、[0]:red [1]:blue [2] green です。
1644         *
1645         * ※ カラーの設定を、XSSFSimpleShape#setFillColor(int,int,int) で行うと、XSLXファイルが
1646         * 壊れるようです。POIが対応できていないのか、カラー化設定方法を間違っているのか…
1647         *
1648         * @og.rev 8.1.0.1 (2022/01/07) テキストベースのカラー作成
1649         *
1650         * @param       shape   XSSFSimpleShapeオブジェクト
1651         * @param       col             色配列
1652         */
1653        public static void makeShapeColor( final XSSFSimpleShape shape , final int[] col ) {
1654                if( col != null ) {
1655                        shape.setFillColor(col[0],col[1],col[2]);
1656                }
1657        }
1658
1659        /**
1660         * アプリケーションのサンプルです。
1661         *
1662         * 入力ファイル名 は必須で、第一引数固定です。
1663         * 第二引数は、処理方法で、-ALL か、-LINE を指定します。何も指定しなければ、-ALL です。
1664         * 第三引数を指定した場合は、Encode を指定します。
1665         *
1666         * Usage: java org.opengion.fukurou.model.POIUtil 入力ファイル名 [処理方式] [エンコード]
1667         *   -A(LL)        ・・・ ALL 一括処理(初期値)
1668         *   -L(INE)       ・・・ LINE 行単位処理
1669         *   -S(heet)      ・・・ Sheet名一覧
1670         *   -N(AME)       ・・・ NAME:名前定義
1671         *   -C(ellStyle)  ・・・ CellStyle:書式のスタイル
1672         *
1673         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
1674         * @og.rev 6.2.3.0 (2015/05/01) パラメータ変更、textReader → extractor に変更
1675         * @og.rev 6.2.4.2 (2015/05/29) 引数判定の true/false の処理が逆でした。
1676         * @og.rev 6.3.9.0 (2015/11/06) Java 8 ラムダ式に変更
1677         *
1678         * @param       args    コマンド引数配列
1679         */
1680        public static void main( final String[] args ) {
1681                final String usageMsg = "Usage: java org.opengion.fukurou.model.POIUtil 入力ファイル名 [処理方式] [エンコード]" + "\n" +
1682                                                                "\t -A(LL)        ・・・ ALL 一括処理(初期値)      \n" +
1683                                                                "\t -L(INE)       ・・・ LINE 行単位処理           \n" +
1684                                                                "\t -S(heet)      ・・・ Sheet名一覧               \n" +
1685                                                                "\t -N(AME)       ・・・ NAME:名前定義             \n" +
1686                                                                "\t -C(ellStyle)  ・・・ CellStyle:書式のスタイル  \n" ;
1687                if( args.length == 0 ) {
1688                        System.err.println( usageMsg );
1689                        return ;
1690                }
1691
1692                final File file = new File( args[0] );
1693                final char type     = args.length >= 2 ? args[1].charAt(1) : 'A' ;
1694                final String encode = args.length >= 3 ? args[2] : null ;                               // 6.2.4.2 (2015/05/29) true/false の処理が逆でした。
1695
1696                switch( type ) {
1697                        case 'A' :  if( encode == null ) {
1698                                                        System.out.println( POIUtil.extractor( file ) );
1699                                                }
1700                                                else {
1701                                                        System.out.println( POIUtil.extractor( file,encode ) );
1702                                                }
1703                                                break;
1704                        // 6.3.9.0 (2015/11/06) Java 8 ラムダ式に変更
1705                        case 'L' : final TextConverter<String,String> conv =
1706                                                                ( val,cmnt ) -> {
1707                                                                        System.out.println( "val=" + val + " , cmnt=" + cmnt );
1708                                                                        return null;
1709                                                                };
1710
1711                                        //              new TextConverter<String,String>() {
1712                                        //                      /**
1713                                        //                       * 入力文字列を、変換します。
1714                                        //                       *
1715                                        //                       * @param       val  入力文字列
1716                                        //                       * @param       cmnt コメント
1717                                        //                       * @return      変換文字列(変換されない場合は、null)
1718                                        //                       */
1719                                        //                      @Override
1720                                        //                      public String change( final String val , final String cmnt ) {
1721                                        //                              System.out.println( "val=" + val + " , cmnt=" + cmnt );
1722                                        //                              return null;
1723                                        //                      }
1724                                        //              };
1725
1726                                                if( encode == null ) {
1727                                                        POIUtil.textReader( file,conv );
1728                                                }
1729                                                else {
1730                                                        POIUtil.textReader( file,conv,encode );
1731                                                }
1732                                                break;
1733                        case 'S' :  final String[] shts = POIUtil.getSheetNames( POIUtil.createWorkbook(file) );
1734                                                System.out.println( "No:\tSheetName" );
1735                                                for( int i=0; i<shts.length; i++ ) {
1736                                                        System.out.println( i + "\t" + shts[i] );
1737                                                }
1738                                                break;
1739                        case 'N' :  final String[] nms = POIUtil.getNames( POIUtil.createWorkbook(file) );
1740                                                System.out.println( "No:\tName\tFormula" );
1741                                                for( int i=0; i<nms.length; i++ ) {
1742                                                        System.out.println( i + "\t" + nms[i] );
1743                                                }
1744                                                break;
1745                        case 'C' :  final String[] sns = POIUtil.getStyleNames( POIUtil.createWorkbook(file) );
1746                                                System.out.println( "No:\tStyleName" );
1747                                                for( int i=0; i<sns.length; i++ ) {
1748                                                        System.out.println( i + "\t" + sns[i] );
1749                                                }
1750                                                break;
1751                        default :   System.err.println( usageMsg );
1752                                                break;
1753                }
1754        }
1755}