001// Generated by delombok at Sat Feb 25 17:58:34 UTC 2023 002package io.cui.tools.io; 003 004import static java.util.Objects.requireNonNull; 005import java.io.File; 006import java.io.FileInputStream; 007import java.io.IOException; 008import java.io.InputStream; 009import java.nio.file.Files; 010import java.nio.file.Path; 011import java.nio.file.Paths; 012import java.nio.file.StandardCopyOption; 013import java.text.SimpleDateFormat; 014import java.util.Date; 015import io.cui.tools.logging.CuiLogger; 016import lombok.NonNull; 017 018/** 019 * Provides {@link Path} related utilities 020 * 021 * @author Oliver Wolff 022 */ 023public final class MorePaths { 024 private static final CuiLogger log = new CuiLogger(MorePaths.class); 025 /** 026 * ".backup" 027 */ 028 public static final String BACKUP_DIR_NAME = ".backup"; 029 /** 030 * "File or Directory {} is not accessible, reason: {}". 031 */ 032 public static final String MSG_DIRECTORY_NOT_ACCESSIBLE = "File or Directory {} is not accessible, reason: {}"; 033 /** 034 * The prefix to be attached to a backup-file 035 */ 036 public static final String BACKUP_FILE_SUFFIX = ".bck_"; 037 038 /** 039 * Tries to determine the real-path by calling 040 * {@link Path#toRealPath(java.nio.file.LinkOption...)} with no further parameter passed. In 041 * case the real path can not be resolved it will LOG at warn-level and return 042 * {@link Path#toAbsolutePath()}. 043 * 044 * @param path must not be null 045 * @return the real-path if applicable, {@link Path#toAbsolutePath()} otherwise. 046 */ 047 public static Path getRealPathSafely(Path path) { 048 requireNonNull(path, "Path must not be null"); 049 try { 050 return path.toRealPath(); 051 } catch (IOException e) { 052 log.warn("Unable to resolve real path for \'{}\', due to \'{}\'. Returning absolutePath.", path, e.getMessage(), e); 053 return path.toAbsolutePath(); 054 } 055 } 056 057 /** 058 * Tries to determine the real-path, see {@link #getRealPathSafely(Path)} for details and 059 * {@link Paths#get(String, String...)} for details regarding the parameter 060 * 061 * @param first 062 * the path string or initial part of the path string 063 * @param more 064 * additional strings to be joined to form the path string 065 * @return the real-path if applicable, {@link Path#toAbsolutePath()} otherwise. 066 */ 067 public static Path getRealPathSafely(String first, String... more) { 068 return getRealPathSafely(Paths.get(first, more)); 069 } 070 071 /** 072 * Tries to determine the real-path, see {@link #getRealPathSafely(Path)} for details 073 * 074 * @param file the {@link Path} to be looked up 075 * 076 * @return the real-path if applicable, {@link Path#toAbsolutePath()} otherwise. 077 */ 078 public static Path getRealPathSafely(File file) { 079 requireNonNull(file, "File must not be null"); 080 return getRealPathSafely(file.toPath()); 081 } 082 083 /** 084 * Checks whether the given {@link Path} denotes an existing read and 085 * writable directory or file. 086 * 087 * @param path to checked, must not be null 088 * @param checkForDirectory check whether it is a file or directory 089 * @param verbose indicates whether to log errors at warn-level 090 * 091 * @return boolean indicating whether the given {@link Path} denotes an existing read and 092 * writable directory. 093 */ 094 public static boolean checkAccessiblePath(@NonNull final Path path, final boolean checkForDirectory, final boolean verbose) { 095 if (path == null) { 096 throw new java.lang.NullPointerException("path is marked non-null but is null"); 097 } 098 if (!checkReadablePath(path, checkForDirectory, verbose)) { 099 return false; 100 } 101 final var pathFile = path.toFile(); 102 final var absolutePath = pathFile.getAbsolutePath(); 103 if (!pathFile.canWrite()) { 104 if (verbose) { 105 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not Writable"); 106 } 107 return false; 108 } 109 log.debug("{} denotes an existing file / directory with read and write permissions", absolutePath); 110 return true; 111 } 112 113 /** 114 * Checks whether the given {@link Path} denotes an existing readable directory or file. 115 * 116 * @param path to checked, must not be null 117 * @param checkForDirectory check whether it is a file or directory 118 * @param verbose indicates whether to log errors at warn-level 119 * 120 * @return boolean indicating whether the given {@link Path} denotes an existing readable 121 * directory. 122 */ 123 public static boolean checkReadablePath(@NonNull final Path path, final boolean checkForDirectory, final boolean verbose) { 124 if (path == null) { 125 throw new java.lang.NullPointerException("path is marked non-null but is null"); 126 } 127 final var pathFile = path.toFile(); 128 final var absolutePath = pathFile.getAbsolutePath(); 129 if (!pathFile.exists()) { 130 if (verbose) { 131 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not Existing"); 132 } 133 return false; 134 } 135 if (checkForDirectory) { 136 if (!pathFile.isDirectory()) { 137 if (verbose) { 138 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not a directory"); 139 } 140 return false; 141 } 142 } else if (!pathFile.isFile()) { 143 if (verbose) { 144 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not a file"); 145 } 146 return false; 147 } 148 if (!pathFile.canRead()) { 149 if (verbose) { 150 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not Readable"); 151 } 152 return false; 153 } 154 log.debug("{} denotes an existing file / directory with read permissions", absolutePath); 155 return true; 156 } 157 158 /** 159 * Checks whether the given {@link Path} denotes an existing executable file. 160 * 161 * @param path to checked, must not be null 162 * @param verbose indicates whether to log errors at warn-level 163 * 164 * @return boolean indicating whether the given {@link Path} denotes an existing readable 165 * directory. 166 */ 167 public static boolean checkExecutablePath(@NonNull final Path path, final boolean verbose) { 168 if (path == null) { 169 throw new java.lang.NullPointerException("path is marked non-null but is null"); 170 } 171 final var pathFile = path.toFile(); 172 final var absolutePath = pathFile.getAbsolutePath(); 173 if (!pathFile.exists()) { 174 if (verbose) { 175 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not Existing"); 176 } 177 return false; 178 } 179 if (!pathFile.isFile()) { 180 if (verbose) { 181 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not a file"); 182 } 183 return false; 184 } 185 if (!pathFile.canExecute()) { 186 if (verbose) { 187 log.warn(MSG_DIRECTORY_NOT_ACCESSIBLE, absolutePath, "Not Executable"); 188 } 189 return false; 190 } 191 log.debug("{} denotes an existing file / directory with execute permission", absolutePath); 192 return true; 193 } 194 195 /** 196 * Creates / or references a backup-directory named ".backup" within the given directory and 197 * returns it 198 * 199 * @param directory must not null and denote an existing writable directory, otherwise am 200 * {@link IllegalArgumentException} will be thrown. 201 * @return the ".backup" directory 202 */ 203 public static Path getBackupDirectoryForPath(final Path directory) { 204 if (!checkAccessiblePath(directory, true, true)) { 205 throw new IllegalArgumentException(String.format("Given path \'%s\' does not denote an existing writable directory", directory.toFile().getAbsolutePath())); 206 } 207 final var backup = directory.resolve(BACKUP_DIR_NAME); 208 final var backupAsFile = backup.toFile(); 209 if (!backupAsFile.exists() && !backupAsFile.mkdir()) { 210 throw new IllegalStateException(String.format("Unable to create directory \'%s\'", backup.toFile().getAbsolutePath())); 211 } 212 return backup; 213 } 214 215 /** 216 * Backups the file, identified by the given path into the backup directory, derived with 217 * {@link #getBackupDirectoryForPath(Path)}. The original file attributes will be applied to the 218 * copied filed, See {@link StandardCopyOption#COPY_ATTRIBUTES}. 219 * 220 * @param path must not be null and denote an existing read and writable file 221 * @return Path on the newly created file 222 * @throws IOException if an I/O error occurs 223 */ 224 public static Path backupFile(final Path path) throws IOException { 225 assertAccessibleFile(path); 226 var backupDir = getBackupDirectoryForPath(path.getParent()); 227 var backupFile = createNonExistingPath(backupDir, path.getFileName() + BACKUP_FILE_SUFFIX + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); 228 Files.copy(path, backupFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); 229 log.debug("Created backup from \'{}\' at \'{}\'", path.toFile().getAbsolutePath(), backupFile.toFile().getAbsolutePath()); 230 return backupFile; 231 } 232 233 // owolff: See hint Caution: Security-Impact 234 /** 235 * Creates a temp-copy of the given file, identified by the given path. The original file 236 * attributes will be applied to the 237 * copied filed, See {@link StandardCopyOption#COPY_ATTRIBUTES}. 238 * <h2>Caution: Security-Impact</h2> 239 * Creating a temp-file might introduce a security issue. Never ever use this location for 240 * sensitive information that might be of interest for an attacker 241 * 242 * @param path must not be null and denote an existing read and writable file 243 * @return Path on the newly created file 244 * @throws IOException if an I/O error occurs 245 */ 246 @SuppressWarnings("java:S5443") 247 public static Path copyToTempLocation(final Path path) throws IOException { 248 assertAccessibleFile(path); 249 var filename = new StructuredFilename(path.getFileName()); 250 var tempFile = Files.createTempFile(filename.getNamePart(), filename.getSuffix()); 251 Files.copy(path, tempFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); 252 log.debug("Created temp-file from \'{}\' at \'{}\'", path.toFile().getAbsolutePath(), tempFile.toFile().getAbsolutePath()); 253 return tempFile; 254 } 255 256 /** 257 * Asserts whether a given {@link Path} is accessible, saying it exits as a file and is read 258 * and writable. If not it will throw an {@link IllegalArgumentException} 259 * 260 * @param path to be checked, must not be null 261 */ 262 public static void assertAccessibleFile(final Path path) { 263 requireNonNull(path, "path"); 264 if (!checkAccessiblePath(path, false, true)) { 265 throw new IllegalArgumentException(String.format("Given path \'%s\' does not denote an existing readable file", path.toFile().getAbsolutePath())); 266 } 267 } 268 269 static Path createNonExistingPath(final Path parentDir, final String fileName) { 270 var backupFile = parentDir.resolve(fileName); 271 if (!backupFile.toFile().exists()) { 272 return backupFile; 273 } 274 for (var counter = 1; counter < 20; counter++) { 275 var newName = fileName + "_" + counter; 276 var newBackupFile = parentDir.resolve(newName); 277 if (!newBackupFile.toFile().exists()) { 278 return newBackupFile; 279 } 280 } 281 throw new IllegalStateException(String.format("Unable to determine a non-existing file within \'%s\' for file-name \'%s\'", parentDir.toFile().getAbsolutePath(), fileName)); 282 } 283 284 /** 285 * Deletes a file, never throwing an exception. If file is a directory, delete it and all 286 * subdirectories. 287 * Inspired by org.apache.commons.io.FileUtils#deleteQuietly 288 * <p> 289 * The difference between File.delete() and this method are: 290 * <ul> 291 * <li>A directory to be deleted does not have to be empty.</li> 292 * <li>No exceptions are thrown when a file or directory cannot be deleted.</li> 293 * </ul> 294 * 295 * @param path file or directory to delete, can be {@code null} 296 * @return {@code true} if the file or directory was deleted, otherwise 297 * {@code false} 298 */ 299 public static boolean deleteQuietly(final Path path) { 300 log.trace("Deleting file {}", path); 301 if (path == null) { 302 return false; 303 } 304 var file = path.toFile(); 305 final var absolutePath = file.getAbsolutePath(); 306 if (!file.exists()) { 307 log.trace("Path {} does not exist", absolutePath); 308 return false; 309 } 310 var recursiveSucceful = true; 311 try { 312 if (file.isDirectory()) { 313 log.trace("Path {} is directory, checking children", absolutePath); 314 for (String child : file.list()) { 315 if (!deleteQuietly(path.resolve(child))) { 316 recursiveSucceful = false; 317 } 318 } 319 } 320 } catch (final Exception e) { 321 log.trace(e, "Unable to check Path {} whether it is a directory", absolutePath); 322 } 323 try { 324 if (Files.deleteIfExists(path)) { 325 log.trace("Successully deleted path {}", absolutePath); 326 } else { 327 recursiveSucceful = false; 328 } 329 } catch (final Exception e) { 330 log.trace(e, "Unable to delete Path {}", absolutePath); 331 return false; 332 } 333 return recursiveSucceful; 334 } 335 336 /** 337 * Compares the contents of two files to determine if they are equal or not. 338 * <p> 339 * This method checks to see if the two files are different lengths 340 * or if they point to the same file, before resorting to byte-by-byte 341 * comparison of the contents. 342 * <p> 343 * Taken from org.apache.commons.io.FileUtils.contentEquals(File, File) 344 * Code origin: Avalon 345 * 346 * @param path1 the first file 347 * @param path2 the second file 348 * @return true if the content of the files are equal or they both don't 349 * exist, false otherwise 350 * @throws IOException in case of an I/O error 351 */ 352 public static boolean contentEquals(final Path path1, final Path path2) throws IOException { 353 requireNonNull(path1); 354 requireNonNull(path2); 355 var file1 = path1.toFile(); 356 var file2 = path2.toFile(); 357 final var file1Exists = file1.exists(); 358 if (file1Exists != file2.exists()) { 359 return false; 360 } 361 if (!file1Exists) { 362 // two not existing files are equal 363 return true; 364 } 365 if (file1.isDirectory() || file2.isDirectory()) { 366 // don't want to compare directory contents 367 throw new IOException("Can\'t compare directories, only files"); 368 } 369 if (file1.length() != file2.length()) { 370 // lengths differ, cannot be equal 371 return false; 372 } 373 if (file1.getCanonicalFile().equals(file2.getCanonicalFile())) { 374 // same file 375 return true; 376 } 377 try ( 378 InputStream input1 = new FileInputStream(file1); 379 InputStream input2 = new FileInputStream(file2)) { 380 return IOStreams.contentEquals(input1, input2); 381 } 382 } 383 384 385 /** 386 * Command pattern interface delegating the file write operation to its caller. 387 * 388 * @author Sven Haag 389 */ 390 public static interface FileWriteHandler { 391 /** 392 * @param filePath where the write operation should take place on. 393 * @throws IOException if an I/O error occurs 394 */ 395 void write(final Path filePath) throws IOException; 396 } 397 398 /** 399 * Save a file by maintaining all its attributes and permissions. Also creates a backup, see 400 * {@linkplain #backupFile(Path)}. 401 * 402 * <h1>Usage</h1> 403 * <p> 404 * PathUtils.saveAndBackup(myOriginalFilePath, targetPath -> 405 * JdomHelper.writeJdomToFile(document, targetPath)); 406 * </p> 407 * 408 * @param filePath path to the original / target file 409 * @param fileWriteHandler do your write operation to the given file path provided by 410 * {@linkplain FileWriteHandler#write(Path)}. 411 * @throws IOException if an I/O error occurs 412 */ 413 public static void saveAndBackup(final Path filePath, final FileWriteHandler fileWriteHandler) throws IOException { 414 // Copy original file to temp 415 final var temp = copyToTempLocation(filePath); 416 // Save data to temp file 417 fileWriteHandler.write(temp); 418 // Create backup from original 419 backupFile(filePath); 420 // Replace original with temp file 421 java.nio.file.Files.copy(temp, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); 422 } 423 424 /** 425 * Checks, if the two given paths are pointing to the same location. 426 * <p> 427 * If both paths are not {@code null} and do {@link File#exists()}, the 428 * {@link Files#isSameFile(Path, Path)} 429 * method is used to check if both paths are pointing to the same location. 430 * Otherwise, if one of the paths does not exist, the {@link Paths#equals(Object)} method is 431 * used. 432 * 433 * @param path to be compared with path2 434 * @param path2 to be compared with path 435 * 436 * @return {@code true}, if both paths are {@code null}. 437 * {@code true}, if both paths not {@code null}, do exist, and 438 * {@link Files#isSameFile(Path, Path)}. 439 * {@code true}, if both paths not {@code null} and {@link Paths#equals(Object)} 440 * {@code false} otherwise. 441 */ 442 public static boolean isSameFile(Path path, Path path2) { 443 if (null == path && null == path2) { 444 return true; 445 } 446 if (null != path && null != path2) { 447 if (!path.toFile().exists() || !path2.toFile().exists()) { 448 log.debug("Comparing paths with #equals, as at least one path does not exist. " + "path_a={}, path_b={}", path, path2); 449 return path.equals(path2); 450 } 451 try { 452 return Files.isSameFile(path, path2); 453 } catch (final IOException e) { 454 log.error(e, "Portal-123: Unable to compare path_a={} and path_b={}", path, path2); 455 } 456 } else { 457 log.trace("at least one path is null: path_a={}, path_b={}", path, path2); 458 } 459 return false; 460 } 461 462 @java.lang.SuppressWarnings("all") 463 @lombok.Generated 464 private MorePaths() { 465 throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated"); 466 } 467}