Skip to content

Commit 17fe701

Browse files
Reworked filtering (#7)
* Reworked filtering * Fix sonar
1 parent 784fcf7 commit 17fe701

File tree

11 files changed

+220
-104
lines changed

11 files changed

+220
-104
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Changed
1515
- `PathUtils` removed, `PathPredicates` rework
1616
- Line extension: empty string is permitted
17+
- Filtering: split into distinct directories and files filters
1718

1819
---
1920
## [0.0.4] - 2025-09-27

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,17 +276,23 @@ sorting/
276276
## Filtering
277277
Files and directories can be selectively included or excluded using a custom `Predicate<Path>`.
278278

279-
Filtering is **recursive by default**: directory's contents will always be traversed.
280-
However, if a directory does not match and none of its children match, the directory itself will not be displayed.
279+
Filtering is independant for files & directories. Files are filtered only if their parent directory pass the directory filter.
280+
If none of some directory's children match, the directory is still displayed.
281281

282282
The `PathPredicates` class provides several ready-to-use methods for creating common predicates, as well as a builder for creating more advanced predicates.
283283

284284
```java
285285
// Example: Filtering.java
286-
var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build();
286+
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
287+
var isJavaFilePredicate = PathPredicates.builder().hasExtension("java").build();
288+
287289
var prettyPrinter = FileTreePrettyPrinter.builder()
288-
.customizeOptions(options -> options.filter(hasJavaExtensionPredicate))
289-
.build();
290+
.customizeOptions(
291+
options -> options
292+
.filterDirectories(excludeDirWithNoJavaFiles)
293+
.filterFiles(hasJavaExtensionPredicate)
294+
)
295+
.build();
290296
```
291297
```
292298
filtering/
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.github.computerdaddyguy.jfiletreeprettyprinter.example;
2+
3+
import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder;
4+
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
5+
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
6+
import io.github.computerdaddyguy.jfiletreeprettyprinter.PrettyPrintOptions.Sorts;
7+
import java.nio.file.Path;
8+
import java.util.function.Function;
9+
10+
public class CompleteExample {
11+
12+
public static void main(String[] args) {
13+
14+
var filterDir = PathPredicates.builder()
15+
.pathTest(path -> !PathPredicates.hasName(path, ".git"))
16+
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.git"))
17+
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.github"))
18+
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.settings"))
19+
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/example"))
20+
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/test"))
21+
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/target"))
22+
.build();
23+
24+
var filterFiles = PathPredicates.builder()
25+
.pathTest(path -> !PathPredicates.hasNameStartingWith(path, "."))
26+
.pathTest(path -> {
27+
if (PathPredicates.hasParentMatching(path, parent -> PathPredicates.hasName(parent, "jfiletreeprettyprinter"))) {
28+
return PathPredicates.hasName(path, "FileTreePrettyPrinter.java");
29+
}
30+
return true;
31+
})
32+
.build();
33+
34+
var childLimitFunction = ChildLimitBuilder.builder()
35+
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0)
36+
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0)
37+
.build();
38+
39+
Function<Path, String> lineExtension = path -> {
40+
if (PathPredicates.hasName(path, "JfileTreePrettyPrinter-structure.png")) {
41+
return "\t// This image";
42+
} else if (PathPredicates.hasName(path, "FileTreePrettyPrinter.java")) {
43+
return "\t// Main entry point";
44+
} else if (PathPredicates.hasName(path, "README.md")) {
45+
return "\t\t// You're reading at this!";
46+
} else if (PathPredicates.hasName(path, "java")) {
47+
return "";
48+
}
49+
return null;
50+
};
51+
52+
var prettyPrinter = FileTreePrettyPrinter.builder()
53+
.customizeOptions(
54+
options -> options
55+
.withEmojis(true)
56+
.withCompactDirectories(true)
57+
.filterDirectories(filterDir)
58+
.filterFiles(filterFiles)
59+
.withChildLimit(childLimitFunction)
60+
.withLineExtension(lineExtension)
61+
.sort(Sorts.DIRECTORY_FIRST)
62+
)
63+
.build();
64+
var tree = prettyPrinter.prettyPrint(".");
65+
System.out.println(tree);
66+
}
67+
68+
}

src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Filtering.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22

33
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
44
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
5+
import java.nio.file.Path;
6+
import java.util.function.Predicate;
57

68
public class Filtering {
79

810
public static void main(String[] args) {
11+
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
912
var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build();
13+
1014
var prettyPrinter = FileTreePrettyPrinter.builder()
11-
.customizeOptions(options -> options.filter(hasJavaExtensionPredicate))
15+
.customizeOptions(
16+
options -> options
17+
.filterDirectories(excludeDirWithNoJavaFiles)
18+
.filterFiles(hasJavaExtensionPredicate)
19+
)
1220
.build();
1321

1422
var tree = prettyPrinter.prettyPrint("src/example/resources/filtering");

src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PrettyPrintOptions.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -297,25 +297,37 @@ public PrettyPrintOptions sort(Comparator<Path> pathComparator) {
297297

298298
// ---------- Filtering ----------
299299

300-
@Nullable
301-
private Predicate<Path> pathFilter = null;
300+
private Predicate<Path> dirFilter = dir -> true;
301+
private Predicate<Path> fileFilter = dir -> true;
302302

303303
@Override
304-
@Nullable
305304
public Predicate<Path> pathFilter() {
306-
return pathFilter;
305+
return path -> PathPredicates.isDirectory(path)
306+
? dirFilter.test(path)
307+
: fileFilter.test(path);
308+
}
309+
310+
/**
311+
* Use a custom filter for retain only some directories.
312+
*
313+
* Directories that do not pass this filter will not be displayed.
314+
*
315+
* @param filter The filter to apply on directories, cannot be <code>null</code>
316+
*/
317+
public PrettyPrintOptions filterDirectories(@Nullable Predicate<Path> filter) {
318+
this.dirFilter = Objects.requireNonNull(filter, "filter is null");
319+
return this;
307320
}
308321

309322
/**
310-
* Use a custom filter for retain only some files and/or directories.
323+
* Use a custom filter for retain only some files.
324+
*
325+
* Files that do not pass this filter will not be displayed.
311326
*
312-
* Filtering is recursive by default: directory's contents will always be traversed.
313-
* However, if a directory does not match and none of its children match, the directory itself will not be displayed.
314-
315-
* @param filter The filter, <code>null</code> to disable filtering
327+
* @param filter The filter to apply on files, cannot be <code>null</code>
316328
*/
317-
public PrettyPrintOptions filter(@Nullable Predicate<Path> filter) {
318-
this.pathFilter = filter;
329+
public PrettyPrintOptions filterFiles(@Nullable Predicate<Path> filter) {
330+
this.fileFilter = Objects.requireNonNull(filter, "filter is null");
319331
return this;
320332
}
321333

src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer/DefaultTreeEntryRenderer.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.Objects;
1414
import java.util.Optional;
1515
import org.jspecify.annotations.NullMarked;
16+
import org.jspecify.annotations.Nullable;
1617

1718
@NullMarked
1819
class DefaultTreeEntryRenderer implements TreeEntryRenderer {
@@ -41,25 +42,28 @@ private String renderTree(TreeEntry entry, Depth depth) {
4142

4243
private String renderDirectory(Depth depth, DirectoryEntry dirEntry, List<Path> compactPaths) {
4344

44-
Optional<String> extension = null;
45+
boolean extensionEvaluated = false;
46+
String extension = null;
47+
4548
if (options.areCompactDirectoriesUsed()
4649
&& !depth.isRoot()
4750
&& dirEntry.getEntries().size() == 1
4851
&& dirEntry.getEntries().get(0) instanceof DirectoryEntry childDirEntry) {
4952

5053
extension = computeLineExtension(dirEntry.getDir());
51-
if (extension.isEmpty()) {
54+
extensionEvaluated = true;
55+
if (extension == null) {
5256
var newCompactPaths = new ArrayList<>(compactPaths);
5357
newCompactPaths.add(childDirEntry.getDir());
5458
return renderDirectory(depth, childDirEntry, newCompactPaths);
5559
}
5660
}
5761

5862
var line = lineRenderer.renderDirectoryBegin(depth, dirEntry, compactPaths);
59-
if (extension == null) {
63+
if (!extensionEvaluated) {
6064
extension = computeLineExtension(dirEntry.getDir());
6165
}
62-
line += extension.orElse("");
66+
line += Optional.ofNullable(extension).orElse("");
6367

6468
var childIt = dirEntry.getEntries().iterator();
6569

@@ -81,16 +85,17 @@ private String renderDirectory(Depth depth, DirectoryEntry dirEntry, List<Path>
8185
return line + childLines.toString();
8286
}
8387

84-
private Optional<String> computeLineExtension(Path path) {
88+
@Nullable
89+
private String computeLineExtension(Path path) {
8590
if (options.getLineExtension() == null) {
86-
return Optional.empty();
91+
return null;
8792
}
88-
return Optional.ofNullable(options.getLineExtension().apply(path));
93+
return options.getLineExtension().apply(path);
8994
}
9095

9196
private String renderFile(Depth depth, FileEntry fileEntry) {
9297
var line = lineRenderer.renderFile(depth, fileEntry);
93-
line += computeLineExtension(fileEntry.getFile()).orElse("");
98+
line += Optional.ofNullable(computeLineExtension(fileEntry.getFile())).orElse("");
9499
return line;
95100
}
96101

src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner/DefaultPathToTreeScanner.java

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.computerdaddyguy.jfiletreeprettyprinter.scanner;
22

3-
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
43
import io.github.computerdaddyguy.jfiletreeprettyprinter.scanner.TreeEntry.DirectoryEntry;
54
import io.github.computerdaddyguy.jfiletreeprettyprinter.scanner.TreeEntry.FileEntry;
65
import io.github.computerdaddyguy.jfiletreeprettyprinter.scanner.TreeEntry.MaxDepthReachEntry;
@@ -35,14 +34,14 @@ public TreeEntry scan(Path fileOrDir) {
3534
}
3635

3736
@Nullable
38-
private TreeEntry handle(int depth, Path fileOrDir, @Nullable Predicate<Path> filter) {
37+
private TreeEntry handle(int depth, Path fileOrDir, Predicate<Path> filter) {
3938
return fileOrDir.toFile().isDirectory()
4039
? handleDirectory(depth, fileOrDir, filter)
4140
: handleFile(fileOrDir);
4241
}
4342

4443
@Nullable
45-
private TreeEntry handleDirectory(int depth, Path dir, @Nullable Predicate<Path> filter) {
44+
private TreeEntry handleDirectory(int depth, Path dir, Predicate<Path> filter) {
4645

4746
if (depth >= options.getMaxDepth()) {
4847
var maxDepthEntry = new MaxDepthReachEntry(depth);
@@ -58,15 +57,10 @@ private TreeEntry handleDirectory(int depth, Path dir, @Nullable Predicate<Path>
5857
throw new UncheckedIOException("Unable to list files for directory: " + dir, e);
5958
}
6059

61-
// Filter is active and no children match
62-
if (depth > 0 && filter != null && childEntries.isEmpty() && !filter.test(dir)) {
63-
return null; // Do no show this directory at all
64-
}
65-
6660
return new DirectoryEntry(dir, childEntries);
6761
}
6862

69-
private List<TreeEntry> handleDirectoryChildren(int depth, Path dir, Iterator<Path> pathIterator, @Nullable Predicate<Path> filter) {
63+
private List<TreeEntry> handleDirectoryChildren(int depth, Path dir, Iterator<Path> pathIterator, Predicate<Path> filter) {
7064

7165
var childEntries = new ArrayList<TreeEntry>();
7266
int maxChildEntries = options.getChildLimit().applyAsInt(dir);
@@ -95,39 +89,29 @@ private List<TreeEntry> handleDirectoryChildren(int depth, Path dir, Iterator<Pa
9589
return childEntries;
9690
}
9791

98-
private List<TreeEntry> handleLeftOverChildren(int depth, Iterator<Path> pathIterator, @Nullable Predicate<Path> filter) {
92+
private List<TreeEntry> handleLeftOverChildren(int depth, Iterator<Path> pathIterator, Predicate<Path> filter) {
9993
var childEntries = new ArrayList<TreeEntry>();
10094

101-
if (filter == null) {
102-
var skippedChildren = new ArrayList<Path>();
103-
pathIterator.forEachRemaining(skippedChildren::add);
95+
var skippedChildren = new ArrayList<Path>();
96+
while (pathIterator.hasNext()) {
97+
var child = pathIterator.next();
98+
var childEntry = handle(depth + 1, child, filter);
99+
if (childEntry != null) { // Is null if no children file is retained by filter
100+
skippedChildren.add(child);
101+
}
102+
}
103+
if (!skippedChildren.isEmpty()) {
104104
var childrenSkippedEntry = new SkippedChildrenEntry(skippedChildren);
105105
childEntries.add(childrenSkippedEntry);
106-
} else {
107-
var skippedChildren = new ArrayList<Path>();
108-
while (pathIterator.hasNext()) {
109-
var child = pathIterator.next();
110-
var childEntry = handle(depth + 1, child, filter);
111-
if (childEntry != null) { // Is null if no children file is retained by filter
112-
skippedChildren.add(child);
113-
}
114-
}
115-
if (!skippedChildren.isEmpty()) {
116-
var childrenSkippedEntry = new SkippedChildrenEntry(skippedChildren);
117-
childEntries.add(childrenSkippedEntry);
118-
}
119106
}
120107

121108
return childEntries;
122109
}
123110

124-
private Iterator<Path> directoryStreamToIterator(DirectoryStream<Path> childrenStream, @Nullable Predicate<Path> filter) {
125-
var stream = StreamSupport.stream(childrenStream.spliterator(), false);
126-
if (filter != null) {
127-
var recursiveFilter = PathPredicates.builder().isDirectory().build().or(filter);
128-
stream = stream.filter(recursiveFilter);
129-
}
130-
return stream
111+
private Iterator<Path> directoryStreamToIterator(DirectoryStream<Path> childrenStream, Predicate<Path> filter) {
112+
return StreamSupport
113+
.stream(childrenStream.spliterator(), false)
114+
.filter(filter)
131115
.sorted(options.pathComparator())
132116
.iterator();
133117
}

src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner/ScanningOptions.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import java.util.function.Predicate;
66
import java.util.function.ToIntFunction;
77
import org.jspecify.annotations.NullMarked;
8-
import org.jspecify.annotations.Nullable;
98

109
@NullMarked
1110
public interface ScanningOptions {
@@ -16,7 +15,6 @@ public interface ScanningOptions {
1615

1716
Comparator<Path> pathComparator();
1817

19-
@Nullable
2018
Predicate<Path> pathFilter();
2119

2220
}

src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterTest.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,4 @@ void prettyPrint_by_path_and_string_are_same() {
2121
assertThat(printer.prettyPrint(path)).isEqualTo(printer.prettyPrint(path.toString()));
2222
}
2323

24-
@Test
25-
void prettyPrintWithFilter_by_path_and_string_are_same() {
26-
var path = FileStructures.simpleDirectoryWithFilesAndFolders(root, 3, 3);
27-
28-
FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder()
29-
.customizeOptions(options -> options.filter(PathPredicates::isFile))
30-
.build();
31-
32-
assertThat(printer.prettyPrint(path)).isEqualTo(printer.prettyPrint(path.toString()));
33-
}
34-
3524
}

0 commit comments

Comments
 (0)