• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ljacqu / FileDuplicateFinder / 14006930974

22 Mar 2025 08:35AM UTC coverage: 23.055%. Remained the same
14006930974

push

github

ljacqu
Add Nullable annotation next to return value in methods

111 of 610 branches covered (18.2%)

Branch coverage included in aggregate %.

378 of 1511 relevant lines covered (25.02%)

1.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main/java/ch/jalu/fileduplicatefinder/tree/FileTreeRunner.java
1
package ch.jalu.fileduplicatefinder.tree;
2

3
import ch.jalu.fileduplicatefinder.config.FileUtilConfiguration;
4
import ch.jalu.fileduplicatefinder.config.property.JfuDoubleProperty;
5
import ch.jalu.fileduplicatefinder.config.property.JfuIntegerProperty;
6
import ch.jalu.fileduplicatefinder.config.property.JfuRegexProperty;
7
import ch.jalu.fileduplicatefinder.utils.ConsoleProgressListener;
8
import ch.jalu.fileduplicatefinder.utils.FileSizeUtils;
9
import com.google.common.base.CharMatcher;
10
import com.google.common.base.Strings;
11
import org.jetbrains.annotations.Nullable;
12

13
import java.io.File;
14
import java.nio.file.Path;
15
import java.util.Comparator;
16
import java.util.HashMap;
17
import java.util.HashSet;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Scanner;
21
import java.util.Set;
22
import java.util.regex.Pattern;
23
import java.util.stream.Collectors;
24
import java.util.stream.Stream;
25

26
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.FORMAT_FILE_SIZE;
27
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_DIRECTORY_REGEX;
28
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_FILES_PROCESSED_INTERVAL;
29
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_FILE_MAX_SIZE_MB;
30
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_FILE_MIN_SIZE_MB;
31
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_FILE_REGEX;
32
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_FOLDER;
33
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_INDENT_ELEMENTS;
34
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_MAX_ITEMS_IN_FOLDER;
35
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_MIN_ITEMS_IN_FOLDER;
36
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_OUTPUT_ELEMENT_TYPES;
37
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_SHOW_ABSOLUTE_PATH;
38
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.TREE_SORT_FILES_BY_SIZE;
39

40
/**
41
 * Outputs a file tree with configurable filters.
42
 */
