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