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