    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.io.File;
    019import java.io.IOException;
    020
    021import java.nio.file.Path;
    022import java.nio.file.PathMatcher;
    023import java.nio.file.Files;
    024import java.nio.file.DirectoryStream;
    025
    026import java.util.concurrent.Executors;
    027import java.util.concurrent.TimeUnit;
    028import java.util.concurrent.ScheduledFuture;
    029import java.util.concurrent.ScheduledExecutorService;
    030import java.util.function.Consumer;
    031
    032/**
    033 * フォルダに残っているファイルを再実行するためのプログラムです。
    034 *
    035 * 通常は、FileWatch で、パスを監視していますが、場合によっては、
    036 * イベントを拾いそこねることがあります。それを、フォルダスキャンして、拾い上げます。
    037 * １０秒間隔で繰り返しスキャンします。条件は、３０秒以上前のファイルです。
    038 *
    039 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
    040 *
    041 * @version  7.0
    042 * @author   Kazuhiko Hasegawa
    043 * @since    JDK1.8,
    044 */
    045public class DirWatch implements Runnable {
    046        private static final XLogger LOGGER= XLogger.getLogger( DirWatch.class.getName() );             // ログ出力
    047
    048        /** 最初にスキャンを実行するまでの遅延時間(秒) の初期値 */
    049        public static final long INIT_DELAY     = 5;                    // (秒)
    050
    051        /** スキャンする間隔(秒) の初期値 */
    052        public static final long PERIOD         = 10;                   // (秒)
    053
    054        /** ファイルのタイムスタンプとの差のチェック(秒) の初期値 */
    055        public static final long TIME_DIFF      = 30;                   // (秒)
    056
    057        private final   Path            sPath;                                          // スキャンパス
    058        private final   boolean         useTree;                                        // フォルダ階層をスキャンするかどうか
    059
    060        // callbackするための、関数型インターフェース(メソッド参照)
    061        private Consumer<Path> action = path -> System.out.println( "DirWatch Path=" + path ) ;
    062
    063        // DirectoryStreamで、パスのフィルタに使用します。
    064        private final PathMatcherSet pathMch = new PathMatcherSet();            // PathMatcher インターフェースを継承
    065
    066        // フォルダスキャンする条件
    067        private DirectoryStream.Filter<Path> filter;
    068
    069        // スキャンを停止する場合に使用します。
    070        private ScheduledFuture<?> stFuture ;
    071
    072        /**
    073         * スキャンパスを引数に作成される、コンストラクタです。
    074         *
    075         * ここでは、階層検索しない(useTree=false)で、インスタンス化します。
    076         *
    077         * @param       sPath   検索対象となるスキャンパス
    078         */
    079        public DirWatch( final Path sPath ) {
    080                this( sPath , false );
    081        }
    082
    083        /**
    084         * スキャンパスと関数型インターフェースフォルダを引数に作成される、コンストラクタです。
    085         *
    086         * @param       sPath   検索対象となるスキャンパス
    087         * @param       useTree 階層スキャンするかどうか(true:する/false:しない)
    088         */
    089        public DirWatch( final Path sPath, final boolean useTree ) {
    090                this.sPath              = sPath;
    091                this.useTree    = useTree;
    092        }
    093
    094        /**
    095         * 指定のパスの照合操作で、パターンに一致したパスのみ、callback されます。
    096         *
    097         * ここで指定したパターンの一致を判定し、一致した場合は、callback されます。
    098         * 指定しない場合は、すべて許可されたことになります。
    099         * なお、#setPathEndsWith(String...) と、この設定は同時には行うことは出来ません。
    100         *
    101         * @param       pathMch パスの照合操作のパターン
    102         * @see         java.nio.file.PathMatcher
    103         * @see         #setPathEndsWith(String...)
    104         */
    105        public void setPathMatcher( final PathMatcher pathMch ) {
    106                this.pathMch.addPathMatcher( pathMch );
    107        }
    108
    109        /**
    110         * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、callback されます。
    111         *
    112         * これは、#setPathMatcher(PathMatcher) の簡易指定版です。
    113         * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となりcallback されます。
    114         * 指定しない場合(null)は、すべて許可されたことになります。
    115         * 終端文字列の判定には、大文字小文字の区別を行いません。
    116         * なお、#setPathMatcher(PathMatcher) と、この設定は同時には行うことは出来ません。
    117         *
    118         * @param       endKey パスの終端一致のパターン
    119         * @see         #setPathMatcher(PathMatcher)
    120         */
    121        public void setPathEndsWith( final String... endKey ) {
    122                pathMch.addEndsWith( endKey );
    123        }
    124
    125        /**
    126         * ファイルパスを、引数に取る Consumer ダオブジェクトを設定します。
    127         *
    128         * これは、関数型インタフェースなので、ラムダ式またはメソッド参照の代入先として使用できます。
    129         * イベントが発生したときの ファイルパス(監視フォルダで、resolveされた、正式なフルパス)を引数に、
    130         * accept(Path) メソッドが呼ばれます。
    131         *
    132         * @param       act 1つの入力(ファイルパス) を受け取る関数型インタフェース
    133         * @see         Consumer#accept(Object)
    134         */
    135        public void callback( final Consumer<Path> act ) {
    136                if( act != null ) {
    137                        action = act ;
    138                }
    139        }
    140
    141        /**
    142         * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
    143         *
    144         * 初期値( initDelay={@value #INIT_DELAY} , period={@value #PERIOD} , timeDiff={@value #TIME_DIFF} ) で、
    145         * スキャンを開始します。
    146         *
    147         * #start( {@value #INIT_DELAY} , {@value #PERIOD} , {@value #TIME_DIFF} ) と同じです。
    148         *
    149         */
    150        public void start() {
    151                start( INIT_DELAY , TIME_DIFF , PERIOD );
    152        }
    153
    154        /**
    155         * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
    156         *
    157         * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較を指定して、スキャンを開始します。
    158         * ファイルのタイムスタンプとの差とは、ある一定時間経過したファイルのみ、action をcall します。
    159         *
    160         * @param       initDelay 最初にスキャンを実行するまでの遅延時間(秒)
    161         * @param       period    スキャンする間隔(秒)
    162         * @param       timeDiff  ファイルのタイムスタンプとの差のチェック(秒)
    163         */
    164        public void start( final long initDelay , final long period , final long timeDiff ) {
    165                LOGGER.info( () -> "DirWatch Start: " + sPath + " Tree=" + useTree + " Delay=" + initDelay + " Period=" + period + " TimeDiff=" + timeDiff );
    166
    167                // DirectoryStream.Filter<Path> インターフェースは、#accept(Path) しかメソッドを持っていないため、ラムダ式で代用できる。
    168                filter = path -> Files.isDirectory( path ) || pathMch.matches( path ) && timeDiff*1000 < ( System.currentTimeMillis() - path.toFile().lastModified() );
    169
    170        //      filter = path -> Files.isDirectory( path ) ||
    171        //                                              pathMch.matches( path ) &&
    172        //                                              FileTime.fromMillis( System.currentTimeMillis() - timeDiff*1000L )
    173        //                                                              .compareTo( Files.getLastModifiedTime( path ) ) > 0 ;
    174
    175                final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    176                stFuture = scheduler.scheduleAtFixedRate( this , initDelay , period , TimeUnit.SECONDS );
    177        }
    178
    179        /**
    180         * 内部で作成した ScheduledFutureをキャンセルします。
    181         */
    182        public void stop() {
    183                if( stFuture != null && !stFuture.isDone() ) {                  // 完了(正常終了、例外、取り消し)以外は、キャンセルします。
    184                        LOGGER.info( () -> "DirWatch Stop: [" + sPath  + "]" );
    185                        stFuture.cancel(true);
    186        //              try {
    187        //                      stFuture.get();                                                                 // 必要に応じて計算が完了するまで待機します。
    188        //              }
    189        //              catch( InterruptedException | ExecutionException ex) {
    190        //                      LOGGER.info( () -> "DirWatch Stop  Error: [" + sPath  + "]" + ex.getMessage() );
    191        //              }
    192                }
    193        }
    194
    195        /**
    196         * Runnableインターフェースのrunメソッドです。
    197         *
    198         * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。
    199         *
    200         * ここで、条件に一致したPathオブジェクトが存在すれば、コンストラクタで渡した
    201         * 関数型インターフェースがcallされます。
    202         *
    203         * @og.rev 6.8.2.2 (2017/11/02) ネットワークパスのチェックを行います。
    204         */
    205        @Override
    206        public void run() {
    207                try {
    208                        LOGGER.debug( () -> "DirWatch Running: " + sPath + " Tree=" + useTree );
    209
    210                        if( Files.exists( sPath ) ) {                           // 6.8.2.2 (2017/11/02) ネットワークパスのチェック
    211                                execute( sPath );
    212                        }
    213                        else {
    214                                // MSG0002 = ファイル/フォルダは存在しません。file=[{0}]
    215                                MsgUtil.errPrintln( "MSG0002" , sPath );
    216                        }
    217                }
    218                catch( final Throwable th ) {
    219                        // MSG0021 = 予期せぬエラーが発生しました。\n\tﾒｯｾｰｼﾞ=[{0}]
    220                        MsgUtil.errPrintln( th , "MSG0021" , toString() );
    221                }
    222        }
    223
    224        /**
    225         * フォルダ階層を順番にスキャンする再帰定義用の関数です。
    226         *
    227         * run() メソッドから呼ばれます。
    228         *
    229         * @param       inPpath 検索対象となるパス
    230         */
    231        private void execute( final Path inPpath ) {
    232                try( final DirectoryStream<Path> stream = Files.newDirectoryStream( inPpath, filter ) ) {
    233                        for( final Path path : stream ) {
    234                                if( Files.isDirectory( path ) ) {
    235                                        if( useTree ) { execute( path ); }              // 階層スキャンする場合のみ、再帰処理する。
    236                                }
    237                                else {
    238                                        synchronized( action ) {
    239                                                action.accept( path );
    240                                        }
    241                                }
    242                        }
    243                }
    244                catch( final IOException ex ) {
    245                        // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}]
    246                        throw MsgUtil.throwException( ex , "MSG0005" , inPpath );
    247                }
    248        }
    249
    250        /** main メソッドから呼ばれる ヘルプメッセージです。 {@value}  */
    251        public static final String USAGE = "Usage: java jp.euromap.eu63.util.DirWatch dir [-SF sufix]... [-S] [-D delay(s)] [-P period(s)] [-T timeDiff(s)]" ;
    252
    253        /**
    254         * 引数に監視対象のフォルダと、拡張子を指定します。
    255         *
    256         * スキャンパスの初期値は、起動ﾌｫﾙﾀﾞです。
    257         *
    258         * -D 最初にスキャンを実行するまでの遅延時間(秒)
    259         * -P スキャンする間隔(秒)
    260         * -T ファイルのタイムスタンプとの差のチェック(秒)
    261         * -S 階層スキャンする場合は、-S を付けます。
    262         * -SF ファイルの拡張子(後ろからのマッチング)を指定します。複数指定することが出来ます。
    263         *
    264         * テストなので、６０秒後に、終了します。
    265         *
    266         * {@value #USAGE}
    267         *
    268         * @param       args    コマンド引数配列
    269         */
    270        public static void main( final String[] args ) {
    271                // ********** 【整合性チェック】 **********
    272                if( args.length < 1 ) {
    273                        System.out.println( USAGE );
    274                        return;
    275                }
    276
    277                // ********** 【引数定義】 **********
    278                long delay              = DirWatch.INIT_DELAY;          // 最初にスキャンを実行するまでの遅延時間(秒) の初期値
    279                long period             = DirWatch.PERIOD;                      // スキャンする間隔(秒) の初期値
    280                long timeDiff   = DirWatch.TIME_DIFF;           // ファイルのタイムスタンプとの差のチェック(秒) の初期値
    281
    282                Path    sPath   = new File( "." ).toPath();     // スキャンパス の初期値
    283
    284                boolean useTree = false;
    285                final java.util.List<String> sufixList = new java.util.ArrayList<>();   // main でしか使わないので、import しない。
    286
    287                // ********** 【引数処理】 **********
    288                for( int i=0; i<args.length; i++ ) {
    289                        final String arg = args[i];
    290
    291                        // 6.9.8.0 (2018/05/28) FindBugs:ボクシング/アンボクシングはプリミティブを解析する( valueOf → parseLong )
    292                        if(      "-help".equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; }
    293                        else if( "-D".equalsIgnoreCase( arg ) ) { delay    = Long.parseLong( args[++i] ); }             // 記号を見つけたら、次の引数をセットします。
    294                        else if( "-P".equalsIgnoreCase( arg ) ) { period   = Long.parseLong( args[++i] ); }             // 記号を見つけたら、次の引数をセットします。
    295                        else if( "-T".equalsIgnoreCase( arg ) ) { timeDiff = Long.parseLong( args[++i] ); }             // 記号を見つけたら、次の引数をセットします。
    296                        else if( "-S".equalsIgnoreCase( arg ) ) { useTree  = true; continue; }                                  // 階層処理
    297                        else if( "-SF".equalsIgnoreCase( arg ) ) {
    298                                sufixList.add( args[++i] );                                                                                                                     // スキャン対象の拡張子のリスト
    299                        }
    300                        else {
    301                                sPath = new File( arg ).toPath();
    302                        }
    303                }
    304
    305                // ********** 【本体処理】 **********
    306                final DirWatch watch = new DirWatch( sPath , useTree );                 // 監視先
    307
    308                if( !sufixList.isEmpty() ) {
    309                        watch.setPathEndsWith( sufixList.toArray( new String[sufixList.size()] ) );
    310                }
    311
    312                watch.start( delay,period,timeDiff );
    313
    314                try{ Thread.sleep( 60000 ); } catch( final InterruptedException ex ){}          // テスト的に60秒待ちます。
    315
    316                watch.stop();
    317
    318                System.out.println( "done." );
    319        }
    320}