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