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}