    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;
    020
    021import java.nio.file.WatchEvent;
    022import java.nio.file.Path;
    023import java.nio.file.PathMatcher;
    024import java.nio.file.FileSystem;
    025import java.nio.file.WatchKey;
    026import java.nio.file.StandardWatchEventKinds;
    027import java.nio.file.WatchService;
    028
    029import java.util.function.BiConsumer;
    030import java.util.concurrent.atomic.AtomicBoolean;                               // 7.2.9.4 (2020/11/20) volatile boolean の代替え
    031
    032/**
    033 * FileWatch は、ファイル監視を行うクラスです。
    034 *
    035 *<pre>
    036 * ファイルが、追加(作成)、変更、削除された場合に、イベントが発生します。
    037 * このクラスは、Runnable インターフェースを実装しているため、Thread で実行することで、
    038 * 個々のフォルダの監視を行います。
    039 *
    040 *</pre>
    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 FileWatch implements Runnable {
    048        private static final XLogger LOGGER= XLogger.getLogger( FileWatch.class.getSimpleName() );              // ログ出力
    049
    050        /** Path に、WatchService を register するときの作成イベントの簡易指定できるように。 */
    051        public static final WatchEvent.Kind<Path> CREATE = StandardWatchEventKinds.ENTRY_CREATE ;
    052
    053        /** Path に、WatchService を register するときの変更イベントの簡易指定できるように。 */
    054        public static final WatchEvent.Kind<Path> MODIFY = StandardWatchEventKinds.ENTRY_MODIFY ;
    055
    056        /** Path に、WatchService を register するときの削除イベントの簡易指定できるように。  */
    057        public static final WatchEvent.Kind<Path> DELETE = StandardWatchEventKinds.ENTRY_DELETE ;
    058
    059        /** Path に、WatchService を register するときの特定不能時イベントの簡易指定できるように。 */
    060        public static final WatchEvent.Kind<?>    OVERFLOW = StandardWatchEventKinds.OVERFLOW ;
    061
    062        // Path に、WatchService を register するときのイベント
    063        private static final WatchEvent.Kind<?>[] WE_KIND = new WatchEvent.Kind<?>[] {
    064                        CREATE , MODIFY , DELETE , OVERFLOW
    065        };
    066
    067        // Path に、WatchService を register するときの登録方法の修飾子(修飾子 なしの場合)
    068        private static final WatchEvent.Modifier[] WE_MOD_ONE  = new WatchEvent.Modifier[0];    // Modifier なし
    069
    070        // Path に、WatchService を register するときの登録方法の修飾子(以下の階層も監視対象にします)
    071        private static final WatchEvent.Modifier[] WE_MOD_TREE = new WatchEvent.Modifier[] {    // ツリー階層
    072                                        com.sun.nio.file.ExtendedWatchEventModifier.FILE_TREE
    073                        };
    074
    075        /** DirWatch でスキャンした場合のｲﾍﾞﾝﾄ名 {@value} */
    076        public static final String DIR_WATCH_EVENT = "DirWatch";
    077
    078        // 監視対象のフォルダ
    079        private final Path dirPath ;
    080
    081        // 監視方法
    082        private final boolean   useTree ;
    083        private final WatchEvent.Modifier[] extModifiers ;
    084
    085        // callbackするための、関数型インターフェース(メソッド参照)
    086        private BiConsumer<String,Path> action = (event,path) -> System.out.println( "Event=" + event + " , Path=" + path ) ;
    087
    088        // Path に、WatchService を register するときのイベント
    089        private WatchEvent.Kind<?>[] weKind = WE_KIND ;                                         // 初期値は、すべて
    090
    091        // パスの照合操作を行うPathMatcher の初期値
    092        private final PathMatcherSet pathMchSet = new PathMatcherSet();         // PathMatcher インターフェースを継承
    093
    094        // DirWatchのパスの照合操作を行うPathMatcher の初期値
    095        private final PathMatcherSet dirWatchMch = new PathMatcherSet();        // PathMatcher インターフェースを継承
    096
    097        // 何らかの原因でイベントもれした場合、フォルダスキャンを行います。
    098        private boolean         useDirWatch     = true;                                                         // 初期値は、イベント漏れ監視を行います。
    099        private DirWatch        dWatch ;                                                                                // DirWatch のstop時に呼び出すための変数
    100        private Thread          thread ;                                                                                // 停止するときに呼び出すため
    101
    102//      private volatile boolean running ;                                                                      // 状態とThreadの停止に使用する。
    103        private final AtomicBoolean running = new AtomicBoolean();                      // 7.2.9.4 (2020/11/20) volatile boolean の代替え ( 状態とThreadの停止に使用する。)
    104
    105        /**
    106         * 処理対象のフォルダのパスオブジェクトを指定して、ファイル監視インスタンスを作成します。
    107         *
    108         * ここでは、指定のフォルダの内のファイルのみ監視します。
    109         * これは、new FileWatch( dir , false ) とまったく同じです。
    110         *
    111         * @param dir   処理対象のフォルダオブジェクト
    112         */
    113        public FileWatch( final Path dir ) {
    114                this( dir , false );
    115        }
    116
    117        /**
    118         * 処理対象のフォルダのパスオブジェクトと、監視対象方法を指定して、ファイル監視インスタンスを作成します。
    119         *
    120         * useTree を true に設定すると、指定のフォルダの内のフォルダ階層を、すべて監視対象とします。
    121         *
    122         * @param dir   処理対象のフォルダのパスオブジェクト
    123         * @param useTree       フォルダツリーの階層をさかのぼって監視するかどうか(true:フォルダ階層を下る)
    124         */
    125        public FileWatch( final Path dir , final boolean useTree ) {
    126                dirPath          = dir ;
    127                this.useTree = useTree;
    128                extModifiers = useTree ? WE_MOD_TREE : WE_MOD_ONE ;
    129        }
    130
    131        /**
    132         * 指定のイベントの種類のみ、監視対象に設定します。
    133         *
    134         * ここで指定したイベントのみ、監視対象になり、callback されます。
    135         * 第一引数は、イベントの種類(ENTRY_CREATE,ENTRY_MODIFY,ENTRY_DELETE,OVERFLOW)
    136         *
    137         * @param       kind 監視対象に設定するイベントの種類
    138         * @see         java.nio.file.StandardWatchEventKinds
    139         */
    140        public void setEventKinds( final WatchEvent.Kind<?>... kind ) {
    141                if( kind != null && kind.length > 0 ) {
    142                        weKind = kind;
    143                }
    144        }
    145
    146        /**
    147         * 指定のパスの照合操作で、パターンに一致したパスのみ、callback されます。
    148         *
    149         * ここで指定したパターンの一致を判定し、一致した場合は、callback されます。
    150         * 指定しない場合は、すべて許可されたことになります。
    151         * なお、#setPathEndsWith(String...) と、この設定は同時には行うことは出来ません。
    152         * (最後に登録した条件が、適用されます。)
    153         *
    154         * @param       pathMch パスの照合操作のパターン
    155         * @see         java.nio.file.PathMatcher
    156         * @see         #setPathEndsWith(String...)
    157         */
    158        public void setPathMatcher( final PathMatcher pathMch ) {
    159                pathMchSet.addPathMatcher( pathMch );
    160        }
    161
    162        /**
    163         * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、callback されます。
    164         *
    165         * これは、#setPathMatcher(PathMatcher) の簡易指定版です。
    166         * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となりcallback されます。
    167         * 指定しない場合(null)は、すべて許可されたことになります。
    168         * 終端文字列の判定には、大文字小文字の区別を行いません。
    169         * なお、#setPathMatcher(PathMatcher) と、この設定は同時には行うことは出来ません。
    170         * (最後に登録した条件が、適用されます。)
    171         *
    172         * @param       endKey パスの終端一致のパターン
    173         * @see         #setPathMatcher(PathMatcher)
    174         */
    175        public void setPathEndsWith( final String... endKey ) {
    176                pathMchSet.addEndsWith( endKey );
    177        }
    178
    179        /**
    180         * イベントの種類と、ファイルパスを、引数に取る BiConsumer ダオブジェクトを設定します。
    181         *
    182         * これは、関数型インタフェースなので、ラムダ式またはメソッド参照の代入先として使用できます。
    183         * イベントが発生したときの イベントの種類と、そのファイルパスを引数に、accept(String,Path) メソッドが呼ばれます。
    184         * 第一引数は、イベントの種類(ENTRY_CREATE,ENTRY_MODIFY,ENTRY_DELETE,OVERFLOW)
    185         * 第二引数は、ファイルパス(監視フォルダで、resolveされた、正式なフルパス)
    186         *
    187         * @param       act 2つの入力(イベントの種類 とファイルパス) を受け取る関数型インタフェース
    188         * @see         BiConsumer#accept(Object,Object)
    189         */
    190        public void callback( final BiConsumer<String,Path> act ) {
    191                if( act != null ) {
    192                        action = act ;
    193                }
    194        }
    195
    196        /**
    197         * 何らかの原因でイベントを掴み損ねた場合に、フォルダスキャンするかどうかを指定します。
    198         *
    199         * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較時間等は、
    200         * DirWatch の初期値をそのまま使用するため、ここでは指定できません。
    201         * 個別に指定したい場合は、このﾌﾗｸﾞをfalse にセットして、個別に、DirWatch を作成してください。
    202         * このメソッドでは、#setPathEndsWith( String... )や、#setPathMatcher( PathMatcher ) で
    203         * 指定した条件が、そのまま適用されます。
    204         *
    205         * @param       flag フォルダスキャンするかどうか(true:する/false:しない)
    206         * @see         DirWatch
    207         */
    208        public void setUseDirWatch( final boolean flag ) {
    209                useDirWatch = flag;
    210        }
    211
    212        /**
    213         * 何らかの原因でイベントを掴み損ねた場合の、フォルダスキャンの対象ファイルの拡張子を指定します。
    214         *
    215         * このメソッドを使用する場合は、useDirWatch は、true にセットされます。
    216         * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較時間等は、
    217         * DirWatch の初期値をそのまま使用するため、ここでは指定できません。
    218         * このメソッドでは、DirWatch 対象の終端パターンを独自に指定できますが、FileWatch で
    219         * で指定した条件も、クリアされるので、含める必要があります。
    220         *
    221         * @param       endKey パスの終端一致のパターン
    222         * @see         DirWatch
    223         */
    224        public void setDirWatchEndsWith( final String... endKey ) {
    225                if( endKey != null && endKey.length > 0 ) {
    226                        useDirWatch = true;                                     // 対象があれば、実行するが、true になる。
    227
    228                        dirWatchMch.addEndsWith( endKey );
    229                }
    230        }
    231
    232        /**
    233         * このファイル監視で、最後に処理した結果が、エラーの場合に、true を返します。
    234         *
    235         * 通常は、対象フォルダが見つからない場合や、フォルダスキャン（DirWatch）で
    236         * エラーが発生した場合に、true にセットされます。
    237         * また、stop() メソッドが呼ばれた場合も、true にセットされます。
    238         *
    239         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。
    240         *
    241         * @return      エラー状態(true:エラー,false:正常)
    242         */
    243        public boolean isErrorStatus() {
    244                // DirWatch を使用している場合は、その結果も加味します。
    245//              return isError || dWatch != null && dWatch.isErrorStatus() ;
    246//              return !running || dWatch != null && dWatch.isErrorStatus() ;
    247                return !running.get() || dWatch != null && dWatch.isErrorStatus() ;             // 7.2.9.4 (2020/11/20)
    248        }
    249
    250        /**
    251         * フォルダの監視を開始します。
    252         *
    253         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。
    254         *
    255         * 自身を、Threadに登録して、Thread#start() を実行します。
    256         * 内部の Thread オブジェクトがなければ、新しく作成します。
    257         * すでに、実行中の場合は、何もしません。
    258         * 条件を変えて、実行したい場合は、stop() メソッドで、一旦スレッドを
    259         * 停止させてから、再び、#start() メソッドを呼び出してください。
    260         */
    261        public void start() {
    262                if( thread == null ) {
    263                        thread = new Thread( this );
    264//                      running = true;
    265                        running.set( true );    // 7.2.9.4 (2020/11/20)
    266                        thread.start();                 // running=true; を先に行わないと、すぐに終了してしまう。
    267                }
    268
    269                // 監視漏れのファイルを、一定時間でスキャンする
    270                if( useDirWatch ) {
    271                        dWatch = new DirWatch( dirPath,useTree );
    272                        if( dirWatchMch.isEmpty() ) {                   // 初期値は、未登録時は、本体と同じPathMatcher を使用します。
    273                                dWatch.setPathMatcher( pathMchSet );
    274                        }
    275                        else {
    276                                dWatch.setPathMatcher( dirWatchMch );
    277                        }
    278                        dWatch.callback( path -> action.accept( DIR_WATCH_EVENT , path ) ) ;    // BiConsumer<String,Path> を Consumer<Path> に変換しています。
    279                        dWatch.start();
    280                }
    281        }
    282
    283        /**
    284         * フォルダの監視を終了します。
    285         *
    286         * 自身を登録しているThreadに、割り込みをかけるため、
    287         * Thread#interrupt() を実行します。
    288         * フォルダ監視は、ファイル変更イベントが発生するまで待機していますが、
    289         * interrupt() を実行すると、強制的に中断できます。
    290         * 内部の Thread オブジェクトは、破棄するため、再び、start() メソッドで
    291         * 実行再開することが可能です。
    292         *
    293         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。
    294         */
    295        public void stop() {
    296                if( thread != null ) {
    297//                      running = false;
    298                        running.set( false );           // 7.2.9.4 (2020/11/20)
    299                        thread.interrupt();
    300        //              thread = null;                  1.1.0 (2018/02/01) stop() 時に null を入れると、interrupt() 後の処理が継続できなくなる。
    301        //              なので、run()の最後に、thread = null を入れておきます。
    302                }
    303
    304                if( dWatch != null ) {
    305                        dWatch.stop();
    306                        dWatch = null;
    307                }
    308        }
    309
    310        /**
    311         * Runnableインターフェースのrunメソッドです。
    312         *
    313         * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。
    314         *
    315         * @og.rev 7.2.5.0 (2020/06/01) LOGGERを使用します。
    316         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。
    317         */
    318        @Override
    319        public void run() {
    320                try {
    321                        execute();
    322                }
    323                catch( final IOException ex ) {
    324                        // MSG0102 = ファイル監視に失敗しました。 Path=[{0}]
    325//                      MsgUtil.errPrintln( ex , "MSG0102" , dirPath );
    326                        final String errMsg = "FileWatch#run : Path=" + dirPath ;
    327                        LOGGER.warning( ex , "MSG0102" , errMsg );
    328                }
    329                catch( final Throwable th ) {
    330                        // MSG0021 = 予期せぬエラーが発生しました。\n\tﾒｯｾｰｼﾞ=[{0}]
    331//                      MsgUtil.errPrintln( th , "MSG0021" , toString() );
    332                        final String errMsg = "FileWatch#run : Path=" + dirPath ;
    333                        LOGGER.warning( th , "MSG0021" , errMsg );
    334                }
    335                finally {
    336                        thread  = null;                         // 7.2.5.0 (2020/06/01) 停止処理
    337//                      running = false;
    338                        running.set( false );           // 7.2.9.4 (2020/11/20)
    339                }
    340        }
    341
    342        /**
    343         * runメソッドから呼ばれる、実際の処理。
    344         *
    345         * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加
    346         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。
    347         *
    348         * try ･･･ catch( Throwable ) 構文を、runメソッドの標準的な作りにしておきたいため、
    349         * あえて、実行メソッドを分けているだけです。
    350         */
    351        private void execute() throws IOException {
    352                // ファイル監視などの機能は新しいNIO2クラスで拡張されたので
    353                // 旧File型から、新しいPath型に変換する.
    354                LOGGER.info( () -> "FileWatch Start: " + dirPath );
    355
    356                // デフォルトのファイル・システムを閉じることはできません。(UnsupportedOperationException がスローされる)
    357                // なので、try-with-resources 文 (AutoCloseable) に、入れません。
    358                final FileSystem fs = dirPath.getFileSystem();                  // フォルダが属するファイルシステムを得る()
    359                // try-with-resources 文 (AutoCloseable)
    360                // ファイルシステムに対応する監視サービスを構築する.
    361                // (一つのサービスで複数の監視が可能)
    362                try( WatchService watcher = fs.newWatchService() ) {
    363                        // フォルダに対して監視サービスを登録する.
    364                        final WatchKey watchKey = dirPath.register( watcher , weKind , extModifiers );
    365
    366                        // 監視が有効であるかぎり、ループする.
    367                        // (監視がcancelされるか、監視サービスが停止した場合はfalseとなる)
    368                        try{
    369                                boolean flag = true;
    370//                              while( flag && running ) {
    371                                while( flag && running.get() ) {                                                                // 7.2.9.4 (2020/11/20)
    372                                        // スレッドの割り込み = 終了要求を判定する.
    373                        //              if( Thread.currentThread().isInterrupted() ) {
    374                        //                      throw new InterruptedException();
    375                        //              }
    376
    377                                        // ファイル変更イベントが発生するまで待機する.
    378                                        final WatchKey detecedtWatchKey = watcher.take();
    379
    380                                        // イベント発生元を判定する
    381                                        if( detecedtWatchKey.equals( watchKey ) ) {
    382                                                // 発生したイベント内容をプリントする.
    383                                                for( final WatchEvent<?> event : detecedtWatchKey.pollEvents() ) {
    384                                                        // 追加・変更・削除対象のファイルを取得する.
    385                                                        // (ただし、overflow時などはnullとなることに注意)
    386                                                        final Path path = (Path)event.context();
    387                                                        if( path != null && pathMchSet.matches( path ) ) {
    388                        //                                      synchronized( action ) {
    389//                                                                      action.accept( event.kind().name() , dirPath.resolve( path ) );
    390                                                                        final Path fpath = dirPath.resolve( path );
    391                                                                        if( dWatch == null || dWatch.setAdd( fpath) ) {         // このセット内に、指定された要素がなかった場合はtrue
    392                                                                                action.accept( event.kind().name() , fpath );
    393                                                                        }
    394                                                                        else {
    395                                                                                // CREATE と MODIFY などのイベントが連続して発生するケースへの対応
    396                                                                                LOGGER.info( () -> "WatchEvent Duplication: " + fpath );
    397                                                                        }
    398                        //                                      }
    399                                                        }
    400                                                }
    401                                        }
    402
    403                                        // イベントの受付を再開する.
    404                                        detecedtWatchKey.reset();
    405
    406                                        if( dWatch != null ) {
    407                                                dWatch.setClear();                      // Path重複ﾁｪｯｸ用のSetは、一連のｲﾍﾞﾝﾄ完了時にｸﾘｱしておきます。
    408                                        }
    409
    410                                        // 監視サービスが活きている、または、スレッドの割り込み( = 終了要求)がないことを、をチェックする。
    411                                        flag = watchKey.isValid() && !Thread.currentThread().isInterrupted() ;
    412                                }
    413                        }
    414                        catch( final InterruptedException ex ) {
    415//                              LOGGER.warning( () -> "【WARNING】 FileWatch Canceled:" + dirPath );
    416                                LOGGER.warning( () -> "FileWatch Canceled : [" + dirPath + "]" );
    417                        }
    418                        finally {
    419                                // スレッドの割り込み = 終了要求なので監視をキャンセルしループを終了する。
    420                                if( watchKey != null ) {
    421                                        watchKey.cancel();
    422                                }
    423                        }
    424                }
    425                // FileSystemの実装(sun.nio.fs.WindowsFileSystem)は、close() 未サポート
    426                catch( final UnsupportedOperationException ex ) {
    427                        LOGGER.warning( () -> "FileSystem close : [" + dirPath + "]" );
    428                }
    429
    430//              LOGGER.info( () -> "FileWatch End: " + dirPath );
    431                LOGGER.info( () -> "FileWatch End : [" + dirPath + "]" );
    432
    433//              thread  = null;                                 // 1.1.0 (2018/02/01) 停止処理
    434        //      isError = true;                                 // 何らかの原因で停止すれば、エラーと判断します。
    435        }
    436
    437        /**
    438         *このオブジェクトの文字列表現を返します。
    439         *
    440         * @return      このオブジェクトの文字列表現
    441         */
    442        @Override
    443        public String toString() {
    444                return getClass().getSimpleName() + ":" + dirPath + " , " + DIR_WATCH_EVENT + "=[" + useDirWatch + "]" ;
    445        }
    446}