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.util.List; 019import java.util.function.Consumer; 020 021import java.io.File; 022import java.io.PrintWriter; 023import java.io.BufferedReader; 024import java.io.FileInputStream ; 025import java.io.InputStreamReader ; 026import java.io.IOException; 027 028import java.nio.file.Path; 029import java.nio.file.Files; 030import java.nio.file.Paths; 031import java.nio.file.FileVisitor; 032import java.nio.file.SimpleFileVisitor; 033import java.nio.file.FileVisitResult; 034import java.nio.file.StandardOpenOption; 035import java.nio.file.StandardCopyOption; 036import java.nio.file.attribute.BasicFileAttributes; 037import java.nio.file.OpenOption; 038import java.nio.channels.FileChannel; 039import java.nio.channels.OverlappingFileLockException; 040import java.nio.charset.Charset; 041import java.nio.charset.StandardCharsets; 042 043/** 044 * FileUtilは、共通的に使用されるファイル操作関連のメソッドを集約した、ユーティリティークラスです。 045 * 046 *<pre> 047 * 読み込みチェックや、書き出しチェックなどの簡易的な処理をまとめているだけです。 048 * 049 *</pre> 050 * @og.rev 7.0.0.0 (2017/07/07) 新規作成 051 * 052 * @version 7.0 053 * @author Kazuhiko Hasegawa 054 * @since JDK1.8, 055 */ 056public final class FileUtil { 057 /** ファイルが安定するまでの待ち時間(ミリ秒) {@value} */ 058 public static final int STABLE_SLEEP_TIME = 2000 ; // ファイルが安定するまで、2秒待つ 059 /** ファイルが安定するまでのリトライ回数 {@value} */ 060 public static final int STABLE_RETRY_COUNT = 10 ; // ファイルが安定するまで、10回リトライする。 061 062 /** ファイルロックの獲得までの待ち時間(ミリ秒) {@value} */ 063 public static final int LOCK_SLEEP_TIME = 2000 ; // ロックの獲得まで、2秒待つ 064 /** ファイルロックの獲得までのリトライ回数 {@value} */ 065 public static final int LOCK_RETRY_COUNT = 10 ; // ロックの獲得まで、10回リトライする。 066 067 /** 日本語用の、Windows-31J の、Charset */ 068 public static final Charset WINDOWS_31J = Charset.forName( "Windows-31J" ); 069 070 /** 日本語用の、UTF-8 の、Charset (Windows-31Jと同じように指定できるようにしておきます。) */ 071 public static final Charset UTF_8 = StandardCharsets.UTF_8; 072 073 private static final OpenOption[] CREATE = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.TRUNCATE_EXISTING }; 074 private static final OpenOption[] APPEND = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.APPEND }; 075 076 private static final Object STATIC_LOCK = new Object(); // staticレベルのロック 077 078 /** 079 * デフォルトコンストラクターをprivateにして、 080 * オブジェクトの生成をさせないようにする。 081 */ 082 private FileUtil() {} 083 084 /** 085 * 引数の文字列を連結した読み込み用パスのチェックを行い、存在する場合は、そのパスオブジェクトを返します。 086 * 087 * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加えたものです。 088 * そのパスが存在しなければ、例外をThrowします。 089 * 090 * @og.rev 1.0.0 (2016/04/28) 新規追加 091 * 092 * @param first パス文字列またはパス文字列の最初の部分 093 * @param more 結合してパス文字列を形成するための追加文字列 094 * @return 指定の文字列を連結したパスオブジェクト 095 * @throws RuntimeException ファイル/フォルダは存在しない場合 096 * @see Paths#get(String,String...) 097 */ 098 public static Path readPath( final String first , final String... more ) { 099 final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ; 100 101 if( !Files.exists( path ) ) { 102 // MSG0002 = ファイル/フォルダは存在しません。file=[{0}] 103 throw MsgUtil.throwException( "MSG0002" , path ); 104 } 105 106 return path; 107 } 108 109 /** 110 * 引数の文字列を連結した書き込み用パスを作成します。 111 * 112 * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加え、 113 * そのパスが存在しなければ、作成します。 114 * パスが、フォルダの場合は、そのまま作成し、ファイルの場合は、親フォルダまでを作成します。 115 * パスがフォルダかファイルかの区別は、拡張子があるかどうかで判定します。 116 * 117 * @og.rev 1.0.0 (2016/04/28) 新規追加 118 * 119 * @param first パス文字列またはパス文字列の最初の部分 120 * @param more 結合してパス文字列を形成するための追加文字列 121 * @return 指定の文字列を連結したパスオブジェクト 122 * @throws RuntimeException ファイル/フォルダが作成できなかった場合 123 * @see Paths#get(String,String...) 124 */ 125 public static Path writePath( final String first , final String... more ) { 126 final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ; 127 128 mkdirs( path ); 129 130 return path; 131 } 132 133 /** 134 * ファイルオブジェクトを作成します。 135 * 136 * 通常は、フォルダ+ファイル名で、新しいファイルオブジェクトを作成します。 137 * ここでは、第2引数のファイル名に、絶対パスを指定した場合は、第1引数の 138 * フォルダを使用せず、ファイル名だけで、ファイルオブジェクトを作成します。 139 * 第2引数のファイル名が、null か、ゼロ文字列の場合は、第1引数の 140 * フォルダを返します。 141 * 142 * @param path 基準となるフォルダ(ファイルの場合は、親フォルダ基準) 143 * @param fname ファイル名(絶対パス、または、相対パス) 144 * @return 合成されたファイルオブジェクト 145 */ 146 public static Path newPath( final Path path , final String fname ) { 147 if( fname == null || fname.isEmpty() ) { 148 return path; 149 } 150 else if( fname.charAt(0) == '/' || // 実フォルダが UNIX 151 fname.charAt(0) == '\\' || // 実フォルダが ネットワークパス 152 fname.length() > 1 && fname.charAt(1) == ':' ) { // 実フォルダが Windows 153 return new File( fname ).toPath(); 154 } 155 else { 156 return path.resolve( fname ); 157 } 158 } 159 160 /** 161 * 引数のファイルパスを親階層を含めて生成します。 162 * 163 * すでに存在している場合や作成が成功した場合は、true を返します。 164 * 作成に失敗した場合は、false です。 165 * 指定のファイルパスは、フォルダであることが前提ですが、簡易的に 166 * ファイルの場合は、その親階層のフォルダを作成します。 167 * ファイルかフォルダの判定は、拡張子があるか、ないかで判定します。 168 * 169 * @og.rev 1.0.0 (2016/04/28) 新規追加 170 * 171 * @param target ターゲットのファイルパス 172 * @throws RuntimeException フォルダの作成に失敗した場合 173 */ 174 public static void mkdirs( final Path target ) { 175 if( Files.notExists( target ) ) { // 存在しない場合 176 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 177// final boolean isFile = target.getFileName().toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 178 179 final Path tgtName = target.getFileName(); 180 if( tgtName == null ) { 181 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 182 throw MsgUtil.throwException( "MSG0007" , target.toString() ); 183 } 184 185 final boolean isFile = tgtName.toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 186// final Path dir = isFile ? target.toAbsolutePath().getParent() : target ; // ファイルなら、親フォルダを取り出す。 187 final Path dir = isFile ? target.getParent() : target ; // ファイルなら、親フォルダを取り出す。 188 if( dir == null ) { 189 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 190 throw MsgUtil.throwException( "MSG0007" , target.toString() ); 191 } 192 193 if( Files.notExists( dir ) ) { // 存在しない場合 194 try { 195 Files.createDirectories( dir ); 196 } 197 catch( final IOException ex ) { 198 // MSG0007 = ファイル/フォルダの作成に失敗しました。dir=[{0}] 199 throw MsgUtil.throwException( ex , "MSG0007" , dir ); 200 } 201 } 202 } 203 } 204 205 /** 206 * 単体ファイルをコピーします。 207 * 208 * コピー先がなければ、コピー先のフォルダ階層を作成します。 209 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。 210 * コピー先のファイルがすでに存在する場合は、上書きされますので、 211 * 必要であれば、先にバックアップしておいて下さい。 212 * 213 * @og.rev 1.0.0 (2016/04/28) 新規追加 214 * 215 * @param from コピー元となるファイル 216 * @param to コピー先となるファイル 217 * @throws RuntimeException ファイル操作に失敗した場合 218 * @see #copy(Path,Path,boolean) 219 */ 220 public static void copy( final Path from , final Path to ) { 221 copy( from,to,false ); 222 } 223 224 /** 225 * パスの共有ロックを指定した、単体ファイルをコピーします。 226 * 227 * コピー先がなければ、コピー先のフォルダ階層を作成します。 228 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。 229 * コピー先のファイルがすでに存在する場合は、上書きされますので、 230 * 必要であれば、先にバックアップしておいて下さい。 231 * 232 * ※ copy に関しては、コピー時間を最小化する意味で、synchronized しています。 233 * 234 * @og.rev 1.0.0 (2016/04/28) 新規追加 235 * 236 * @param from コピー元となるファイル 237 * @param to コピー先となるファイル 238 * @param useLock パスを共有ロックするかどうか 239 * @throws RuntimeException ファイル操作に失敗した場合 240 * @see #copy(Path,Path) 241 */ 242 public static void copy( final Path from , final Path to , final boolean useLock ) { 243 if( Files.exists( from ) ) { 244 mkdirs( to ); 245 246 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 247// final boolean isFile = to.getFileName().toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 248 249 final Path toName = to.getFileName(); 250 if( toName == null ) { 251 // MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}] 252 throw MsgUtil.throwException( "MSG0008" , from.toString() , to.toString() ); 253 } 254 255 final boolean isFile = toName.toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 256 257 // コピー先がフォルダの場合は、コピー元と同じ名前のファイルにする。 258 final Path save = isFile ? to : to.resolve( from.getFileName() ); 259 260 synchronized( STATIC_LOCK ) { 261 if( useLock ) { 262 lockPath( from , in -> localCopy( in , save ) ); 263 } 264 else { 265 localCopy( from , save ); 266 } 267 } 268 } 269 else { 270 // MSG0002 = ファイル/フォルダが存在しません。file=[{0}] 271 MsgUtil.errPrintln( "MSG0002" , from ); 272 } 273 } 274 275 /** 276 * 単体ファイルをコピーします。 277 * 278 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。 279 * 280 * @og.rev 1.0.0 (2016/04/28) 新規追加 281 * 282 * @param from コピー元となるファイル 283 * @param to コピー先となるファイル 284 */ 285 private static void localCopy( final Path from , final Path to ) { 286 try { 287 // 直前に存在チェックを行います。 288 if( Files.exists( from ) ) { 289 Files.copy( from , to , StandardCopyOption.REPLACE_EXISTING ); 290 } 291 } 292 catch( final IOException ex ) { 293 // MSG0012 = ファイルがコピーできませんでした。from=[{0}] to=[{1}] 294 MsgUtil.errPrintln( ex , "MSG0012" , from , to ); 295 } 296 } 297 298 /** 299 * 単体ファイルを移動します。 300 * 301 * 移動先がなければ、移動先のフォルダ階層を作成します。 302 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。 303 * 移動先のファイルがすでに存在する場合は、上書きされますので、 304 * 必要であれば、先にバックアップしておいて下さい。 305 * 306 * @og.rev 1.0.0 (2016/04/28) 新規追加 307 * 308 * @param from 移動元となるファイル 309 * @param to 移動先となるファイル 310 * @throws RuntimeException ファイル操作に失敗した場合 311 * @see #move(Path,Path,boolean) 312 */ 313 public static void move( final Path from , final Path to ) { 314 move( from,to,false ); 315 } 316 317 /** 318 * パスの共有ロックを指定した、単体ファイルを移動します。 319 * 320 * 移動先がなければ、移動先のフォルダ階層を作成します。 321 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。 322 * 移動先のファイルがすでに存在する場合は、上書きされますので、 323 * 必要であれば、先にバックアップしておいて下さい。 324 * 325 * ※ move に関しては、ムーブ時間を最小化する意味で、synchronized しています。 326 * 327 * @og.rev 1.0.0 (2016/04/28) 新規追加 328 * 329 * @param from 移動元となるファイル 330 * @param to 移動先となるファイル 331 * @param useLock パスを共有ロックするかどうか 332 * @throws RuntimeException ファイル操作に失敗した場合 333 * @see #move(Path,Path) 334 */ 335 public static void move( final Path from , final Path to , final boolean useLock ) { 336 if( Files.exists( from ) ) { 337 mkdirs( to ); 338 339 // ファイルかどうかは、拡張子の有無で判定する。 340 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 341// final boolean isFile = to.getFileName().toString().contains( "." ); 342 final Path toName = to.getFileName(); 343 if( toName == null ) { 344 // MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}] 345 throw MsgUtil.throwException( "MSG0008" , to.toString() ); 346 } 347 348 final boolean isFile = toName.toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 349 350 // 移動先がフォルダの場合は、コピー元と同じ名前のファイルにする。 351 final Path save = isFile ? to : to.resolve( from.getFileName() ); 352 353 synchronized( STATIC_LOCK ) { 354 if( useLock ) { 355 lockPath( from , in -> localMove( in , save ) ); 356 } 357 else { 358 localMove( from , save ); 359 } 360 } 361 } 362 else { 363 // MSG0002 = ファイル/フォルダが存在しません。file=[{0}] 364 MsgUtil.errPrintln( "MSG0002" , from ); 365 } 366 } 367 368 /** 369 * 単体ファイルを移動します。 370 * 371 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。 372 * 373 * @og.rev 1.0.0 (2016/04/28) 新規追加 374 * 375 * @param from 移動元となるファイル 376 * @param to 移動先となるファイル 377 */ 378 private static void localMove( final Path from , final Path to ) { 379 try { 380 // 直前に存在チェックを行います。 381 if( Files.exists( from ) ) { 382 // CopyOption に、StandardCopyOption.ATOMIC_MOVE を指定すると、別サーバー等へのMOVEは、出来なくなります。 383 Files.move( from , to , StandardCopyOption.REPLACE_EXISTING ); 384 } 385 } 386 catch( final IOException ex ) { 387 // MSG0008 = ファイルが移動できませんでした。from=[{0}] to=[{1}] 388 MsgUtil.errPrintln( ex , "MSG0008" , from , to ); 389 } 390 } 391 392 /** 393 * 単体ファイルをバックアップフォルダに移動します。 394 * 395 * これは、#backup( from,to,true,false,sufix ); と同じ処理を実行します。 396 * 397 * 移動先は、フォルダ指定で、ファイル名は存在チェックせずに、必ず変更します。 398 * その際、移動元+サフィックス のファイルを作成します。 399 * ファイルのロックを行います。 400 * 401 * @og.rev 1.0.0 (2016/04/28) 新規追加 402 * 403 * @param from 移動元となるファイル 404 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 405 * @param sufix バックアップファイル名の後ろに付ける文字列 406 * @return バックアップしたファイルパス。 407 * @throws RuntimeException ファイル操作に失敗した場合 408 * @see #backup( Path , Path , boolean , boolean , String ) 409 */ 410 public static Path backup( final Path from , final Path to , final String sufix ) { 411 return backup( from,to,true,false,sufix ); 412 } 413 414 /** 415 * 単体ファイルをバックアップフォルダに移動します。 416 * 417 * これは、#backup( from,to,true,true ); と同じ処理を実行します。 418 * 419 * 移動先は、フォルダ指定で、ファイル名は存在チェックの上で、無ければ移動、 420 * あれば、移動元+時間情報 のファイルを作成します。 421 * ファイルのロックを行います。 422 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。 423 * 424 * @og.rev 1.0.0 (2016/04/28) 新規追加 425 * 426 * @param from 移動元となるファイル 427 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 428 * @return バックアップしたファイルパス。 429 * @throws RuntimeException ファイル操作に失敗した場合 430 * @see #backup( Path , Path , boolean , boolean , String ) 431 */ 432 public static Path backup( final Path from , final Path to ) { 433 return backup( from,to,true,true,null ); 434 } 435 436 /** 437 * パスの共有ロックを指定して、単体ファイルをバックアップフォルダに移動します。 438 * 439 * 移動先のファイル名は、existsCheckが、trueの場合は、移動先のファイル名をチェックして、 440 * 存在しなければ、移動元と同じファイル名で、バックアップフォルダに移動します。 441 * 存在すれば、ファイル名+サフィックス のファイルを作成します。(拡張子より後ろにサフィックスを追加します。) 442 * existsCheckが、false の場合は、無条件に、移動元のファイル名に、サフィックスを追加します。 443 * サフィックスがnullの場合は、時間情報になります。 444 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。 445 * 446 * @og.rev 1.0.0 (2016/04/28) 新規追加 447 * @og.rev 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 448 * 449 * @param from 移動元となるファイル 450 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 451 * @param useLock パスを共有ロックするかどうか 452 * @param existsCheck 移動先のファイル存在チェックを行うかどうか(true:行う/false:行わない) 453 * @param sufix バックアップファイル名の後ろに付ける文字列 454 * 455 * @return バックアップしたファイルパス。 456 * @throws RuntimeException ファイル操作に失敗した場合 457 * @see #backup( Path , Path ) 458 */ 459 public static Path backup( final Path from , final Path to , final boolean useLock , final boolean existsCheck , final String sufix ) { 460 final Path movePath = to == null ? from.getParent() : to ; 461 462 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 463 if( movePath == null ) { 464 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 465 throw MsgUtil.throwException( "MSG0007" , from.toString() ); 466 } 467 468// final String fileName = from.getFileName().toString(); 469 final Path fName = from.getFileName(); 470 if( fName == null ) { 471 // MSG0002 = ファイル/フォルダが存在しません。\n\tfile=[{0}] 472 throw MsgUtil.throwException( "MSG0002" , from.toString() ); 473 } 474 475// final Path moveFile = movePath.resolve( fileName ); // 移動先のファイルパスを構築 476 final Path moveFile = movePath.resolve( fName ); // 移動先のファイルパスを構築 477 478// final boolean isExChk = existsCheck && Files.notExists( moveFile ); // 存在しない場合、true。存在するか、不明の場合は、false。 479 480 final Path bkupPath; 481// if( isExChk ) { 482 if( existsCheck && Files.notExists( moveFile ) ) { // 存在しない場合、true。存在するか、不明の場合は、false。 483 bkupPath = moveFile; 484 } 485 else { 486 final String fileName = fName.toString(); // from パスの名前 487 final int ad = fileName.lastIndexOf( '.' ); // ピリオドの手前に、タイムスタンプを入れる。 488 bkupPath = movePath.resolve( 489 fileName.substring( 0,ad ) 490 + "_" 491 + StringUtil.nval( sufix , StringUtil.getTimeFormat() ) 492 + fileName.substring( ad ) // ad 以降なので、ピリオドも含む 493 ); 494 } 495 496 move( from,bkupPath,useLock ); 497 498 return bkupPath; 499 } 500 501 /** 502 * ファイルまたはフォルダ階層を削除します。 503 * 504 * これは、指定のパスが、フォルダの場合、階層すべてを削除します。 505 * 階層の途中にファイル等が存在していたとしても、削除します。 506 * 507 * Files.walkFileTree(Path,FileVisitor) を使用したファイル・ツリーの削除方式です。 508 * 509 * @og.rev 1.0.0 (2016/04/28) 新規追加 510 * 511 * @param start 削除開始ファイル 512 * @throws RuntimeException ファイル操作に失敗した場合 513 */ 514 public static void delete( final Path start ) { 515 try { 516 if( Files.exists( start ) ) { 517 Files.walkFileTree( start, DELETE_VISITOR ); 518 } 519 } 520 catch( final IOException ex ) { 521 // MSG0011 = ファイルが削除できませんでした。file=[{0}] 522 throw MsgUtil.throwException( ex , "MSG0011" , start ); 523 } 524 } 525 526 /** 527 * delete(Path)で使用する、Files.walkFileTree の引数の FileVisitor オブジェクトです。 528 * 529 * staticオブジェクトを作成しておき、使いまわします。 530 */ 531 private static final FileVisitor<Path> DELETE_VISITOR = new SimpleFileVisitor<Path>() { 532 /** 533 * ディレクトリ内のファイルに対して呼び出されます。 534 * 535 * @param file ファイルへの参照 536 * @param attrs ファイルの基本属性 537 * @throws IOException 入出力エラーが発生した場合 538 */ 539 @Override 540 public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException { 541 Files.deleteIfExists( file ); // ファイルが存在する場合は削除 542 return FileVisitResult.CONTINUE; 543 } 544 545 /** 546 * ディレクトリ内のエントリ、およびそのすべての子孫がビジットされたあとにそのディレクトリに対して呼び出されます。 547 * 548 * @param dir ディレクトリへの参照 549 * @param ex エラーが発生せずにディレクトリの反復が完了した場合はnull、そうでない場合はディレクトリの反復が早く完了させた入出力例外 550 * @throws IOException 入出力エラーが発生した場合 551 */ 552 @Override 553 public FileVisitResult postVisitDirectory( final Path dir, final IOException ex ) throws IOException { 554 if( ex == null ) { 555 Files.deleteIfExists( dir ); // ファイルが存在する場合は削除 556 return FileVisitResult.CONTINUE; 557 } else { 558 // directory iteration failed 559 throw ex; 560 } 561 } 562 }; 563 564 /** 565 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。 566 * 567 * FileUtil.stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); と同じです。 568 * 569 * @param path チェックするパスオブジェクト 570 * @return true:安定した/false:安定しなかった。またはファイルが存在していない。 571 * @see #STABLE_SLEEP_TIME 572 * @see #STABLE_RETRY_COUNT 573 */ 574 public static boolean stablePath( final Path path ) { 575 return stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); 576 } 577 578 /** 579 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。 580 * 581 * ファイルの安定は、ファイルのサイズをチェックすることで求めます。まず、サイズをチェックし、 582 * sleepで指定した時間だけ、Thread.sleepします。再び、サイズをチェックして、同じであれば、 583 * 安定したとみなします。 584 * なので、必ず、sleep で指定したミリ秒だけは、待ちます。 585 * ファイルが存在しない、サイズが、0のままか、チェック回数を過ぎても安定しない場合は、 586 * false が返ります。 587 * サイズを求める際に、IOExceptionが発生した場合でも、falseを返します。 588 * 589 * @param path チェックするパスオブジェクト 590 * @param sleep 待機する時間(ミリ秒) 591 * @param cnt チェックする回数 592 * @return true:安定した/false:安定しなかった。またはファイルが存在していない。 593 */ 594 public static boolean stablePath( final Path path , final long sleep , final int cnt ) { 595 // 存在しない場合は、即抜けます。 596 if( Files.exists( path ) ) { 597 try { 598 for( int i=0; i<cnt; i++ ) { 599 if( Files.notExists( path ) ) { return false; } // 存在チェック。無ければ、false 600 final long size1 = Files.size( path ); // exit point 警告が出ますが、Thread.sleep 前に、値を取得しておきたい。 601 602 try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){} // 無条件に待ちます。 603 604 if( Files.notExists( path ) ) { return false; } // 存在チェック。無ければ、false 605 final long size2 = Files.size( path ); 606 if( size1 != 0L && size1 == size2 ) { return true; } // 安定した 607 } 608 } 609 catch( final IOException ex ) { 610 // Exception は発生させません。 611 // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 612 MsgUtil.errPrintln( ex , "MSG0005" , path ); 613 } 614 } 615 616 return false; 617 } 618 619 /** 620 * 指定のパスを共有ロックして、Consumer#action(Path) メソッドを実行します。 621 * 共有ロック中は、ファイルを読み込むことは出来ますが、書き込むことは出来なくなります。 622 * 623 * 共有ロックの取得は、{@value #LOCK_RETRY_COUNT} 回実行し、{@value #LOCK_SLEEP_TIME} ミリ秒待機します。 624 * 625 * @param inPath 処理対象のPathオブジェクト 626 * @param action パスを引数に取るConsumerオブジェクト 627 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 628 * @see #forEach(Path,Consumer) 629 * @see #LOCK_RETRY_COUNT 630 * @see #LOCK_SLEEP_TIME 631 */ 632 public static void lockPath( final Path inPath , final Consumer<Path> action ) { 633 // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。 634 if( Files.exists( inPath ) ) { 635 // try-with-resources 文 (AutoCloseable) 636 try( final FileChannel channel = FileChannel.open( inPath, StandardOpenOption.READ ) ) { 637 for( int i=0; i<LOCK_RETRY_COUNT; i++ ) { 638 try { 639 if( channel.tryLock( 0L,Long.MAX_VALUE,true ) != null ) { // 共有ロック獲得成功 640 action.accept( inPath ); 641 return; // 共有ロック獲得成功したので、ループから抜ける。 642 } 643 } 644 catch( final OverlappingFileLockException ex ) { 645 // 要求された領域をオーバーラップするロックがこのJava仮想マシンにすでに確保されている場合。 646 // または、このメソッド内でブロックされている別のスレッドが同じファイルのオーバーラップした領域をロックしようとしている場合 647 System.err.println( ex.getMessage() ); 648 } 649 try{ Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ex ){} 650 } 651 } 652 catch( final IOException ex ) { 653 // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 654 throw MsgUtil.throwException( ex , "MSG0005" , inPath ); 655 } 656 657 // Exception は発生させません。 658 // MSG0015 = ファイルのロック取得に失敗しました。file=[{0}] WAIT=[{1}](ms) COUNT=[{2}] 659 MsgUtil.errPrintln( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT ); 660 } 661 } 662 663 /** 664 * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 665 * 1行単位に、Consumer#action が呼ばれます。 666 * このメソッドでは、Charset は、UTF-8 です。 667 * 668 * ファイルを順次読み込むため、内部メモリを圧迫しません。 669 * 670 * @param inPath 処理対象のPathオブジェクト 671 * @param action 行を引数に取るConsumerオブジェクト 672 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 673 * @see #lockForEach(Path,Consumer) 674 */ 675 public static void forEach( final Path inPath , final Consumer<String> action ) { 676 forEach( inPath , UTF_8 , action ); 677 } 678 679 /** 680 * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 681 * 1行単位に、Consumer#action が呼ばれます。 682 * 683 * ファイルを順次読み込むため、内部メモリを圧迫しません。 684 * 685 * @param inPath 処理対象のPathオブジェクト 686 * @param chset ファイルを読み取るときのCharset 687 * @param action 行を引数に取るConsumerオブジェクト 688 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 689 * @see #lockForEach(Path,Consumer) 690 */ 691 public static void forEach( final Path inPath , final Charset chset , final Consumer<String> action ) { 692 // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。 693 if( Files.exists( inPath ) ) { 694 // try-with-resources 文 (AutoCloseable) 695 String line = null; 696 int no = 0; 697 // // こちらの方法では、lockForEach から来た場合に、エラーになります。 698 // try( final BufferedReader reader = Files.newBufferedReader( inPath , chset ) ) { 699 // 万一、コンストラクタでエラーが発生すると、リソース開放されない場合があるため、個別にインスタンスを作成しておきます。(念のため) 700 try( final FileInputStream fin = new FileInputStream( inPath.toFile() ); 701 final InputStreamReader isr = new InputStreamReader( fin , chset ); 702 final BufferedReader reader = new BufferedReader( isr ) ) { 703 while( ( line = reader.readLine() ) != null ) { 704 action.accept( line ); 705 no++; 706 } 707 } 708 catch( final IOException ex ) { 709 // MSG0016 = ファイルの行データ読み込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2} 710 throw MsgUtil.throwException( ex , "MSG0016" , inPath , no , line ); 711 } 712 } 713 } 714 715 /** 716 * 指定のパスを共有ロックして、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 717 * 1行単位に、Consumer#action が呼ばれます。 718 * 719 * ファイルを順次読み込むため、内部メモリを圧迫しません。 720 * 721 * @param inPath 処理対象のPathオブジェクト 722 * @param action 行を引数に取るConsumerオブジェクト 723 * @see #forEach(Path,Consumer) 724 */ 725 public static void lockForEach( final Path inPath , final Consumer<String> action ) { 726 lockPath( inPath , in -> forEach( in , UTF_8 , action ) ); 727 } 728 729 /** 730 * 指定のパスを共有ロックして、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 731 * 1行単位に、Consumer#action が呼ばれます。 732 * 733 * ファイルを順次読み込むため、内部メモリを圧迫しません。 734 * 735 * @param inPath 処理対象のPathオブジェクト 736 * @param chset エンコードを指定するCharsetオブジェクト 737 * @param action 行を引数に取るConsumerオブジェクト 738 * @see #forEach(Path,Consumer) 739 */ 740 public static void lockForEach( final Path inPath , final Charset chset , final Consumer<String> action ) { 741 lockPath( inPath , in -> forEach( in , chset , action ) ); 742 } 743 744 /** 745 * 指定のパスに1行単位の文字列のListを書き込んでいきます。 746 * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。 747 * 748 * 書き込むパスの親フォルダがなければ作成します。 749 * 第2引数は、書き込む行データです。 750 * このメソッドでは、Charset は、UTF-8 です。 751 * 752 * @og.rev 1.0.0 (2016/04/28) 新規追加 753 * 754 * @param savePath セーブするパスオブジェクト 755 * @param lines 行単位の書き込むデータ 756 * @throws RuntimeException ファイル操作に失敗した場合 757 * @see #save( Path , List , boolean , Charset ) 758 */ 759 public static void save( final Path savePath , final List<String> lines ) { 760 save( savePath , lines , false , UTF_8 ); // 新規作成 761 } 762 763 /** 764 * 指定のパスに1行単位の文字列のListを書き込んでいきます。 765 * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。 766 * 767 * 書き込むパスの親フォルダがなければ作成します。 768 * 769 * 第2引数は、書き込む行データです。 770 * 771 * @og.rev 1.0.0 (2016/04/28) 新規追加 772 * 773 * @param savePath セーブするパスオブジェクト 774 * @param lines 行単位の書き込むデータ 775 * @param append trueの場合、ファイルの先頭ではなく最後に書き込まれる。 776 * @param chset ファイルを読み取るときのCharset 777 * @throws RuntimeException ファイル操作に失敗した場合 778 */ 779 public static void save( final Path savePath , final List<String> lines , final boolean append , final Charset chset ) { 780 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 781 // ※ toAbsolutePath() する必要はないのと、getParent() は、null を返すことがある 782// mkdirs( savePath.toAbsolutePath().getParent() ); // savePathはファイルなので、親フォルダを作成する。 783 final Path parent = savePath.getParent(); 784 if( parent == null ) { 785 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 786 throw MsgUtil.throwException( "MSG0007" , savePath.toString() ); 787 } 788 else { 789 mkdirs( parent ); 790 } 791 792 String line = null; // エラー出力のための変数 793 int no = 0; 794 795 // try-with-resources 文 (AutoCloseable) 796 try( final PrintWriter out = new PrintWriter( Files.newBufferedWriter( savePath, chset , append ? APPEND : CREATE ) ) ) { 797 for( final String ln : lines ) { 798 line = ln ; 799 no++; 800 out.println( line ); 801 } 802 out.flush(); 803 } 804 catch( final IOException ex ) { 805 // MSG0017=ファイルのデータ書き込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2} 806 throw MsgUtil.throwException( ex , "MSG0017" , savePath , no , line ); 807 } 808 } 809 810 /** 811 * 指定のパスの最終更新日付を、文字列で返します。 812 * 文字列のフォーマット指定も可能です。 813 * 814 * パスが無い場合や、最終更新日付を、取得できない場合は、現在時刻をベースに返します。 815 * 816 * @param path 処理対象のPathオブジェクト 817 * @param format 文字列化する場合のフォーマット(yyyyMMddHHmmss) 818 * @return 指定のパスの最終更新日付の文字列 819 */ 820 public static String timeStamp( final Path path , final String format ) { 821 long tempTime = 0L; 822 try { 823 // 存在チェックを直前に入れますが、厳密には、非同期なので確率の問題です。 824 if( Files.exists( path ) ) { 825 tempTime = Files.getLastModifiedTime( path ).toMillis(); 826 } 827 } 828 catch( final IOException ex ) { 829 // ファイルのタイムスタンプの取得に失敗しました。file=[{0}] 830 MsgUtil.errPrintln( ex , "MSG0018" , path , ex.getMessage() ); 831 } 832 if( tempTime == 0L ) { 833 tempTime = System.currentTimeMillis(); // パスが無い場合や、エラー時は、現在時刻を使用 834 } 835 836 return StringUtil.getTimeFormat( tempTime , format ); 837 } 838 839 /** main メソッドから呼ばれる ヘルプメッセージです。 {@value} */ 840 public static final String USAGE = "Usage: java jp.euromap.eu63.util.FileUtil [-MOVE|-COPY|-DELETE|-BACKUP|-SAVE] from to [-useLock] [-append] [-help]" ; 841 842 /** 843 * リソース一覧を表示する main メソッドです。 844 * 845 * @param args コマンド引数配列 846 */ 847 public static void main( final String[] args ) { 848 // ********** 【整合性チェック】 ********** 849 if( args.length < 1 ) { 850 System.out.println( USAGE ); 851 return; 852 } 853 854 // ********** 【引数定義】 ********** 855 Path fromPath = null; // 入力ファイル 856 Path toPath = null; // 出力ファイル 857 boolean isLock = false; // ロック処理(初期値:false しない) 858 boolean isAppend = false; // 追記処理(初期値:false しない) 859 int type = -1; // 0:MOVE , 1:COPY , 2:DELETE 860 861 // ********** 【引数処理】 ********** 862 int cnt = 0 ; 863 for( final String arg : args ) { 864 if( "-help" .equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; } 865 else if( "-useLock" .equalsIgnoreCase( arg ) ) { isLock = true; } 866 else if( "-append " .equalsIgnoreCase( arg ) ) { isAppend = true; } 867 else if( "-MOVE" .equalsIgnoreCase( arg ) ) { type = 0; } 868 else if( "-COPY" .equalsIgnoreCase( arg ) ) { type = 1; } 869 else if( "-DELETE" .equalsIgnoreCase( arg ) ) { type = 2; } 870 else if( "-BACKUP" .equalsIgnoreCase( arg ) ) { type = 3; } 871 else if( "-SAVE" .equalsIgnoreCase( arg ) ) { type = 4; } 872 else { 873 if( cnt == 0 ) { fromPath = FileUtil.readPath( arg ); } 874 else if( cnt == 1 ) { toPath = FileUtil.writePath( arg ); } // 親フォルダがなければ作成されます。 875 cnt++ ; 876 } 877 } 878 879 // ********** 【本体処理】 ********** 880 switch( type ) { 881 case 0: System.out.println( "TYPE=MOVE FROM=" + fromPath + " , TO=" + toPath ); 882 FileUtil.move( fromPath , toPath , isLock ); 883 break; 884 case 1: System.out.println( "TYPE=COPY FROM=" + fromPath + " , TO=" + toPath ); 885 FileUtil.copy( fromPath , toPath , isLock ); 886 break; 887 case 2: System.out.println( "TYPE=DELETE START=" + fromPath ); 888 FileUtil.delete( fromPath ); 889 break; 890 case 3: System.out.println( "TYPE=BACKUP FROM=" + fromPath + " , TO=" + toPath ); 891 FileUtil.backup( fromPath , toPath , isLock , false , null ); 892 break; 893 case 4: System.out.println( "TYPE=SAVE FROM=" + fromPath + " , TO=" + toPath ); 894 if( isLock ) { 895 final List<String> lines = new java.util.ArrayList<>(); 896 FileUtil.lockForEach( fromPath , str -> lines.add( str ) ); 897 } 898 else { 899 final List<String> lines = new java.util.ArrayList<>(); 900 FileUtil.forEach( fromPath , str -> lines.add( str ) ); 901 FileUtil.save( toPath , lines , isAppend , FileUtil.UTF_8 ); 902 } 903 break; 904 default : System.out.println( USAGE ); 905 break; 906 } 907 } 908}