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
018import java.io.File;
019import java.io.IOException;
020
021import java.nio.file.Path;
022import java.nio.file.PathMatcher;
023import java.nio.file.Files;
024import java.nio.file.DirectoryStream;
025
026import java.util.concurrent.Executors;
027import java.util.concurrent.TimeUnit;
028import java.util.concurrent.ScheduledFuture;
029import java.util.concurrent.ScheduledExecutorService;
030import java.util.function.Consumer;
031
032/**
033 * フォルダに残っているファイルを再実行するためのプログラムです。
034 *
035 * 通常は、FileWatch で、パスを監視していますが、場合によっては、
036 * イベントを拾いそこねることがあります。それを、フォルダスキャンして、拾い上げます。
037 * 10秒間隔で繰り返しスキャンします。条件は、30秒以上前のファイルです。
038 *
039 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
040 *
041 * @version  7.0
042 * @author   Kazuhiko Hasegawa
043 * @since    JDK1.8,
044 */
045public class DirWatch implements Runnable {
046        private static final XLogger LOGGER= XLogger.getLogger( DirWatch.class.getName() );             // ログ出力
047
048        /** 最初にスキャンを実行するまでの遅延時間(秒) の初期値 */
049        public static final long INIT_DELAY     = 5;                    // (秒)
050
051        /** スキャンする間隔(秒) の初期値 */
052        public static final long PERIOD         = 10;                   // (秒)
053
054        /** ファイルのタイムスタンプとの差のチェック(秒) の初期値 */
055        public static final long TIME_DIFF      = 30;                   // (秒)
056
057        private final   Path            sPath;                                          // スキャンパス
058        private final   boolean         useTree;                                        // フォルダ階層をスキャンするかどうか
059
060        // callbackするための、関数型インターフェース(メソッド参照)
061        private Consumer<Path> action = path -> System.out.println( "DirWatch Path=" + path ) ;
062
063        // DirectoryStreamで、パスのフィルタに使用します。
064        private final PathMatcherSet pathMch = new PathMatcherSet();            // PathMatcher インターフェースを継承
065
066        // フォルダスキャンする条件
067        private DirectoryStream.Filter<Path> filter;
068
069        // スキャンを停止する場合に使用します。
070        private ScheduledFuture<?> stFuture ;
071
072        /**
073         * スキャンパスを引数に作成される、コンストラクタです。
074         *
075         * ここでは、階層検索しない(useTree=false)で、インスタンス化します。
076         *
077         * @param       sPath   検索対象となるスキャンパス
078         */
079        public DirWatch( final Path sPath ) {
080                this( sPath , false );
081        }
082
083        /**
084         * スキャンパスと関数型インターフェースフォルダを引数に作成される、コンストラクタです。
085         *
086         * @param       sPath   検索対象となるスキャンパス
087         * @param       useTree 階層スキャンするかどうか(true:する/false:しない)
088         */
089        public DirWatch( final Path sPath, final boolean useTree ) {
090                this.sPath              = sPath;
091                this.useTree    = useTree;
092        }
093
094        /**
095         * 指定のパスの照合操作で、パターンに一致したパスのみ、callback されます。
096         *
097         * ここで指定したパターンの一致を判定し、一致した場合は、callback されます。
098         * 指定しない場合は、すべて許可されたことになります。
099         * なお、#setPathEndsWith(String...) と、この設定は同時には行うことは出来ません。
100         *
101         * @param       pathMch パスの照合操作のパターン
102         * @see         java.nio.file.PathMatcher
103         * @see         #setPathEndsWith(String...)
104         */
105        public void setPathMatcher( final PathMatcher pathMch ) {
106                this.pathMch.addPathMatcher( pathMch );
107        }
108
109        /**
110         * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、callback されます。
111         *
112         * これは、#setPathMatcher(PathMatcher) の簡易指定版です。
113         * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となりcallback されます。
114         * 指定しない場合(null)は、すべて許可されたことになります。
115         * 終端文字列の判定には、大文字小文字の区別を行いません。
116         * なお、#setPathMatcher(PathMatcher) と、この設定は同時には行うことは出来ません。
117         *
118         * @param       endKey パスの終端一致のパターン
119         * @see         #setPathMatcher(PathMatcher)
120         */
121        public void setPathEndsWith( final String... endKey ) {
122                pathMch.addEndsWith( endKey );
123        }
124
125        /**
126         * ファイルパスを、引数に取る Consumer ダオブジェクトを設定します。
127         *
128         * これは、関数型インタフェースなので、ラムダ式またはメソッド参照の代入先として使用できます。
129         * イベントが発生したときの ファイルパス(監視フォルダで、resolveされた、正式なフルパス)を引数に、
130         * accept(Path) メソッドが呼ばれます。
131         *
132         * @param       act 1つの入力(ファイルパス) を受け取る関数型インタフェース
133         * @see         Consumer#accept(Object)
134         */
135        public void callback( final Consumer<Path> act ) {
136                if( act != null ) {
137                        action = act ;
138                }
139        }
140
141        /**
142         * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
143         *
144         * 初期値( initDelay={@value #INIT_DELAY} , period={@value #PERIOD} , timeDiff={@value #TIME_DIFF} ) で、
145         * スキャンを開始します。
146         *
147         * #start( {@value #INIT_DELAY} , {@value #PERIOD} , {@value #TIME_DIFF} ) と同じです。
148         *
149         */
150        public void start() {
151                start( INIT_DELAY , TIME_DIFF , PERIOD );
152        }
153
154        /**
155         * 内部でScheduledExecutorServiceを作成して、ScheduledFuture に、自身をスケジュールします。
156         *
157         * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較を指定して、スキャンを開始します。
158         * ファイルのタイムスタンプとの差とは、ある一定時間経過したファイルのみ、action をcall します。
159         *
160         * @param       initDelay 最初にスキャンを実行するまでの遅延時間(秒)
161         * @param       period    スキャンする間隔(秒)
162         * @param       timeDiff  ファイルのタイムスタンプとの差のチェック(秒)
163         */
164        public void start( final long initDelay , final long period , final long timeDiff ) {
165                LOGGER.info( () -> "DirWatch Start: " + sPath + " Tree=" + useTree + " Delay=" + initDelay + " Period=" + period + " TimeDiff=" + timeDiff );
166
167                // DirectoryStream.Filter<Path> インターフェースは、#accept(Path) しかメソッドを持っていないため、ラムダ式で代用できる。
168                filter = path -> Files.isDirectory( path ) || pathMch.matches( path ) && timeDiff*1000 < ( System.currentTimeMillis() - path.toFile().lastModified() );
169
170        //      filter = path -> Files.isDirectory( path ) || 
171        //                                              pathMch.matches( path ) &&
172        //                                              FileTime.fromMillis( System.currentTimeMillis() - timeDiff*1000L )
173        //                                                              .compareTo( Files.getLastModifiedTime( path ) ) > 0 ;
174
175                final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
176                stFuture = scheduler.scheduleAtFixedRate( this , initDelay , period , TimeUnit.SECONDS );
177        }
178
179        /**
180         * 内部で作成した ScheduledFutureをキャンセルします。
181         */
182        public void stop() {
183                if( stFuture != null && !stFuture.isDone() ) {                  // 完了(正常終了、例外、取り消し)以外は、キャンセルします。
184                        LOGGER.info( () -> "DirWatch Stop: [" + sPath  + "]" );
185                        stFuture.cancel(true);
186        //              try {
187        //                      stFuture.get();                                                                 // 必要に応じて計算が完了するまで待機します。
188        //              }
189        //              catch( InterruptedException | ExecutionException ex) {
190        //                      LOGGER.info( () -> "DirWatch Stop  Error: [" + sPath  + "]" + ex.getMessage() );
191        //              }
192                }
193        }
194
195        /**
196         * Runnableインターフェースのrunメソッドです。
197         *
198         * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。
199         *
200         * ここで、条件に一致したPathオブジェクトが存在すれば、コンストラクタで渡した
201         * 関数型インターフェースがcallされます。
202         *
203         * @og.rev 6.8.2.2 (2017/11/02) ネットワークパスのチェックを行います。
204         */
205        @Override
206        public void run() {
207                try {
208                        LOGGER.debug( () -> "DirWatch Running: " + sPath + " Tree=" + useTree );
209
210                        if( Files.exists( sPath ) ) {                           // 6.8.2.2 (2017/11/02) ネットワークパスのチェック
211                                execute( sPath );
212                        }
213                        else {
214                                // MSG0002 = ファイル/フォルダは存在しません。file=[{0}]
215                                MsgUtil.errPrintln( "MSG0002" , sPath );
216                        }
217                }
218                catch( final Throwable th ) {
219                        // MSG0021 = 予期せぬエラーが発生しました。\n\tメッセージ=[{0}]
220                        MsgUtil.errPrintln( th , "MSG0021" , toString() );
221                }
222        }
223
224        /**
225         * フォルダ階層を順番にスキャンする再帰定義用の関数です。
226         *
227         * run() メソッドから呼ばれます。
228         *
229         * @param       inPpath 検索対象となるパス
230         */
231        private void execute( final Path inPpath ) {
232                try( final DirectoryStream<Path> stream = Files.newDirectoryStream( inPpath, filter ) ) {
233                        for( final Path path : stream ) {
234                                if( Files.isDirectory( path ) ) {
235                                        if( useTree ) { execute( path ); }              // 階層スキャンする場合のみ、再帰処理する。
236                                }
237                                else {
238                                        synchronized( action ) {
239                                                action.accept( path );
240                                        }
241                                }
242                        }
243                }
244                catch( final IOException ex ) {
245                        // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}]
246                        throw MsgUtil.throwException( ex , "MSG0005" , inPpath );
247                }
248        }
249
250        /** main メソッドから呼ばれる ヘルプメッセージです。 {@value}  */
251        public static final String USAGE = "Usage: java jp.euromap.eu63.util.DirWatch dir [-SF sufix]... [-S] [-D delay(s)] [-P period(s)] [-T timeDiff(s)]" ;
252
253        /**
254         * 引数に監視対象のフォルダと、拡張子を指定します。
255         *
256         * スキャンパスの初期値は、起動フォルダです。
257         *
258         * -D 最初にスキャンを実行するまでの遅延時間(秒)
259         * -P スキャンする間隔(秒)
260         * -T ファイルのタイムスタンプとの差のチェック(秒)
261         * -S 階層スキャンする場合は、-S を付けます。
262         * -SF ファイルの拡張子(後ろからのマッチング)を指定します。複数指定することが出来ます。
263         *
264         * テストなので、60秒後に、終了します。
265         *
266         * {@value #USAGE}
267         *
268         * @param       args    コマンド引数配列
269         */
270        public static void main( final String[] args ) {
271                // ********** 【整合性チェック】 **********
272                if( args.length < 1 ) {
273                        System.out.println( USAGE );
274                        return;
275                }
276
277                // ********** 【引数定義】 **********
278                long delay              = DirWatch.INIT_DELAY;          // 最初にスキャンを実行するまでの遅延時間(秒) の初期値
279                long period             = DirWatch.PERIOD;                      // スキャンする間隔(秒) の初期値
280                long timeDiff   = DirWatch.TIME_DIFF;           // ファイルのタイムスタンプとの差のチェック(秒) の初期値
281
282                Path    sPath   = new File( "." ).toPath();     // スキャンパス の初期値
283
284                boolean useTree = false;
285                final java.util.List<String> sufixList = new java.util.ArrayList<>();   // main でしか使わないので、import しない。
286
287                // ********** 【引数処理】 **********
288                for( int i=0; i<args.length; i++ ) {
289                        final String arg = args[i];
290
291                        if(      "-help".equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; }
292                        else if( "-D".equalsIgnoreCase( arg ) ) { delay    = Long.valueOf( args[++i] ); }               // 記号を見つけたら、次の引数をセットします。
293                        else if( "-P".equalsIgnoreCase( arg ) ) { period   = Long.valueOf( args[++i] ); }               // 記号を見つけたら、次の引数をセットします。
294                        else if( "-T".equalsIgnoreCase( arg ) ) { timeDiff = Long.valueOf( args[++i] ); }               // 記号を見つけたら、次の引数をセットします。
295                        else if( "-S".equalsIgnoreCase( arg ) ) { useTree  = true; continue; }                                  // 階層処理
296                        else if( "-SF".equalsIgnoreCase( arg ) ) {
297                                sufixList.add( args[++i] );                                                                                                                     // スキャン対象の拡張子のリスト
298                        }
299                        else {
300                                sPath = new File( arg ).toPath();
301                        }
302                }
303
304                // ********** 【本体処理】 **********
305                final DirWatch watch = new DirWatch( sPath , useTree );                 // 監視先
306
307                if( !sufixList.isEmpty() ) {
308                        watch.setPathEndsWith( sufixList.toArray( new String[sufixList.size()] ) );
309                }
310
311                watch.start( delay,period,timeDiff );
312
313                try{ Thread.sleep( 60000 ); } catch( final InterruptedException ex ){}          // テスト的に60秒待ちます。
314
315                watch.stop();
316
317                System.out.println( "done." );
318        }
319}