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