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