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