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 ){}          // テスト的に30秒待ちます。
420                thList.forEach( th -> th.interrupt() );         // テスト的に停止させます。
421
422                System.out.println( "done." );
423        }
424}