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