43
public class FileTreeRunner {
44

45
    public static final String ID = "tree";
46

47
    private static final CharMatcher FILE_SEPARATOR_MATCHER = CharMatcher.is(File.separatorChar);
×
48

49
    private final Scanner scanner;
50
    private final FileUtilConfiguration configuration;
51

52
    public FileTreeRunner(Scanner scanner, FileUtilConfiguration configuration) {
×
53
        this.scanner = scanner;
×
54
        this.configuration = configuration;
×
55
    }
×
56

57
    public void run() {
58
        Path folder = configuration.getValueOrPrompt(TREE_FOLDER);
×
59
        FileTreeGenerator fileTreeGenerator = new FileTreeGenerator(folder);
×
60

61
        System.out.print("Collecting all items in " + folder.toAbsolutePath().normalize() + ":");
×
62
        ConsoleProgressListener progressCallback = new ConsoleProgressListener(
×
63
            configuration.getValue(TREE_FILES_PROCESSED_INTERVAL));
×
64
        FileTreeEntry treeRoot = fileTreeGenerator.generateTree(progressCallback::notifyItemProcessed);
×
65
        int totalItems = progressCallback.getCount();
×
66
        System.out.println("\nFound " + totalItems + " files and directories");
×
67

68
        TreeParameters params = createParams(false);
×
69
        List<FileTreeEntry> relevantEntries = filterAndOutputRelevantEntries(treeRoot, params, totalItems);
×
70

71
        String task = "help";
×
72
        boolean previousTaskWasHelp = false;
×
73
        do {
74
            boolean currentTaskIsHelp = false;
×
75

76
            switch (task) {
×
77
                case "dump":
78
                    throw new UnsupportedOperationException("Not yet implemented"); // TODO :)
×
79

80
                case "config":
81
                    params = createParams(true);
×
82
                    relevantEntries = filterAndOutputRelevantEntries(treeRoot, params, totalItems);
×
83
                    break;
×
84

85
                case "debug":
86
                    System.out.println("Conflicting filters: " + params.hasConflictingFilters());
×
87
                    filterRelevantEntries(treeRoot, params, true);
×
88
                    break;
×
89

90
                default:
91
                    if (!previousTaskWasHelp || "help".equals(task)) { // Always show 'help' if explicitly requested
×
92
                        System.out.println();
×
93
                        System.out.println("- Type 'dump' to dump the results to a file");
×
94
                        System.out.println("- Type 'config' to reconfigure all parameters");
×
95
                        System.out.println("- Type 'debug' to debug the parameters");
×
96
                        System.out.println("- Type 'help' to see this help");
×
97
                        System.out.println("- Type 'exit' to stop");
×
98
                    }
99
                    currentTaskIsHelp = true;
×
100
            }
101
            previousTaskWasHelp = currentTaskIsHelp;
×
102

103
            System.out.print("Command: ");
×
104
            task = scanner.nextLine();
×
105
        } while (!task.equals("exit"));
×
106
    }
×
107

108
    private List<FileTreeEntry> filterAndOutputRelevantEntries(FileTreeEntry treeRoot, TreeParameters params,
109
                                                               int totalItems) {
110
        List<FileTreeEntry> relevantEntries = filterRelevantEntries(treeRoot, params, false);
×
111

112
        System.out.println("Matched " + relevantEntries.size() + " out of " + totalItems + " items");
×
113
        if (relevantEntries.size() == 1) {
×
114
            System.out.println("Note: The root is never filtered out.");
×
115
        }
116
        if (params.hasConflictingFilters()) {
×
117
            System.out.println("Warning: Conflicting filters were found, which may never match any items. "
×
118
                + "Please recheck your filters.");
119
        }
120
        System.out.println();
×
121

122
        Path root = treeRoot.getPath();
×
123
        relevantEntries.forEach(elem -> printElement(elem, root, params));
×
124
        return relevantEntries;
×
125
    }
126

127
    private List<FileTreeEntry> filterRelevantEntries(FileTreeEntry root, TreeParameters params,
128
                                                      boolean printDebug) {
129
        RelevantFileEntryCollector collector = new RelevantFileEntryCollector(root, printDebug);
×
130

131
        for (FileTreeEntry child : root.getChildren()) {
×
132
            addEntryAndChildrenToListIfRelevantRecursively(collector, child, params);
×
133
        }
×
134
        return collector.getRelevantEntriesSorted(params);
×
135
    }
136

137
    private boolean addEntryAndChildrenToListIfRelevantRecursively(RelevantFileEntryCollector collector,
138
                                                                   FileTreeEntry entry, TreeParameters params) {
139
        collector.registerEntry(entry);
×
140

141
        Path path = entry.getPath();
×
142
        String relativeName = collector.getNameRelativeToRoot(path);
×
143
        boolean isMatch = params.matchesRegexFilters(path, relativeName)
×
144
            && params.matchesSizeFilters(entry)
×
145
            && params.matchesItemsInDirFilters(entry);
×
146

147
        boolean hasRelevantChild = false;
×
148
        for (FileTreeEntry child : entry.getChildren()) {
×
149
            hasRelevantChild |= addEntryAndChildrenToListIfRelevantRecursively(collector, child, params);
×
150
        }
×
151

152
        boolean isRelevant = isMatch || (!params.isShowAbsolutePath() && hasRelevantChild);
×
153
        if (isRelevant) {
×
154
            collector.addRelevantEntry(entry);
×
155
        }
156

157
        if (collector.isDebug()) {
×
158
            String filterInfo = isMatch
×
159
                ? "passed"
×
160
                : createDebugTextForChecks(relativeName, entry, params) + "; hasRelevantChild=" + hasRelevantChild;
×
161
            System.out.println(relativeName + ": " + filterInfo);
×
162
        }
163
        return isRelevant;
×
164
    }
165

166
    private String createDebugTextForChecks(String relativeName, FileTreeEntry entry, TreeParameters params) {
167
        return "regex=" + params.matchesRegexFilters(entry.getPath(), relativeName)
×
168
            + ", size=" + params.matchesSizeFilters(entry)
×
169
            + ", itemsInDir=" + params.matchesItemsInDirFilters(entry);
×
170
    }
171

172
    private void printElement(FileTreeEntry entry, Path root, TreeParameters params) {
173
        final String nameRelativeToRoot = root.relativize(entry.getPath()).toString();
×
174
        final int level = FILE_SEPARATOR_MATCHER.countIn(nameRelativeToRoot);
×
175
        final boolean isRoot = nameRelativeToRoot.isEmpty();
×
176

177
        if (params.matchesTypeFilter(entry.getPath()) || isRoot) {
×
178
            String indent = (params.isIndentElements() && !isRoot)
×
179
                ? Strings.repeat("  ", level) + "- "
×
180
                : "";
×
181
            if (isRoot) {
×
182
                indent = "Folder: ";
×
183
            }
184

185
            String filename = params.isShowAbsolutePath()
×
186
                ? entry.getPath().toAbsolutePath().toString()
×
187
                : (params.isIndentElements() ? entry.getPath().getFileName().toString() : nameRelativeToRoot);
×
188
            String fileSize = params.isFormatFileSize()
×
189
                ? FileSizeUtils.formatToHumanReadableSize(entry.getSize())
×
190
                : entry.getSize().toString();
×
191

192
            System.out.println(indent + filename + " (" + fileSize + ")");
×
193
        }
194
    }
×
195

196
    private TreeParameters createParams(boolean forcePrompt) {
197
        TreeParameters parameters = new TreeParameters();
×
198

199
        // Filter configs
200
        parameters.setFilePattern(getConfiguredPatternOrNull(TREE_FILE_REGEX, forcePrompt));
×
201
        parameters.setDirectoryPattern(getConfiguredPatternOrNull(TREE_DIRECTORY_REGEX, forcePrompt));
×
202
        parameters.setMinSizeBytes(getConfiguredNumberOfBytesOrNull(TREE_FILE_MIN_SIZE_MB, forcePrompt));
×
203
        parameters.setMaxSizeBytes(getConfiguredNumberOfBytesOrNull(TREE_FILE_MAX_SIZE_MB, forcePrompt));
×
204
        parameters.setMinItemsInDir(getConfiguredIntOrNullIfNegative(TREE_MIN_ITEMS_IN_FOLDER, forcePrompt));
×
205
        parameters.setMaxItemsInDir(getConfiguredIntOrNullIfNegative(TREE_MAX_ITEMS_IN_FOLDER, forcePrompt));
×
206

207
        // Output configs
208
        parameters.setDisplayMode(configuration.getValue(TREE_OUTPUT_ELEMENT_TYPES, forcePrompt));
×
209
        parameters.setSortBySize(configuration.getValue(TREE_SORT_FILES_BY_SIZE, forcePrompt));
×
210
        parameters.setFormatFileSize(configuration.getValue(FORMAT_FILE_SIZE, forcePrompt));
×
211

212
        if (!parameters.isSortBySize() || parameters.getDisplayMode() == TreeDisplayMode.ALL) {
×
213
            parameters.setIndentElements(configuration.getValue(TREE_INDENT_ELEMENTS, forcePrompt));
×
214
        }
215
        parameters.setShowAbsolutePath(configuration.getValue(TREE_SHOW_ABSOLUTE_PATH, forcePrompt));
×
216

217
        return parameters;
×
218
    }
219

220
    private @Nullable Long getConfiguredNumberOfBytesOrNull(JfuDoubleProperty megaBytesProperty, boolean forcePrompt) {
221
        double megabytes = configuration.getValue(megaBytesProperty, forcePrompt);
×
222
        if (megabytes < 0.0) {
×
223
            return null;
×
224
        }
225
        return FileSizeUtils.megaBytesToBytes(megabytes);
×
226
    }
227

228
    private @Nullable Integer getConfiguredIntOrNullIfNegative(JfuIntegerProperty intProperty, boolean forcePrompt) {
229
        int value = configuration.getValue(intProperty, forcePrompt);
×
230
        return value < 0 ? null : value;
×
231
    }
232

233
    private @Nullable Pattern getConfiguredPatternOrNull(JfuRegexProperty patternProperty,
234
                                                         boolean forcePrompt) {
235
        Pattern pattern = configuration.getValue(patternProperty, forcePrompt);
×
236
        return pattern.pattern().isEmpty() ? null : pattern;
×
237
    }
238

239
    /**
240
     * Helper to save relevant entries in any order but to get them in original order at the end.
241
     */
242
    private static final class RelevantFileEntryCollector {
243

244
        private final Path root;
245
        private final boolean isDebug;
246
        private final Map<Path, Integer> orderByEntry = new HashMap<>();
×
247
        private final Set<FileTreeEntry> relevantEntries = new HashSet<>();
×
248
        private int currentPosition = 0;
×
249

250
        /**
251
         * Constructor. Registers and adds the given entry as a relevant entry (the root is always deemed relevant).
252
         *
253
         * @param rootEntry the root entry
254
         * @param isDebug defines whether we should log debug output
255
         */
256
        RelevantFileEntryCollector(FileTreeEntry rootEntry, boolean isDebug) {
×
257
            this.root = rootEntry.getPath();
×
258
            this.isDebug = isDebug;
×
259
            registerEntry(rootEntry);
×
260
            addRelevantEntry(rootEntry);
×
261
        }
×
262

263
        boolean isDebug() {
264
            return isDebug;
×
265
        }
266

267
        /**
268
         * Called for <b>every</b> entry as soon as it's encountered as to know about its encounter order.
269
         *
270
         * @param entry the entry to register
271
         */
272
        void registerEntry(FileTreeEntry entry) {
273
            orderByEntry.put(entry.getPath(), currentPosition);
×
274
            ++currentPosition;
×
275
        }
×
276

277
        /**
278
         * Returns the path's file name relative to the root file of this collector.
279
         *
280
         * @param path the path to relativize
281
         * @return file name relative to the root
282
         */
283
        String getNameRelativeToRoot(Path path) {
284
            return root.relativize(path).toString();
×
285
        }
286

287
        /**
288
         * Adds the given entry to this collector as a relevant entry.
289
         *
290
         * @param entry the relevant entry to add
291
         */
292
        void addRelevantEntry(FileTreeEntry entry) {
293
            relevantEntries.add(entry);
×
294
        }
×
295

296
        /**
297
         * Returns all relevant entries by original encounter order.
298
         *
299
         * @param params tree parameters to sort by
300
         * @return relevant entries (sorted)
301
         */
302
        List<FileTreeEntry> getRelevantEntriesSorted(TreeParameters params) {
303
            if (params.isSortBySize() && params.getDisplayMode() == TreeDisplayMode.ALL) {
×
304
                return sortByHierarchyAndSize();
×
305
            }
306

307
            Comparator<FileTreeEntry> treeEntryComparator = params.isSortBySize()
×
308
                ? Comparator.comparing(FileTreeEntry::getSize)
×
309
                : Comparator.comparing(entry -> orderByEntry.get(entry.getPath()));
×
310

311
            return relevantEntries.stream()
×
312
                .sorted(treeEntryComparator)
×
313
                .collect(Collectors.toUnmodifiableList());
×
314
        }
315

316
        List<FileTreeEntry> sortByHierarchyAndSize() {
317
            FileTreeEntry rootEntry = findRootTreeEntry();
×
318
            return streamThroughEntryAndChildrenSortedBySize(rootEntry)
×
319
                .collect(Collectors.toUnmodifiableList());
×
320
        }
321

322
        private Stream<FileTreeEntry> streamThroughEntryAndChildrenSortedBySize(FileTreeEntry parent) {
323
            Stream<FileTreeEntry> selfStream = Stream.of(parent);
×
324

325
            Stream<FileTreeEntry> relevantSortedChildren = parent.getChildren().stream()
×
326
                .filter(relevantEntries::contains)
×
327
                .sorted(Comparator.comparing(FileTreeEntry::getSize))
×
328
                .flatMap(this::streamThroughEntryAndChildrenSortedBySize);
×
329

330
            return Stream.concat(selfStream, relevantSortedChildren);
×
331
        }
332

333
        private FileTreeEntry findRootTreeEntry() {
334
            for (FileTreeEntry entry : relevantEntries) {
×
335
                if (entry.getPath().equals(root)) {
×
336
                    return entry;
×
337
                }
338
            }
×
339

340
            throw new IllegalStateException("Could not find root entry");
×
341
        }
342
    }
343
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc