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         *
264         * 自身を、Threadに登録して、Thread#start() を実行します。
265         * 内部の Thread オブジェクトがなければ、新しく作成します。
266         * すでに、実行中の場合は、何もしません。
267         * 条件を変えて、実行したい場合は、stop() メソッドで、一旦スレッドを
268         * 停止させてから、再び、#start() メソッドを呼び出してください。
269         */
270        public void start() {
271                // 7.4.4.0 (2021/06/30) stop() してから、実際に止まるまでの時間、待機します。
272                int cnt = 0;
273                while( running ) {
274                        cnt++ ;
275                        try{ Thread.sleep( STOP_WATI_TIME ); } catch( final InterruptedException ex ){}
276                        if( cnt >= STOP_WATI_CNT ) {    // ループ後も、まだ、stop() 出来ていない場合。
277                                LOGGER.warning( () -> "FileWatch Stop Error : [" + dirPath + "]" );
278                        }
279                }
280
281                running = true;                         // 7.4.4.0 (2021/06/30) 復活
282
283                if( thread == null ) {
284                        thread = new Thread( this );
285//                      running = true;
286//                      running.set( true );    // 7.2.9.4 (2020/11/20)
287                        thread.start();                 // running=true; を先に行わないと、すぐに終了してしまう。
288                }
289
290                // 監視漏れのファイルを、一定時間でスキャンする
291                if( useDirWatch ) {
292                        dWatch = new DirWatch( dirPath,useTree );
293                        if( dirWatchMch.isEmpty() ) {                   // 初期値は、未登録時は、本体と同じPathMatcher を使用します。
294                                dWatch.setPathMatcher( pathMchSet );
295                        }
296                        else {
297                                dWatch.setPathMatcher( dirWatchMch );
298                        }
299                        dWatch.callback( path -> action.accept( DIR_WATCH_EVENT , path ) ) ;    // BiConsumer<String,Path> を Consumer<Path> に変換しています。
300                        dWatch.start();
301                }
302        }
303
304        /**
305         * フォルダの監視を終了します。
306         *
307         * 自身を登録しているThreadに、割り込みをかけるため、
308         * Thread#interrupt() を実行します。
309         * フォルダ監視は、ファイル変更イベントが発生するまで待機していますが、
310         * interrupt() を実行すると、強制的に中断できます。
311         * 内部の Thread オブジェクトは、破棄するため、再び、start() メソッドで
312         * 実行再開することが可能です。
313         *
314         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。
315         * @og.rev 7.4.4.0 (2021/06/30) thread の存在有無にかかわらず、running は停止状態にする。
316         */
317        public void stop() {
318                // 7.4.4.0 (2021/06/30) thread の存在有無にかかわらず、running は停止状態にする。
319                running = false;                // 7.4.4.0 (2021/06/30) 復活
320
321                if( thread != null ) {
322//                      running = false;
323        //              running.set( false );           // 7.2.9.4 (2020/11/20)
324                        thread.interrupt();
325        //              thread = null;                  1.1.0 (2018/02/01) stop() 時に null を入れると、interrupt() 後の処理が継続できなくなる。
326        //              なので、run()の最後に、thread = null を入れておきます。
327                }
328
329                if( dWatch != null ) {
330                        dWatch.stop();
331                        dWatch = null;
332                }
333        }
334
335        /**
336         * Runnableインターフェースのrunメソッドです。
337         *
338         * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。
339         *
340         * @og.rev 7.2.5.0 (2020/06/01) LOGGERを使用します。
341         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。
342         */
343        @Override
344        public void run() {
345                try {
346                        execute();
347                }
348                catch( final IOException ex ) {
349                        // MSG0102 = ファイル監視に失敗しました。 Path=[{0}]
350//                      MsgUtil.errPrintln( ex , "MSG0102" , dirPath );
351                        final String errMsg = "FileWatch#run : Path=" + dirPath ;
352                        LOGGER.warning( ex , "MSG0102" , errMsg );
353                }
354                catch( final Throwable th ) {
355                        // MSG0021 = 予期せぬエラーが発生しました。\n\tメッセージ=[{0}]
356//                      MsgUtil.errPrintln( th , "MSG0021" , toString() );
357                        final String errMsg = "FileWatch#run : Path=" + dirPath ;
358                        LOGGER.warning( th , "MSG0021" , errMsg );
359                }
360                finally {
361                        running = false;                        // 7.4.4.0 (2021/06/30) 停止条件だが、予期せぬエラーで停止した場合も、設定する。
362                        thread  = null;                         // 7.2.5.0 (2020/06/01) 停止処理
363//                      running = false;
364//                      running.set( false );           // 7.2.9.4 (2020/11/20)
365                }
366        }
367
368        /**
369         * runメソッドから呼ばれる、実際の処理。
370         *
371         * try ・・・ catch( Throwable ) 構文を、runメソッドの標準的な作りにしておきたいため、
372         * あえて、実行メソッドを分けているだけです。
373         *
374         * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加
375         * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え
376         * @og.rev 8.0.0.0 (2021/07/01) dirPathのsynchronized作成
377         */
378        private void execute() throws IOException {
379                // ファイル監視などの機能は新しいNIO2クラスで拡張されたので
380                // 旧File型から、新しいPath型に変換する.
381                LOGGER.info( () -> "FileWatch Start: " + dirPath );
382
383                // デフォルトのファイル・システムを閉じることはできません。(UnsupportedOperationException がスローされる)
384                // なので、try-with-resources 文 (AutoCloseable) に、入れません。
385//              final FileSystem fs = dirPath.getFileSystem();                  // フォルダが属するファイルシステムを得る()
386                final FileSystem fs = FileSystems.getDefault();                 // 7.4.4.0 (2021/06/30) 上記と同じオブジェクトだから。
387
388                // try-with-resources 文 (AutoCloseable)
389                // ファイルシステムに対応する監視サービスを構築する.
390                // (一つのサービスで複数の監視が可能)
391                try( WatchService watcher = fs.newWatchService() ) {
392                        // フォルダに対して監視サービスを登録する.
393                        final WatchKey watchKey = dirPath.register( watcher , weKind , extModifiers );
394
395                        // 監視が有効であるかぎり、ループする.
396                        // (監視がcancelされるか、監視サービスが停止した場合はfalseとなる)
397                        try{
398                                boolean flag = true;
399                                while( flag && running ) {                                                                              // 7.4.4.0 (2021/06/30) 復活
400//                              while( flag && running.get() ) {                                                                // 7.2.9.4 (2020/11/20)
401                                        // スレッドの割り込み = 終了要求を判定する.
402                        //              if( Thread.currentThread().isInterrupted() ) {
403                        //                      throw new InterruptedException();
404                        //              }
405
406                                        // take は、ファイル変更イベントが発生するまで待機する.
407                                        final WatchKey detectKey = watcher.take();                      // poll() は、キューが空の場合はブロックせずに null を返す
408
409                                        // イベント発生元を判定する
410//                                      if( detectKey.equals( watchKey ) ) {
411                                        if( watchKey.equals( detectKey ) ) {                            // 8.0.0.0 (2021/07/01) 入れ替え(null対応)
412                                                // 発生したイベント内容をプリントする.
413                                                for( final WatchEvent<?> event : detectKey.pollEvents() ) {
414                                                        // 追加・変更・削除対象のファイルを取得する.
415                                                        // (ただし、overflow時などはnullとなることに注意)
416                                                        final Path path = (Path)event.context();
417                                                        if( path != null && pathMchSet.matches( path ) ) {
418                                                                final Path fpath = dirPath.resolve( path );
419                                                                synchronized( dirPath ) {                               // 8.0.0.0 (2021/07/01) dirPathのsynchronized作成
420                                                                        if( dWatch == null || dWatch.setAdd( fpath) ) {         // このセット内に、指定された要素がなかった場合はtrue
421                                                                                action.accept( event.kind().name() , fpath );
422                                                                        }
423                                                                        else {
424                                                                                // CREATE と MODIFY などのイベントが連続して発生するケースへの対応
425                                                                                LOGGER.info( () -> "WatchEvent Duplication: " + fpath );
426                                                                        }
427                                                                }
428                                                        }
429                                                }
430                                        }
431
432                                        // イベントの受付を再開する.
433                                        if( detectKey != null ) {               // 8.0.0.0 (2021/07/01) null対応
434                                                detectKey.reset();
435                                        }
436
437                                        if( dWatch != null ) {
438                                                dWatch.setClear();                      // Path重複チェック用のSetは、一連のイベント完了時にクリアしておきます。
439                                        }
440
441                                        // 監視サービスが活きている、または、スレッドの割り込み( = 終了要求)がないことを、をチェックする。
442                                        flag = watchKey.isValid() && !Thread.currentThread().isInterrupted() ;
443
444                                        // 7.4.4.0 (2021/06/30) ※ 63フォルダ以上は、監視できない?(Tomcat上では?)
445                                        if( !watchKey.isValid() ) {
446                                                LOGGER.warning( () -> "FileWatch No isValid : [" + dirPath + "]" );
447                                        }
448                                }
449                        }
450                        catch( final InterruptedException ex ) {
451//                              LOGGER.warning( () -> "【WARNING】 FileWatch Canceled:" + dirPath );
452                                LOGGER.warning( () -> "FileWatch Canceled : [" + dirPath + "]" );
453                        }
454                        finally {
455                                // スレッドの割り込み = 終了要求なので監視をキャンセルしループを終了する。
456                                if( watchKey != null ) {
457                                        watchKey.cancel();
458                                }
459                        }
460                }
461                // FileSystemの実装(sun.nio.fs.WindowsFileSystem)は、close() 未サポート
462                catch( final UnsupportedOperationException ex ) {
463                        LOGGER.warning( () -> "FileSystem close : [" + dirPath + "]" );
464                }
465
466                // 7.4.4.0 (2021/06/30) 念のため、入れておきます。
467                catch( final Throwable th ) {
468                        // MSG0021 = 予期せぬエラーが発生しました。\n\tメッセージ=[{0}]
469                        final String errMsg = "FileWatch#execute : Path=" + dirPath ;
470                        LOGGER.warning( th , "MSG0021" , errMsg );
471                }
472
473//              LOGGER.info( () -> "FileWatch End: " + dirPath );
474                LOGGER.info( () -> "FileWatch End : [" + dirPath + "]" );
475
476//              thread  = null;                                 // 1.1.0 (2018/02/01) 停止処理
477        //      isError = true;                                 // 何らかの原因で停止すれば、エラーと判断します。
478        }
479
480        /**
481         *このオブジェクトの文字列表現を返します。
482         *
483         * @return      このオブジェクトの文字列表現
484         */
485        @Override
486        public String toString() {
487                return getClass().getSimpleName() + ":" + dirPath + " , " + DIR_WATCH_EVENT + "=[" + useDirWatch + "]" ;
488        }
489}