diff --git a/CHANGELOG.md b/CHANGELOG.md index a3b246a..328406e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +--- +## [0.0.5] - Unreleased + +### Added +- New path predicates: `hasParentMatching`, `hasAncestorMatching`, `hasDirectChildMatching`, `hasDescendantMatching`, `hasSiblingMatching`, `hasFullPathMatchingGlob`, `hasFullPathMatching`, `hasNameMatchingGlob` + +### Changed +- `PathUtils` removed, `PathPredicates`rework + --- ## [0.0.4] - 2025-09-27 diff --git a/README.md b/README.md index 9c6cedc..f30e5ee 100644 --- a/README.md +++ b/README.md @@ -178,13 +178,14 @@ child_limit_static/ Or you can also set a limitation function, to dynamically choose the number of children displayed in each directory. It avoids cluttering the whole console with known large folders (e.g. `node_modules`) but continue to pretty print normally other folders. -Use the `ChildLimitBuilder` and `PathPredicates` classes to help you build the limit function that fits your needs.. +Use the `ChildLimitBuilder` and `PathPredicates` classes to help you build the limit function that fits your needs. ```java // Example: ChildLimitDynamic.java +var isNodeModulePredicate = PathPredicates.builder().hasName("node_modules").build(); var childLimit = ChildLimitBuilder.builder() .defaultLimit(ChildLimitBuilder.UNLIMITED) - .limit(PathPredicates.hasName("node_modules"), 0) + .limit(isNodeModulePredicate, 0) .build(); var prettyPrinter = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withChildLimit(childLimit)) @@ -278,12 +279,13 @@ Files and directories can be selectively included or excluded using a custom `Pr Filtering is **recursive by default**: directory's contents will always be traversed. However, if a directory does not match and none of its children match, the directory itself will not be displayed. -The `PathPredicates` class provides several ready-to-use `Predicate` implementations for common cases, as well as a builder for creating more advanced predicates. +The `PathPredicates` class provides several ready-to-use methods for creating common predicates, as well as a builder for creating more advanced predicates. ```java // Example: Filtering.java +var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build(); var prettyPrinter = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.filter(PathPredicates.hasExtension("java"))) + .customizeOptions(options -> options.filter(hasJavaExtensionPredicate)) .build(); ``` ``` @@ -308,16 +310,16 @@ If the function returns `null`, nothing is added. ```java // Example: LineExtension.java Function lineExtension = path -> { - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "api")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/api")) { return "\t\t\t// All API code: controllers, etc."; } - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "domain")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/domain")) { return "\t\t\t// All domain code: value objects, etc."; } - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "infra")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/infra")) { return "\t\t\t// All infra code: database, email service, etc."; } - if (PathUtils.isFile(path) && PathUtils.hasName(path, "application.properties")) { + if (PathPredicates.hasNameMatchingGlob(path, "*.properties")) { return "\t// Config file"; } return null; diff --git a/ROADMAP.md b/ROADMAP.md index 12266be..a3f3cd8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -21,7 +21,7 @@ - [x] Option: Line extension (=additional text after the file name) ## To do -- [ ] More `PathPredicates` functions! +- [x] More `PathPredicates` functions! - [ ] Option: custom emojis ## Backlog / To analyze / To implement if requested diff --git a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java index 1d7d7f6..d7483e9 100644 --- a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java +++ b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java @@ -7,9 +7,10 @@ public class ChildLimitDynamic { public static void main(String[] args) { + var isNodeModulePredicate = PathPredicates.builder().hasName("node_modules").build(); var childLimit = ChildLimitBuilder.builder() .defaultLimit(ChildLimitBuilder.UNLIMITED) - .limit(PathPredicates.hasName("node_modules"), 0) + .limit(isNodeModulePredicate, 0) .build(); var prettyPrinter = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withChildLimit(childLimit)) diff --git a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Filtering.java b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Filtering.java index 860e474..81d9699 100644 --- a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Filtering.java +++ b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Filtering.java @@ -6,8 +6,9 @@ public class Filtering { public static void main(String[] args) { + var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build(); var prettyPrinter = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.filter(PathPredicates.hasExtension("java"))) + .customizeOptions(options -> options.filter(hasJavaExtensionPredicate)) .build(); var tree = prettyPrinter.prettyPrint("src/example/resources/filtering"); diff --git a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java index 99795d2..c9b3f2e 100644 --- a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java +++ b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java @@ -1,7 +1,7 @@ package io.github.computerdaddyguy.jfiletreeprettyprinter.example; import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter; -import io.github.computerdaddyguy.jfiletreeprettyprinter.PathUtils; +import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates; import java.nio.file.Path; import java.util.function.Function; @@ -9,16 +9,16 @@ public class LineExtension { public static void main(String[] args) { Function lineExtension = path -> { - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "api")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/api")) { return "\t\t\t// All API code: controllers, etc."; } - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "domain")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/domain")) { return "\t\t\t// All domain code: value objects, etc."; } - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "infra")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/infra")) { return "\t\t\t// All infra code: database, email service, etc."; } - if (PathUtils.isFile(path) && PathUtils.hasName(path, "application.properties")) { + if (PathPredicates.hasNameMatchingGlob(path, "*.properties")) { return "\t// Config file"; } return null; diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java index afd2723..f66f26b 100644 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java @@ -26,8 +26,8 @@ *
{@code
  * var childLimit = ChildLimitBuilder.builder()
  *     .defaultLimit(ChildLimit.UNLIMITED)   // unlimited unless specified
- *     .limit(path -> PathUtils.hasName("bigDir"), 10)  // max 10 children in "bigDir"
- *     .limit(path -> PathUtils.hasName("emptyDir"), 0) // disallow children in "emptyDir"
+ *     .limit(path -> PathPredicates.hasName(path, "bigDir"), 10)  // max 10 children in "bigDir"
+ *     .limit(path -> PathPredicates.hasName(path, "emptyDir"), 0) // disallow children in "emptyDir"
  *     .build();
  *
  * }
diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilder.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilder.java index 66d03d1..08d903b 100644 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilder.java +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilder.java @@ -2,6 +2,7 @@ import java.io.File; import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.util.Objects; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -75,6 +76,32 @@ public PathPredicateBuilder fileTest(Predicate predicate) { return pathTest(path -> predicate.test(path.toFile())); } + // ---------- PathMatcher ---------- + + /** + * Adds a condition that tests whether the path matches the specified glob pattern. + * + * @param glob the glob pattern to match; must not be {@code null} + * + * @return this builder for chaining + */ + public PathPredicateBuilder hasFullPathMatchingGlob(String glob) { + Objects.requireNonNull(glob, "glob is null"); + return pathTest(path -> PathPredicates.hasFullPathMatchingGlob(path, glob)); + } + + /** + * Adds a condition that tests whether the path matches the provided {@link PathMatcher}. + * + * @param matcher the {@code PathMatcher} to use; must not be {@code null} + * + * @return this builder for chaining + */ + public PathPredicateBuilder hasFullPathMatching(PathMatcher matcher) { + Objects.requireNonNull(matcher, "matcher is null"); + return pathTest(path -> PathPredicates.hasFullPathMatching(path, matcher)); + } + // ---------- Name ---------- /** @@ -86,7 +113,8 @@ public PathPredicateBuilder fileTest(Predicate predicate) { * @return this builder for chaining */ public PathPredicateBuilder hasName(String name) { - return pathTest(PathPredicates.hasName(name)); + Objects.requireNonNull(name, "name is null"); + return pathTest(path -> PathPredicates.hasName(path, name)); } /** @@ -98,7 +126,8 @@ public PathPredicateBuilder hasName(String name) { * @return this builder for chaining */ public PathPredicateBuilder hasNameIgnoreCase(String name) { - return pathTest(PathPredicates.hasNameIgnoreCase(name)); + Objects.requireNonNull(name, "name is null"); + return pathTest(path -> PathPredicates.hasNameIgnoreCase(path, name)); } /** @@ -109,7 +138,28 @@ public PathPredicateBuilder hasNameIgnoreCase(String name) { * @return this builder for chaining */ public PathPredicateBuilder hasNameMatching(Pattern pattern) { - return pathTest(PathPredicates.hasNameMatching(pattern)); + Objects.requireNonNull(pattern, "pattern is null"); + return pathTest(path -> PathPredicates.hasNameMatching(path, pattern)); + } + + /** + * Adds a condition that tests whether the file name of the given path + * matches the specified glob pattern. + * + *

Note: Only the file name (the last element of the path) is tested, + * not the entire path. For example, {@code "*.txt"} will match {@code "file.txt"}. + * + *

