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