    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         * @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         * ファイルが存在しない、サイズが、０のままか、チェック回数を過ぎても安定しない場合は、
    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         * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    697         * １行単位に、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         * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    713         * １行単位に、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         * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    749         * １行単位に、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         * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    763         * １行単位に、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         * 指定のパスに１行単位の文字列のListを書き込んでいきます。
    778         * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
    779         *
    780         * 書き込むパスの親フォルダがなければ作成します。
    781         * 第２引数は、書き込む行データです。
    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         * 指定のパスに１行単位の文字列のListを書き込んでいきます。
    797         * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
    798         *
    799         * 書き込むパスの親フォルダがなければ作成します。
    800         *
    801         * 第２引数は、書き込む行データです。
    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}