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