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}