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; 024// import java.io.FileInputStream ; // 8.5.4.2 (2024/01/12) 025// import java.io.InputStream ; // 8.5.4.2 (2024/01/12) delete 026// import java.io.InputStreamReader ; // 8.5.4.2 (2024/01/12) delete 027import java.io.IOException; 028 029import java.nio.file.Path; 030import java.nio.file.Files; 031import java.nio.file.Paths; 032import java.nio.file.FileVisitor; 033import java.nio.file.SimpleFileVisitor; 034import java.nio.file.FileVisitResult; 035import java.nio.file.StandardOpenOption; 036import java.nio.file.StandardCopyOption; 037import java.nio.file.attribute.BasicFileAttributes; 038import java.nio.file.OpenOption; 039import java.nio.file.NoSuchFileException; // 7.2.5.0 (2020/06/01) 040import java.nio.file.AccessDeniedException; // 8.0.0.0 (2021/07/31) 041import java.nio.channels.FileChannel; 042import java.nio.channels.OverlappingFileLockException; 043import java.nio.charset.Charset; 044import java.nio.charset.MalformedInputException; // 7.2.5.0 (2020/06/01) 045import static java.nio.charset.StandardCharsets.UTF_8; // 7.2.5.0 (2020/06/01) 046 047/** 048 * FileUtilは、共通的に使用されるファイル操作関連のメソッドを集約した、ユーティリティークラスです。 049 * 050 *<pre> 051 * 読み込みチェックや、書き出しチェックなどの簡易的な処理をまとめているだけです。 052 * 053 *</pre> 054 * @og.rev 7.0.0.0 (2017/07/07) 新規作成 055 * 056 * @version 7.0 057 * @author Kazuhiko Hasegawa 058 * @since JDK1.8, 059 */ 060public final class FileUtil { 061 private static final XLogger LOGGER= XLogger.getLogger( FileUtil.class.getSimpleName() ); // ログ出力 062 063 /** ファイルが安定するまでの待ち時間(ミリ秒) {@value} */ 064 public static final int STABLE_SLEEP_TIME = 2000 ; // ファイルが安定するまで、2秒待つ 065 /** ファイルが安定するまでのリトライ回数 {@value} */ 066 public static final int STABLE_RETRY_COUNT = 10 ; // ファイルが安定するまで、10回リトライする。 067 068 /** ファイルロックの獲得までの待ち時間(ミリ秒) {@value} */ 069 public static final int LOCK_SLEEP_TIME = 2000 ; // ロックの獲得まで、2秒待つ 070 /** ファイルロックの獲得までのリトライ回数 {@value} */ 071 public static final int LOCK_RETRY_COUNT = 10 ; // ロックの獲得まで、10回リトライする。 072 073 /** 日本語用の、Windows-31J の、Charset */ 074 public static final Charset WINDOWS_31J = Charset.forName( "Windows-31J" ); 075 076// /** 日本語用の、UTF-8 の、Charset (Windows-31Jと同じように指定できるようにしておきます。) */ 077// public static final Charset UTF_8 = StandardCharsets.UTF_8; 078 079 // 8.5.4.2 (2024/01/12) PMD 7.0.0 UseShortArrayInitializer 080// private static final OpenOption[] CREATE = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.TRUNCATE_EXISTING }; 081// private static final OpenOption[] APPEND = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.APPEND }; 082 private static final OpenOption[] CREATE = { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.TRUNCATE_EXISTING }; 083 private static final OpenOption[] APPEND = { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.APPEND }; 084 // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応 085 private static final OpenOption[] READ = { StandardOpenOption.READ }; 086 087 private static final Object STATIC_LOCK = new Object(); // staticレベルのロック 088 089 /** 090 * デフォルトコンストラクターをprivateにして、 091 * オブジェクトの生成をさせないようにする。 092 */ 093 private FileUtil() {} 094 095 /** 096 * 引数の文字列を連結した読み込み用パスのチェックを行い、存在する場合は、そのパスオブジェクトを返します。 097 * 098 * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加えたものです。 099 * そのパスが存在しなければ、例外をThrowします。 100 * 101 * @og.rev 1.0.0 (2016/04/28) 新規追加 102 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 103 * 104 * @param first パス文字列またはパス文字列の最初の部分 105 * @param more 結合してパス文字列を形成するための追加文字列 106 * @return 指定の文字列を連結したパスオブジェクト 107 * @throws RuntimeException ファイル/フォルダは存在しない場合 108 * @see Paths#get(String,String...) 109 */ 110 public static Path readPath( final String first , final String... more ) { 111 final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ; 112 113// if( !Files.exists( path ) ) { 114 if( !exists( path ) ) { // 7.2.5.0 (2020/06/01) 115 // MSG0002 = ファイル/フォルダは存在しません。file=[{0}] 116// throw MsgUtil.throwException( "MSG0002" , path ); 117 final String errMsg = "FileUtil#readPath : Path=" + path ; 118 throw MsgUtil.throwException( "MSG0002" , errMsg ); 119 } 120 121 return path; 122 } 123 124 /** 125 * 引数の文字列を連結した書き込み用パスを作成します。 126 * 127 * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加え、 128 * そのパスが存在しなければ、作成します。 129 * パスが、フォルダの場合は、そのまま作成し、ファイルの場合は、親フォルダまでを作成します。 130 * パスがフォルダかファイルかの区別は、拡張子があるかどうかで判定します。 131 * 132 * @og.rev 1.0.0 (2016/04/28) 新規追加 133 * 134 * @param first パス文字列またはパス文字列の最初の部分 135 * @param more 結合してパス文字列を形成するための追加文字列 136 * @return 指定の文字列を連結したパスオブジェクト 137 * @throws RuntimeException ファイル/フォルダが作成できなかった場合 138 * @see Paths#get(String,String...) 139 */ 140 public static Path writePath( final String first , final String... more ) { 141 final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ; 142 143 mkdirs( path,false ); 144 145 return path; 146 } 147 148 /** 149 * ファイルオブジェクトを作成します。 150 * 151 * 通常は、フォルダ+ファイル名で、新しいファイルオブジェクトを作成します。 152 * ここでは、第2引数のファイル名に、絶対パスを指定した場合は、第1引数の 153 * フォルダを使用せず、ファイル名だけで、ファイルオブジェクトを作成します。 154 * 第2引数のファイル名が、null か、ゼロ文字列の場合は、第1引数の 155 * フォルダを返します。 156 * 157 * @og.rev 7.2.1.0 (2020/03/13) isAbsolute(String)を利用します。 158 * 159 * @param path 基準となるフォルダ(ファイルの場合は、親フォルダ基準) 160 * @param fname ファイル名(絶対パス、または、相対パス) 161 * @return 合成されたファイルオブジェクト 162 */ 163 public static Path newPath( final Path path , final String fname ) { 164 if( fname == null || fname.isEmpty() ) { return path; } 165// else if( fname.charAt(0) == '/' || // 実フォルダが UNIX 166// fname.charAt(0) == '\\' || // 実フォルダが ネットワークパス 167// fname.length() > 1 && fname.charAt(1) == ':' ) { // 実フォルダが Windows 168 169 // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要 170 return isAbsolute( fname ) 171 ? new File( fname ).toPath() 172 : path.resolve( fname ); 173 174// else if( isAbsolute( fname ) ) { 175// return new File( fname ).toPath(); 176// } 177// else { 178// return path.resolve( fname ); 179// } 180 } 181 182 /** 183 * ファイルアドレスが絶対パスかどうか[絶対パス:true]を判定します。 184 * 185 * ファイル名が、絶対パス('/' か、'\\' か、2文字目が ':' の場合)かどうかを 186 * 判定して、絶対パスの場合は、true を返します。 187 * それ以外(nullやゼロ文字列も含む)は、false になります。 188 * 189 * @og.rev 7.2.1.0 (2020/03/13) 新規追加 190 * 191 * @param fname ファイルパスの文字列(絶対パス、相対パス、null、ゼロ文字列) 192 * @return 絶対パスの場合は true 193 */ 194 public static boolean isAbsolute( final String fname ) { 195// return fname != null && ( 196 return fname != null && !fname.isEmpty() && ( 197 fname.charAt(0) == '/' // 実フォルダが UNIX 198 || fname.charAt(0) == '\\' // 実フォルダが ネットワークパス 199 || fname.length() > 1 && fname.charAt(1) == ':' ); // 実フォルダが Windows 200 } 201 202 /** 203 * 引数のファイルパスを親階層を含めて生成します。 204 * 205 * すでに存在している場合や作成が成功した場合は、true を返します。 206 * 作成に失敗した場合は、false です。 207 * 指定のファイルパスは、フォルダであることが前提ですが、簡易的に 208 * ファイルの場合は、その親階層のフォルダを作成します。 209 * ファイルかフォルダの判定は、拡張子があるか、ないかで判定します。 210 * 211 * @og.rev 1.0.0 (2016/04/28) 新規追加 212 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 213 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成 214 * 215 * @param target ターゲットのファイルパス 216 * @param parentCheck 先に親フォルダの作成を行うかどうか(true:行う) 217 * @throws RuntimeException フォルダの作成に失敗した場合 218 */ 219// public static void mkdirs( final Path target ) { 220 public static void mkdirs( final Path target,final boolean parentCheck ) { 221// if( Files.notExists( target ) ) { // 存在しない場合 222 if( !exists( target ) ) { // 存在しない場合 7.2.5.0 (2020/06/01) 223 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 224// final boolean isFile = target.getFileName().toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 225 226 final Path tgtName = target.getFileName(); 227 if( tgtName == null ) { 228 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 229 throw MsgUtil.throwException( "MSG0007" , target.toString() ); 230 } 231 232 final boolean isFile = tgtName.toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 233// final Path dir = isFile ? target.toAbsolutePath().getParent() : target ; // ファイルなら、親フォルダを取り出す。 234 final Path dir = isFile ? target.getParent() : target ; // ファイルなら、親フォルダを取り出す。 235 if( dir == null ) { 236 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 237 throw MsgUtil.throwException( "MSG0007" , target.toString() ); 238 } 239 240// if( Files.notExists( dir ) ) { // 存在しない場合 241 if( !exists( dir ) ) { // 存在しない場合 7.2.5.0 (2020/06/01) 242 try { 243 synchronized( STATIC_LOCK ) { // 8.0.0.0 (2021/07/01) 意味があるかどうかは不明 244 Files.createDirectories( dir ); 245 } 246 } 247 catch( final IOException ex ) { 248 // MSG0007 = ファイル/フォルダの作成に失敗しました。dir=[{0}] 249 throw MsgUtil.throwException( ex , "MSG0007" , dir ); 250 } 251 } 252 } 253 } 254 255 /** 256 * 単体ファイルをコピーします。 257 * 258 * コピー先がなければ、コピー先のフォルダ階層を作成します。 259 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。 260 * コピー先のファイルがすでに存在する場合は、上書きされますので、 261 * 必要であれば、先にバックアップしておいて下さい。 262 * 263 * @og.rev 1.0.0 (2016/04/28) 新規追加 264 * 265 * @param from コピー元となるファイル 266 * @param to コピー先となるファイル 267 * @throws RuntimeException ファイル操作に失敗した場合 268 * @see #copy(Path,Path,boolean) 269 */ 270 public static void copy( final Path from , final Path to ) { 271 copy( from,to,false ); 272 } 273 274 /** 275 * パスの共有ロックを指定した、単体ファイルをコピーします。 276 * 277 * コピー先がなければ、コピー先のフォルダ階層を作成します。 278 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。 279 * コピー先のファイルがすでに存在する場合は、上書きされますので、 280 * 必要であれば、先にバックアップしておいて下さい。 281 * 282 * ※ copy に関しては、コピー時間を最小化する意味で、synchronized しています。 283 * 284 * @og.rev 1.0.0 (2016/04/28) 新規追加 285 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 286 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 287 * 288 * @param from コピー元となるファイル 289 * @param to コピー先となるファイル 290 * @param useLock パスを共有ロックするかどうか 291 * @throws RuntimeException ファイル操作に失敗した場合 292 * @see #copy(Path,Path) 293 */ 294 public static void copy( final Path from , final Path to , final boolean useLock ) { 295// if( Files.exists( from ) ) { 296 if( exists( from ) ) { // 7.2.5.0 (2020/06/01) 297 mkdirs( to,false ); 298 299 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 300// final boolean isFile = to.getFileName().toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 301 302 final Path toName = to.getFileName(); 303 if( toName == null ) { 304 // MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}] 305 throw MsgUtil.throwException( "MSG0008" , from.toString() , to.toString() ); 306 } 307 308 final boolean isFile = toName.toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 309 310 // コピー先がフォルダの場合は、コピー元と同じ名前のファイルにする。 311 final Path save = isFile ? to : to.resolve( from.getFileName() ); 312 313 synchronized( STATIC_LOCK ) { 314 // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 315 if( exists( from ) ) { 316 if( useLock ) { 317 lockPath( from , in -> localCopy( in , save ) ); 318 } 319 else { 320 localCopy( from , save ); 321 } 322 } 323 } 324 } 325 else { 326 // 7.2.5.0 (2020/06/01) 327 // MSG0002 = ファイル/フォルダが存在しません。file=[{0}] 328// MsgUtil.errPrintln( "MSG0002" , from ); 329 final String errMsg = "FileUtil#copy : from=" + from ; 330 LOGGER.warning( "MSG0002" , errMsg ); 331 } 332 } 333 334 /** 335 * 単体ファイルをコピーします。 336 * 337 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。 338 * 339 * @og.rev 1.0.0 (2016/04/28) 新規追加 340 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 341 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 342 * @og.rev 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ファイルサイズチェック) 343 * 344 * @param from コピー元となるファイル 345 * @param to コピー先となるファイル 346 */ 347 private static void localCopy( final Path from , final Path to ) { 348 try { 349 // 直前に存在チェックを行います。 350// if( Files.exists( from ) ) { 351 // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 352 // synchronized( STATIC_LOCK ) { // 7.4.4.0 (2021/06/30) 意味がないので外す。 353 if( exists( from ) ) { // 7.2.5.0 (2020/06/01) 354 final long fromSize = Files.size(from); // 7.4.4.0 (2021/06/30) 355 Files.copy( from , to , StandardCopyOption.REPLACE_EXISTING ); 356 357 // 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ファイルサイズチェック) 358 for( int i=0; i<STABLE_RETRY_COUNT; i++ ) { 359 // 8.0.0.0 (2021/07/31) Avoid if (x != y) ..; else ..; 360 if( fromSize == Files.size(to) ) { 361 return ; 362 } 363 else { 364// try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){} 365 try { Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ignored ) {} // 8.5.4.2 (2024/01/12) PMD 7.0.0 EmptyCatchBlock 366 } 367 368// final long toSize = Files.size(to); 369// if( fromSize != toSize ) { 370// try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){} 371// } 372// else { 373// break; 374// } 375 } 376 } 377 // } 378 } 379 catch( final NoSuchFileException ex ) { // 8.0.0.0 (2021/07/31) 380 // MSG0002 = ファイル/フォルダが存在しません。\n\tfile=[{0}] 381 LOGGER.warning( "MSG0002" , from ); 382 // MSG0012 = ファイルがコピーできませんでした。from=[{0}] to=[{1}] 383 LOGGER.warning( "MSG0012" , from , to ); // 原因不明:FileWatchとDirWatchの両方が動いているから? 384 } 385 catch( final IOException ex ) { 386 // MSG0012 = ファイルがコピーできませんでした。from=[{0}] to=[{1}] 387// MsgUtil.errPrintln( ex , "MSG0012" , from , to ); 388 LOGGER.warning( ex , "MSG0012" , from , to ); 389 } 390 } 391 392 /** 393 * 単体ファイルを移動します。 394 * 395 * 移動先がなければ、移動先のフォルダ階層を作成します。 396 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。 397 * 移動先のファイルがすでに存在する場合は、上書きされますので、 398 * 必要であれば、先にバックアップしておいて下さい。 399 * 400 * @og.rev 1.0.0 (2016/04/28) 新規追加 401 * 402 * @param from 移動元となるファイル 403 * @param to 移動先となるファイル 404 * @throws RuntimeException ファイル操作に失敗した場合 405 * @see #move(Path,Path,boolean) 406 */ 407 public static void move( final Path from , final Path to ) { 408 move( from,to,false ); 409 } 410 411 /** 412 * パスの共有ロックを指定した、単体ファイルを移動します。 413 * 414 * 移動先がなければ、移動先のフォルダ階層を作成します。 415 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。 416 * 移動先のファイルがすでに存在する場合は、上書きされますので、 417 * 必要であれば、先にバックアップしておいて下さい。 418 * 419 * ※ move に関しては、ムーブ時間を最小化する意味で、synchronized しています。 420 * 421 * @og.rev 1.0.0 (2016/04/28) 新規追加 422 * @og.rev 7.2.1.0 (2020/03/13) from,to が null の場合、処理しない。 423 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 424 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 425 * 426 * @param from 移動元となるファイル 427 * @param to 移動先となるファイル 428 * @param useLock パスを共有ロックするかどうか 429 * @throws RuntimeException ファイル操作に失敗した場合 430 * @see #move(Path,Path) 431 */ 432 public static void move( final Path from , final Path to , final boolean useLock ) { 433 if( from == null || to == null ) { return; } // 7.2.1.0 (2020/03/13) 434 435// if( Files.exists( from ) ) { 436 if( exists( from ) ) { // 1.4.0 (2019/09/01) 437 mkdirs( to,false ); 438 439 // ファイルかどうかは、拡張子の有無で判定する。 440 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 441// final boolean isFile = to.getFileName().toString().contains( "." ); 442 final Path toName = to.getFileName(); 443 if( toName == null ) { 444 // MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}] 445 throw MsgUtil.throwException( "MSG0008" , to.toString() ); 446 } 447 448 final boolean isFile = toName.toString().contains( "." ); // ファイルかどうかは、拡張子の有無で判定する。 449 450 // 移動先がフォルダの場合は、コピー元と同じ名前のファイルにする。 451 final Path save = isFile ? to : to.resolve( from.getFileName() ); 452 453 synchronized( STATIC_LOCK ) { 454 // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 455 if( exists( from ) ) { 456 if( useLock ) { 457 lockPath( from , in -> localMove( in , save ) ); 458 } 459 else { 460 localMove( from , save ); 461 } 462 } 463 } 464 } 465 else { 466 // MSG0002 = ファイル/フォルダが存在しません。file=[{0}] 467// MsgUtil.errPrintln( "MSG0002" , from ); 468 final String errMsg = "FileUtil#move : from=" + from ; 469 LOGGER.warning( "MSG0002" , errMsg ); 470 } 471 } 472 473 /** 474 * 単体ファイルを移動します。 475 * 476 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。 477 * 478 * @og.rev 1.0.0 (2016/04/28) 新規追加 479 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 480 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 481 * @og.rev 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ファイルサイズチェック) 482 * 483 * @param from 移動元となるファイル 484 * @param to 移動先となるファイル 485 */ 486 private static void localMove( final Path from , final Path to ) { 487 try { 488 // synchronized( from ) { 489 // 直前に存在チェックを行います。 490// if( Files.exists( from ) ) { 491 // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。 492 // synchronized( STATIC_LOCK ) { // 7.4.4.0 (2021/06/30) 意味がないので外す。 493 if( exists( from ) ) { // このメソッドの結果がすぐに古くなることに注意してください。 494 // CopyOption に、StandardCopyOption.ATOMIC_MOVE を指定すると、別サーバー等へのMOVEは、出来なくなります。 495 // try{ Thread.sleep( 2000 ); } catch( final InterruptedException ex ){} // 先に、無条件に待ちます。 496 final long fromSize = Files.size(from); // 7.4.4.0 (2021/06/30) 497 Files.move( from , to , StandardCopyOption.REPLACE_EXISTING ); 498 499 // 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ファイルサイズチェック) 500 for( int i=0; i<STABLE_RETRY_COUNT; i++ ) { 501 // 8.0.0.0 (2021/07/31) Avoid if (x != y) ..; else ..; 502 if( fromSize == Files.size(to) ) { 503 return ; 504 } 505 else { 506// try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){} 507 try { Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ignored ) {} // 8.5.4.2 (2024/01/12) PMD 7.0.0 EmptyCatchBlock 508 } 509 510// final long toSize = Files.size(to); 511// if( fromSize != toSize ) { 512// try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){} 513// } 514// else { 515// break; 516// } 517 } 518 } 519 // } 520 // } 521 } 522 catch( final AccessDeniedException ex ) { // 8.0.0.0 (2021/07/31) 523 // MSG0034 = ファイルサイズの取得ができませんでした。\n\tfile=[{0}] 524 LOGGER.warning( "MSG0034" , from ); 525 // MSG0008 = ファイルが移動できませんでした。from=[{0}] to=[{1}] 526 LOGGER.warning( "MSG0008" , from , to ); // 原因不明:FileWatchとDirWatchの両方が動いているから? 527 } 528 catch( final NoSuchFileException ex ) { // 7.2.5.0 (2020/06/01) 529 // MSG0002 = ファイル/フォルダが存在しません。\n\tfile=[{0}] 530 LOGGER.warning( "MSG0002" , from ); 531 // MSG0008 = ファイルが移動できませんでした。from=[{0}] to=[{1}] 532 LOGGER.warning( "MSG0008" , from , to ); // 原因不明:FileWatchとDirWatchの両方が動いているから? 533 } 534 catch( final IOException ex ) { 535 // MSG0008 = ファイルが移動できませんでした。from=[{0}] to=[{1}] 536// MsgUtil.errPrintln( ex , "MSG0008" , from , to ); 537 LOGGER.warning( ex , "MSG0008" , from , to ); 538 } 539 } 540 541 /** 542 * 単体ファイルをバックアップフォルダに移動します。 543 * 544 * これは、#backup( from,to,true,false,sufix ); と同じ処理を実行します。 545 * 546 * 移動先は、フォルダ指定で、ファイル名は存在チェックせずに、必ず変更します。 547 * その際、移動元+サフィックス のファイルを作成します。 548 * ファイルのロックを行います。 549 * 550 * @og.rev 1.0.0 (2016/04/28) 新規追加 551 * 552 * @param from 移動元となるファイル 553 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 554 * @param sufix バックアップファイル名の後ろに付ける文字列 555 * @return バックアップしたファイルパス。 556 * @throws RuntimeException ファイル操作に失敗した場合 557 * @see #backup( Path , Path , boolean , boolean , String ) 558 */ 559 public static Path backup( final Path from , final Path to , final String sufix ) { 560 return backup( from,to,true,false,sufix ); // sufix を無条件につける為、existsCheck=false で登録 561 } 562 563 /** 564 * 単体ファイルをバックアップフォルダに移動します。 565 * 566 * これは、#backup( from,to,true,true ); と同じ処理を実行します。 567 * 568 * 移動先は、フォルダ指定で、ファイル名は存在チェックの上で、無ければ移動、 569 * あれば、移動元+時間情報 のファイルを作成します。 570 * ファイルのロックを行います。 571 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。 572 * 573 * @og.rev 1.0.0 (2016/04/28) 新規追加 574 * 575 * @param from 移動元となるファイル 576 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 577 * @return バックアップしたファイルパス。 578 * @throws RuntimeException ファイル操作に失敗した場合 579 * @see #backup( Path , Path , boolean , boolean , String ) 580 */ 581 public static Path backup( final Path from , final Path to ) { 582 return backup( from,to,true,true,null ); 583 } 584 585 /** 586 * パスの共有ロックを指定して、単体ファイルをバックアップフォルダに移動します。 587 * 588 * 移動先のファイル名は、existsCheckが、trueの場合は、移動先のファイル名をチェックして、 589 * 存在しなければ、移動元と同じファイル名で、バックアップフォルダに移動します。 590 * 存在すれば、ファイル名+サフィックス のファイルを作成します。(拡張子より後ろにサフィックスを追加します。) 591 * existsCheckが、false の場合は、無条件に、移動元のファイル名に、サフィックスを追加します。 592 * サフィックスがnullの場合は、時間情報になります。 593 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。 594 * 595 * @og.rev 1.0.0 (2016/04/28) 新規追加 596 * @og.rev 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 597 * @og.rev 7.2.1.0 (2020/03/13) ファイル名変更処理の修正 598 * @og.rev 7.2.5.0 (2020/06/01) toパスに、環境変数と日付文字列置換機能を追加します。 599 * 600 * @param from 移動元となるファイル 601 * @param to 移動先となるフォルダ(nullの場合は、移動元と同じフォルダ) 602 * @param useLock パスを共有ロックするかどうか 603 * @param existsCheck 移動先のファイル存在チェックを行うかどうか(true:行う/false:行わない) 604 * @param sufix バックアップファイル名の後ろに付ける文字列 605 * 606 * @return バックアップしたファイルパス。 607 * @throws RuntimeException ファイル操作に失敗した場合 608 * @see #backup( Path , Path ) 609 */ 610 public static Path backup( final Path from , final Path to , final boolean useLock , final boolean existsCheck , final String sufix ) { 611// final Path movePath = to == null ? from.getParent() : to ; 612 Path movePath = to == null ? from.getParent() : to ; 613 614 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 615 if( movePath == null ) { 616 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 617 throw MsgUtil.throwException( "MSG0007" , from.toString() ); 618 } 619 620 // 7.2.5.0 (2020/06/01) toパスに、環境変数と日付文字列置換機能を追加します。 621 String toStr = movePath.toString(); 622 // toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@ENV." , "}" , System::getenv ); // 環境変数置換 623 // toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@DATE." , "}" , StringUtil::getTimeFormat ); // 日付文字列置換 624 toStr = StringUtil.replaceText( toStr ); // 環境変数,日付文字列置換 625 movePath = Paths.get( toStr ); 626 627// final String fileName = from.getFileName().toString(); 628 final Path fName = from.getFileName(); 629 if( fName == null ) { 630 // MSG0002 = ファイル/フォルダが存在しません。\n\tfile=[{0}] 631 throw MsgUtil.throwException( "MSG0002" , from.toString() ); 632 } 633 634// final Path moveFile = movePath.resolve( fileName ); // 移動先のファイルパスを構築 635 final Path moveFile = movePath.resolve( fName ); // 移動先のファイルパスを構築 636 637// final boolean isExChk = existsCheck && Files.notExists( moveFile ); // 存在しない場合、true。存在するか、不明の場合は、false。 638 639 final Path bkupPath; 640// if( isExChk ) { 641 if( existsCheck && Files.notExists( moveFile ) ) { // 存在しない場合、true。存在するか、不明の場合は、false。 642 bkupPath = moveFile; 643 } 644 else { 645 final String fileName = fName.toString(); // from パスの名前 646 final int ad = fileName.lastIndexOf( '.' ); // ピリオドの手前に、タイムスタンプを入れる。 647 // 7.2.1.0 (2020/03/13) ファイル名変更処理の修正 648 if( ad > 0 ) { 649 bkupPath = movePath.resolve( 650 fileName.substring( 0,ad ) 651 + "_" 652 + StringUtil.nval( sufix , StringUtil.getTimeFormat() ) 653 + fileName.substring( ad ) // ad 以降なので、ピリオドも含む 654 ); 655 } 656 else { 657 bkupPath = null; 658 } 659 } 660 661 move( from,bkupPath,useLock ); 662 663 return bkupPath; 664 } 665 666 /** 667 * オリジナルファイルにバックアップファイルの行を追記します。 668 * 669 * オリジナルファイルに、バックアップファイルから読み取った行を追記していきます。 670 * 処理する条件は、オリジナルファイルとバックアップファイルが異なる場合のみ、実行されます。 671 * また、バックアップファイルから、追記する行で、COUNT,TIME,DATE の要素を持つ 672 * 行は、RPTファイルの先頭行なので、除外します。 673 * 674 * @og.rev 7.2.5.0 (2020/06/01) 新規追加。 675 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成 676 * 677 * @param orgPath 追加されるオリジナルのパス名 678 * @param bkup 行データを取り出すバックアップファイル 679 */ 680 public static void mergeFile( final Path orgPath , final Path bkup ) { 681 if( exists( bkup ) && !bkup.equals( orgPath ) ) { // 追記するバックアップファイルの存在を条件に加える。 682 try { 683// final List<String> lines = FileUtil.readAllLines( bkup ); // 1.4.0 (2019/10/01) 684 final List<String> lines = readAllLines( bkup ); // 1.4.0 (2019/10/01) // 8.5.4.2 (2024/01/12) PMD 7.0.0 UnnecessaryFullyQualifiedName 685 // RPT,STS など、書き込み都度ヘッダー行を入れるファイルは、ヘッダー行を削除しておきます。 686 if( lines.size() >= 2 ) { 687 final String first = lines.get(0); // RPTの先頭行で、COUNT,TIME,DATE を持っていれば、その行は削除します。 688 if( first.contains( "COUNT" ) && first.contains( "DATE" ) && first.contains( "TIME" ) ) { lines.remove(0); } 689 } // 先頭行はトークン名 690 // ※ lockSave がうまく動きません。 691 // if( useLock ) { 692 // lockSave( orgPath , lines , true ); 693 // } 694 // else { 695// save( orgPath , lines , true ); 696 save( orgPath , lines , true , UTF_8 ); 697 // } 698 synchronized( STATIC_LOCK ) { 699 Files.deleteIfExists( bkup ); 700 } 701 } 702 catch( final IOException ex ) { 703 // MSG0003 = ファイルがオープン出来ませんでした。file=[{0}] 704 throw MsgUtil.throwException( ex , "MSG0003" , bkup.toAbsolutePath().normalize() ); 705 } 706 } 707 } 708 709 /** 710 * ファイルまたはフォルダ階層を削除します。 711 * 712 * これは、指定のパスが、フォルダの場合、階層すべてを削除します。 713 * 階層の途中にファイル等が存在していたとしても、削除します。 714 * 715 * Files.walkFileTree(Path,FileVisitor) を使用したファイル・ツリーの削除方式です。 716 * 717 * @og.rev 1.0.0 (2016/04/28) 新規追加 718 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 719 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成 720 * 721 * @param start 削除開始ファイル 722 * @throws RuntimeException ファイル操作に失敗した場合 723 */ 724 public static void delete( final Path start ) { 725 try { 726// if( Files.exists( start ) ) { 727 if( exists( start ) ) { // 7.2.5.0 (2020/06/01) 728 synchronized( STATIC_LOCK ) { 729 Files.walkFileTree( start, DELETE_VISITOR ); 730 } 731 } 732 } 733 catch( final IOException ex ) { 734 // MSG0011 = ファイルが削除できませんでした。file=[{0}] 735 throw MsgUtil.throwException( ex , "MSG0011" , start ); 736 } 737 } 738 739 /** 740 * delete(Path)で使用する、Files.walkFileTree の引数の FileVisitor オブジェクトです。 741 * 742 * staticオブジェクトを作成しておき、使いまわします。 743 */ 744 // 8.5.4.2 (2024/01/12) PMD 7.0.0 UseDiamondOperator 対応 745// private static final FileVisitor<Path> DELETE_VISITOR = new SimpleFileVisitor<Path>() { 746 private static final FileVisitor<Path> DELETE_VISITOR = new SimpleFileVisitor<>() { 747 /** 748 * ディレクトリ内のファイルに対して呼び出されます。 749 * 750 * @param file ファイルへの参照 751 * @param attrs ファイルの基本属性 752 * @throws IOException 入出力エラーが発生した場合 753 */ 754 @Override // FileVisitor 755 public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException { 756 Files.deleteIfExists( file ); // ファイルが存在する場合は削除 757 return FileVisitResult.CONTINUE; 758 } 759 760 /** 761 * ディレクトリ内のエントリ、およびそのすべての子孫がビジットされたあとにそのディレクトリに対して呼び出されます。 762 * 763 * @param dir ディレクトリへの参照 764 * @param ex エラーが発生せずにディレクトリの反復が完了した場合はnull、そうでない場合はディレクトリの反復が早く完了させた入出力例外 765 * @throws IOException 入出力エラーが発生した場合 766 */ 767 @Override // FileVisitor 768 public FileVisitResult postVisitDirectory( final Path dir, final IOException ex ) throws IOException { 769 if( ex == null ) { 770 Files.deleteIfExists( dir ); // ファイルが存在する場合は削除 771 return FileVisitResult.CONTINUE; 772 } else { 773 // directory iteration failed 774 throw ex; 775 } 776 } 777 }; 778 779 /** 780 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。 781 * 782 * FileUtil.stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); と同じです。 783 * 784 * @param path チェックするパスオブジェクト 785 * @return true:安定した/false:安定しなかった。またはファイルが存在していない。 786 * @see #STABLE_SLEEP_TIME 787 * @see #STABLE_RETRY_COUNT 788 */ 789 public static boolean stablePath( final Path path ) { 790 return stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); 791 } 792 793 /** 794 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。 795 * 796 * ファイルの安定は、ファイルのサイズをチェックすることで求めます。まず、サイズをチェックし、 797 * sleepで指定した時間だけ、Thread.sleepします。再び、サイズをチェックして、同じであれば、 798 * 安定したとみなします。 799 * なので、必ず、sleep で指定したミリ秒だけは、待ちます。 800 * ファイルが存在しない、サイズが、0のままか、チェック回数を過ぎても安定しない場合は、 801 * false が返ります。 802 * サイズを求める際に、IOExceptionが発生した場合でも、falseを返します。 803 * 804 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 805 * 806 * @param path チェックするパスオブジェクト 807 * @param sleep 待機する時間(ミリ秒) 808 * @param cnt チェックする回数 809 * @return true:安定した/false:安定しなかった。またはファイルが存在していない。 810 */ 811 public static boolean stablePath( final Path path , final long sleep , final int cnt ) { 812 // 存在しない場合は、即抜けます。 813// if( Files.exists( path ) ) { 814 if( exists( path ) ) { // 仮想フォルダなどの場合、実態が存在しないことがある。 815// try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){} // 先に、無条件に待ちます。 816 try { Thread.sleep( sleep ); } catch( final InterruptedException ignored ) {} // 8.5.4.2 (2024/01/12) PMD 7.0.0 EmptyCatchBlock 817 try { 818 if( !exists( path ) ) { return false; } // 存在チェック。無ければ、false 819 long size1 = Files.size( path ); // 7.3.1.3 (2021/03/09) forの前に移動 820 for( int i=0; i<cnt; i++ ) { 821// if( Files.notExists( path ) ) { return false; } // 存在チェック。無ければ、false 822 // if( !exists( path ) ) { break; } // 存在チェック。無ければ、false 823 // final long size1 = Files.size( path ); // exit point 警告が出ますが、Thread.sleep 前に、値を取得しておきたい。 824 825// try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){} // 無条件に待ちます。 826 try { Thread.sleep( sleep ); } catch( final InterruptedException ignored ) {} // 8.5.4.2 (2024/01/12) PMD 7.0.0 EmptyCatchBlock 827 828// if( Files.notExists( path ) ) { return false; } // 存在チェック。無ければ、false 829 if( !exists( path ) ) { break; } // 存在チェック。無ければ、false 830 final long size2 = Files.size( path ); 831 if( size1 != 0L && size1 == size2 ) { return true; } // 安定した 832 size1 = size2 ; // 7.3.1.3 (2021/03/09) 次のチェックループ 833 } 834 } 835 catch( final IOException ex ) { 836 // Exception は発生させません。 837 // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 838 MsgUtil.errPrintln( ex , "MSG0005" , path ); 839 } 840 } 841 842 return false; 843 } 844 845 /** 846 * 指定のパスを共有ロックして、Consumer#action(Path) メソッドを実行します。 847 * 共有ロック中は、ファイルを読み込むことは出来ますが、書き込むことは出来なくなります。 848 * 849 * 共有ロックの取得は、{@value #LOCK_RETRY_COUNT} 回実行し、{@value #LOCK_SLEEP_TIME} ミリ秒待機します。 850 * 851 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 852 * @og.rev 7.4.4.0 (2021/06/30) NoSuchFileException 時は、メッセージのみ表示する。 853 * 854 * @param inPath 処理対象のPathオブジェクト 855 * @param action パスを引数に取るConsumerオブジェクト 856 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 857 * @see #forEach(Path,Consumer) 858 * @see #LOCK_RETRY_COUNT 859 * @see #LOCK_SLEEP_TIME 860 */ 861 public static void lockPath( final Path inPath , final Consumer<Path> action ) { 862 // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。 863// if( Files.exists( inPath ) ) { 864 if( exists( inPath ) ) { // 7.2.5.0 (2020/06/01) 865 // try-with-resources 文 (AutoCloseable) 866 // 8.5.4.2 (2024/01/12) PMD 7.0.0 UseShortArrayInitializer 867 // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応 868// try( FileChannel channel = FileChannel.open( inPath, StandardOpenOption.READ ) ) { 869 try( FileChannel channel = FileChannel.open( inPath, READ ) ) { 870 for( int i=0; i<LOCK_RETRY_COUNT; i++ ) { 871 try { 872 if( channel.tryLock( 0L,Long.MAX_VALUE,true ) != null ) { // 共有ロック獲得成功 873 action.accept( inPath ); 874 return; // 共有ロック獲得成功したので、ループから抜ける。 875 } 876 } 877 // 要求された領域をオーバーラップするロックがこのJava仮想マシンにすでに確保されている場合。 878 // または、このメソッド内でブロックされている別のスレッドが同じファイルのオーバーラップした領域をロックしようとしている場合 879 catch( final OverlappingFileLockException ex ) { 880 // System.err.println( ex.getMessage() ); 881 if( i >= 3 ) { // とりあえず3回までは、何も出さない 882 // MSG0104 = 要求された領域のロックは、このJava仮想マシンにすでに確保されています。 \n\tfile=[{0}] 883 // LOGGER.warning( ex , "MSG0104" , inPath ); 884 LOGGER.warning( "MSG0104" , inPath ); // 1.5.0 (2020/04/01) メッセージだけにしておきます。 885 } 886 } 887// try{ Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ex ){} 888 try { Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ignored ) {} // 8.5.4.2 (2024/01/12) PMD 7.0.0 EmptyCatchBlock 889 } 890 } 891 // 7.4.4.0 (2021/06/30) NoSuchFileException 時は、メッセージのみ表示する。 892 catch( final NoSuchFileException ex ) { 893 // MSG0002 = ファイル/フォルダが存在しません。\n\tfile=[{0}] 894 LOGGER.warning( "MSG0002" , inPath ); // 原因不明:FileWatchとDirWatchの両方が動いているから? 895 } 896 catch( final IOException ex ) { 897 // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 898 throw MsgUtil.throwException( ex , "MSG0005" , inPath ); 899 } 900 901 // Exception は発生させません。 902 // MSG0015 = ファイルのロック取得に失敗しました。file=[{0}] WAIT=[{1}](ms) COUNT=[{2}] 903// MsgUtil.errPrintln( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT ); 904 LOGGER.warning( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT ); 905 } 906 } 907 908 /** 909 * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 910 * 1行単位に、Consumer#action が呼ばれます。 911 * このメソッドでは、Charset は、UTF-8 です。 912 * 913 * ファイルを順次読み込むため、内部メモリを圧迫しません。 914 * 915 * @param inPath 処理対象のPathオブジェクト 916 * @param action 行を引数に取るConsumerオブジェクト 917 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 918 * @see #lockForEach(Path,Consumer) 919 */ 920 public static void forEach( final Path inPath , final Consumer<String> action ) { 921 forEach( inPath , UTF_8 , action ); 922 } 923 924 /** 925 * 指定のパスから、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 926 * 1行単位に、Consumer#action が呼ばれます。 927 * 928 * ファイルを順次読み込むため、内部メモリを圧迫しません。 929 * 930 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 931 * 932 * @param inPath 処理対象のPathオブジェクト 933 * @param chset ファイルを読み取るときのCharset 934 * @param action 行を引数に取るConsumerオブジェクト 935 * @throws RuntimeException ファイル読み込み時にエラーが発生した場合 936 * @see #lockForEach(Path,Consumer) 937 */ 938 public static void forEach( final Path inPath , final Charset chset , final Consumer<String> action ) { 939 // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。 940// if( Files.exists( inPath ) ) { 941 if( exists( inPath ) ) { // 7.2.5.0 (2020/06/01) 942 // try-with-resources 文 (AutoCloseable) 943 String line = null; 944 int no = 0; 945 // // こちらの方法では、lockForEach から来た場合に、エラーになります。 946 // try( BufferedReader reader = Files.newBufferedReader( inPath , chset ) ) { 947 // 万一、コンストラクタでエラーが発生すると、リソース開放されない場合があるため、個別にインスタンスを作成しておきます。(念のため) 948 // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応 949// try( FileInputStream fin = new FileInputStream( inPath.toFile() ); 950// InputStreamReader isr = new InputStreamReader( fin , chset ); 951// BufferedReader reader = new BufferedReader( isr ) ) { 952 try( BufferedReader reader = Files.newBufferedReader( inPath,chset ) ) { 953 while( ( line = reader.readLine() ) != null ) { 954 // 1.2.0 (2018/09/01) UTF-8 BOM 対策 955 // UTF-8 の BOM(0xEF 0xBB 0xBF) は、Java内部文字コードの UTF-16 BE では、0xFE 0xFF になる。 956 // ファイルの先頭文字が、feff の場合は、その文字を削除します。 957 // if( no == 0 && !line.isEmpty() && Integer.toHexString(line.charAt(0)).equalsIgnoreCase("feff") ) { 958 // 8.5.4.2 (2024/01/12) PMD 7.0.0 UnnecessaryCast 対応 959// if( no == 0 && !line.isEmpty() && (int)line.charAt(0) == (int)'\ufeff' ) { 960 if( no == 0 && !line.isEmpty() && line.charAt(0) == '\ufeff' ) { 961 // MSG0105 = 指定のファイルは、UTF-8 BOM付きです。BOM無しファイルで、運用してください。 \n\tfile=[{0}] 962 System.out.println( MsgUtil.getMsg( "MSG0105" , inPath ) ); 963 line = line.substring(1); // BOM の削除 : String#replace("\ufeff","") の方が良い? 964 } 965 966 action.accept( line ); 967 no++; 968 } 969 } 970 catch( final IOException ex ) { 971 // MSG0016 = ファイルの行データ読み込みに失敗しました。\n\tfile={0} , 行番号:{1} , 行:{2} 972 throw MsgUtil.throwException( ex , "MSG0016" , inPath , no , line ); 973 } 974 } 975 } 976 977 /** 978 * 指定のパスを共有ロックして、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 979 * 1行単位に、Consumer#action が呼ばれます。 980 * 981 * ファイルを順次読み込むため、内部メモリを圧迫しません。 982 * 983 * @param inPath 処理対象のPathオブジェクト 984 * @param action 行を引数に取るConsumerオブジェクト 985 * @see #forEach(Path,Consumer) 986 */ 987 public static void lockForEach( final Path inPath , final Consumer<String> action ) { 988 lockPath( inPath , in -> forEach( in , UTF_8 , action ) ); 989 } 990 991 /** 992 * 指定のパスを共有ロックして、1行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。 993 * 1行単位に、Consumer#action が呼ばれます。 994 * 995 * ファイルを順次読み込むため、内部メモリを圧迫しません。 996 * 997 * @param inPath 処理対象のPathオブジェクト 998 * @param chset エンコードを指定するCharsetオブジェクト 999 * @param action 行を引数に取るConsumerオブジェクト 1000 * @see #forEach(Path,Consumer) 1001 */ 1002 public static void lockForEach( final Path inPath , final Charset chset , final Consumer<String> action ) { 1003 lockPath( inPath , in -> forEach( in , chset , action ) ); 1004 } 1005 1006 /** 1007 * 指定のパスに1行単位の文字列のListを書き込んでいきます。 1008 * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。 1009 * 1010 * 書き込むパスの親フォルダがなければ作成します。 1011 * 第2引数は、書き込む行データです。 1012 * このメソッドでは、Charset は、UTF-8 です。 1013 * 1014 * @og.rev 1.0.0 (2016/04/28) 新規追加 1015 * 1016 * @param savePath セーブするパスオブジェクト 1017 * @param lines 行単位の書き込むデータ 1018 * @throws RuntimeException ファイル操作に失敗した場合 1019 * @see #save( Path , List , boolean , Charset ) 1020 */ 1021 public static void save( final Path savePath , final List<String> lines ) { 1022 save( savePath , lines , false , UTF_8 ); // 新規作成 1023 } 1024 1025 /** 1026 * 指定のパスに1行単位の文字列のListを書き込んでいきます。 1027 * 1行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。 1028 * 1029 * 書き込むパスの親フォルダがなければ作成します。 1030 * 1031 * 第2引数は、書き込む行データです。 1032 * 1033 * @og.rev 1.0.0 (2016/04/28) 新規追加 1034 * @og.rev 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処 1035 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成 1036 * 1037 * @param savePath セーブするパスオブジェクト 1038 * @param lines 行単位の書き込むデータ 1039 * @param append trueの場合、ファイルの先頭ではなく最後に書き込まれる。 1040 * @param chset ファイルを読み取るときのCharset 1041 * @throws RuntimeException ファイル操作に失敗した場合 1042 */ 1043 public static void save( final Path savePath , final List<String> lines , final boolean append , final Charset chset ) { 1044 // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している 1045 // ※ toAbsolutePath() する必要はないのと、getParent() は、null を返すことがある 1046// mkdirs( savePath.toAbsolutePath().getParent() ); // savePathはファイルなので、親フォルダを作成する。 1047 final Path parent = savePath.getParent(); 1048 if( parent == null ) { 1049 // MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}] 1050 throw MsgUtil.throwException( "MSG0007" , savePath.toString() ); 1051 } 1052 else { 1053 mkdirs( parent,false ); 1054 } 1055 1056 String line = null; // エラー出力のための変数 1057 int no = 0; 1058 1059 synchronized( STATIC_LOCK ) { 1060 // try-with-resources 文 (AutoCloseable) 1061 try( PrintWriter out = new PrintWriter( Files.newBufferedWriter( savePath, chset , append ? APPEND : CREATE ) ) ) { 1062 for( final String ln : lines ) { 1063 // line = ln ; 1064 // 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処 1065 // 8.5.4.2 (2024/01/12) PMD 7.0.0 UnnecessaryCast 対応 1066// if( !ln.isEmpty() && (int)ln.charAt(0) == (int)'\ufeff' ) { 1067 if( !ln.isEmpty() && ln.charAt(0) == '\ufeff' ) { 1068 line = ln.substring(1); // BOM の削除 : String#replace("\ufeff","") の方が良い? 1069 } 1070 else { 1071 line = ln ; 1072 } 1073 no++; 1074 out.println( line ); 1075 } 1076 out.flush(); 1077 } 1078 catch( final IOException ex ) { 1079 // MSG0017 = ファイルのデータ書き込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2} 1080 throw MsgUtil.throwException( ex , "MSG0017" , savePath , no , line ); 1081 } 1082 } 1083 } 1084 1085 /** 1086 * 指定のパスの最終更新日付を、文字列で返します。 1087 * 文字列のフォーマット指定も可能です。 1088 * 1089 * パスが無い場合や、最終更新日付を、取得できない場合は、現在時刻をベースに返します。 1090 * 1091 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。 1092 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成 1093 * 1094 * @param path 処理対象のPathオブジェクト 1095 * @param format 文字列化する場合のフォーマット(yyyyMMddHHmmss) 1096 * @return 指定のパスの最終更新日付の文字列 1097 */ 1098 public static String timeStamp( final Path path , final String format ) { 1099 long tempTime = 0L; 1100 try { 1101 // 存在チェックを直前に入れますが、厳密には、非同期なので確率の問題です。 1102// if( Files.exists( path ) ) { 1103 if( exists( path ) ) { // 7.2.5.0 (2020/06/01) 1104 synchronized( STATIC_LOCK ) { 1105 tempTime = Files.getLastModifiedTime( path ).toMillis(); 1106 } 1107 } 1108 } 1109 catch( final IOException ex ) { 1110 // MSG0018 = ファイルのタイムスタンプの取得に失敗しました。file=[{0}] 1111// MsgUtil.errPrintln( ex , "MSG0018" , path , ex.getMessage() ); 1112 // MSG0018 = ファイルのタイムスタンプの取得に失敗しました。\n\tfile=[{0}] 1113 LOGGER.warning( ex , "MSG0018" , path ); 1114 } 1115 if( tempTime == 0L ) { 1116 tempTime = System.currentTimeMillis(); // パスが無い場合や、エラー時は、現在時刻を使用 1117 } 1118 1119 return StringUtil.getTimeFormat( tempTime , format ); 1120 } 1121 1122 /** 1123 * ファイルからすべての行を読み取って、文字列のListとして返します。 1124 * 1125 * java.nio.file.Files#readAllLines(Path ) と同等ですが、ファイルが UTF-8 でない場合 1126 * 即座にエラーにするのではなく、Windows-31J でも読み取りを試みます。 1127 * それでもダメな場合は、IOException をスローします。 1128 * 1129 * @og.rev 7.2.5.0 (2020/06/01) Files.readAllLines の代用 1130 * @og.rev 7.3.1.3 (2021/03/09) 読み込み処理全体に、try ~ catch を掛けておきます。 1131 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成 1132 * 1133 * @param path 読み取り対象のPathオブジェクト 1134 * @return Listとしてファイルからの行 1135 * @throws IOException 読み取れない場合エラー 1136 */ 1137 public static List<String> readAllLines( final Path path ) throws IOException { 1138 // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要 1139 1140 // 7.3.1.3 (2021/03/09) 読み込み処理全体に、try ~ catch を掛けておきます。 1141 try { 1142 synchronized( STATIC_LOCK ) { 1143 List<String> rtn ; 1144 try { 1145// return Files.readAllLines( path ); // StandardCharsets.UTF_8 指定と同等。 1146 rtn = Files.readAllLines( path ); // StandardCharsets.UTF_8 指定と同等。 1147 } 1148 catch( final MalformedInputException ex ) { 1149 // MSG0030 = 指定のファイルは、UTF-8でオープン出来なかったため、Windows-31J で再実行します。\n\tfile=[{0}] 1150 LOGGER.warning( "MSG0030" , path ); // Exception は、引数に渡さないでおきます。 1151 1152// return Files.readAllLines( path,WINDOWS_31J ); 1153 rtn = Files.readAllLines( path,WINDOWS_31J ); 1154 } 1155 return rtn; 1156 } 1157 } 1158 catch( final IOException ex ) { 1159 // MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 1160 throw MsgUtil.throwException( ex , "MSG0005" , path ); 1161 } 1162 } 1163 1164 /** 1165 * Pathオブジェクトが存在しているかどうかを判定します。 1166 * 1167 * java.nio.file.Files#exists( Path ) を使用せず、java.io.File.exists() で判定します。 1168 * https://codeday.me/jp/qa/20190302/349168.html 1169 * ネットワークフォルダに存在するファイルの判定において、Files#exists( Path )と 1170 * File.exists() の結果が異なることがあります。 1171 * ここでは、File#exists() を使用して判定します。 1172 * 1173 * @og.rev 7.2.5.0 (2020/06/01) Files.exists の代用 1174 * 1175 * @param path 判定対象のPathオブジェクト 1176 * @return ファイルの存在チェック(あればtrue) 1177 */ 1178 public static boolean exists( final Path path ) { 1179 // return Files.exists( path ); 1180 return path != null && path.toFile().exists(); 1181 } 1182 1183 /** 1184 * Pathオブジェクトのファイル名(getFileName().toString()) を取得します。 1185 * 1186 * Path#getFileName() では、結果が null になる場合もあり、そのままでは、toString() できません。 1187 * また、引数の Path も null チェックが必要なので、それらを簡易的に行います。 1188 * 何らかの結果が、null の場合は、""(空文字列)を返します。 1189 * 1190 * @og.rev 7.2.9.4 (2020/11/20) Path.getFileName().toString() の簡易版 1191 * 1192 * @param path ファイル名取得元のPathオブジェクト(nullも可) 1193 * @return ファイル名(nullの場合は、空文字列) 1194 * @og.rtnNotNull 1195 */ 1196 public static String pathFileName( final Path path ) { 1197 // 対応済み:spotbugs:null になっている可能性があるメソッドの戻り値を利用している 1198// return path == null || path.getFileName() == null ? "" : path.getFileName().toString(); 1199 1200 // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要 1201 String rtn = ""; 1202 1203 if( path != null ) { 1204 final Path fname = path.getFileName(); 1205 if( fname != null ) { 1206// return fname.toString(); 1207 rtn = fname.toString(); 1208 } 1209 } 1210// return "" ; 1211 return rtn; 1212 } 1213}