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