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