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.WatchKey; 026import java.nio.file.StandardWatchEventKinds; 027import java.nio.file.WatchService; 028 029import java.util.function.BiConsumer; 030import java.util.concurrent.atomic.AtomicBoolean; // 7.2.9.4 (2020/11/20) volatile boolean の代替え 031 032/** 033 * FileWatch は、ファイル監視を行うクラスです。 034 * 035 *<pre> 036 * ファイルが、追加(作成)、変更、削除された場合に、イベントが発生します。 037 * このクラスは、Runnable インターフェースを実装しているため、Thread で実行することで、 038 * 個々のフォルダの監視を行います。 039 * 040 *</pre> 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 FileWatch implements Runnable { 048 private static final XLogger LOGGER= XLogger.getLogger( FileWatch.class.getSimpleName() ); // ログ出力 049 050 /** Path に、WatchService を register するときの作成イベントの簡易指定できるように。 */ 051 public static final WatchEvent.Kind<Path> CREATE = StandardWatchEventKinds.ENTRY_CREATE ; 052 053 /** Path に、WatchService を register するときの変更イベントの簡易指定できるように。 */ 054 public static final WatchEvent.Kind<Path> MODIFY = StandardWatchEventKinds.ENTRY_MODIFY ; 055 056 /** Path に、WatchService を register するときの削除イベントの簡易指定できるように。 */ 057 public static final WatchEvent.Kind<Path> DELETE = StandardWatchEventKinds.ENTRY_DELETE ; 058 059 /** Path に、WatchService を register するときの特定不能時イベントの簡易指定できるように。 */ 060 public static final WatchEvent.Kind<?> OVERFLOW = StandardWatchEventKinds.OVERFLOW ; 061 062 // Path に、WatchService を register するときのイベント 063 private static final WatchEvent.Kind<?>[] WE_KIND = new WatchEvent.Kind<?>[] { 064 CREATE , MODIFY , DELETE , OVERFLOW 065 }; 066 067 // Path に、WatchService を register するときの登録方法の修飾子(修飾子 なしの場合) 068 private static final WatchEvent.Modifier[] WE_MOD_ONE = new WatchEvent.Modifier[0]; // Modifier なし 069 070 // Path に、WatchService を register するときの登録方法の修飾子(以下の階層も監視対象にします) 071 private static final WatchEvent.Modifier[] WE_MOD_TREE = new WatchEvent.Modifier[] { // ツリー階層 072 com.sun.nio.file.ExtendedWatchEventModifier.FILE_TREE 073 }; 074 075 /** DirWatch でスキャンした場合のイベント名 {@value} */ 076 public static final String DIR_WATCH_EVENT = "DirWatch"; 077 078 // 監視対象のフォルダ 079 private final Path dirPath ; 080 081 // 監視方法 082 private final boolean useTree ; 083 private final WatchEvent.Modifier[] extModifiers ; 084 085 // callbackするための、関数型インターフェース(メソッド参照) 086 private BiConsumer<String,Path> action = (event,path) -> System.out.println( "Event=" + event + " , Path=" + path ) ; 087 088 // Path に、WatchService を register するときのイベント 089 private WatchEvent.Kind<?>[] weKind = WE_KIND ; // 初期値は、すべて 090 091 // パスの照合操作を行うPathMatcher の初期値 092 private final PathMatcherSet pathMchSet = new PathMatcherSet(); // PathMatcher インターフェースを継承 093 094 // DirWatchのパスの照合操作を行うPathMatcher の初期値 095 private final PathMatcherSet dirWatchMch = new PathMatcherSet(); // PathMatcher インターフェースを継承 096 097 // 何らかの原因でイベントもれした場合、フォルダスキャンを行います。 098 private boolean useDirWatch = true; // 初期値は、イベント漏れ監視を行います。 099 private DirWatch dWatch ; // DirWatch のstop時に呼び出すための変数 100 private Thread thread ; // 停止するときに呼び出すため 101 102// private volatile boolean running ; // 状態とThreadの停止に使用する。 103 private final AtomicBoolean running = new AtomicBoolean(); // 7.2.9.4 (2020/11/20) volatile boolean の代替え ( 状態とThreadの停止に使用する。) 104 105 /** 106 * 処理対象のフォルダのパスオブジェクトを指定して、ファイル監視インスタンスを作成します。 107 * 108 * ここでは、指定のフォルダの内のファイルのみ監視します。 109 * これは、new FileWatch( dir , false ) とまったく同じです。 110 * 111 * @param dir 処理対象のフォルダオブジェクト 112 */ 113 public FileWatch( final Path dir ) { 114 this( dir , false ); 115 } 116 117 /** 118 * 処理対象のフォルダのパスオブジェクトと、監視対象方法を指定して、ファイル監視インスタンスを作成します。 119 * 120 * useTree を true に設定すると、指定のフォルダの内のフォルダ階層を、すべて監視対象とします。 121 * 122 * @param dir 処理対象のフォルダのパスオブジェクト 123 * @param useTree フォルダツリーの階層をさかのぼって監視するかどうか(true:フォルダ階層を下る) 124 */ 125 public FileWatch( final Path dir , final boolean useTree ) { 126 dirPath = dir ; 127 this.useTree = useTree; 128 extModifiers = useTree ? WE_MOD_TREE : WE_MOD_ONE ; 129 } 130 131 /** 132 * 指定のイベントの種類のみ、監視対象に設定します。 133 * 134 * ここで指定したイベントのみ、監視対象になり、callback されます。 135 * 第一引数は、イベントの種類(ENTRY_CREATE,ENTRY_MODIFY,ENTRY_DELETE,OVERFLOW) 136 * 137 * @param kind 監視対象に設定するイベントの種類 138 * @see java.nio.file.StandardWatchEventKinds 139 */ 140 public void setEventKinds( final WatchEvent.Kind<?>... kind ) { 141 if( kind != null && kind.length > 0 ) { 142 weKind = kind; 143 } 144 } 145 146 /** 147 * 指定のパスの照合操作で、パターンに一致したパスのみ、callback されます。 148 * 149 * ここで指定したパターンの一致を判定し、一致した場合は、callback されます。 150 * 指定しない場合は、すべて許可されたことになります。 151 * なお、#setPathEndsWith(String...) と、この設定は同時には行うことは出来ません。 152 * (最後に登録した条件が、適用されます。) 153 * 154 * @param pathMch パスの照合操作のパターン 155 * @see java.nio.file.PathMatcher 156 * @see #setPathEndsWith(String...) 157 */ 158 public void setPathMatcher( final PathMatcher pathMch ) { 159 pathMchSet.addPathMatcher( pathMch ); 160 } 161 162 /** 163 * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、callback されます。 164 * 165 * これは、#setPathMatcher(PathMatcher) の簡易指定版です。 166 * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となりcallback されます。 167 * 指定しない場合(null)は、すべて許可されたことになります。 168 * 終端文字列の判定には、大文字小文字の区別を行いません。 169 * なお、#setPathMatcher(PathMatcher) と、この設定は同時には行うことは出来ません。 170 * (最後に登録した条件が、適用されます。) 171 * 172 * @param endKey パスの終端一致のパターン 173 * @see #setPathMatcher(PathMatcher) 174 */ 175 public void setPathEndsWith( final String... endKey ) { 176 pathMchSet.addEndsWith( endKey ); 177 } 178 179 /** 180 * イベントの種類と、ファイルパスを、引数に取る BiConsumer ダオブジェクトを設定します。 181 * 182 * これは、関数型インタフェースなので、ラムダ式またはメソッド参照の代入先として使用できます。 183 * イベントが発生したときの イベントの種類と、そのファイルパスを引数に、accept(String,Path) メソッドが呼ばれます。 184 * 第一引数は、イベントの種類(ENTRY_CREATE,ENTRY_MODIFY,ENTRY_DELETE,OVERFLOW) 185 * 第二引数は、ファイルパス(監視フォルダで、resolveされた、正式なフルパス) 186 * 187 * @param act 2つの入力(イベントの種類 とファイルパス) を受け取る関数型インタフェース 188 * @see BiConsumer#accept(Object,Object) 189 */ 190 public void callback( final BiConsumer<String,Path> act ) { 191 if( act != null ) { 192 action = act ; 193 } 194 } 195 196 /** 197 * 何らかの原因でイベントを掴み損ねた場合に、フォルダスキャンするかどうかを指定します。 198 * 199 * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較時間等は、 200 * DirWatch の初期値をそのまま使用するため、ここでは指定できません。 201 * 個別に指定したい場合は、このフラグをfalse にセットして、個別に、DirWatch を作成してください。 202 * このメソッドでは、#setPathEndsWith( String... )や、#setPathMatcher( PathMatcher ) で 203 * 指定した条件が、そのまま適用されます。 204 * 205 * @param flag フォルダスキャンするかどうか(true:する/false:しない) 206 * @see DirWatch 207 */ 208 public void setUseDirWatch( final boolean flag ) { 209 useDirWatch = flag; 210 } 211 212 /** 213 * 何らかの原因でイベントを掴み損ねた場合の、フォルダスキャンの対象ファイルの拡張子を指定します。 214 * 215 * このメソッドを使用する場合は、useDirWatch は、true にセットされます。 216 * スキャン開始の遅延時間と、スキャン間隔、ファイルのタイムスタンプとの比較時間等は、 217 * DirWatch の初期値をそのまま使用するため、ここでは指定できません。 218 * このメソッドでは、DirWatch 対象の終端パターンを独自に指定できますが、FileWatch で 219 * で指定した条件も、クリアされるので、含める必要があります。 220 * 221 * @param endKey パスの終端一致のパターン 222 * @see DirWatch 223 */ 224 public void setDirWatchEndsWith( final String... endKey ) { 225 if( endKey != null && endKey.length > 0 ) { 226 useDirWatch = true; // 対象があれば、実行するが、true になる。 227 228 dirWatchMch.addEndsWith( endKey ); 229 } 230 } 231 232 /** 233 * このファイル監視で、最後に処理した結果が、エラーの場合に、true を返します。 234 * 235 * 通常は、対象フォルダが見つからない場合や、フォルダスキャン(DirWatch)で 236 * エラーが発生した場合に、true にセットされます。 237 * また、stop() メソッドが呼ばれた場合も、true にセットされます。 238 * 239 * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。 240 * 241 * @return エラー状態(true:エラー,false:正常) 242 */ 243 public boolean isErrorStatus() { 244 // DirWatch を使用している場合は、その結果も加味します。 245// return isError || dWatch != null && dWatch.isErrorStatus() ; 246// return !running || dWatch != null && dWatch.isErrorStatus() ; 247 return !running.get() || dWatch != null && dWatch.isErrorStatus() ; // 7.2.9.4 (2020/11/20) 248 } 249 250 /** 251 * フォルダの監視を開始します。 252 * 253 * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。 254 * 255 * 自身を、Threadに登録して、Thread#start() を実行します。 256 * 内部の Thread オブジェクトがなければ、新しく作成します。 257 * すでに、実行中の場合は、何もしません。 258 * 条件を変えて、実行したい場合は、stop() メソッドで、一旦スレッドを 259 * 停止させてから、再び、#start() メソッドを呼び出してください。 260 */ 261 public void start() { 262 if( thread == null ) { 263 thread = new Thread( this ); 264// running = true; 265 running.set( true ); // 7.2.9.4 (2020/11/20) 266 thread.start(); // running=true; を先に行わないと、すぐに終了してしまう。 267 } 268 269 // 監視漏れのファイルを、一定時間でスキャンする 270 if( useDirWatch ) { 271 dWatch = new DirWatch( dirPath,useTree ); 272 if( dirWatchMch.isEmpty() ) { // 初期値は、未登録時は、本体と同じPathMatcher を使用します。 273 dWatch.setPathMatcher( pathMchSet ); 274 } 275 else { 276 dWatch.setPathMatcher( dirWatchMch ); 277 } 278 dWatch.callback( path -> action.accept( DIR_WATCH_EVENT , path ) ) ; // BiConsumer<String,Path> を Consumer<Path> に変換しています。 279 dWatch.start(); 280 } 281 } 282 283 /** 284 * フォルダの監視を終了します。 285 * 286 * 自身を登録しているThreadに、割り込みをかけるため、 287 * Thread#interrupt() を実行します。 288 * フォルダ監視は、ファイル変更イベントが発生するまで待機していますが、 289 * interrupt() を実行すると、強制的に中断できます。 290 * 内部の Thread オブジェクトは、破棄するため、再び、start() メソッドで 291 * 実行再開することが可能です。 292 * 293 * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。 294 */ 295 public void stop() { 296 if( thread != null ) { 297// running = false; 298 running.set( false ); // 7.2.9.4 (2020/11/20) 299 thread.interrupt(); 300 // thread = null; 1.1.0 (2018/02/01) stop() 時に null を入れると、interrupt() 後の処理が継続できなくなる。 301 // なので、run()の最後に、thread = null を入れておきます。 302 } 303 304 if( dWatch != null ) { 305 dWatch.stop(); 306 dWatch = null; 307 } 308 } 309 310 /** 311 * Runnableインターフェースのrunメソッドです。 312 * 313 * 規定のスケジュール時刻が来ると、呼ばれる runメソッドです。 314 * 315 * @og.rev 7.2.5.0 (2020/06/01) LOGGERを使用します。 316 * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。 317 */ 318 @Override 319 public void run() { 320 try { 321 execute(); 322 } 323 catch( final IOException ex ) { 324 // MSG0102 = ファイル監視に失敗しました。 Path=[{0}] 325// MsgUtil.errPrintln( ex , "MSG0102" , dirPath ); 326 final String errMsg = "FileWatch#run : Path=" + dirPath ; 327 LOGGER.warning( ex , "MSG0102" , errMsg ); 328 } 329 catch( final Throwable th ) { 330 // MSG0021 = 予期せぬエラーが発生しました。\n\tメッセージ=[{0}] 331// MsgUtil.errPrintln( th , "MSG0021" , toString() ); 332 final String errMsg = "FileWatch#run : Path=" + dirPath ; 333 LOGGER.warning( th , "MSG0021" , errMsg ); 334 } 335 finally { 336 thread = null; // 7.2.5.0 (2020/06/01) 停止処理 337// running = false; 338 running.set( false ); // 7.2.9.4 (2020/11/20) 339 } 340 } 341 342 /** 343 * runメソッドから呼ばれる、実際の処理。 344 * 345 * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加 346 * @og.rev 7.2.9.4 (2020/11/20) PMD:volatile boolean の代替え。 347 * 348 * try ・・・ catch( Throwable ) 構文を、runメソッドの標準的な作りにしておきたいため、 349 * あえて、実行メソッドを分けているだけです。 350 */ 351 private void execute() throws IOException { 352 // ファイル監視などの機能は新しいNIO2クラスで拡張されたので 353 // 旧File型から、新しいPath型に変換する. 354 LOGGER.info( () -> "FileWatch Start: " + dirPath ); 355 356 // デフォルトのファイル・システムを閉じることはできません。(UnsupportedOperationException がスローされる) 357 // なので、try-with-resources 文 (AutoCloseable) に、入れません。 358 final FileSystem fs = dirPath.getFileSystem(); // フォルダが属するファイルシステムを得る() 359 // try-with-resources 文 (AutoCloseable) 360 // ファイルシステムに対応する監視サービスを構築する. 361 // (一つのサービスで複数の監視が可能) 362 try( WatchService watcher = fs.newWatchService() ) { 363 // フォルダに対して監視サービスを登録する. 364 final WatchKey watchKey = dirPath.register( watcher , weKind , extModifiers ); 365 366 // 監視が有効であるかぎり、ループする. 367 // (監視がcancelされるか、監視サービスが停止した場合はfalseとなる) 368 try{ 369 boolean flag = true; 370// while( flag && running ) { 371 while( flag && running.get() ) { // 7.2.9.4 (2020/11/20) 372 // スレッドの割り込み = 終了要求を判定する. 373 // if( Thread.currentThread().isInterrupted() ) { 374 // throw new InterruptedException(); 375 // } 376 377 // ファイル変更イベントが発生するまで待機する. 378 final WatchKey detecedtWatchKey = watcher.take(); 379 380 // イベント発生元を判定する 381 if( detecedtWatchKey.equals( watchKey ) ) { 382 // 発生したイベント内容をプリントする. 383 for( final WatchEvent<?> event : detecedtWatchKey.pollEvents() ) { 384 // 追加・変更・削除対象のファイルを取得する. 385 // (ただし、overflow時などはnullとなることに注意) 386 final Path path = (Path)event.context(); 387 if( path != null && pathMchSet.matches( path ) ) { 388 // synchronized( action ) { 389// action.accept( event.kind().name() , dirPath.resolve( path ) ); 390 final Path fpath = dirPath.resolve( path ); 391 if( dWatch == null || dWatch.setAdd( fpath) ) { // このセット内に、指定された要素がなかった場合はtrue 392 action.accept( event.kind().name() , fpath ); 393 } 394 else { 395 // CREATE と MODIFY などのイベントが連続して発生するケースへの対応 396 LOGGER.info( () -> "WatchEvent Duplication: " + fpath ); 397 } 398 // } 399 } 400 } 401 } 402 403 // イベントの受付を再開する. 404 detecedtWatchKey.reset(); 405 406 if( dWatch != null ) { 407 dWatch.setClear(); // Path重複チェック用のSetは、一連のイベント完了時にクリアしておきます。 408 } 409 410 // 監視サービスが活きている、または、スレッドの割り込み( = 終了要求)がないことを、をチェックする。 411 flag = watchKey.isValid() && !Thread.currentThread().isInterrupted() ; 412 } 413 } 414 catch( final InterruptedException ex ) { 415// LOGGER.warning( () -> "【WARNING】 FileWatch Canceled:" + dirPath ); 416 LOGGER.warning( () -> "FileWatch Canceled : [" + dirPath + "]" ); 417 } 418 finally { 419 // スレッドの割り込み = 終了要求なので監視をキャンセルしループを終了する。 420 if( watchKey != null ) { 421 watchKey.cancel(); 422 } 423 } 424 } 425 // FileSystemの実装(sun.nio.fs.WindowsFileSystem)は、close() 未サポート 426 catch( final UnsupportedOperationException ex ) { 427 LOGGER.warning( () -> "FileSystem close : [" + dirPath + "]" ); 428 } 429 430// LOGGER.info( () -> "FileWatch End: " + dirPath ); 431 LOGGER.info( () -> "FileWatch End : [" + dirPath + "]" ); 432 433// thread = null; // 1.1.0 (2018/02/01) 停止処理 434 // isError = true; // 何らかの原因で停止すれば、エラーと判断します。 435 } 436 437 /** 438 *このオブジェクトの文字列表現を返します。 439 * 440 * @return このオブジェクトの文字列表現 441 */ 442 @Override 443 public String toString() { 444 return getClass().getSimpleName() + ":" + dirPath + " , " + DIR_WATCH_EVENT + "=[" + useDirWatch + "]" ; 445 } 446}