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 final boolean isFile = target.getFileName().toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 177 final Path dir = isFile ? target.toAbsolutePath().getParent() : target ; // ファイルなら、親フォルダを取り出す。 178 if( Files.notExists( dir ) ) { // 存在しない場合 179 try { 180 Files.createDirectories( dir ); 181 } 182 catch( final IOException ex ) { 183 // MSG0007 = ファイル/フォルダの作成に失敗しました。dir=[{0}] 184 throw MsgUtil.throwException( ex , "MSG0007" , dir ); 185 } 186 } 187 } 188 } 189 190 /** 191 * 単体ファイルをコピーします。 192 * 193 * コピー先がなければ、コピー先のフォルダ階層を作成します。 194 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。 195 * コピー先のファイルがすでに存在する場合は、上書きされますので、 196 * 必要であれば、先にバックアップしておいて下さい。 197 * 198 * @og.rev 1.0.0 (2016/04/28) 新規追加 199 * 200 * @param from コピー元となるファイル 201 * @param to コピー先となるファイル 202 * @throws RuntimeException ファイル操作に失敗した場合 203 * @see #copy(Path,Path,boolean) 204 */ 205 public static void copy( final Path from , final Path to ) { 206 copy( from,to,false ); 207 } 208 209 /** 210 * パスの共有ロックを指定した、単体ファイルをコピーします。 211 * 212 * コピー先がなければ、コピー先のフォルダ階層を作成します。 213 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。 214 * コピー先のファイルがすでに存在する場合は、上書きされますので、 215 * 必要であれば、先にバックアップしておいて下さい。 216 * 217 * ※ copy に関しては、コピー時間を最小化する意味で、synchronized しています。 218 * 219 * @og.rev 1.0.0 (2016/04/28) 新規追加 220 * 221 * @param from コピー元となるファイル 222 * @param to コピー先となるファイル 223 * @param useLock パスを共有ロックするかどうか 224 * @throws RuntimeException ファイル操作に失敗した場合 225 * @see #copy(Path,Path) 226 */ 227 public static void copy( final Path from , final Path to , final boolean useLock ) { 228 if( Files.exists( from ) ) { 229 mkdirs( to ); 230 231 final boolean isFile = to.getFileName().toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 232 233 // コピー先がフォルダの場合は、コピー元と同じ名前のファイルにする。 234 final Path save = isFile ? to : to.resolve( from.getFileName() ); 235 236 synchronized( STATIC_LOCK ) { 237 if( useLock ) { 238 lockPath( from , in -> localCopy( in , save ) ); 239 } 240 else { 241 localCopy( from , save ); 242 } 243 } 244 } 245 else { 246 // MSG0002 = ファイル/フォルダが存在しません。file=[{0}] 247 MsgUtil.errPrintln( "MSG0002" , from ); 248 } 249 } 250 251 /** 252 * 単体ファイルをコピーします。 253 * 254 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。 255 * 256 * @og.rev 1.0.0 (2016/04/28) 新規追加 257 * 258 * @param from コピー元となるファイル 259 * @param to コピー先となるファイル 260 */ 261 private static void localCopy( final Path from , final Path to ) { 262 try { 263 // 直前に存在チェックを行います。 264 if( Files.exists( from ) ) { 265 Files.copy( from , to , StandardCopyOption.REPLACE_EXISTING ); 266 } 267 } 268 catch( final IOException ex ) { 269 // MSG0012 = ファイルがコピーできませんでした。from=[{0}] to=[{1}] 270 MsgUtil.errPrintln( ex , "MSG0012" , from , to ); 271 } 272 } 273 274 /** 275 * 単体ファイルを移動します。 276 * 277 * 移動先がなければ、移動先のフォルダ階層を作成します。 278 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。 279 * 移動先のファイルがすでに存在する場合は、上書きされますので、 280 * 必要であれば、先にバックアップしておいて下さい。 281 * 282 * @og.rev 1.0.0 (2016/04/28) 新規追加 283 * 284 * @param from 移動元となるファイル 285 * @param to 移動先となるファイル 286 * @throws RuntimeException ファイル操作に失敗した場合 287 * @see #move(Path,Path,boolean) 288 */ 289 public static void move( final Path from , final Path to ) { 290 move( from,to,false ); 291 } 292 293 /** 294 * パスの共有ロックを指定した、単体ファイルを移動します。 295 * 296 * 移動先がなければ、移動先のフォルダ階層を作成します。 297 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。 298 * 移動先のファイルがすでに存在する場合は、上書きされますので、 299 * 必要であれば、先にバックアップしておいて下さい。 300 * 301 * ※ move に関しては、ムーブ時間を最小化する意味で、synchronized しています。 302 * 303 * @og.rev 1.0.0 (2016/04/28) 新規追加 304 * 305 * @param from 移動元となるファイル 306 * @param to 移動先となるファイル 307 * @param useLock パスを共有ロックするかどうか 308 * @throws RuntimeException ファイル操作に失敗した場合 309 * @see #move(Path,Path) 310 */ 311 public static void move( final Path from , final Path to , final boolean useLock ) { 312 if( Files.exists( from ) ) { 313 mkdirs( to ); 314 315 // ファイルかどうかは、拡張子の有無で判定する。 316 final boolean isFile = to.getFileName().toString().contains( "." ); 317 318 // 移動先がフォルダの場合は、コピー元と同じ名前のファイルにする。 319 final Path save = isFile ? to : to.resolve( from.getFileName() ); 320 321 synchronized( STATIC_LOCK ) { 322 if( useLock ) { 323 lockPath( from , in -> localMove( in , save ) ); 324 } 325 else { 326 localMove( from , save ); 327 } 328 } 329 } 330 else { 331 // MSG0002 = ファイル/フォルダが存在しません。file=[{0}] 332 MsgUtil.errPrintln( "MSG0002" , from ); 333 } 334 } 335 336 /** 337 * 単体ファイルを移動します。 338 * 339 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。 340 * 341 * @og.rev 1.0.0 (2016/04/28) 新規追加 342 * 343 * @param from 移動元となるファイル 344 * @param to 移動先となるファイル 345 */ 346 private static void localMove( final Path from , final Path to ) { 347 try { 348 // 直前に存在チェックを行います。 349 if( Files.exists( from ) ) { 350 // CopyOption に、StandardCopyOption.ATOMIC_MOVE を指定すると、別サーバー等へのMOVEは、出来なくなります。 351 Files.move( from , to , StandardCopyOption.REPLACE_EXISTING ); 352 } 353 } 354 catch( final IOException ex ) { 355 // MSG0008 = ファイルが移動できませんでした。from=[{0}] to=[{1}] 356 MsgUtil.errPrintln( ex , "MSG0008" , from , to ); 357 } 358 } 359 360 /** 361 * 単体ファイルをバックアップフォルダに移動します。 362 * 363 * これは、#backup( from,to,true,false,sufix ); と同じ処理を実行します。 364 * 365 * 移動先は、フォルダ指定で、ファイル名は存在チェックせずに、必ず変更します。 366 * その際、移動元+サフィックス のファイルを作成します。 367 * ファイルのロックを行います。 368 * 369 * @og.rev 1.0.0 (2016/04/28) 新規追加 370 * 371 * @param from 移動元となるファイル 372 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 373 * @param sufix バックアップファイル名の後ろに付ける文字列 374 * @return バックアップしたファイルパス。 375 * @throws RuntimeException ファイル操作に失敗した場合 376 * @see #backup( Path , Path , boolean , boolean , String ) 377 */ 378 public static Path backup( final Path from , final Path to , final String sufix ) { 379 return backup( from,to,true,false,sufix ); 380 } 381 382 /** 383 * 単体ファイルをバックアップフォルダに移動します。 384 * 385 * これは、#backup( from,to,true,true ); と同じ処理を実行します。 386 * 387 * 移動先は、フォルダ指定で、ファイル名は存在チェックの上で、無ければ移動、 388 * あれば、移動元+時間情報 のファイルを作成します。 389 * ファイルのロックを行います。 390 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。 391 * 392 * @og.rev 1.0.0 (2016/04/28) 新規追加 393 * 394 * @param from 移動元となるファイル 395 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 396 * @return バックアップしたファイルパス。 397 * @throws RuntimeException ファイル操作に失敗した場合 398 * @see #backup( Path , Path , boolean , boolean , String ) 399 */ 400 public static Path backup( final Path from , final Path to ) { 401 return backup( from,to,true,true,null ); 402 } 403 404 /** 405 * パスの共有ロックを指定して、単体ファイルをバックアップフォルダに移動します。 406 * 407 * 移動先のファイル名は、existsCheckが、trueの場合は、移動先のファイル名をチェックして、 408 * 存在しなければ、移動元と同じファイル名で、バックアップフォルダに移動します。 409 * 存在すれば、ファイル名+サフィックス のファイルを作成します。(拡張子より後ろにサフィックスを追加します。) 410 * existsCheckが、false の場合は、無条件に、移動元のファイル名に、サフィックスを追加します。 411 * サフィックスがnullの場合は、時間情報になります。 412 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。 413 * 414 * @og.rev 1.0.0 (2016/04/28) 新規追加 415 * 416 * @param from 移動元となるファイル 417 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 418 * @param useLock パスを共有ロックするかどうか 419 * @param existsCheck 移動先のファイル存在チェックを行うかどうか(true:行う/false:行わない) 420 * @param sufix バックアップファイル名の後ろに付ける文字列 421 * 422 * @return バックアップしたファイルパス。 423 * @throws RuntimeException ファイル操作に失敗した場合 424 * @see #backup( Path , Path ) 425 */ 426 public static Path backup( final Path from , final Path to , final boolean useLock , final boolean existsCheck , final String sufix ) { 427 final Path movePath = to == null ? from.getParent() : to ; 428 429 final String fileName = from.getFileName().toString(); 430 final Path moveFile = movePath.resolve( fileName ); // 移動先のファイルパスを構築 431 432 final boolean isExChk = existsCheck && Files.notExists( moveFile ); // 存在しない場合、true。存在するか、不明の場合は、false。 433 434 final Path bkupPath; 435 if( isExChk ) { bkupPath = moveFile; } 436 else { 437 final int ad = fileName.lastIndexOf( '.' ); // ピリオドの手前に、タイムスタンプを入れる。 438 bkupPath = movePath.resolve( 439 fileName.substring( 0,ad ) 440 + "_" 441 + StringUtil.nval( sufix , StringUtil.getTimeFormat() ) 442 + fileName.substring( ad ) // ad 以降なので、ピリオドも含む 443 ); 444 } 445 446 move( from,bkupPath,useLock ); 447 448 return bkupPath; 449 } 450 451 /** 452 * ファイルまたはフォルダ階層を削除します。 453 * 454 * これは、指定のパスが、フォルダの場合、階層すべてを削除します。 455 * 階層の途中にファイル等が存在していたとしても、削除します。 456 * 457 * Files.walkFileTree(Path,FileVisitor) を使用したファイル・ツリーの削除方式です。 458 * 459 * @og.rev 1.0.0 (2016/04/28) 新規追加 460 * 461 * @param start 削除開始ファイル 462 * @throws RuntimeException ファイル操作に失敗した場合 463 */ 464 public static void delete( final Path start ) { 465 try { 466 if( Files.exists( start ) ) { 467 Files.walkFileTree( start, DELETE_VISITOR ); 468 } 469 } 470 catch( final IOException ex ) { 471 // MSG0011 = ファイルが削除できませんでした。file=[{0}] 472 throw MsgUtil.throwException( ex , "MSG0011" , start ); 473 } 474 } 475 476 /** 477 * delete(Path)で使用する、Files.walkFileTree の引数の FileVisitor オブジェクトです。 478 * 479 * staticオブジェクトを作成しておき、使いまわします。 480 */ 481 private static final FileVisitor<Path> DELETE_VISITOR = new SimpleFileVisitor<Path>() { 482 /** 483 * ディレクトリ内のファイルに対して呼び出されます。 484 * 485 * @param file ファイルへの参照 486 * @param attrs ファイルの基本属性 487 * @throws IOException 入出力エラーが発生した場合 488 */ 489 @Override 490 public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException { 491 Files.deleteIfExists( file ); // ファイルが存在する場合は削除 492 return FileVisitResult.CONTINUE; 493 } 494 495 /** 496 * ディレクトリ内のエントリ、およびそのすべての子孫がビジットされたあとにそのディレクトリに対して呼び出されます。 497 * 498 * @param dir ディレクトリへの参照 499 * @param ex エラーが発生せずにディレクトリの反復が完了した場合はnull、そうでない場合はディレクトリの反復が早く完了させた入出力例外 500 * @throws IOException 入出力エラーが発生した場合 501 */ 502 @Override 503 public FileVisitResult postVisitDirectory( final Path dir, final IOException ex ) throws IOException { 504 if( ex == null ) { 505 Files.deleteIfExists( dir ); // ファイルが存在する場合は削除 506 return FileVisitResult.CONTINUE; 507 } else { 508 // directory iteration failed 509 throw ex; 510 } 511 } 512 }; 513 514 /** 515 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。 516 * 517 * FileUtil.stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); と同じです。 518 * 519 * @param path チェックするパスオブジェクト 520 * @return true:安定した/false:安定しなかった。またはファイルが存在していない。 521 * @see #STABLE_SLEEP_TIME 522 * @see #STABLE_RETRY_COUNT 523 */ 524 public static boolean stablePath( final Path path ) { 525 return stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); 526 } 527 528 /** 529 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。 530 * 531 * ファイルの安定は、ファイルのサイズをチェックすることで求めます。まず、サイズをチェックし、 532 * sleepで指定した時間だけ、Thread.sleepします。再び、サイズをチェックして、同じであれば、 533 * 安定したとみなします。 534 * なので、必ず、sleep で指定したミリ秒だけは、待ちます。 535 * ファイルが存在しない、サイズが、0のままか、チェック回数を過ぎても安定しない場合は、 536 * false が返ります。 537 * サイズを求める際に、IOExceptionが発生した場合でも、falseを返します。 538 * 539 * @param path チェックするパスオブジェクト 540 * @param sleep 待機する時間(ミリ秒) 541 * @param cnt チェックする回数 542 * @return true:安定した/false:安定しなかった。またはファイルが存在していない。 543 */ 544 public static boolean stablePath( final Path path , final long sleep , final int cnt ) { 545 // 存在しない場合は、即抜けます。 546 if( Files.exists( path ) ) { 547 try { 548 for( int i=0; i<cnt; i++ ) { 549 if( Files.notExists( path ) ) { return false; } // 存在チェック。無ければ、false 550 final long size1 = Files.size( path ); // exit point 警告が出ますが、Thread.sleep 前に、値を取得しておきたい。 551 552 try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){} // 無条件に待ちます。 553 554 if( Files.notExists( path ) ) { return false; } // 存在チェック。無ければ、false 555 final long size2 = Files.size( path ); 556 if( size1 != 0L && size1 == size2 ) { return true; } // 安定した 557 } 558 } 559 catch( final IOException ex ) { 560 // Exception は発生させません。 561 // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 562 MsgUtil.errPrintln( ex , "MSG0005" , path ); 563 } 564 } 565 566 return false; 567 } 568 569 /** 570 * 指定のパスを共有ロックして、Consumer#action(Path) メソッドを実行します。 571 * 共有ロック中は、ファイルを読み込むことは出来ますが、書き込むことは出来なくなります。 572 * 573 * 共有ロックの取得は、{@value #LOCK_RETRY_COUNT} 回実行し、{@value #LOCK_SLEEP_TIME} ミリ秒待機します。 574 * 575 * @param inPath 処理対象のPathオブジェクト 576 * @param action パスを引数に取るConsumerオブジェクト 577 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 578 * @see #forEach(Path,Consumer) 579 * @see #LOCK_RETRY_COUNT 580 * @see #LOCK_SLEEP_TIME 581 */ 582 public static void lockPath( final Path inPath , final Consumer<Path> action ) { 583 // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。 584 if( Files.exists( inPath ) ) { 585 // try-with-resources 文 (AutoCloseable) 586 try( final FileChannel channel = FileChannel.open( inPath, StandardOpenOption.READ ) ) { 587 for( int i=0; i<LOCK_RETRY_COUNT; i++ ) { 588 try { 589 if( channel.tryLock( 0L,Long.MAX_VALUE,true ) != null ) { // 共有ロック獲得成功 590 action.accept( inPath ); 591 return; // 共有ロック獲得成功したので、ループから抜ける。 592 } 593 } 594 catch( final OverlappingFileLockException ex ) { 595 // 要求された領域をオーバーラップするロックがこのJava仮想マシンにすでに確保されている場合。 596 // または、このメソッド内でブロックされている別のスレッドが同じファイルのオーバーラップした領域をロックしようとしている場合 597 System.err.println( ex.getMessage() ); 598 } 599 try{ Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ex ){} 600 } 601 } 602 catch( final IOException ex ) { 603 // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 604 throw MsgUtil.throwException( ex , "MSG0005" , inPath ); 605 } 606 607 // Exception は発生させません。 608 // MSG0015 = ファイルのロック取得に失敗しました。file=[{0}] WAIT=[{1}](ms) COUNT=[{2}] 609 MsgUtil.errPrintln( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT ); 610 } 611 } 612 613 /** 614 * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 615 * 1行単位に、Consumer#action が呼ばれます。 616 * このメソッドでは、Charset は、UTF-8 です。 617 * 618 * ファイルを順次読み込むため、内部メモリを圧迫しません。 619 * 620 * @param inPath 処理対象のPathオブジェクト 621 * @param action 行を引数に取るConsumerオブジェクト 622 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 623 * @see #lockForEach(Path,Consumer) 624 */ 625 public static void forEach( final Path inPath , final Consumer<String> action ) { 626 forEach( inPath , UTF_8 , action ); 627 } 628 629 /** 630 * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 631 * 1行単位に、Consumer#action が呼ばれます。 632 * 633 * ファイルを順次読み込むため、内部メモリを圧迫しません。 634 * 635 * @param inPath 処理対象のPathオブジェクト 636 * @param chset ファイルを読み取るときのCharset 637 * @param action 行を引数に取るConsumerオブジェクト 638 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 639 * @see #lockForEach(Path,Consumer) 640 */ 641 public static void forEach( final Path inPath , final Charset chset , final Consumer<String> action ) { 642 // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。 643 if( Files.exists( inPath ) ) { 644 // try-with-resources 文 (AutoCloseable) 645 String line = null; 646 int no = 0; 647 // // こちらの方法では、lockForEach から来た場合に、エラーになります。 648 // try( final BufferedReader reader = Files.newBufferedReader( inPath , chset ) ) { 649 // 万一、コンストラクタでエラーが発生すると、リソース開放されない場合があるため、個別にインスタンスを作成しておきます。(念のため) 650 try( final FileInputStream fin = new FileInputStream( inPath.toFile() ); 651 final InputStreamReader isr = new InputStreamReader( fin , chset ); 652 final BufferedReader reader = new BufferedReader( isr ) ) { 653 while( ( line = reader.readLine() ) != null ) { 654 action.accept( line ); 655 no++; 656 } 657 } 658 catch( final IOException ex ) { 659 // MSG0016 = ファイルの行データ読み込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2} 660 throw MsgUtil.throwException( ex , "MSG0016" , inPath , no , line ); 661 } 662 } 663 } 664 665 /** 666 * 指定のパスを共有ロックして、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 667 * 1行単位に、Consumer#action が呼ばれます。 668 * 669 * ファイルを順次読み込むため、内部メモリを圧迫しません。 670 * 671 * @param inPath 処理対象のPathオブジェクト 672 * @param action 行を引数に取るConsumerオブジェクト 673 * @see #forEach(Path,Consumer) 674 */ 675 public static void lockForEach( final Path inPath , final Consumer<String> action ) { 676 lockPath( inPath , in -> forEach( in , 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 * @see #forEach(Path,Consumer) 689 */ 690 public static void lockForEach( final Path inPath , final Charset chset , final Consumer<String> action ) { 691 lockPath( inPath , in -> forEach( in , chset , action ) ); 692 } 693 694 /** 695 * 指定のパスに1行単位の文字列のListを書き込んでいきます。 696 * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。 697 * 698 * 書き込むパスの親フォルダがなければ作成します。 699 * 第2引数は、書き込む行データです。 700 * このメソッドでは、Charset は、UTF-8 です。 701 * 702 * @og.rev 1.0.0 (2016/04/28) 新規追加 703 * 704 * @param savePath セーブするパスオブジェクト 705 * @param lines 行単位の書き込むデータ 706 * @throws RuntimeException ファイル操作に失敗した場合 707 * @see #save( Path , List , boolean , Charset ) 708 */ 709 public static void save( final Path savePath , final List<String> lines ) { 710 save( savePath , lines , false , UTF_8 ); // 新規作成 711 } 712 713 /** 714 * 指定のパスに1行単位の文字列のListを書き込んでいきます。 715 * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。 716 * 717 * 書き込むパスの親フォルダがなければ作成します。 718 * 719 * 第2引数は、書き込む行データです。 720 * 721 * @og.rev 1.0.0 (2016/04/28) 新規追加 722 * 723 * @param savePath セーブするパスオブジェクト 724 * @param lines 行単位の書き込むデータ 725 * @param append trueの場合、ファイルの先頭ではなく最後に書き込まれる。 726 * @param chset ファイルを読み取るときのCharset 727 * @throws RuntimeException ファイル操作に失敗した場合 728 */ 729 public static void save( final Path savePath , final List<String> lines , final boolean append , final Charset chset ) { 730 mkdirs( savePath.toAbsolutePath().getParent() ); // savePathはファイルなので、親フォルダを作成する。 731 732 String line = null; // エラー出力のための変数 733 int no = 0; 734 735 // try-with-resources 文 (AutoCloseable) 736 try( final PrintWriter out = new PrintWriter( Files.newBufferedWriter( savePath, chset , append ? APPEND : CREATE ) ) ) { 737 for( final String ln : lines ) { 738 line = ln ; 739 no++; 740 out.println( line ); 741 } 742 out.flush(); 743 } 744 catch( final IOException ex ) { 745 // MSG0017=ファイルのデータ書き込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2} 746 throw MsgUtil.throwException( ex , "MSG0017" , savePath , no , line ); 747 } 748 } 749 750 /** 751 * 指定のパスの最終更新日付を、文字列で返します。 752 * 文字列のフォーマット指定も可能です。 753 * 754 * パスが無い場合や、最終更新日付を、取得できない場合は、現在時刻をベースに返します。 755 * 756 * @param path 処理対象のPathオブジェクト 757 * @param format 文字列化する場合のフォーマット(yyyyMMddHHmmss) 758 * @return 指定のパスの最終更新日付の文字列 759 */ 760 public static String timeStamp( final Path path , final String format ) { 761 long tempTime = 0L; 762 try { 763 // 存在チェックを直前に入れますが、厳密には、非同期なので確率の問題です。 764 if( Files.exists( path ) ) { 765 tempTime = Files.getLastModifiedTime( path ).toMillis(); 766 } 767 } 768 catch( final IOException ex ) { 769 // ファイルのタイムスタンプの取得に失敗しました。file=[{0}] 770 MsgUtil.errPrintln( ex , "MSG0018" , path , ex.getMessage() ); 771 } 772 if( tempTime == 0L ) { 773 tempTime = System.currentTimeMillis(); // パスが無い場合や、エラー時は、現在時刻を使用 774 } 775 776 return StringUtil.getTimeFormat( tempTime , format ); 777 } 778 779 /** main メソッドから呼ばれる ヘルプメッセージです。 {@value} */ 780 public static final String USAGE = "Usage: java jp.euromap.eu63.util.FileUtil [-MOVE|-COPY|-DELETE|-BACKUP|-SAVE] from to [-useLock] [-append] [-help]" ; 781 782 /** 783 * リソース一覧を表示する main メソッドです。 784 * 785 * @param args コマンド引数配列 786 */ 787 public static void main( final String[] args ) { 788 // ********** 【整合性チェック】 ********** 789 if( args.length < 1 ) { 790 System.out.println( USAGE ); 791 return; 792 } 793 794 // ********** 【引数定義】 ********** 795 Path fromPath = null; // 入力ファイル 796 Path toPath = null; // 出力ファイル 797 boolean isLock = false; // ロック処理(初期値:false しない) 798 boolean isAppend = false; // 追記処理(初期値:false しない) 799 int type = -1; // 0:MOVE , 1:COPY , 2:DELETE 800 801 // ********** 【引数処理】 ********** 802 int cnt = 0 ; 803 for( final String arg : args ) { 804 if( "-help" .equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; } 805 else if( "-useLock" .equalsIgnoreCase( arg ) ) { isLock = true; } 806 else if( "-append " .equalsIgnoreCase( arg ) ) { isAppend = true; } 807 else if( "-MOVE" .equalsIgnoreCase( arg ) ) { type = 0; } 808 else if( "-COPY" .equalsIgnoreCase( arg ) ) { type = 1; } 809 else if( "-DELETE" .equalsIgnoreCase( arg ) ) { type = 2; } 810 else if( "-BACKUP" .equalsIgnoreCase( arg ) ) { type = 3; } 811 else if( "-SAVE" .equalsIgnoreCase( arg ) ) { type = 4; } 812 else { 813 if( cnt == 0 ) { fromPath = FileUtil.readPath( arg ); } 814 else if( cnt == 1 ) { toPath = FileUtil.writePath( arg ); } // 親フォルダがなければ作成されます。 815 cnt++ ; 816 } 817 } 818 819 // ********** 【本体処理】 ********** 820 switch( type ) { 821 case 0: System.out.println( "TYPE=MOVE FROM=" + fromPath + " , TO=" + toPath ); 822 FileUtil.move( fromPath , toPath , isLock ); 823 break; 824 case 1: System.out.println( "TYPE=COPY FROM=" + fromPath + " , TO=" + toPath ); 825 FileUtil.copy( fromPath , toPath , isLock ); 826 break; 827 case 2: System.out.println( "TYPE=DELETE START=" + fromPath ); 828 FileUtil.delete( fromPath ); 829 break; 830 case 3: System.out.println( "TYPE=BACKUP FROM=" + fromPath + " , TO=" + toPath ); 831 FileUtil.backup( fromPath , toPath , isLock , false , null ); 832 break; 833 case 4: System.out.println( "TYPE=SAVE FROM=" + fromPath + " , TO=" + toPath ); 834 if( isLock ) { 835 final List<String> lines = new java.util.ArrayList<>(); 836 FileUtil.lockForEach( fromPath , str -> lines.add( str ) ); 837 } 838 else { 839 final List<String> lines = new java.util.ArrayList<>(); 840 FileUtil.forEach( fromPath , str -> lines.add( str ) ); 841 FileUtil.save( toPath , lines , isAppend , FileUtil.UTF_8 ); 842 } 843 break; 844 default : System.out.println( USAGE ); 845 break; 846 } 847 } 848}