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