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