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