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 // 6.9.8.0 (2018/05/28) FindBugs:ボクシング/アンボクシングはプリミティブを解析する( valueOf → parseLong ) 292 if( "-help".equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; } 293 else if( "-D".equalsIgnoreCase( arg ) ) { delay = Long.parseLong( args[++i] ); } // 記号を見つけたら、次の引数をセットします。 294 else if( "-P".equalsIgnoreCase( arg ) ) { period = Long.parseLong( args[++i] ); } // 記号を見つけたら、次の引数をセットします。 295 else if( "-T".equalsIgnoreCase( arg ) ) { timeDiff = Long.parseLong( args[++i] ); } // 記号を見つけたら、次の引数をセットします。 296 else if( "-S".equalsIgnoreCase( arg ) ) { useTree = true; continue; } // 階層処理 297 else if( "-SF".equalsIgnoreCase( arg ) ) { 298 sufixList.add( args[++i] ); // スキャン対象の拡張子のリスト 299 } 300 else { 301 sPath = new File( arg ).toPath(); 302 } 303 } 304 305 // ********** 【本体処理】 ********** 306 final DirWatch watch = new DirWatch( sPath , useTree ); // 監視先 307 308 if( !sufixList.isEmpty() ) { 309 watch.setPathEndsWith( sufixList.toArray( new String[sufixList.size()] ) ); 310 } 311 312 watch.start( delay,period,timeDiff ); 313 314 try{ Thread.sleep( 60000 ); } catch( final InterruptedException ex ){} // テスト的に60秒待ちます。 315 316 watch.stop(); 317 318 System.out.println( "done." ); 319 } 320}