001/*
002 * Copyright (c) 2017 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.fileexec;
017
018import java.util.List;
019import java.util.function.Consumer;
020
021import java.io.File;
022import java.io.PrintWriter;
023import java.io.BufferedReader;
024import java.io.FileInputStream ;
025import java.io.InputStreamReader ;
026import java.io.IOException;
027
028import java.nio.file.Path;
029import java.nio.file.Files;
030import java.nio.file.Paths;
031import java.nio.file.FileVisitor;
032import java.nio.file.SimpleFileVisitor;
033import java.nio.file.FileVisitResult;
034import java.nio.file.StandardOpenOption;
035import java.nio.file.StandardCopyOption;
036import java.nio.file.attribute.BasicFileAttributes;
037import java.nio.file.OpenOption;
038import java.nio.file.NoSuchFileException;                                       // 7.2.5.0 (2020/06/01)
039import java.nio.channels.FileChannel;
040import java.nio.channels.OverlappingFileLockException;
041import java.nio.charset.Charset;
042import java.nio.charset.MalformedInputException;                        // 7.2.5.0 (2020/06/01)
043import static java.nio.charset.StandardCharsets.UTF_8;          // 7.2.5.0 (2020/06/01)
044
045/**
046 * FileUtilは、共通的に使用されるファイル操作関連のメソッドを集約した、ユーティリティークラスです。
047 *
048 *<pre>
049 * 読み込みチェックや、書き出しチェックなどの簡易的な処理をまとめているだけです。
050 *
051 *</pre>
052 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
053 *
054 * @version  7.0
055 * @author   Kazuhiko Hasegawa
056 * @since    JDK1.8,
057 */
058public final class FileUtil {
059        private static final XLogger LOGGER= XLogger.getLogger( FileUtil.class.getSimpleName() );               // ログ出力
060
061        /** ファイルが安定するまでの待ち時間(ミリ秒) {@value} */
062        public static final int STABLE_SLEEP_TIME  = 2000 ;     // ファイルが安定するまで、2秒待つ
063        /** ファイルが安定するまでのリトライ回数 {@value} */
064        public static final int STABLE_RETRY_COUNT = 10 ;       // ファイルが安定するまで、10回リトライする。
065
066        /** ファイルロックの獲得までの待ち時間(ミリ秒) {@value} */
067        public static final int LOCK_SLEEP_TIME  = 2000 ;       // ロックの獲得まで、2秒待つ
068        /** ファイルロックの獲得までのリトライ回数 {@value} */
069        public static final int LOCK_RETRY_COUNT = 10 ;         // ロックの獲得まで、10回リトライする。
070
071        /** 日本語用の、Windows-31J の、Charset  */
072        public static final Charset WINDOWS_31J = Charset.forName( "Windows-31J" );
073
074//      /** 日本語用の、UTF-8 の、Charset (Windows-31Jと同じように指定できるようにしておきます。)  */
075//      public static final Charset UTF_8               = StandardCharsets.UTF_8;
076
077        private static final OpenOption[] CREATE = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.TRUNCATE_EXISTING };
078        private static final OpenOption[] APPEND = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.APPEND };
079
080        private static final Object STATIC_LOCK = new Object();         // staticレベルのロック
081
082        /**
083         * デフォルトコンストラクターをprivateにして、
084         * オブジェクトの生成をさせないようにする。
085         */
086        private FileUtil() {}
087
088        /**
089         * 引数の文字列を連結した読み込み用パスのチェックを行い、存在する場合は、そのパスオブジェクトを返します。
090         *
091         * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加えたものです。
092         * そのパスが存在しなければ、例外をThrowします。
093         *
094         * @og.rev 1.0.0 (2016/04/28) 新規追加
095         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
096         *
097         * @param       first   パス文字列またはパス文字列の最初の部分
098         * @param       more    結合してパス文字列を形成するための追加文字列
099         * @return      指定の文字列を連結したパスオブジェクト
100         * @throws      RuntimeException ファイル/フォルダは存在しない場合
101         * @see         Paths#get(String,String...)
102         */
103        public static Path readPath( final String first , final String... more ) {
104                final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ;
105
106//              if( !Files.exists( path ) ) {
107                if( !exists( path ) ) {                                                 // 7.2.5.0 (2020/06/01)
108                        // MSG0002 = ファイル/フォルダは存在しません。file=[{0}]
109//                      throw MsgUtil.throwException( "MSG0002" , path );
110                        final String errMsg = "FileUtil#readPath : Path=" + path ;
111                        throw MsgUtil.throwException( "MSG0002" , errMsg );
112                }
113
114                return path;
115        }
116
117        /**
118         * 引数の文字列を連結した書き込み用パスを作成します。
119         *
120         * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加え、
121         * そのパスが存在しなければ、作成します。
122         * パスが、フォルダの場合は、そのまま作成し、ファイルの場合は、親フォルダまでを作成します。
123         * パスがフォルダかファイルかの区別は、拡張子があるかどうかで判定します。
124         *
125         * @og.rev 1.0.0 (2016/04/28) 新規追加
126         *
127         * @param       first   パス文字列またはパス文字列の最初の部分
128         * @param       more    結合してパス文字列を形成するための追加文字列
129         * @return      指定の文字列を連結したパスオブジェクト
130         * @throws      RuntimeException ファイル/フォルダが作成できなかった場合
131         * @see         Paths#get(String,String...)
132         */
133        public static Path writePath( final String first , final String... more ) {
134                final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ;
135
136                mkdirs( path,false );
137
138                return path;
139        }
140
141        /**
142         * ファイルオブジェクトを作成します。
143         *
144         * 通常は、フォルダ+ファイル名で、新しいファイルオブジェクトを作成します。
145         * ここでは、第2引数のファイル名に、絶対パスを指定した場合は、第1引数の
146         * フォルダを使用せず、ファイル名だけで、ファイルオブジェクトを作成します。
147         * 第2引数のファイル名が、null か、ゼロ文字列の場合は、第1引数の
148         * フォルダを返します。
149         *
150         * @og.rev 7.2.1.0 (2020/03/13) isAbsolute(String)を利用します。
151         *
152         * @param path  基準となるフォルダ(ファイルの場合は、親フォルダ基準)
153         * @param fname ファイル名(絶対パス、または、相対パス)
154         * @return 合成されたファイルオブジェクト
155         */
156        public static Path newPath( final Path path , final String fname ) {
157                if( fname == null || fname.isEmpty() ) {
158                        return path;
159                }
160//              else if( fname.charAt(0) == '/'                                                 ||              // 実フォルダが UNIX
161//                               fname.charAt(0) == '\\'                                                ||              // 実フォルダが ネットワークパス
162//                               fname.length() > 1 && fname.charAt(1) == ':' ) {               // 実フォルダが Windows
163                else if( isAbsolute( fname ) ) {
164                        return new File( fname ).toPath();
165                }
166                else {
167                        return path.resolve( fname );
168                }
169        }
170
171        /**
172         * ファイルアドレスが絶対パスかどうか[絶対パス:true]を判定します。
173         *
174         * ファイル名が、絶対パス('/' か、'\\' か、2文字目が ':' の場合)かどうかを
175         * 判定して、絶対パスの場合は、true を返します。
176         * それ以外(nullやゼロ文字列も含む)は、false になります。
177         *
178         * @og.rev 7.2.1.0 (2020/03/13) 新規追加
179         *
180         * @param fname ファイルパスの文字列(絶対パス、相対パス、null、ゼロ文字列)
181         * @return 絶対パスの場合は true
182         */
183        public static boolean isAbsolute( final String fname ) {
184//              return fname != null && (
185                return fname != null && !fname.isEmpty() && (
186                                   fname.charAt(0) == '/'                                                               // 実フォルダが UNIX
187                                || fname.charAt(0) == '\\'                                                              // 実フォルダが ネットワークパス
188                                || fname.length() > 1 && fname.charAt(1) == ':' );              // 実フォルダが Windows
189        }
190
191        /**
192         * 引数のファイルパスを親階層を含めて生成します。
193         *
194         * すでに存在している場合や作成が成功した場合は、true を返します。
195         * 作成に失敗した場合は、false です。
196         * 指定のファイルパスは、フォルダであることが前提ですが、簡易的に
197         * ファイルの場合は、その親階層のフォルダを作成します。
198         * ファイルかフォルダの判定は、拡張子があるか、ないかで判定します。
199         *
200         * @og.rev 1.0.0 (2016/04/28) 新規追加
201         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
202         *
203         * @param       target  ターゲットのファイルパス
204         * @param       parentCheck     先に親フォルダの作成を行うかどうか(true:行う)
205         * @throws      RuntimeException フォルダの作成に失敗した場合
206         */
207//      public static void mkdirs( final Path target ) {
208        public static void mkdirs( final Path target,final boolean parentCheck ) {
209//              if( Files.notExists( target ) ) {               // 存在しない場合
210                if( !exists( target ) ) {                               // 存在しない場合 7.2.5.0 (2020/06/01)
211                        // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
212//                      final boolean isFile = target.getFileName().toString().contains( "." );         // ファイルかどうかは、拡張子の有無で判定する。
213
214                        final Path tgtName = target.getFileName();
215                        if( tgtName == null ) {
216                                // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
217                                throw MsgUtil.throwException( "MSG0007" , target.toString() );
218                        }
219
220                        final boolean isFile = tgtName.toString().contains( "." );                                      // ファイルかどうかは、拡張子の有無で判定する。
221//                      final Path dir = isFile ? target.toAbsolutePath().getParent() : target ;        // ファイルなら、親フォルダを取り出す。
222                        final Path dir = isFile ? target.getParent() : target ;                                         // ファイルなら、親フォルダを取り出す。
223                        if( dir == null ) {
224                                // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
225                                throw MsgUtil.throwException( "MSG0007" , target.toString() );
226                        }
227
228//                      if( Files.notExists( dir ) ) {          // 存在しない場合
229                        if( !exists( dir ) ) {                          // 存在しない場合 7.2.5.0 (2020/06/01)
230                                try {
231                                        Files.createDirectories( dir );
232                                }
233                                catch( final IOException ex ) {
234                                        // MSG0007 = ファイル/フォルダの作成に失敗しました。dir=[{0}]
235                                        throw MsgUtil.throwException( ex , "MSG0007" , dir );
236                                }
237                        }
238                }
239        }
240
241        /**
242         * 単体ファイルをコピーします。
243         *
244         * コピー先がなければ、コピー先のフォルダ階層を作成します。
245         * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。
246         * コピー先のファイルがすでに存在する場合は、上書きされますので、
247         * 必要であれば、先にバックアップしておいて下さい。
248         *
249         * @og.rev 1.0.0 (2016/04/28) 新規追加
250         *
251         * @param from  コピー元となるファイル
252         * @param to    コピー先となるファイル
253         * @throws      RuntimeException ファイル操作に失敗した場合
254         * @see         #copy(Path,Path,boolean)
255         */
256        public static void copy( final Path from , final Path to ) {
257                copy( from,to,false );
258        }
259
260        /**
261         * パスの共有ロックを指定した、単体ファイルをコピーします。
262         *
263         * コピー先がなければ、コピー先のフォルダ階層を作成します。
264         * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。
265         * コピー先のファイルがすでに存在する場合は、上書きされますので、
266         * 必要であれば、先にバックアップしておいて下さい。
267         *
268         * ※ copy に関しては、コピー時間を最小化する意味で、synchronized しています。
269         *
270         * @og.rev 1.0.0 (2016/04/28) 新規追加
271         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
272         *
273         * @param from  コピー元となるファイル
274         * @param to    コピー先となるファイル
275         * @param useLock       パスを共有ロックするかどうか
276         * @throws      RuntimeException ファイル操作に失敗した場合
277         * @see         #copy(Path,Path)
278         */
279        public static void copy( final Path from , final Path to , final boolean useLock ) {
280//              if( Files.exists( from ) ) {
281                if( exists( from ) ) {                                                  // 7.2.5.0 (2020/06/01)
282                        mkdirs( to,false );
283
284                        // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
285//                      final boolean isFile = to.getFileName().toString().contains( "." );                     // ファイルかどうかは、拡張子の有無で判定する。
286
287                        final Path toName = to.getFileName();
288                        if( toName == null ) {
289                                // MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}]
290                                throw MsgUtil.throwException( "MSG0008" , from.toString() , to.toString() );
291                        }
292
293                        final boolean isFile = toName.toString().contains( "." );               // ファイルかどうかは、拡張子の有無で判定する。
294
295                        // コピー先がフォルダの場合は、コピー元と同じ名前のファイルにする。
296                        final Path save = isFile ? to : to.resolve( from.getFileName() );
297
298                        synchronized( STATIC_LOCK ) {
299                                if( useLock ) {
300                                        lockPath( from , in -> localCopy( in , save ) );
301                                }
302                                else {
303                                        localCopy( from , save );
304                                }
305                        }
306                }
307                else {
308                        // 7.2.5.0 (2020/06/01)
309                        // MSG0002 = ファイル/フォルダが存在しません。file=[{0}]
310//                      MsgUtil.errPrintln( "MSG0002" , from );
311                        final String errMsg = "FileUtil#copy : from=" + from ;
312                        LOGGER.warning( "MSG0002" , errMsg );
313                }
314        }
315
316        /**
317         * 単体ファイルをコピーします。
318         *
319         * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
320         *
321         * @og.rev 1.0.0 (2016/04/28) 新規追加
322         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
323         *
324         * @param from  コピー元となるファイル
325         * @param to    コピー先となるファイル
326         */
327        private static void localCopy( final Path from , final Path to ) {
328                try {
329                        // 直前に存在チェックを行います。
330//                      if( Files.exists( from ) ) {
331                        if( exists( from ) ) {                                  // 7.2.5.0 (2020/06/01)
332                                Files.copy( from , to , StandardCopyOption.REPLACE_EXISTING );
333                        }
334                }
335                catch( final IOException ex ) {
336                        // MSG0012 = ファイルがコピーできませんでした。from=[{0}] to=[{1}]
337//                      MsgUtil.errPrintln( ex , "MSG0012" , from , to );
338                        LOGGER.warning( ex , "MSG0012" , from , to );
339                }
340        }
341
342        /**
343         * 単体ファイルを移動します。
344         *
345         * 移動先がなければ、移動先のフォルダ階層を作成します。
346         * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。
347         * 移動先のファイルがすでに存在する場合は、上書きされますので、
348         * 必要であれば、先にバックアップしておいて下さい。
349         *
350         * @og.rev 1.0.0 (2016/04/28) 新規追加
351         *
352         * @param from  移動元となるファイル
353         * @param to    移動先となるファイル
354         * @throws      RuntimeException ファイル操作に失敗した場合
355         * @see         #move(Path,Path,boolean)
356         */
357        public static void move( final Path from , final Path to ) {
358                move( from,to,false );
359        }
360
361        /**
362         * パスの共有ロックを指定した、単体ファイルを移動します。
363         *
364         * 移動先がなければ、移動先のフォルダ階層を作成します。
365         * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。
366         * 移動先のファイルがすでに存在する場合は、上書きされますので、
367         * 必要であれば、先にバックアップしておいて下さい。
368         *
369         * ※ move に関しては、ムーブ時間を最小化する意味で、synchronized しています。
370         *
371         * @og.rev 1.0.0 (2016/04/28) 新規追加
372         * @og.rev 7.2.1.0 (2020/03/13) from,to が null の場合、処理しない。
373         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
374         *
375         * @param from  移動元となるファイル
376         * @param to    移動先となるファイル
377         * @param useLock       パスを共有ロックするかどうか
378         * @throws      RuntimeException ファイル操作に失敗した場合
379         * @see         #move(Path,Path)
380         */
381        public static void move( final Path from , final Path to , final boolean useLock ) {
382                if( from == null || to == null ) { return; }                    // 7.2.1.0 (2020/03/13)
383
384//              if( Files.exists( from ) ) {
385                if( exists( from ) ) {                                  // 1.4.0 (2019/09/01)
386                        mkdirs( to,false );
387
388                        // ファイルかどうかは、拡張子の有無で判定する。
389                        // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
390//                      final boolean isFile = to.getFileName().toString().contains( "." );
391                        final Path toName = to.getFileName();
392                        if( toName == null ) {
393                                // MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}]
394                                throw MsgUtil.throwException( "MSG0008" , to.toString() );
395                        }
396
397                        final boolean isFile = toName.toString().contains( "." );               // ファイルかどうかは、拡張子の有無で判定する。
398
399                        // 移動先がフォルダの場合は、コピー元と同じ名前のファイルにする。
400                        final Path save = isFile ? to : to.resolve( from.getFileName() );
401
402                        synchronized( STATIC_LOCK ) {
403                                if( useLock ) {
404                                        lockPath( from , in -> localMove( in , save ) );
405                                }
406                                else {
407                                        localMove( from , save );
408                                }
409                        }
410                }
411                else {
412                        // MSG0002 = ファイル/フォルダが存在しません。file=[{0}]
413//                      MsgUtil.errPrintln( "MSG0002" , from );
414                        final String errMsg = "FileUtil#move : from=" + from ;
415                        LOGGER.warning( "MSG0002" , errMsg );
416                }
417        }
418
419        /**
420         * 単体ファイルを移動します。
421         *
422         * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
423         *
424         * @og.rev 1.0.0 (2016/04/28) 新規追加
425         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
426         *
427         * @param from  移動元となるファイル
428         * @param to    移動先となるファイル
429         */
430        private static void localMove( final Path from , final Path to ) {
431                try {
432        //              synchronized( from ) {
433                                // 直前に存在チェックを行います。
434//                              if( Files.exists( from ) ) {
435                                if( exists( from ) ) {                                          // このメソッドの結果がすぐに古くなることに注意してください。
436                                        // CopyOption に、StandardCopyOption.ATOMIC_MOVE を指定すると、別サーバー等へのMOVEは、出来なくなります。
437                                //      try{ Thread.sleep( 2000 ); } catch( final InterruptedException ex ){}                           // 先に、無条件に待ちます。
438                                        Files.move( from , to , StandardCopyOption.REPLACE_EXISTING );
439                                }
440        //              }
441                }
442                catch( final NoSuchFileException ex ) {                         // 7.2.5.0 (2020/06/01)
443                        LOGGER.warning( "MSG0008" , from , to );                // 原因不明:FileWatchとDirWatchの両方が動いているから?
444                }
445                catch( final IOException ex ) {
446                        // MSG0008 = ファイルが移動できませんでした。from=[{0}] to=[{1}]
447//                      MsgUtil.errPrintln( ex , "MSG0008" , from , to );
448                        LOGGER.warning( ex , "MSG0008" , from , to );
449                }
450        }
451
452        /**
453         * 単体ファイルをバックアップフォルダに移動します。
454         *
455         * これは、#backup( from,to,true,false,sufix ); と同じ処理を実行します。
456         *
457         * 移動先は、フォルダ指定で、ファイル名は存在チェックせずに、必ず変更します。
458         * その際、移動元+サフィックス のファイルを作成します。
459         * ファイルのロックを行います。
460         *
461         * @og.rev 1.0.0 (2016/04/28) 新規追加
462         *
463         * @param from  移動元となるファイル
464         * @param to    移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
465         * @param sufix バックアップファイル名の後ろに付ける文字列
466         * @return      バックアップしたファイルパス。
467         * @throws      RuntimeException ファイル操作に失敗した場合
468         * @see #backup( Path , Path , boolean , boolean , String )
469         */
470        public static Path backup( final Path from , final Path to , final String sufix ) {
471                return backup( from,to,true,false,sufix );                      // sufix を無条件につける為、existsCheck=false で登録
472        }
473
474        /**
475         * 単体ファイルをバックアップフォルダに移動します。
476         *
477         * これは、#backup( from,to,true,true ); と同じ処理を実行します。
478         *
479         * 移動先は、フォルダ指定で、ファイル名は存在チェックの上で、無ければ移動、
480         * あれば、移動元+時間情報 のファイルを作成します。
481         * ファイルのロックを行います。
482         * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。
483         *
484         * @og.rev 1.0.0 (2016/04/28) 新規追加
485         *
486         * @param from  移動元となるファイル
487         * @param to    移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
488         * @return      バックアップしたファイルパス。
489         * @throws      RuntimeException ファイル操作に失敗した場合
490         * @see #backup( Path , Path , boolean , boolean , String )
491         */
492        public static Path backup( final Path from , final Path to ) {
493                return backup( from,to,true,true,null );
494        }
495
496        /**
497         * パスの共有ロックを指定して、単体ファイルをバックアップフォルダに移動します。
498         *
499         * 移動先のファイル名は、existsCheckが、trueの場合は、移動先のファイル名をチェックして、
500         * 存在しなければ、移動元と同じファイル名で、バックアップフォルダに移動します。
501         * 存在すれば、ファイル名+サフィックス のファイルを作成します。(拡張子より後ろにサフィックスを追加します。)
502         * existsCheckが、false の場合は、無条件に、移動元のファイル名に、サフィックスを追加します。
503         * サフィックスがnullの場合は、時間情報になります。
504         * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。
505         *
506         * @og.rev 1.0.0 (2016/04/28) 新規追加
507         * @og.rev 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
508         * @og.rev 7.2.1.0 (2020/03/13) ファイル名変更処理の修正
509         * @og.rev 7.2.5.0 (2020/06/01) toパスに、環境変数と日付文字列置換機能を追加します。
510         *
511         * @param from  移動元となるファイル
512         * @param to    移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
513         * @param useLock       パスを共有ロックするかどうか
514         * @param existsCheck   移動先のファイル存在チェックを行うかどうか(true:行う/false:行わない)
515         * @param sufix バックアップファイル名の後ろに付ける文字列
516         *
517         * @return      バックアップしたファイルパス。
518         * @throws      RuntimeException ファイル操作に失敗した場合
519         * @see #backup( Path , Path )
520         */
521        public static Path backup( final Path from , final Path to , final boolean useLock , final boolean existsCheck , final String sufix ) {
522//              final Path movePath = to == null ? from.getParent() : to ;
523                Path movePath = to == null ? from.getParent() : to ;
524
525                // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
526                if( movePath == null ) {
527                        // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
528                        throw MsgUtil.throwException( "MSG0007" , from.toString() );
529                }
530
531                // 7.2.5.0 (2020/06/01) toパスに、環境変数と日付文字列置換機能を追加します。
532                String toStr = movePath.toString();
533        //      toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@ENV."  , "}" , System::getenv );                           // 環境変数置換
534        //      toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@DATE." , "}" , StringUtil::getTimeFormat );        // 日付文字列置換
535                toStr = StringUtil.replaceText( toStr );                                // 環境変数,日付文字列置換
536                movePath = Paths.get( toStr );
537
538//              final String fileName = from.getFileName().toString();
539                final Path      fName = from.getFileName();
540                if( fName == null ) {
541                        // MSG0002 = ファイル/フォルダが存在しません。\n\tfile=[{0}]
542                        throw MsgUtil.throwException( "MSG0002" , from.toString() );
543                }
544
545//              final Path      moveFile = movePath.resolve( fileName );                                        // 移動先のファイルパスを構築
546                final Path      moveFile = movePath.resolve( fName );                                           // 移動先のファイルパスを構築
547
548//              final boolean isExChk = existsCheck && Files.notExists( moveFile );             // 存在しない場合、true。存在するか、不明の場合は、false。
549
550                final Path bkupPath;
551//              if( isExChk ) {
552                if( existsCheck && Files.notExists( moveFile ) ) {                              // 存在しない場合、true。存在するか、不明の場合は、false。
553                        bkupPath = moveFile;
554                }
555                else {
556                        final String fileName = fName.toString();                                       // from パスの名前
557                        final int ad = fileName.lastIndexOf( '.' );                                     // ピリオドの手前に、タイムスタンプを入れる。
558                        // 7.2.1.0 (2020/03/13) ファイル名変更処理の修正
559                        if( ad > 0 ) {
560                                bkupPath = movePath.resolve(
561                                                                fileName.substring( 0,ad )
562                                                                + "_"
563                                                                + StringUtil.nval( sufix , StringUtil.getTimeFormat() )
564                                                                + fileName.substring( ad )                              // ad 以降なので、ピリオドも含む
565                                                );
566                        }
567                        else {
568                                bkupPath = null;
569                        }
570                }
571
572                move( from,bkupPath,useLock );
573
574                return bkupPath;
575        }
576
577        /**
578         * オリジナルファイルにバックアップファイルの行を追記します。
579         *
580         * オリジナルファイルに、バックアップファイルから読み取った行を追記していきます。
581         * 処理する条件は、オリジナルファイルとバックアップファイルが異なる場合のみ、実行されます。
582         * また、バックアップファイルから、追記する行で、COUNT,TIME,DATE の要素を持つ
583         * 行は、RPTファイルの先頭行なので、除外します。
584         *
585         * @og.rev 7.2.5.0 (2020/06/01) 新規追加。
586         *
587         * @param orgPath       追加されるオリジナルのパス名
588         * @param bkup          行データを取り出すバックアップファイル
589         */
590        public static void mergeFile( final Path orgPath , final Path bkup ) {
591                if( exists( bkup ) && !bkup.equals( orgPath ) ) {                       // 追記するバックアップファイルの存在を条件に加える。
592                        try {
593                                final List<String> lines = FileUtil.readAllLines( bkup );               // 1.4.0 (2019/10/01)
594                                // RPT,STS など、書き込み都度ヘッダー行を入れるファイルは、ヘッダー行を削除しておきます。
595                                if( lines.size() >= 2 ) {
596                                        final String first = lines.get(0);      // RPTの先頭行で、COUNT,TIME,DATE を持っていれば、その行は削除します。
597                                        if( first.contains( "COUNT" ) && first.contains( "DATE" ) && first.contains( "TIME" ) ) { lines.remove(0); }
598                                }                                                                               // 先頭行はトークン名
599        // ※ lockSave がうまく動きません。
600        //                      if( useLock ) {
601        //                              lockSave( orgPath , lines , true );
602        //                      }
603        //                      else {
604//                                      save( orgPath , lines , true );
605                                        save( orgPath , lines , true , UTF_8 );
606        //                      }
607                                Files.deleteIfExists( bkup );
608                        }
609                        catch( final IOException ex ) {
610                                // MSG0003 = ファイルがオープン出来ませんでした。file=[{0}]
611                                throw MsgUtil.throwException( ex , "MSG0003" , bkup.toAbsolutePath().normalize() );
612                        }
613                }
614        }
615
616        /**
617         * ファイルまたはフォルダ階層を削除します。
618         *
619         * これは、指定のパスが、フォルダの場合、階層すべてを削除します。
620         * 階層の途中にファイル等が存在していたとしても、削除します。
621         *
622         * Files.walkFileTree(Path,FileVisitor) を使用したファイル・ツリーの削除方式です。
623         *
624         * @og.rev 1.0.0 (2016/04/28) 新規追加
625         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
626         *
627         * @param start 削除開始ファイル
628         * @throws      RuntimeException ファイル操作に失敗した場合
629         */
630        public static void delete( final Path start ) {
631                try {
632//                      if( Files.exists( start ) ) {
633                        if( exists( start ) ) {                                 // 7.2.5.0 (2020/06/01)
634                                Files.walkFileTree( start, DELETE_VISITOR );
635                        }
636                }
637                catch( final IOException ex ) {
638                        // MSG0011 = ファイルが削除できませんでした。file=[{0}]
639                        throw MsgUtil.throwException( ex , "MSG0011" , start );
640                }
641        }
642
643        /**
644         * delete(Path)で使用する、Files.walkFileTree の引数の FileVisitor オブジェクトです。
645         *
646         * staticオブジェクトを作成しておき、使いまわします。
647         */
648        private static final FileVisitor<Path> DELETE_VISITOR = new SimpleFileVisitor<Path>() {
649                /**
650                 * ディレクトリ内のファイルに対して呼び出されます。
651                 *
652                 * @param file  ファイルへの参照
653                 * @param attrs ファイルの基本属性
654                 * @throws      IOException 入出力エラーが発生した場合
655                 */
656                @Override
657                public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException {
658                        Files.deleteIfExists( file );           // ファイルが存在する場合は削除
659                        return FileVisitResult.CONTINUE;
660                }
661
662                /**
663                 * ディレクトリ内のエントリ、およびそのすべての子孫がビジットされたあとにそのディレクトリに対して呼び出されます。
664                 *
665                 * @param dir   ディレクトリへの参照
666                 * @param ex    エラーが発生せずにディレクトリの反復が完了した場合はnull、そうでない場合はディレクトリの反復が早く完了させた入出力例外
667                 * @throws      IOException 入出力エラーが発生した場合
668                 */
669                @Override
670                public FileVisitResult postVisitDirectory( final Path dir, final IOException ex ) throws IOException {
671                        if( ex == null ) {
672                                Files.deleteIfExists( dir );            // ファイルが存在する場合は削除
673                                return FileVisitResult.CONTINUE;
674                        } else {
675                                // directory iteration failed
676                                throw ex;
677                        }
678                }
679        };
680
681        /**
682         * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。
683         *
684         * FileUtil.stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); と同じです。
685         *
686         * @param       path  チェックするパスオブジェクト
687         * @return      true:安定した/false:安定しなかった。またはファイルが存在していない。
688         * @see         #STABLE_SLEEP_TIME
689         * @see         #STABLE_RETRY_COUNT
690         */
691        public static boolean stablePath( final Path path ) {
692                return stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT );
693        }
694
695        /**
696         * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。
697         *
698         * ファイルの安定は、ファイルのサイズをチェックすることで求めます。まず、サイズをチェックし、
699         * sleepで指定した時間だけ、Thread.sleepします。再び、サイズをチェックして、同じであれば、
700         * 安定したとみなします。
701         * なので、必ず、sleep で指定したミリ秒だけは、待ちます。
702         * ファイルが存在しない、サイズが、0のままか、チェック回数を過ぎても安定しない場合は、
703         * false が返ります。
704         * サイズを求める際に、IOExceptionが発生した場合でも、falseを返します。
705         *
706         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
707         *
708         * @param       path  チェックするパスオブジェクト
709         * @param       sleep 待機する時間(ミリ秒)
710         * @param       cnt   チェックする回数
711         * @return      true:安定した/false:安定しなかった。またはファイルが存在していない。
712         */
713        public static boolean stablePath( final Path path , final long sleep , final int cnt ) {
714                // 存在しない場合は、即抜けます。
715//              if( Files.exists( path ) ) {
716                if( exists( path ) ) {                                  // 仮想フォルダなどの場合、実態が存在しないことがある。
717                        try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){}                          // 先に、無条件に待ちます。
718                        try {
719                                for( int i=0; i<cnt; i++ ) {
720//                                      if( Files.notExists( path ) ) { return false; }                                                         // 存在チェック。無ければ、false
721                                        if( !exists( path ) ) { break; }                                                                                        // 存在チェック。無ければ、false
722                                        final long size1 = Files.size( path );                                                                          // exit point 警告が出ますが、Thread.sleep 前に、値を取得しておきたい。
723
724                                        try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){}          // 無条件に待ちます。
725
726//                                      if( Files.notExists( path ) ) { return false; }                                                         // 存在チェック。無ければ、false
727                                        if( !exists( path ) ) { break; }                                                                                        // 存在チェック。無ければ、false
728                                        final long size2 = Files.size( path );
729                                        if( size1 != 0L && size1 == size2 ) { return true; }                                            // 安定した
730                                }
731                        }
732                        catch( final IOException ex ) {
733                                // Exception は発生させません。
734                                // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}]
735                                MsgUtil.errPrintln( ex , "MSG0005" , path );
736                        }
737                }
738
739                return false;
740        }
741
742        /**
743         * 指定のパスを共有ロックして、Consumer#action(Path) メソッドを実行します。
744         * 共有ロック中は、ファイルを読み込むことは出来ますが、書き込むことは出来なくなります。
745         *
746         * 共有ロックの取得は、{@value #LOCK_RETRY_COUNT} 回実行し、{@value #LOCK_SLEEP_TIME} ミリ秒待機します。
747         *
748         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
749         *
750         * @param inPath        処理対象のPathオブジェクト
751         * @param action        パスを引数に取るConsumerオブジェクト
752         * @throws      RuntimeException ファイル読み込み時にエラーが発生した場合
753         * @see         #forEach(Path,Consumer)
754         * @see         #LOCK_RETRY_COUNT
755         * @see         #LOCK_SLEEP_TIME
756         */
757        public static void lockPath( final Path inPath , final Consumer<Path> action ) {
758                // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。
759//              if( Files.exists( inPath ) ) {
760                if( exists( inPath ) ) {                                        // 7.2.5.0 (2020/06/01)
761                        // try-with-resources 文 (AutoCloseable)
762                        try( FileChannel channel = FileChannel.open( inPath, StandardOpenOption.READ ) ) {
763                                 for( int i=0; i<LOCK_RETRY_COUNT; i++ ) {
764                                        try {
765                                                if( channel.tryLock( 0L,Long.MAX_VALUE,true ) != null ) {       // 共有ロック獲得成功
766                                                        action.accept( inPath );
767                                                        return;         // 共有ロック獲得成功したので、ループから抜ける。
768                                                }
769                                        }
770                                        // 要求された領域をオーバーラップするロックがこのJava仮想マシンにすでに確保されている場合。
771                                        // または、このメソッド内でブロックされている別のスレッドが同じファイルのオーバーラップした領域をロックしようとしている場合
772                                        catch( final OverlappingFileLockException ex ) {
773                //                              System.err.println( ex.getMessage() );
774                                                if( i >= 3 ) {  // とりあえず3回までは、何も出さない
775                                                        // MSG0104 = 要求された領域のロックは、このJava仮想マシンにすでに確保されています。 \n\tfile=[{0}]
776                //                                      LOGGER.warning( ex , "MSG0104" , inPath );
777                                                        LOGGER.warning( "MSG0104" , inPath );                                   // 1.5.0 (2020/04/01) メッセージだけにしておきます。
778                                                }
779                                        }
780                                        try{ Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ex ){}
781                                }
782                        }
783                        catch( final IOException ex ) {
784                                // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}]
785                                throw MsgUtil.throwException( ex , "MSG0005" , inPath );
786                        }
787
788                        // Exception は発生させません。
789                        // MSG0015 = ファイルのロック取得に失敗しました。file=[{0}] WAIT=[{1}](ms) COUNT=[{2}]
790//                      MsgUtil.errPrintln( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT );
791                        LOGGER.warning( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT );
792                }
793        }
794
795        /**
796         * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
797         * 1行単位に、Consumer#action が呼ばれます。
798         * このメソッドでは、Charset は、UTF-8 です。
799         *
800         * ファイルを順次読み込むため、内部メモリを圧迫しません。
801         *
802         * @param inPath        処理対象のPathオブジェクト
803         * @param action        行を引数に取るConsumerオブジェクト
804         * @throws      RuntimeException ファイル読み込み時にエラーが発生した場合
805         * @see         #lockForEach(Path,Consumer)
806         */
807        public static void forEach( final Path inPath , final Consumer<String> action ) {
808                forEach( inPath , UTF_8 , action );
809        }
810
811        /**
812         * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
813         * 1行単位に、Consumer#action が呼ばれます。
814         *
815         * ファイルを順次読み込むため、内部メモリを圧迫しません。
816         *
817         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
818         *
819         * @param inPath        処理対象のPathオブジェクト
820         * @param chset         ファイルを読み取るときのCharset
821         * @param action        行を引数に取るConsumerオブジェクト
822         * @throws      RuntimeException ファイル読み込み時にエラーが発生した場合
823         * @see         #lockForEach(Path,Consumer)
824         */
825        public static void forEach( final Path inPath , final Charset chset , final Consumer<String> action ) {
826                // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。
827//              if( Files.exists( inPath ) ) {
828                if( exists( inPath ) ) {                                        // 7.2.5.0 (2020/06/01)
829                        // try-with-resources 文 (AutoCloseable)
830                        String line = null;
831                        int no = 0;
832        //              // こちらの方法では、lockForEach から来た場合に、エラーになります。
833        //              try( BufferedReader reader = Files.newBufferedReader( inPath , chset ) ) {
834                        // 万一、コンストラクタでエラーが発生すると、リソース開放されない場合があるため、個別にインスタンスを作成しておきます。(念のため)
835                        try( FileInputStream   fin = new FileInputStream( inPath.toFile() );
836                                 InputStreamReader isr = new InputStreamReader( fin , chset );
837                                 BufferedReader reader = new BufferedReader( isr ) ) {
838
839                                while( ( line = reader.readLine() ) != null ) {
840                                        // 1.2.0 (2018/09/01) UTF-8 BOM 対策
841                                        // UTF-8 の BOM(0xEF 0xBB 0xBF) は、Java内部文字コードの UTF-16 BE では、0xFE 0xFF になる。
842                                        // ファイルの先頭文字が、feff の場合は、その文字を削除します。
843                        //              if( no == 0 && !line.isEmpty() && Integer.toHexString(line.charAt(0)).equalsIgnoreCase("feff") ) {
844                                        if( no == 0 && !line.isEmpty() && (int)line.charAt(0) == (int)'\ufeff' ) {
845                                                // MSG0105 = 指定のファイルは、UTF-8 BOM付きです。BOM無しファイルで、運用してください。 \n\tfile=[{0}]
846                                                System.out.println( MsgUtil.getMsg( "MSG0105" , inPath ) );
847                                                line = line.substring(1);                       // BOM の削除 : String#replace("\ufeff","") の方が良い?
848                                        }
849
850                                        action.accept( line );
851                                        no++;
852                                }
853                        }
854                        catch( final IOException ex ) {
855                                // MSG0016 = ファイルの行データ読み込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2}
856                                // MSG0016 = ファイルの行データ読み込みに失敗しました。\n\tfile={0} , 行番号:{1} , 行:{2}
857                                throw MsgUtil.throwException( ex , "MSG0016" , inPath , no , line );
858                        }
859                }
860        }
861
862        /**
863         * 指定のパスを共有ロックして、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
864         * 1行単位に、Consumer#action が呼ばれます。
865         *
866         * ファイルを順次読み込むため、内部メモリを圧迫しません。
867         *
868         * @param inPath        処理対象のPathオブジェクト
869         * @param action        行を引数に取るConsumerオブジェクト
870         * @see         #forEach(Path,Consumer)
871         */
872        public static void lockForEach( final Path inPath , final Consumer<String> action ) {
873                lockPath( inPath , in -> forEach( in , UTF_8 , action ) );
874        }
875
876        /**
877         * 指定のパスを共有ロックして、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
878         * 1行単位に、Consumer#action が呼ばれます。
879         *
880         * ファイルを順次読み込むため、内部メモリを圧迫しません。
881         *
882         * @param inPath        処理対象のPathオブジェクト
883         * @param chset         エンコードを指定するCharsetオブジェクト
884         * @param action        行を引数に取るConsumerオブジェクト
885         * @see         #forEach(Path,Consumer)
886         */
887        public static void lockForEach( final Path inPath , final Charset chset , final Consumer<String> action ) {
888                lockPath( inPath , in -> forEach( in , chset , action ) );
889        }
890
891        /**
892         * 指定のパスに1行単位の文字列のListを書き込んでいきます。
893         * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
894         *
895         * 書き込むパスの親フォルダがなければ作成します。
896         * 第2引数は、書き込む行データです。
897         * このメソッドでは、Charset は、UTF-8 です。
898         *
899         * @og.rev 1.0.0 (2016/04/28) 新規追加
900         *
901         * @param       savePath セーブするパスオブジェクト
902         * @param       lines   行単位の書き込むデータ
903         * @throws      RuntimeException ファイル操作に失敗した場合
904         * @see         #save( Path , List , boolean , Charset )
905         */
906        public static void save( final Path savePath , final List<String> lines ) {
907                save( savePath , lines , false , UTF_8 );               // 新規作成
908        }
909
910        /**
911         * 指定のパスに1行単位の文字列のListを書き込んでいきます。
912         * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
913         *
914         * 書き込むパスの親フォルダがなければ作成します。
915         *
916         * 第2引数は、書き込む行データです。
917         *
918         * @og.rev 1.0.0 (2016/04/28) 新規追加
919         * @og.rev 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処
920         *
921         * @param       savePath セーブするパスオブジェクト
922         * @param       lines   行単位の書き込むデータ
923         * @param       append  trueの場合、ファイルの先頭ではなく最後に書き込まれる。
924         * @param       chset   ファイルを読み取るときのCharset
925         * @throws      RuntimeException ファイル操作に失敗した場合
926         */
927        public static void save( final Path savePath , final List<String> lines , final boolean append , final Charset chset ) {
928                // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
929                // ※ toAbsolutePath() する必要はないのと、getParent() は、null を返すことがある
930//              mkdirs( savePath.toAbsolutePath().getParent() );                // savePathはファイルなので、親フォルダを作成する。
931                final Path parent = savePath.getParent();
932                if( parent == null ) {
933                        // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
934                        throw MsgUtil.throwException( "MSG0007" , savePath.toString() );
935                }
936                else {
937                        mkdirs( parent,false );
938                }
939
940                String line = null;             // エラー出力のための変数
941                int no = 0;
942
943                // try-with-resources 文 (AutoCloseable)
944                try( PrintWriter out = new PrintWriter( Files.newBufferedWriter( savePath, chset , append ? APPEND : CREATE ) ) ) {
945                         for( final String ln : lines ) {
946//                              line = ln ;
947                                // 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処
948                                if( !ln.isEmpty() && (int)ln.charAt(0) == (int)'\ufeff' ) {
949                                        line = ln.substring(1);                 // BOM の削除 : String#replace("\ufeff","") の方が良い?
950                                }
951                                else {
952                                        line = ln ;
953                                }
954                                no++;
955                                out.println( line );
956                        }
957                        out.flush();
958                }
959                catch( final IOException ex ) {
960                        // MSG0017=ファイルのデータ書き込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2}
961                        throw MsgUtil.throwException( ex , "MSG0017" , savePath , no , line );
962                }
963        }
964
965        /**
966         * 指定のパスの最終更新日付を、文字列で返します。
967         * 文字列のフォーマット指定も可能です。
968         *
969         * パスが無い場合や、最終更新日付を、取得できない場合は、現在時刻をベースに返します。
970         *
971         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
972         *
973         * @param path          処理対象のPathオブジェクト
974         * @param format        文字列化する場合のフォーマット(yyyyMMddHHmmss)
975         * @return      指定のパスの最終更新日付の文字列
976         */
977        public static String timeStamp( final Path path , final String format ) {
978                long tempTime = 0L;
979                try {
980                        // 存在チェックを直前に入れますが、厳密には、非同期なので確率の問題です。
981//                      if( Files.exists( path ) ) {
982                        if( exists( path ) ) {                                  // 7.2.5.0 (2020/06/01)
983                                tempTime = Files.getLastModifiedTime( path ).toMillis();
984                        }
985                }
986                catch( final IOException ex ) {
987                        // MSG0018 = ファイルのタイムスタンプの取得に失敗しました。file=[{0}]
988//                      MsgUtil.errPrintln( ex , "MSG0018" , path , ex.getMessage() );
989                        // MSG0018 = ファイルのタイムスタンプの取得に失敗しました。\n\tfile=[{0}]
990                        LOGGER.warning( ex , "MSG0018" , path );
991                }
992                if( tempTime == 0L ) {
993                        tempTime = System.currentTimeMillis();          // パスが無い場合や、エラー時は、現在時刻を使用
994                }
995
996                return StringUtil.getTimeFormat( tempTime , format );
997        }
998
999        /**
1000         * ファイルからすべての行を読み取って、文字列のListとして返します。
1001         *
1002         * java.nio.file.Files#readAllLines​(Path ) と同等ですが、ファイルが UTF-8 でない場合
1003         * 即座にエラーにするのではなく、Windows-31J でも読み取りを試みます。
1004         * それでもダメな場合は、IOException をスローします。
1005         *
1006         * @og.rev 7.2.5.0 (2020/06/01) Files.readAllLines の代用
1007         *
1008         * @param path          読み取り対象のPathオブジェクト
1009         * @return      Listとしてファイルからの行
1010         * @throws      IOException 読み取れない場合エラー
1011         */
1012        public static List<String> readAllLines( final Path path ) throws IOException {
1013                try {
1014                        return Files.readAllLines( path );                              // StandardCharsets.UTF_8 指定と同等。
1015                }
1016                catch( final MalformedInputException ex ) {
1017                        // MSG0030 = 指定のファイルは、UTF-8でオープン出来なかったため、Windows-31J で再実行します。\n\tfile=[{0}]
1018                        LOGGER.warning( "MSG0030" , path );                             // Exception は、引数に渡さないでおきます。
1019
1020                        return Files.readAllLines( path,WINDOWS_31J );
1021                }
1022        }
1023
1024        /**
1025         * Pathオブジェクトが存在しているかどうかを判定します。
1026         *
1027         * java.nio.file.Files#exists( Path ) を使用せず、java.io.File.exists() で判定します。
1028         * https://codeday.me/jp/qa/20190302/349168.html
1029         * ネットワークフォルダに存在するファイルの判定において、Files#exists( Path )と
1030         * File.exists() の結果が異なることがあります。
1031         * ここでは、File.exists() を使用して判定します。
1032         *
1033         * @og.rev 7.2.5.0 (2020/06/01) Files.exists の代用
1034         *
1035         * @param path          判定対象のPathオブジェクト
1036         * @return      ファイルの存在チェック(あればtrue)
1037         */
1038        public static boolean exists( final Path path ) {
1039        //      return Files.exists( path );
1040                return path != null && path.toFile().exists();
1041        }
1042
1043        /**
1044         * Pathオブジェクトのファイル名(getFileName().toString()) を取得します。
1045         *
1046         * Path#getFileName() では、結果が null になる場合もあり、そのままでは、toString() できません。
1047         * また、引数の Path も null チェックが必要なので、それらを簡易的に行います。
1048         * 何らかの結果が、null の場合は、""(空文字列)を返します。
1049         *
1050         * @og.rev 7.2.9.4 (2020/11/20) Path.getFileName().toString() の簡易版
1051         *
1052         * @param path          ファイル名取得元のPathオブジェクト(nullも可)
1053         * @return      ファイル名(nullの場合は、空文字列)
1054         * @og.rtnNotNull
1055         */
1056        public static String pathFileName( final Path path ) {
1057                // 対応済み:spotbugs:null になっている可能性があるメソッドの戻り値を利用している
1058                return path == null || path.getFileName() == null ? "" : path.getFileName().toString();
1059        }
1060}