    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 ;     // ファイルが安定するまで、２秒待つ
    059        /** ファイルが安定するまでのリトライ回数 {@value} */
    060        public static final int STABLE_RETRY_COUNT = 10 ;       // ファイルが安定するまで、１０回リトライする。
    061
    062        /** ファイルロックの獲得までの待ち時間(ミリ秒) {@value} */
    063        public static final int LOCK_SLEEP_TIME  = 2000 ;       // ロックの獲得まで、２秒待つ
    064        /** ファイルロックの獲得までのリトライ回数 {@value} */
    065        public static final int LOCK_RETRY_COUNT = 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         * ここでは、第２引数のファイル名に、絶対パスを指定した場合は、第１引数の
    138         * フォルダを使用せず、ファイル名だけで、ファイルオブジェクトを作成します。
    139         * 第２引数のファイル名が、null か、ゼロ文字列の場合は、第１引数の
    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         * ファイルが存在しない、サイズが、０のままか、チェック回数を過ぎても安定しない場合は、
    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         * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    665         * １行単位に、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         * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    681         * １行単位に、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         * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    717         * １行単位に、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         * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    731         * １行単位に、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         * 指定のパスに１行単位の文字列のListを書き込んでいきます。
    746         * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
    747         *
    748         * 書き込むパスの親フォルダがなければ作成します。
    749         * 第２引数は、書き込む行データです。
    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         * 指定のパスに１行単位の文字列のListを書き込んでいきます。
    765         * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
    766         *
    767         * 書き込むパスの親フォルダがなければ作成します。
    768         *
    769         * 第２引数は、書き込む行データです。
    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}