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