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