The glob syntax follows {@link java.nio.file.FileSystem#getPathMatcher(String)} conventions. + * + * @param glob the glob pattern to match against the file name; must not be {@code null} + * + * @return this builder for chaining + * + * @see #hasFullPathMatchingGlob(String) + */ + public PathPredicateBuilder hasNameMatchingGlob(String glob) { + Objects.requireNonNull(glob, "glob is null"); + return pathTest(path -> PathPredicates.hasNameMatchingGlob(path, glob)); } /** @@ -120,7 +170,8 @@ public PathPredicateBuilder hasNameMatching(Pattern pattern) { * @return this builder for chaining */ public PathPredicateBuilder hasNameEndingWith(String suffix) { - return pathTest(PathPredicates.hasNameEndingWith(suffix)); + Objects.requireNonNull(suffix, "suffix is null"); + return pathTest(path -> PathPredicates.hasNameEndingWith(path, suffix)); } /** @@ -135,7 +186,8 @@ public PathPredicateBuilder hasNameEndingWith(String suffix) { * @return this builder for chaining */ public PathPredicateBuilder hasExtension(String extension) { - return pathTest(PathPredicates.hasExtension(extension)); + Objects.requireNonNull(extension, "extension is null"); + return pathTest(path -> PathPredicates.hasExtension(path, extension)); } // ---------- Type ---------- @@ -146,7 +198,7 @@ public PathPredicateBuilder hasExtension(String extension) { * @return this builder for chaining */ public PathPredicateBuilder isDirectory() { - return pathTest(PathPredicates.isDirectory()); + return pathTest(PathPredicates::isDirectory); } /** @@ -155,7 +207,91 @@ public PathPredicateBuilder isDirectory() { * @return this builder for chaining */ public PathPredicateBuilder isFile() { - return pathTest(PathPredicates.isFile()); + return pathTest(PathPredicates::isFile); + } + + // ---------- Hierarchy ---------- + + /** + * Adds a condition that tests the direct parent of the path. + * + * @param parentPredicate the predicate to apply on the direct parent + * + * @return this builder for chaining + * + * @see PathPredicates#hasParentMatching(Predicate) + */ + public PathPredicateBuilder hasParentMatching(Predicate parentPredicate) { + Objects.requireNonNull(parentPredicate, "parentPredicate is null"); + return pathTest(path -> PathPredicates.hasParentMatching(path, parentPredicate)); + } + + /** + * Adds a condition that tests all ancestors of the path (stopping at the first match). + * + * The condition is satisfied if the given {@code ancestorPredicate} evaluates + * to {@code true} for any ancestor in the {@link Path#getParent()} chain. + * + * @param ancestorPredicate the predicate to apply on each ancestor + * + * @return this builder for chaining + * + * @see PathPredicates#hasAncestorMatching(Predicate) + */ + public PathPredicateBuilder hasAncestorMatching(Predicate ancestorPredicate) { + Objects.requireNonNull(ancestorPredicate, "ancestorPredicate is null"); + return pathTest(path -> PathPredicates.hasAncestorMatching(path, ancestorPredicate)); + } + + /** + * Adds a condition that tests the direct children of the path. + * + * The condition is satisfied if the given {@code childPredicate} evaluates + * to {@code true} for at least one direct child of the tested path. + * + * @param childPredicate the predicate to apply on each direct child + * + * @return this builder for chaining + * + * @see PathPredicates#hasDirectChildMatching(Predicate) + */ + public PathPredicateBuilder hasDirectChildMatching(Predicate childPredicate) { + Objects.requireNonNull(childPredicate, "childPredicate is null"); + return pathTest(path -> PathPredicates.hasDirectChildMatching(path, childPredicate)); + } + + /** + * Adds a condition that tests all descendants of the path (children at any depth). + * + * The condition is satisfied if the given {@code descendantPredicate} evaluates + * to {@code true} for at least one descendant in the directory tree. + * + * @param descendantPredicate the predicate to apply on each descendant + * + * @return this builder for chaining + * + * @see PathPredicates#hasDescendantMatching(Predicate) + */ + public PathPredicateBuilder hasDescendantMatching(Predicate descendantPredicate) { + Objects.requireNonNull(descendantPredicate, "descendantPredicate is null"); + return pathTest(path -> PathPredicates.hasDescendantMatching(path, descendantPredicate)); + } + + /** + * Adds a condition that tests the siblings of the path. + * + * The condition is satisfied if the given {@code siblingPredicate} evaluates + * to {@code true} for at least one sibling of the tested path. + * + * @param siblingPredicate the predicate to apply on each sibling + * + * @return this builder for chaining + * + * @see PathPredicates#hasSiblingMatching(Predicate) + */ + public PathPredicateBuilder hasSiblingMatching(Predicate siblingPredicate) { + Objects.requireNonNull(siblingPredicate, "siblingPredicate is null"); + return pathTest(path -> PathPredicates.hasSiblingMatching(path, siblingPredicate)); } } \ No newline at end of file diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicates.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicates.java index 3048a37..bf6f4e5 100644 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicates.java +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicates.java @@ -1,8 +1,15 @@ package io.github.computerdaddyguy.jfiletreeprettyprinter; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.Objects; import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.stream.Stream; import org.jspecify.annotations.NullMarked; /** @@ -33,89 +40,340 @@ public static PathPredicateBuilder builder() { return new PathPredicateBuilder(); } - // ---------- Name ---------- + // ---------- PathMatcher ---------- + + /** + * Tests whether the given {@link Path} matches the specified glob pattern. + * + *

The glob syntax follows {@link java.nio.file.FileSystem#getPathMatcher(String)} conventions. + * + * @param path the path to test; must not be {@code null} + * @param glob the glob pattern; must not be {@code null} + * + * @return {@code true} if the path matches the glob pattern, {@code false} otherwise + * + * @throws NullPointerException if {@code path} or {@code glob} is {@code null} + * + * @see #hasNameMatchingGlob(Path, String) + * @see #hasFullPathMatching(Path, PathMatcher) + */ + public static boolean hasFullPathMatchingGlob(Path path, String glob) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(glob, "glob is null"); + + // From Files.newDirectoryStream(Path dir, String glob) + if (glob.equals("*")) { + return true; + } + var matcher = path.getFileSystem().getPathMatcher("glob:" + glob); + return matcher.matches(path); + } /** - * Creates a predicate that tests whether the path has the specified file name + * Checks if the given {@link Path} matches the provided {@link PathMatcher}. * + * @param path the path to test; must not be {@code null} + * @param matcher the {@code PathMatcher} to use; must not be {@code null} + * + * @return {@code true} if the path matches the matcher, {@code false} otherwise + * + * @throws NullPointerException if {@code path} or {@code matcher} is {@code null} + * + * @see #hasFullPathMatchingGlob(Path, String) + */ + public static boolean hasFullPathMatching(Path path, PathMatcher matcher) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(matcher, "matcher is null"); + return matcher.matches(path); + } + + // ---------- Name ---------- + + /** + * Tests whether the given path has exactly the specified file name. + * + * @param path the path to test * @param name the expected file name (without parent directories) * - * @return a predicate testing for file names matching the given name + * @return {@code true} if the path's file name equals {@code name} + * + * @throws NullPointerException if {@code path} or {@code name} is {@code null} */ - public static Predicate hasName(String name) { - return path -> PathUtils.hasName(path, name); + public static boolean hasName(Path path, String name) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(name, "name is null"); + return path.getFileName().toString().equals(name); } /** - * Creates a predicate that tests whether the path has the specified - * file name, ignoring case. + * Tests whether the given path has the specified file name, + * ignoring case. * - * @param name the expected file name (without parent directories), case-insensitive + * @param path the path to test + * @param name the expected file name (case-insensitive) + * + * @return {@code true} if the path's file name equals {@code name}, ignoring case * - * @return a predicate testing for file names matching the given name, case-insensitive + * @throws NullPointerException if {@code path} or {@code name} is {@code null} */ - public static Predicate hasNameIgnoreCase(String name) { - return path -> PathUtils.hasNameIgnoreCase(path, name); + public static boolean hasNameIgnoreCase(Path path, String name) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(name, "name is null"); + return path.getFileName().toString().equalsIgnoreCase(name); } /** - * Creates a predicate that tests whether a path's file name matches - * the provided pattern. + * Tests whether the given path's file name matches the provided pattern. * + * @param path the path to test * @param pattern the regex pattern to apply to the file name * - * @return a predicate testing for file names matching the pattern + * @return {@code true} if the file name matches the pattern + * + * @throws NullPointerException if {@code path} or {@code pattern} is {@code null} + */ + public static boolean hasNameMatching(Path path, Pattern pattern) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(pattern, "pattern is null"); + return pattern.matcher(path.getFileName().toString()).matches(); + } + + /** + * Tests whether the given path's file name matches the provided glob. + * + *

The glob syntax follows {@link java.nio.file.FileSystem#getPathMatcher(String)} conventions. + * + * @param path the path to test + * @param glob the glob pattern to match against the file name; must not be {@code null} + * + * @return {@code true} if the file name matches the glob + * + * @throws NullPointerException if {@code path} or {@code glob} is {@code null} + * + * @see #hasFullPathMatchingGlob(Path, String) */ - public static Predicate hasNameMatching(Pattern pattern) { - return path -> PathUtils.hasNameMatching(path, pattern); + public static boolean hasNameMatchingGlob(Path path, String glob) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(glob, "glob is null"); + return hasFullPathMatchingGlob(path.getFileName(), glob); } /** - * Creates a predicate that tests whether a path's file name ends - * with the specified suffix. + * Tests whether the given path's file name ends with the specified suffix. * + * @param path the path to test * @param suffix the suffix to test (e.g. ".log", ".txt") * - * @return a predicate testing for file names ending with the given suffix + * @return {@code true} if the file name ends with the given suffix + * + * @throws NullPointerException if {@code path} or {@code suffix} is {@code null} */ - public static Predicate hasNameEndingWith(String suffix) { - return path -> PathUtils.hasNameEndingWith(path, suffix); + public static boolean hasNameEndingWith(Path path, String suffix) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(suffix, "suffix is null"); + return path.getFileName().toString().endsWith(suffix); } /** - * Creates a predicate that tests whether a path's file name has - * the specified extension. + * Tests whether the given path's file name has the specified extension. *

* The extension should be provided without a leading dot, e.g. * {@code "txt"} or {@code "pdf"}. *

* + * @param path the path to test * @param extension the extension to test (without the dot) * - * @return a predicate testing for file names with the given extension + * @return {@code true} if the file name ends with {@code "." + extension} + * + * @throws NullPointerException if {@code path} or {@code extension} is {@code null} */ - public static Predicate hasExtension(String extension) { - return path -> PathUtils.hasExtension(path, extension); + public static boolean hasExtension(Path path, String extension) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(extension, "extension is null"); + return hasNameEndingWith(path, "." + extension); } // ---------- Type ---------- /** - * Creates a predicate that tests whether the path represents a directory. + * Tests whether the given path represents a directory. + * + * @param path the path to test + * + * @return {@code true} if the path is a directory + * + * @throws NullPointerException if {@code path} is {@code null} + */ + public static boolean isDirectory(Path path) { + Objects.requireNonNull(path, "path is null"); + return path.toFile().isDirectory(); + } + + /** + * Tests whether the given path represents a file. + * + * @param path the path to test + * + * @return {@code true} if the path is a file + * + * @throws NullPointerException if {@code path} is {@code null} + */ + public static boolean isFile(Path path) { + Objects.requireNonNull(path, "path is null"); + return path.toFile().isFile(); + } + + // ---------- Hierarchy ---------- + + /** + * Tests whether the direct parent of the given path matches the provided predicate. + * + * @param path the path whose parent should be tested (must not be {@code null}) + * @param parentPredicate the predicate to apply to the direct parent (must not be {@code null}) + * + * @return {@code true} if the path has a parent and that parent matches the predicate, + * {@code false} otherwise * - * @return a predicate testing for files to represent a directory + * @throws NullPointerException if {@code path} or {@code parentPredicate} is {@code null} */ - public static Predicate isDirectory() { - return PathUtils::isDirectory; + public static boolean hasParentMatching(Path path, Predicate parentPredicate) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(parentPredicate, "parentPredicate is null"); + + if (path.getParent() == null) { + return false; + } + return parentPredicate.test(path.getParent()); + } + + /** + * Tests whether any ancestor of the given path matches the provided predicate. + * + *

The test is applied recursively up the parent chain (using {@link Path#getParent()}) + * until the root is reached or the predicate returns {@code true}. + * + * @param path the path whose ancestors should be tested (must not be {@code null}) + * @param ancestorPredicate the predicate to apply to each ancestor (must not be {@code null}) + * + * @return {@code true} if any ancestor of the path matches the predicate, + * {@code false} otherwise + * + * @throws NullPointerException if {@code path} or {@code ancestorPredicate} is {@code null} + */ + public static boolean hasAncestorMatching(Path path, Predicate ancestorPredicate) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(ancestorPredicate, "ancestorPredicate is null"); + + Path parent = path.getParent(); + while (parent != null) { + if (ancestorPredicate.test(parent)) { + return true; + } + parent = parent.getParent(); + } + return false; } /** - * Creates a predicate that tests whether the path represents a file. + * Tests whether the given path has at least one direct child that matches the provided predicate. + * + *

The method checks only immediate children of the path (not recursive). + * If the path is not a directory, the result is always {@code false}. + * + * @param path the directory whose direct children should be tested (must not be {@code null}) + * @param childPredicate the predicate to apply to each direct child (must not be {@code null}) + * + * @return {@code true} if any direct child matches the predicate, + * {@code false} if none match, or if the path is not a directory + * + * @throws NullPointerException if {@code path} or {@code childPredicate} is {@code null} + */ + public static boolean hasDirectChildMatching(Path path, Predicate childPredicate) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(childPredicate, "childPredicate is null"); + + File file = path.toFile(); + if (!file.isDirectory()) { + return false; + } + File[] children = file.listFiles(); + if (children == null) { // Only if I/O error occurred when listing files + return false; + } + for (File child : children) { + if (childPredicate.test(child.toPath())) { + return true; + } + } + return false; + } + + /** + * Tests whether the given path has at least one descendant (child, grandchild, etc.) + * that matches the provided predicate. + * + *

The method walks the file tree starting at the given path, excluding the path itself, + * and applies the predicate to all discovered descendants. + * If the path is not a directory, the result is always {@code false}. + * + * @param path the directory whose descendants should be tested (must not be {@code null}) + * @param descendantPredicate the predicate to apply to each descendant (must not be {@code null}) + * + * @return {@code true} if any descendant matches the predicate, + * {@code false} if none match or if the path is not a directory + * + * @throws NullPointerException if {@code path} or {@code descendantPredicate} is {@code null} + * @throws UncheckedIOException if an I/O error occurs while traversing the directory + */ + public static boolean hasDescendantMatching(Path path, Predicate descendantPredicate) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(descendantPredicate, "descendantPredicate is null"); + File file = path.toFile(); + if (!file.isDirectory()) { + return false; + } + try (Stream stream = Files.walk(path)) { + return stream + .skip(1) // skip the root path itself + .anyMatch(descendantPredicate); + } catch (IOException e) { + throw new UncheckedIOException("Exception while walking files of " + path, e); + } + } + + /** + * Tests whether the given path has at least one sibling that matches the provided predicate. + * + *

The siblings are the other entries in the same parent directory. + * The path itself is excluded from testing. If the path has no parent, + * the result is always {@code false}. + * + * @param path the path whose siblings should be tested (must not be {@code null}) + * @param siblingPredicate the predicate to apply to each sibling (must not be {@code null}) + * + * @return {@code true} if any sibling matches the predicate, + * {@code false} if none match or if the path has no parent * - * @return a predicate testing for files to represent a file + * @throws NullPointerException if {@code path} or {@code siblingPredicate} is {@code null} */ - public static Predicate isFile() { - return PathUtils::isFile; + public static boolean hasSiblingMatching(Path path, Predicate siblingPredicate) { + Objects.requireNonNull(path, "path is null"); + Objects.requireNonNull(siblingPredicate, "siblingPredicate is null"); + Path parent = path.getParent(); + if (parent == null) { + return false; + } + File[] siblings = parent.toFile().listFiles(); + if (siblings == null) { // Only if I/O error occurred when listing files + return false; + } + for (File sibling : siblings) { + if (!sibling.toPath().equals(path) && siblingPredicate.test(sibling.toPath())) { + return true; + } + } + return false; } } diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathUtils.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathUtils.java deleted file mode 100644 index 74ed7ef..0000000 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathUtils.java +++ /dev/null @@ -1,113 +0,0 @@ -package io.github.computerdaddyguy.jfiletreeprettyprinter; - -import java.nio.file.Path; -import java.util.regex.Pattern; - -/** - * Utility class providing common {@link Predicate} implementations - * and helper methods for testing files and directories. - *

- * All methods are {@code static} and can be used directly or wrapped into - * a {@link Predicate} for filtering paths. - *

- * - * This class is not instantiable. - */ -public final class PathUtils { - - private PathUtils() { - // Helper class - } - - // ---------- Name ---------- - - /** - * Tests whether the given path has exactly the specified file name. - * - * @param path the path to test - * @param name the expected file name (without parent directories) - * - * @return {@code true} if the path's file name equals {@code name} - */ - public static boolean hasName(Path path, String name) { - return path.getFileName().toString().equals(name); - } - - /** - * Tests whether the given path has the specified file name, - * ignoring case. - * - * @param path the path to test - * @param name the expected file name (case-insensitive) - * - * @return {@code true} if the path's file name equals {@code name}, ignoring case - */ - public static boolean hasNameIgnoreCase(Path path, String name) { - return path.getFileName().toString().equalsIgnoreCase(name); - } - - /** - * Tests whether the given path's file name matches the provided pattern. - * - * @param path the path to test - * @param pattern the regex pattern to apply to the file name - * - * @return {@code true} if the file name matches the pattern - */ - public static boolean hasNameMatching(Path path, Pattern pattern) { - return pattern.matcher(path.getFileName().toString()).matches(); - } - - /** - * Tests whether the given path's file name ends with the specified suffix. - * - * @param path the path to test - * @param suffix the suffix to test (e.g. ".log", ".txt") - * - * @return {@code true} if the file name ends with the given suffix - */ - public static boolean hasNameEndingWith(Path path, String suffix) { - return path.getFileName().toString().endsWith(suffix); - } - - /** - * Tests whether the given path's file name has the specified extension. - *

- * The extension should be provided without a leading dot, e.g. - * {@code "txt"} or {@code "pdf"}. - *

- * - * @param path the path to test - * @param extension the extension to test (without the dot) - * - * @return {@code true} if the file name ends with {@code "." + extension} - */ - public static boolean hasExtension(Path path, String extension) { - return hasNameEndingWith(path, "." + extension); - } - - // ---------- Type ---------- - - /** - * Tests whether the given path represents a directory. - * - * @param path the path to test - * - * @return {@code true} if the path is a directory - */ - public static boolean isDirectory(Path path) { - return path.toFile().isDirectory(); - } - - /** - * Tests whether the given path represents a file. - * - * @param path the path to test - * - * @return {@code true} if the path is a file - */ - public static boolean isFile(Path path) { - return path.toFile().isFile(); - } - -} diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner/DefaultPathToTreeScanner.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner/DefaultPathToTreeScanner.java index d4aa425..f903a6c 100644 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner/DefaultPathToTreeScanner.java +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner/DefaultPathToTreeScanner.java @@ -124,7 +124,7 @@ private List handleLeftOverChildren(int depth, Iterator pathIte private Iterator directoryStreamToIterator(DirectoryStream childrenStream, @Nullable Predicate filter) { var stream = StreamSupport.stream(childrenStream.spliterator(), false); if (filter != null) { - var recursiveFilter = PathPredicates.isDirectory().or(filter); + var recursiveFilter = PathPredicates.builder().isDirectory().build().or(filter); stream = stream.filter(recursiveFilter); } return stream diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java index d4cc058..d4ac90a 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java @@ -17,8 +17,8 @@ class ChildLimitDynamicTest { .customizeOptions( // @formatter:off options -> options.withChildLimit( - p -> PathUtils.hasName(p, "limit_1") ? 1 : - PathUtils.hasName(p, "limit_3") ? 3 : + p -> PathPredicates.hasName(p, "limit_1") ? 1 : + PathPredicates.hasName(p, "limit_3") ? 3 : -1 ) // @formatter:on diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterTest.java index c6b003c..f257345 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterTest.java @@ -26,7 +26,7 @@ void prettyPrintWithFilter_by_path_and_string_are_same() { var path = FileStructures.simpleDirectoryWithFilesAndFolders(root, 3, 3); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.filter(PathPredicates.isFile())) + .customizeOptions(options -> options.filter(PathPredicates::isFile)) .build(); assertThat(printer.prettyPrint(path)).isEqualTo(printer.prettyPrint(path.toString())); diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java index 91ab0bd..1a46e3b 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java @@ -10,7 +10,7 @@ class FilteringTest { @Test void example() { - var filter = PathPredicates.hasExtension("java"); + var filter = PathPredicates.builder().hasExtension("java").build(); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.filter(filter)) .build(); @@ -32,7 +32,7 @@ void example() { @Test void example_dir_match() { - var filter = PathPredicates.hasNameEndingWith("no_java_file"); + var filter = PathPredicates.builder().hasNameEndingWith("no_java_file").build(); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.filter(filter)) .build(); @@ -49,7 +49,7 @@ void example_dir_match() { @Test void example_and_sorting() { - var filter = PathPredicates.hasExtension("java"); + var filter = PathPredicates.builder().hasExtension("java").build(); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.sort(Sorts.BY_NAME.reversed())) .customizeOptions(options -> options.filter(filter)) @@ -72,7 +72,7 @@ void example_and_sorting() { @Test void example_childLimit_1() { - var filter = PathPredicates.hasExtension("java"); + var filter = PathPredicates.builder().hasExtension("java").build(); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withChildLimit(1)) .customizeOptions(options -> options.filter(filter)) @@ -91,7 +91,7 @@ void example_childLimit_1() { @Test void example_childLimit_2() { - var filter = PathPredicates.hasExtension("java"); + var filter = PathPredicates.builder().hasExtension("java").build(); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withChildLimit(2)) .customizeOptions(options -> options.filter(filter)) @@ -114,7 +114,7 @@ void example_childLimit_2() { @Test void example_childLimit_3() { - var filter = PathPredicates.hasExtension("java"); + var filter = PathPredicates.builder().hasExtension("java").build(); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withChildLimit(3)) .customizeOptions(options -> options.filter(filter)) @@ -137,7 +137,7 @@ void example_childLimit_3() { @Test void example_compact_dir() { - var filter = PathPredicates.hasExtension("java"); + var filter = PathPredicates.builder().hasExtension("java").build(); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withCompactDirectories(true)) .customizeOptions(options -> options.filter(filter)) diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java index 029ae29..7010040 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java @@ -30,16 +30,16 @@ void emptyDir() { void example_dir_match() { Function lineExtension = path -> { - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "api")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/api")) { return "\t\t\t// All API code: controllers, etc."; } - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "domain")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/domain")) { return "\t\t\t// All domain code: value objects, etc."; } - if (PathUtils.isDirectory(path) && PathUtils.hasName(path, "infra")) { + if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/infra")) { return "\t\t\t// All infra code: database, email service, etc."; } - if (PathUtils.isFile(path) && PathUtils.hasName(path, "application.properties")) { + if (PathPredicates.hasNameMatchingGlob(path, "*.properties")) { return "\t// Config file"; } return null; @@ -70,7 +70,7 @@ void example_dir_match() { void compact_dir_first_dir() { Function lineExtension = p -> { - if (PathUtils.hasName(p, "dirA")) { + if (PathPredicates.hasName(p, "dirA")) { return " // 1"; } return null; @@ -92,7 +92,7 @@ void compact_dir_first_dir() { void compact_dir_middle_dir() { Function lineExtension = p -> { - if (PathUtils.hasName(p, "dirB")) { + if (PathPredicates.hasName(p, "dirB")) { return " // 2"; } return null; @@ -114,7 +114,7 @@ void compact_dir_middle_dir() { void compact_dir_last_dir() { Function lineExtension = p -> { - if (PathUtils.hasName(p, "dirC")) { + if (PathPredicates.hasName(p, "dirC")) { return " // 3"; } return null; diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilderTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilderTest.java index 62ecfcd..dba75d4 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilderTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathPredicateBuilderTest.java @@ -1,10 +1,13 @@ package io.github.computerdaddyguy.jfiletreeprettyprinter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.regex.Pattern; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -43,134 +46,603 @@ void noPredicate_then_nulll() { assertThat(filter).isNull(); } - @Test - void pathTest_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().pathTest(p -> p.equals(path)).build(); - assertThat(filter.test(path)).isTrue(); + @Nested + class PathTest { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().pathTest(p -> p.equals(path)).build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().pathTest(p -> p.equals(new Object())).build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void pathTest_noMatch() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().pathTest(p -> p.equals(new Object())).build(); - assertThat(filter.test(path)).isFalse(); + @Nested + class FileTest { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().fileTest(p -> p.equals(path.toFile())).build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().fileTest(p -> p.equals(new Object())).build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void fileTest_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().fileTest(p -> p.equals(path.toFile())).build(); - assertThat(filter.test(path)).isTrue(); + // ---------- PathMatcher ---------- + + @Nested + class HasFullPathMatchingGlob { + + @Test + void match_wildcard() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasFullPathMatchingGlob("*").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasFullPathMatchingGlob("**/*.java").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match_because_full_path() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasFullPathMatchingGlob("*.java").build(); + assertThat(filter.test(path)).isFalse(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasFullPathMatchingGlob("**/*.php").build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void fileTest_noMatch() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().fileTest(p -> p.equals(new Object())).build(); - assertThat(filter.test(path)).isFalse(); + @Nested + class MatchesPathMatcher { + } // ---------- Name ---------- - @Test - void hasName_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasName("myFile.java").build(); - assertThat(filter.test(path)).isTrue(); - } + @Nested + class HasName { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasName("myFile.java").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match_case_sensitive() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasName("MYFILE.JAVA").build(); + assertThat(filter.test(path)).isFalse(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasName("myFile.php").build(); + assertThat(filter.test(path)).isFalse(); + } - @Test - void hasName_is_case_sensitive() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasName("myfile.java").build(); - assertThat(filter.test(path)).isFalse(); } - @Test - void hasNameIgnoreCase_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasNameIgnoreCase("myFile.java").build(); - assertThat(filter.test(path)).isTrue(); + @Nested + class HasNameIgnoreCase { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameIgnoreCase("myFile.java").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void match_case_sensitive() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameIgnoreCase("MYFILE.JAVA").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameIgnoreCase("myFile.php").build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void hasNameIgnoreCase_is_case_insensitive() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasNameIgnoreCase("myfile.java").build(); - assertThat(filter.test(path)).isTrue(); + @Nested + class HasNameMatching { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameMatching(Pattern.compile("my.*")).build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameMatching(Pattern.compile("ma.*")).build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void hasNameMatching_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasNameMatching(Pattern.compile("my.*")).build(); - assertThat(filter.test(path)).isTrue(); + @Nested + class HasNameMatchingGlob { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameMatchingGlob("my*").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameMatchingGlob("ma*").build(); + assertThat(filter.test(path)).isFalse(); + } + + @Test + void no_match_dir() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameMatchingGlob("*/my*").build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void hasNameMatching_noMatch() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasNameMatching(Pattern.compile("ma.*")).build(); - assertThat(filter.test(path)).isFalse(); + @Nested + class HasNameEndingWith { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameEndingWith(".java").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameEndingWith(".php").build(); + assertThat(filter.test(path)).isFalse(); + } + + @Test + void no_match_case_sensitive() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasNameEndingWith(".Java").build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void hasNameEndingWith_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasNameEndingWith(".java").build(); - assertThat(filter.test(path)).isTrue(); + @Nested + class HasExtension { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasExtension("java").build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasExtension("php").build(); + assertThat(filter.test(path)).isFalse(); + } + + @Test + void no_match_case_sensitive() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().hasExtension("Java").build(); + assertThat(filter.test(path)).isFalse(); + } + + @Test + void no_match_no_extension() { + var path = createTempFile("myFilejava"); + var filter = PathPredicates.builder().hasExtension("java").build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void hasNameEndingWith_is_case_sensitive() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasNameEndingWith(".Java").build(); - assertThat(filter.test(path)).isFalse(); + // ---------- Type ---------- + + @Nested + class IsDirectory { + + @Test + void match() { + var path = createTempDir("myDir"); + var filter = PathPredicates.builder().isDirectory().build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().isDirectory().build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void hasExtension_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasExtension("java").build(); - assertThat(filter.test(path)).isTrue(); + @Nested + class IsFile { + + @Test + void match() { + var path = createTempFile("myFile.java"); + var filter = PathPredicates.builder().isFile().build(); + assertThat(filter.test(path)).isTrue(); + } + + @Test + void no_match() { + var path = createTempDir("myDir"); + var filter = PathPredicates.builder().isFile().build(); + assertThat(filter.test(path)).isFalse(); + } + } - @Test - void hasExtension_is_case_sensitive() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().hasExtension("Java").build(); - assertThat(filter.test(path)).isFalse(); + // ---------- Hierarchy ---------- + + @Nested + class HasParentMatching { + + @Test + void null_predicate_throws_NPE() { + assertThatNullPointerException() + .isThrownBy(() -> PathPredicates.builder().hasParentMatching(null)); + } + + @Test + void matches_direct_parent() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandChild = Files.createDirectory(child.resolve("grandchild")); + + var filter = PathPredicates.builder().hasParentMatching( + p -> p.getFileName().toString().equals("child") + ).build(); + + assertThat(filter.test(grandChild)).isTrue(); + } + + @Test + void does_not_match_grandparent() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandChild = Files.createDirectory(child.resolve("grandchild")); + + var filter = PathPredicates.builder().hasParentMatching( + p -> p.getFileName().toString().equals("parent") + ).build(); + + assertThat(filter.test(grandChild)).isFalse(); + } + + @Test + @SuppressWarnings("unused") + void does_not_match_sibling() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandChild1 = Files.createDirectory(child.resolve("grandchild1")); + var grandChild2 = Files.createDirectory(child.resolve("grandchild2")); + + var filter = PathPredicates.builder().hasParentMatching( + p -> p.getFileName().toString().equals("grandchild2") + ).build(); + + assertThat(filter.test(grandChild1)).isFalse(); + } + + @Test + void root_has_no_parent() { + var detachedRoot = Path.of("root"); + + var filter = PathPredicates.builder().hasParentMatching( + p -> true + ).build(); + + assertThat(filter.test(detachedRoot)).isFalse(); + } + } - // ---------- Type ---------- + @Nested + class HasAncestorMatching { + + @Test + void null_predicate_throws_NPE() { + assertThatNullPointerException() + .isThrownBy(() -> PathPredicates.builder().hasAncestorMatching(null)); + } + + @Test + void matches_direct_parent() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandChild = Files.createDirectory(child.resolve("grandchild")); + + var filter = PathPredicates.builder().hasAncestorMatching( + p -> p.getFileName().toString().equals("child") + ).build(); + + assertThat(filter.test(grandChild)).isTrue(); + } + + @Test + void matches_grandparent() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandChild = Files.createDirectory(child.resolve("grandchild")); + + var filter = PathPredicates.builder().hasAncestorMatching( + p -> p.getFileName().toString().equals("parent") + ).build(); + + assertThat(filter.test(grandChild)).isTrue(); + } + + @Test + @SuppressWarnings("unused") + void does_not_match_sibling() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandChild1 = Files.createDirectory(child.resolve("grandchild1")); + var grandChild2 = Files.createDirectory(child.resolve("grandchild2")); + + var filter = PathPredicates.builder().hasAncestorMatching( + p -> p.getNameCount() > 0 && p.getFileName().toString().equals("grandchild2") + ).build(); + + assertThat(filter.test(grandChild1)).isFalse(); + } + + @Test + void root_has_no_parent() { + var detachedRoot = Path.of("root"); + + var filter = PathPredicates.builder().hasAncestorMatching( + p -> true + ).build(); + + assertThat(filter.test(detachedRoot)).isFalse(); + } - @Test - void isDirectory_match() { - var path = createTempDir("myDir"); - var filter = PathPredicates.builder().isDirectory().build(); - assertThat(filter.test(path)).isTrue(); } - @Test - void isDirectory_noMatch() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().isDirectory().build(); - assertThat(filter.test(path)).isFalse(); + @Nested + class HasDirectChildMatching { + + @Test + void null_predicate_throws_NPE() { + assertThatNullPointerException() + .isThrownBy(() -> PathPredicates.builder().hasDirectChildMatching(null)); + } + + @Test + @SuppressWarnings("unused") + void matches_direct_child() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + + var filter = PathPredicates.builder().hasDirectChildMatching( + p -> p.getFileName().toString().equals("child") + ).build(); + + assertThat(filter.test(parent)).isTrue(); + } + + @Test + @SuppressWarnings("unused") + void does_not_match_grandchild() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandchild = Files.createDirectory(child.resolve("grandchild")); + + var filter = PathPredicates.builder().hasDirectChildMatching( + p -> p.getFileName().toString().equals("grandchild") + ).build(); + + assertThat(filter.test(parent)).isFalse(); + } + + @Test + void does_not_match_if_not_directory() { + var parent = createTempFile("parent"); + + var filter = PathPredicates.builder().hasDirectChildMatching( + p -> true + ).build(); + + assertThat(filter.test(parent)).isFalse(); + } + + @Test + void does_not_match_if_no_child() { + var parent = createTempDir("parent"); + + var filter = PathPredicates.builder().hasDirectChildMatching( + p -> true + ).build(); + + assertThat(filter.test(parent)).isFalse(); + } + } - @Test - void isFile_match() { - var path = createTempFile("myFile.java"); - var filter = PathPredicates.builder().isFile().build(); - assertThat(filter.test(path)).isTrue(); + @Nested + class HasDescendantMatching { + + @Test + void null_predicate_throws_NPE() { + assertThatNullPointerException() + .isThrownBy(() -> PathPredicates.builder().hasDescendantMatching(null)); + } + + @Test + @SuppressWarnings("unused") + void matches_grandchild() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandchild = Files.createDirectory(child.resolve("grandchild")); + + var filter = PathPredicates.builder().hasDescendantMatching( + p -> p.getFileName().toString().equals("grandchild") + ).build(); + + assertThat(filter.test(parent)).isTrue(); + } + + @Test + void does_not_match_nonexistent_descendant() { + var parent = createTempDir("parent"); + parent.resolve("child"); + + var filter = PathPredicates.builder().hasDescendantMatching( + p -> p.getFileName().toString().equals("missing") + ).build(); + + assertThat(filter.test(parent)).isFalse(); + } + + @Test + void does_not_match_if_not_directory() { + var parent = createTempFile("parent"); + + var filter = PathPredicates.builder().hasDescendantMatching( + p -> true + ).build(); + + assertThat(filter.test(parent)).isFalse(); + } + + @Test + void does_not_match_if_no_child() { + var parent = createTempDir("parent"); + + var filter = PathPredicates.builder().hasDescendantMatching( + p -> true + ).build(); + + assertThat(filter.test(parent)).isFalse(); + } + } - @Test - void isFile_noMatch() { - var path = createTempDir("myDir"); - var filter = PathPredicates.builder().isFile().build(); - assertThat(filter.test(path)).isFalse(); + @Nested + class HasSiblingMatching { + + @Test + void null_predicate_throws_NPE() { + assertThatNullPointerException() + .isThrownBy(() -> PathPredicates.builder().hasSiblingMatching(null)); + } + + @Test + @SuppressWarnings("unused") + void matches_sibling() throws IOException { + var parent = createTempDir("parent"); + var child1 = Files.createDirectory(parent.resolve("child1")); + var child2 = Files.createDirectory(parent.resolve("child2")); + + var filter = PathPredicates.builder().hasSiblingMatching( + p -> p.getFileName().toString().equals("child2") + ).build(); + + assertThat(filter.test(child1)).isTrue(); + } + + @Test + @SuppressWarnings("unused") + void matches_sibling_failed() throws IOException { + var parent = createTempDir("parent"); + var child1 = Files.createDirectory(parent.resolve("child1")); + var child2 = Files.createDirectory(parent.resolve("child2")); + + var filter = PathPredicates.builder().hasSiblingMatching( + p -> p.getFileName().toString().equals("otherChild") + ).build(); + + assertThat(filter.test(child1)).isFalse(); + } + + @Test + void root_has_no_sibling() { + var detachedRoot = Path.of("root"); + + var filter = PathPredicates.builder().hasSiblingMatching( + p -> true + ).build(); + + assertThat(filter.test(detachedRoot)).isFalse(); + } + + @Test + void does_not_match_self() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + + var filter = PathPredicates.builder().hasSiblingMatching( + p -> true + ).build(); + + assertThat(filter.test(child)).isFalse(); + } + + @Test + @SuppressWarnings("unused") + void does_not_match_parent_or_child() throws IOException { + var parent = createTempDir("parent"); + var child = Files.createDirectory(parent.resolve("child")); + var grandchild = Files.createDirectory(child.resolve("grandchild")); + + var filter = PathPredicates.builder().hasSiblingMatching( + p -> true + ).build(); + + assertThat(filter.test(child)).isFalse(); + } + } }