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}