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;
020import java.util.Set;                                                                   // 7.2.5.0 (2020/06/01)
021import java.util.HashSet;                                                               // 7.2.5.0 (2020/06/01)
022
023import java.nio.file.Path;
024import java.nio.file.PathMatcher;
025import java.nio.file.Files;
026import java.nio.file.DirectoryStream;
027
028import java.util.concurrent.Executors;
029import java.util.concurrent.TimeUnit;
030import java.util.concurrent.ScheduledFuture;
031import java.util.concurrent.ScheduledExecutorService;
032import java.util.function.Consumer;
033
034/**
035 * フォルダに残っているファイルを再実行するためのプログラムです。
036 *
037 * 通常は、FileWatch で、パスを監視していますが、場合によっては、
038 * イベントを拾いそこねることがあります。それを、フォルダスキャンして、拾い上げます。
039 * 10秒間隔で繰り返しスキャンします。条件は、30秒以上前のファイルです。
040 *
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 DirWatch implements Runnable {
048        private static final XLogger LOGGER= XLogger.getLogger( DirWatch.class.getSimpleName() );               // ログ出力
049
050        /** 最初にスキャンを実行するまでの遅延時間(秒) の初期値 */
051        public static final long INIT_DELAY     = 10;                   // (秒)
052
053        /** スキャンする間隔(秒) の初期値 */
054        public static final long PERIOD         = 30;                   // (秒)
055
056        /** ファイルのタイムスタンプとの差のチェック(秒) の初期値 */
057        public static final long TIME_DIFF      = 10;                   // (秒)
058
059        private final   Path            sPath;                                          // スキャンパス
060        private final   boolean         useTree;                                        // フォルダ階層をスキャンするかどうか
061
062        // callbackするための、関数型インターフェース(メソッド参照)
063        private Consumer<Path> action = path -> System.out.println( "DirWatch Path=" + path ) ;
064
065        // DirectoryStreamで、パスのフィルタに使用します。
066        private final PathMatcherSet pathMchSet = new PathMatcherSet();         // PathMatcher インターフェースを継承
067
068        // フォルダスキャンする条件
069        private DirectoryStream.Filter<Path> filter;
070
071        // スキャンを停止する場合に使用します。
072        private ScheduledFuture<?> stFuture ;
073
074        // 指定された遅延時間後または定期的にコマンドを実行するようにスケジュールできるExecutorService
075        // 7.2.5.0 (2020/06/01)
076        private ScheduledExecutorService scheduler;
077
078        private boolean isError ;               // 7.2.5.0 (2020/06/01) 直前に、処理エラーが発生していれば、true にします。
079
080        // 7.2.5.0 (2020/06/01) イベントが同時に発生する可能性があるので、Setで重複を除外します。
081        // 8.0.0.0 (2021/07/01)
082        private final Set<Path> pathSet = new HashSet<>();
083
084        /**
085         * スキャンパスを引数に作成される、コンストラクタです。
086         *
087         * ここでは、階層検索しない(useTree=false)で、インスタンス化します。
088         *
089         * @param       sPath   検索対象となるスキャンパス
090         */
091        public DirWatch( final Path sPath ) {
092                this( sPath , false );
093        }
094
095        /**
096         * スキャンパスと関数型インターフェースフォルダを引数に作成される、コンストラクタです。
097         *
098         * @param       sPath   検索対象となるスキャンパス
099         * @param       useTree 階層スキャンするかどうか(true:する/false:しない)
100         */
101        public DirWatch( final Path sPath, final boolean useTree ) {
102                this.sPath              = sPath;
103                this.useTree    = useTree;
104        }
105
106        /**
107         * 指定のパスの照合操作で、パターンに一致したパスのみ、callback されます。
108         *
109         * ここで指定したパターンの一致を判定し、一致した場合は、callback されます。
110         * 指定しない場合は、すべて許可されたことになります。
111         * なお、#setPathEndsWith(String...) と、この設定は同時には行うことは出来ません。
112         *
113         * @param       pathMch パスの照合操作のパターン
114         * @see         java.nio.file.PathMatcher
115         * @see         #setPathEndsWith(String...)
116         */
117        public void setPathMatcher( final PathMatcher pathMch ) {
118                pathMchSet.addPathMatcher( pathMch );
119        }
120
121        /**
122         * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、callback されます。
123         *
124         * これは、#setPathMatcher(PathMatcher) の簡易指定版です。
125         * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となりcallback されます。
126         * 指定しない場合(null)は、すべて許可されたことになります。
127         * 終端文字列の判定には、大文字小文字の区別を行いません。
128         * なお、#setPathMatcher(PathMatcher) と、この設定は同時には行うことは出来ません。
129         *
130         * @param       endKey パスの終端一致のパターン
131         * @see         #setPathMatcher(PathMatcher)
132         */
133        public void setPathEndsWith( final String... endKey ) {
134                pathMchSet.addEndsWith( endKey );
135        }
136
137        /**
138         * ファイルパスを、引数に取る Consumer ダオブジェクトを設定します。
139         *
140         * これは、関数型インタフェースなので、ラムダ式またはメソッド参照の代入先として使用できます。
141         * イベントが発生したときの ファイルパス(監視フォルダで、resolveされた、正式なフルパス)を引数に、
142         * accept(Path) メソッドが呼ばれます。
143         *
144         * @param       act 1つの入力(ファイルパス) を受け取る関数型インタフェース
145         * @see         Consumer#accept(Object)
146         */
147        public void callback( final Consumer<Path> act ) {
148                if( act != null ) {
149                        action = act ;
150                }
151        }
152
153        /**
154         * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
155         *
156         * 初期値( initDelay={@value #INIT_DELAY} , period={@value #PERIOD} , timeDiff={@value #TIME_DIFF} ) で、
157         * スキャンを開始します。
158         *
159         * #start( {@value #INIT_DELAY} , {@value #PERIOD} , {@value #TIME_DIFF} ) と同じです。
160         *
161         */
162        public void start() {
163                start( INIT_DELAY , PERIOD , TIME_DIFF );
164        }
165
166        /**
167         * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
168         *
169         * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較を指定して、スキャンを開始します。
170         * ファイルのタイムスタンプとの差とは、ある一定時間経過したファイルのみ、action をcall します。
171         *
172         * @og.rev 7.2.5.0 (2020/06/01) ScheduledExecutorServiceをインスタンス変数にする。
173         *
174         * @param       initDelay 最初にスキャンを実行するまでの遅延時間(秒)
175         * @param       period    スキャンする間隔(秒)
176         * @param       timeDiff  ファイルのタイムスタンプとの差のチェック(秒)
177         */
178        public void start( final long initDelay , final long period , final long timeDiff ) {
179//              LOGGER.info( () -> "DirWatch Start: " + sPath + " Tree=" + useTree + " Delay=" + initDelay + " Period=" + period + " TimeDiff=" + timeDiff );
180                LOGGER.debug( () -> "DirWatch Start: " + sPath + " Tree=" + useTree + " Delay=" + initDelay + " Period=" + period + " TimeDiff=" + timeDiff );
181
182                // DirectoryStream.Filter<Path> インターフェースは、#accept(Path) しかメソッドを持っていないため、ラムダ式で代用できる。
183                filter = path -> Files.isDirectory( path ) || pathMchSet.matches( path ) && timeDiff*1000 < ( System.currentTimeMillis() - path.toFile().lastModified() );
184
185        //      filter = path -> Files.isDirectory( path ) ||
186        //                                              pathMchSet.matches( path ) &&
187        //                                              FileTime.fromMillis( System.currentTimeMillis() - timeDiff*1000L )
188        //                                                              .compareTo( Files.getLastModifiedTime( path ) ) > 0 ;
189
190        //      7.2.5.0 (2020/06/01)
191        //      final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
192                if( scheduler == null ) {
193                        scheduler = Executors.newSingleThreadScheduledExecutor();
194                }
195                stFuture = scheduler.scheduleAtFixedRate( this , initDelay , period , TimeUnit.SECONDS );
196        }
197
198        /**
199         * 内部で作成した ScheduledFutureをキャンセルします。
200         *
201         * @og.rev 7.2.5.0 (2020/06/01) ScheduledExecutorServiceを初期化する。
202         */
203        public void stop() {
204                if( stFuture != null && !stFuture.isDone() ) {                  // 完了(正常終了、例外、取り消し)以外は、キャンセルします。
205                        LOGGER.info( () -> "DirWatch Stop: [" + sPath  + "]" );
206                        stFuture.cancel(true);                                                          // true は、実行しているスレッドに割り込む必要がある場合。
207        //              stFuture.cancel(false);                                                         // false は、実行中のタスクを完了できる。
208        //              try {
209        //                      stFuture.get();                                                                 // 必要に応じて計算が完了するまで待機します。
210        //              }
211        //              catch( InterruptedException | ExecutionException ex) {
212        //                      LOGGER.info( () -> "DirWatch Stop  Error: [" + sPath  + "]" + ex.getMessage() );
213        //              }
214                }
215                //      7.2.5.0 (2020/06/01)
216                // stop 漏れが発生した場合、どれかがstop を呼べば、初期化されるようにしておきます。
217                if( scheduler != null ) {
218                        scheduler.shutdownNow();                                                        // 実行中のアクティブなタスクすべての停止を試みます。
219                        scheduler = null;
220                }
221        }
222
223        /**
224         * このフォルダスキャンで、最後に処理した結果が、エラーの場合に、true を返します。
225         *
226         * 対象フォルダが見つからない場合や、検索時にエラーが発生した場合に、true にセットされます。
227         * 正常にスキャンできた場合は、false にリセットされます。
228         *
229         * @og.rev 7.2.5.0 (2020/06/01) 新規追加。
230         *
231         * @return      エラー状態(true:エラー,false:正常)
232         */
233        public boolean isErrorStatus() {
234                return isError;
235        }
236
237        /**
238         * Runnableインターフェースのrunメソッドです。
239         *
240         * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。
241         *
242         * ここで、条件に一致したPathオブジェクトが存在すれば、コンストラクタで渡した
243         * 関数型インターフェースがcallされます。
244         *
245         * @og.rev 6.8.2.2 (2017/11/02) ネットワークパスのチェックを行います。
246         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
247         */
248        @Override       // Runnable
249        public void run() {
250                try {
251                        LOGGER.debug( () -> "DirWatch Running: " + sPath + " Tree=" + useTree );
252
253//                      if( Files.exists( sPath ) ) {                           // 6.8.2.2 (2017/11/02) ネットワークパスのチェック
254                        if( FileUtil.exists( sPath ) ) {                        // 7.2.5.0 (2020/06/01) ネットワークパスのチェック
255                                execute( sPath );
256                                isError = false;                                                // エラーをリセットします。
257                        }
258                        else {
259                                isError = true;                                                 // エラーをセットします。
260
261                                // 7.2.5.0 (2020/06/01)
262//                              MsgUtil.errPrintln( "MSG0002" , sPath );
263                                // MSG0002 = ファイル/フォルダは存在しません。file=[{0}]
264                                final String errMsg = "DirWatch#run : sPath=" + sPath ;
265                                LOGGER.warning( "MSG0002" , errMsg );
266                                stop();
267                        }
268                }
269                catch( final Throwable th ) {
270                        isError = true;                                                         // エラーをセットします。
271
272                        // 7.2.5.0 (2020/06/01)
273//                      MsgUtil.errPrintln( th , "MSG0021" , toString() );
274                        // MSG0021 = 予期せぬエラーが発生しました。\n\tメッセージ=[{0}]
275                        final String errMsg = "DirWatch#run : Path=" + sPath ;
276                        LOGGER.warning( th , "MSG0021" , errMsg );
277                }
278        }
279
280        /**
281         * フォルダ階層を順番にスキャンする再帰定義用の関数です。
282         *
283         * run() メソッドから呼ばれます。
284         *
285         * @og.rev 7.2.5.0 (2020/06/01) 大量のファイルがある場合、FileWatchで重複する部分を削除する
286         * @og.rev 8.0.0.0 (2021/07/01) sPathのsynchronized作成
287         *
288         * @param       inPpath 検索対象となるパス
289         */
290        private void execute( final Path inPpath ) {
291                try( DirectoryStream<Path> stream = Files.newDirectoryStream( inPpath, filter ) ) {
292                        LOGGER.debug( () -> "DirWatch execute: " + inPpath );
293                        for( final Path path : stream ) {
294                                if( Files.isDirectory( path ) ) {
295                                        if( useTree ) { execute( path ); }              // 階層スキャンする場合のみ、再帰処理する。
296                                }
297                                else {
298                                        synchronized( sPath ) {
299                                                // 7.2.5.0 (2020/06/01) 大量のファイルがある場合、FileWatchで重複する
300                                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 LinguisticNaming
301//                                              if( setAdd( path ) ) {                  // このセット内に、指定された要素がなかった場合はtrue
302                                                if( pathSetAdd( path ) ) {              // このセット内に、指定された要素がなかった場合はtrue
303                                                        action.accept( path );
304                                                }
305                                        }
306                                }
307                        }
308                        setClear();                                                             // 7.2.5.0 (2020/06/01)
309                }
310                catch( final IOException ex ) {
311                        // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}]
312                        throw MsgUtil.throwException( ex , "MSG0005" , inPpath );
313                }
314        }
315
316        /**
317         * スキャンファイルの重複チェック用SetにPathを追加します。
318         *
319         * このセット内に、指定された要素がなかった場合はtrueを返します。
320         *
321         * @og.rev 1.3.0 (2019/04/01) イベントが同時に発生する可能性があるので、Setで重複を除外します。
322         * @og.rev 8.5.4.2 (2024/01/12) PMD 7.0.0 LinguisticNaming 対応
323         *
324         * @param       path    登録対象となるパス
325         * @return      このセット内に、指定された要素がなかった場合はtrue
326         */
327//      public boolean setAdd( final Path path ) {
328        public boolean pathSetAdd( final Path path ) {
329                return pathSet.add( path );
330        }
331
332        /**
333         * スキャンファイルの重複チェック用Setをクリアします。
334         *
335         * 短時間に大量のファイルを処理する場合にイベントとDirWatchが重複したり、
336         * DirWatch 自身が繰返しで重複処理する場合を想定して、同じファイル名は処理しません。
337         * ただし、DATファイルは、基本同じファイル名で来るので、あるタイミングでクリアする必要があります。
338         *
339         * @og.rev 1.3.0 (2019/04/01) イベントが同時に発生する可能性があるので、Setで重複を除外します。
340         * @og.rev 8.0.0.0 (2021/07/01) pathSetのsynchronized対応
341         */
342        public void setClear() {
343                synchronized( pathSet ) {
344                        pathSet.clear();
345                }
346        }
347
348        /**
349         *このオブジェクトの文字列表現を返します。
350         *
351         * @return      このオブジェクトの文字列表現
352         */
353        @Override               // Object
354        public String toString() {
355                return getClass().getSimpleName() + ":" + sPath + " , Tree=[" + useTree + "]" ;
356        }
357}