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

ljacqu / FileDuplicateFinder / 13977395174

20 Mar 2025 07:07PM UTC coverage: 23.055% (-0.7%) from 23.77%
13977395174

push

github

ljacqu
Merge remote-tracking branch 'origin/master' into dependencies

# Conflicts:
#	pom.xml

111 of 610 branches covered (18.2%)

Branch coverage included in aggregate %.

22 of 225 new or added lines in 15 files covered. (9.78%)

13 existing lines in 7 files now uncovered.

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/filecount/FileCountRunner.java
1
package ch.jalu.fileduplicatefinder.filecount;
2

3
import ch.jalu.fileduplicatefinder.ExitRunnerException;
4
import ch.jalu.fileduplicatefinder.config.FileUtilConfiguration;
5
import ch.jalu.fileduplicatefinder.output.WriterReader;
6
import ch.jalu.fileduplicatefinder.utils.FileSizeUtils;
7
import org.jetbrains.annotations.Nullable;
8

9
import java.nio.file.Path;
10
import java.util.ArrayList;
11
import java.util.Comparator;
12
import java.util.Iterator;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Map;
16
import java.util.function.Function;
17
import java.util.regex.Pattern;
18
import java.util.stream.Collectors;
19
import java.util.stream.Stream;
20

21
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.FILE_COUNT_DETAILED_GROUPS;
22
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.FILE_COUNT_FOLDER;
23
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.FILE_COUNT_GROUPS;
24
import static ch.jalu.fileduplicatefinder.config.FileUtilSettings.FORMAT_FILE_SIZE;
25
import static ch.jalu.fileduplicatefinder.filecount.FileCounter.NO_EXTENSION_TEXT;
26
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
27
import static com.google.common.base.CaseFormat.UPPER_CAMEL;
28

29
public class FileCountRunner {
30

31
    public static final String ID = "filecount";
32

33
    private final WriterReader logger;
34

35
    private final FileUtilConfiguration configuration;
36

NEW
37
    public FileCountRunner(WriterReader logger, FileUtilConfiguration configuration) {
×
NEW
38
        this.logger = logger;
×
39
        this.configuration = configuration;
×
40
    }
×
41

42
    public void run() throws ExitRunnerException {
43
        Path folder = getFolderFromProperties();
×
44
        Map<String, FileCountEntry> statsByExtension = new FileCounter(folder).gatherExtensionCount();
×
NEW
45
        logger.printLn("Found " + statsByExtension.size() + " different file extensions");
×
46

47
        applyConfiguredGroups(statsByExtension);
×
48

49
        String command = "help";
×
50
        do {
51
            if (command.startsWith("sort ")) {
×
52
                handleSortCommand(command, statsByExtension);
×
53
            } else if (command.startsWith("group ")) {
×
54
                handleGroupCommand(command, statsByExtension, false);
×
55
            } else if (command.startsWith("groups")) {
×
56
                printGroupDefinitions(command, statsByExtension);
×
57
            } else if (command.startsWith("rmgroup ")) {
×
58
                unrollGroup(command, statsByExtension);
×
59
            } else if (command.equals("rmgroups")) {
×
60
                unrollGroups(statsByExtension);
×
61
            } else if (command.equals("details")) {
×
62
                toggleDetails();
×
63
            } else if (command.equals("saveconf")) {
×
64
                saveConfiguration(statsByExtension);
×
NEW
65
            } else if (command.equalsIgnoreCase("q") || command.equalsIgnoreCase("quit")) {
×
NEW
66
                return;
×
NEW
67
            } else if (command.equalsIgnoreCase("x") || command.equalsIgnoreCase("exit")) {
×
NEW
68
                throw new ExitRunnerException();
×
69
            } else {
70
                outputHelp();
×
71
            }
NEW
72
            logger.printLn("Command: ");
×
NEW
73
            command = logger.getNextLine();
×
NEW
74
        } while (true);
×
75
    }
76

77
    private Path getFolderFromProperties() {
78
        return configuration.getValueOrPrompt(FILE_COUNT_FOLDER);
×
79
    }
80

81
    private void applyConfiguredGroups(Map<String, FileCountEntry> statsByExtension) {
82
        String groupProperty = configuration.getValue(FILE_COUNT_GROUPS);
×
83
        if (groupProperty.isEmpty()) {
×
84
            return;
×
85
        }
86

87
        Pattern pat = Pattern.compile("\\w+ \\S+");
×
88
        for (String groupDefinition : groupProperty.split(";")) {
×
89
            groupDefinition = groupDefinition.trim();
×
90
            if (pat.matcher(groupDefinition).matches()) {
×
91
                handleGroupCommand("group " + groupDefinition, statsByExtension, true);
×
92
            } else if (!groupDefinition.isEmpty()) {
×
NEW
93
                logger.printError("Ignoring group definition '" + groupDefinition + "': invalid definition");
×
94
            }
95
        }
96
    }
×
97

98
    private void handleGroupCommand(String groupCommand, Map<String, FileCountEntry> statsByExtension,
99
                                    boolean ignoreMismatches) {
100
        String[] commandParts = groupCommand.split(" ");
×
101
        if (commandParts.length != 3) {
×
NEW
102
            logger.printError("Invalid group command! Expected something like 'group image .jpg,.jpeg,.png'");
×
103
            return;
×
104
        }
105
        String groupName = normalizeGroupNameOrPrintError(commandParts[1]);
×
106
        if (groupName == null) {
×
107
            return;
×
108
        }
109

110
        FileGroupCount group = (FileGroupCount) statsByExtension.get(groupName);
×
111
        if (group == null) {
×
112
            group = new FileGroupCount();
×
113
        }
114
        int initialTotalExtensions = group.getExtensions().size();
×
115

116
        for (String extension : commandParts[2].split(",")) {
×
117
            long oldCount = group.getCount();
×
118
            removeAndReturnExtensions(extension, statsByExtension)
×
119
                .forEach(group::add);
×
120
            group.addDefinition(extension);
×
121

122
            if (!ignoreMismatches && oldCount == group.getCount()) {
×
NEW
123
                logger.printLn(" Note: nothing matched the rule '" + extension + "'");
×
124
            }
125
        }
126

127
        if (initialTotalExtensions == group.getExtensions().size()) {
×
NEW
128
            logger.printLn("Nothing could be added to the group '" + groupName + "'");
×
129
        } else if (initialTotalExtensions == 0) {
×
130
            statsByExtension.put(groupName, group);
×
NEW
131
            logger.printLn("Created new group '" + groupName + "'");
×
132
        } else {
NEW
133
            logger.printLn("Updated group '" + groupName + "'");
×
134
        }
135
    }
×
136

137
    private void printGroupDefinitions(String command, Map<String, FileCountEntry> statsByExtension) {
138
        String[] commandParts = command.split(" ");
×
139
        if (commandParts.length == 1) {
×
140
            long totalGroups = statsByExtension.entrySet().stream()
×
141
                .filter(entry -> entry.getValue() instanceof FileGroupCount)
×
142
                .peek(grpEntry -> {
×
NEW
143
                    logger.printLn("Group " + grpEntry.getKey() + ": "
×
144
                        + String.join(",", ((FileGroupCount) grpEntry.getValue()).getGroupDefinitions()));
×
145
                })
×
146
                .count();
×
147
            if (totalGroups == 0L) {
×
NEW
148
                logger.printLn("No groups exist");
×
149
            }
150
        } else if (commandParts.length == 2) {
×
151
            String groupName = normalizeGroupNameOrPrintError(commandParts[1]);
×
152
            FileGroupCount group = groupName == null ? null : getGroupOrPrintError(groupName, statsByExtension);
×
153
            if (group != null) {
×
NEW
154
                logger.printLn("group " + commandParts[1] + " "
×
155
                    + String.join(",", group.getGroupDefinitions()));
×
156
                printGroupExtensions(group.getExtensions());
×
157
            }
158
        }
159
    }
×
160

161
    private void unrollGroup(String command, Map<String, FileCountEntry> statsByExtension) {
162
        String[] commandParts = command.split(" ");
×
163
        if (commandParts.length != 2) {
×
NEW
164
            logger.printError("Invalid rmgroup command! Expected rmgroup <name>, e.g. rmgroup images");
×
165
            return;
×
166
        }
167

168
        String groupName = normalizeGroupNameOrPrintError(commandParts[1]);
×
169
        FileGroupCount group = groupName == null ? null : getGroupOrPrintError(groupName, statsByExtension);
×
170
        if (group != null) {
×
171
            List<FileExtensionCount> extensions = group.getExtensions();
×
NEW
172
            logger.printLn("Confirm removal of group '" + groupName + "'? This will restore "
×
173
                + extensions.size() + " entries:");
×
174
            printGroupExtensions(extensions);
×
175

NEW
176
            logger.printLn("Type 'y' to confirm removal");
×
NEW
177
            String input = logger.getNextLine();
×
178
            if ("y".equalsIgnoreCase(input)) {
×
179
                group.getExtensions().forEach(ext -> statsByExtension.put(ext.getExtension(), ext));
×
180
                statsByExtension.remove(groupName);
×
181
            } else {
NEW
182
                logger.printLn("Aborted removal of the group");
×
183
            }
184
        }
185
    }
×
186

187
    private void unrollGroups(Map<String, FileCountEntry> statsByExtension) {
188
        List<String> groupNames = new ArrayList<>();
×
189
        List<Map.Entry<String, FileGroupCount>> groupEntries = statsByExtension.entrySet().stream()
×
190
            .filter(entry -> entry.getValue() instanceof FileGroupCount)
×
191
            .map(entry -> (Map.Entry<String, FileGroupCount>) (Map.Entry) entry)
×
192
            .peek(entry -> groupNames.add(entry.getKey()))
×
193
            .collect(Collectors.toList());
×
194
        if (groupEntries.isEmpty()) {
×
NEW
195
            logger.printLn("There currently are no groups.");
×
196
        } else {
NEW
197
            logger.printLn("Confirm removal of " + groupEntries.size() + " groups: "
×
198
                + String.join(", ", groupNames));
×
NEW
199
            logger.printLn("The statistics for the individual file extensions will be restored.");
×
200
            System.out.print("Type 'y' to confirm removal: ");
×
NEW
201
            String input = logger.getNextLine();
×
202

203
            if ("y".equalsIgnoreCase(input)) {
×
204
                long extensionsRestored = groupEntries.stream()
×
205
                    .peek(grp -> statsByExtension.remove(grp.getKey()))
×
206
                    .flatMap(grp -> grp.getValue().getExtensions().stream())
×
207
                    .peek(extEntry -> statsByExtension.put(extEntry.getExtension(), extEntry))
×
208
                    .count();
×
NEW
209
                logger.printLn("Restored " + extensionsRestored + " file extension entries");
×
210
            } else {
×
NEW
211
                logger.printLn("Aborted removal of groups");
×
212
            }
213
        }
214
    }
×
215

216
    private void toggleDetails() {
217
        boolean newDetailsValue = !configuration.getValue(FILE_COUNT_DETAILED_GROUPS);
×
218
        configuration.setValue(FILE_COUNT_DETAILED_GROUPS, newDetailsValue);
×
NEW
219
        logger.printLn("File extensions of groups are now " + (newDetailsValue ? "shown" : "hidden"));
×
220
    }
×
221

222
    private void saveConfiguration(Map<String, FileCountEntry> statsByExtension) {
223
        List<Map.Entry<String, FileGroupCount>> groupEntries = statsByExtension.entrySet().stream()
×
224
            .filter(entry -> entry.getValue() instanceof FileGroupCount)
×
225
            .map(entry -> (Map.Entry<String, FileGroupCount>) (Map.Entry) entry)
×
226
            .collect(Collectors.toList());
×
227
        if (groupEntries.isEmpty()) {
×
NEW
228
            logger.printError("There are no group entries to save");
×
229
        } else {
230
            String groupDefinitions = groupEntries.stream()
×
231
                .map(grp -> grp.getKey() + " " + String.join(",", grp.getValue().getGroupDefinitions()))
×
232
                .collect(Collectors.joining("; "));
×
233
            configuration.setValue(FILE_COUNT_GROUPS, groupDefinitions);
×
234
            configuration.save();
×
NEW
235
            logger.printLn("Saved group definitions to the file");
×
236
        }
237
    }
×
238

239
    private void printGroupExtensions(List<FileExtensionCount> extensions) {
240
        boolean formatSize = configuration.getValue(FORMAT_FILE_SIZE);
×
241

242
        for (FileExtensionCount extension : extensions) {
×
NEW
243
            logger.printLn(" * " + formatEntry(extension.getExtension(), extension, formatSize));
×
244
        }
×
245
    }
×
246

247
    @Nullable
248
    private FileGroupCount getGroupOrPrintError(String groupName, Map<String, FileCountEntry> statsByExtension) {
249
        FileCountEntry entry = statsByExtension.get(groupName);
×
250
        if (entry instanceof FileGroupCount) {
×
251
            return (FileGroupCount) entry;
×
252
        } else if (entry == null) {
×
NEW
253
            logger.printError("There is no group with name '" + groupName + "'");
×
254
        } else {
NEW
255
            logger.printError("Entry '" + groupName + "' is a file extension, not a group!");
×
256
        }
257
        return null;
×
258
    }
259

260
    private Stream<FileExtensionCount> removeAndReturnExtensions(String extension,
261
                                                                 Map<String, FileCountEntry> statsByExtension) {
262
        if (extension.startsWith("p:")) {
×
263
            Pattern pattern = Pattern.compile(extension.substring(2));
×
264
            Iterator<Map.Entry<String, FileCountEntry>> it = statsByExtension.entrySet().iterator();
×
265
            List<FileExtensionCount> matchedStats = new ArrayList<>();
×
266
            while (it.hasNext()) {
×
267
                Map.Entry<String, FileCountEntry> entry = it.next();
×
268
                if (pattern.matcher(entry.getKey()).matches()) {
×
269
                    it.remove();
×
270
                    matchedStats.add((FileExtensionCount) entry.getValue());
×
271
                }
272
            }
×
273
            return matchedStats.stream();
×
274
        }
275

276
        String extensionInProperCase = extension.equalsIgnoreCase(NO_EXTENSION_TEXT)
×
277
            ? NO_EXTENSION_TEXT
×
278
            : extension.toLowerCase(Locale.ROOT);
×
279
        FileCountEntry entryWithExtension = statsByExtension.remove(extensionInProperCase);
×
280
        return entryWithExtension == null ? Stream.empty() : Stream.of((FileExtensionCount) entryWithExtension);
×
281
    }
282

283
    @Nullable
284
    private String normalizeGroupNameOrPrintError(String groupName) {
285
        if (groupName.equals(NO_EXTENSION_TEXT)) {
×
NEW
286
            logger.printError("Invalid group name; \"" + NO_EXTENSION_TEXT
×
287
                + "\" is how files without extension are identified");
288
        } else if (groupName.startsWith(".")) {
×
NEW
289
            logger.printError("Group names may not start with a dot");
×
290
        } else {
291
            return LOWER_CAMEL.to(UPPER_CAMEL, groupName);
×
292
        }
293
        return null;
×
294
    }
295

296
    private void handleSortCommand(String sortCommand, Map<String, FileCountEntry> stats) {
297
        String[] cmdParts = sortCommand.split(" ");
×
298
        if (cmdParts.length < 2) {
×
NEW
299
            logger.printError("Invalid sort command! Expected 'sort size' or 'sort count asc'");
×
300
            return;
×
301
        }
302

303
        boolean isDescending = cmdParts.length >= 3 && "desc".equals(cmdParts[2]);
×
304
        Comparator<FileCountEntry> comparator;
305

306
        switch (cmdParts[1]) {
×
307
            case "size":
308
                comparator = createComparator(FileCountEntry::getTotalSizeInBytes, isDescending);
×
309
                break;
×
310
            case "count":
311
                comparator = createComparator(FileCountEntry::getCount, isDescending);
×
312
                break;
×
313
            default:
NEW
314
                logger.printError("Unknown sort property. Use size or count");
×
315
                return;
×
316
        }
317

318
        boolean formatFileSize = configuration.getValue(FORMAT_FILE_SIZE);
×
319
        boolean includeGroupDetails = configuration.getValue(FILE_COUNT_DETAILED_GROUPS);
×
320
        FileCountTotalEntry totalEntry = new FileCountTotalEntry();
×
321

322
        stats.entrySet().stream()
×
323
            .sorted(Map.Entry.comparingByValue(comparator))
×
324
            .forEach(entry -> {
×
NEW
325
                logger.printLn(formatEntry(entry.getKey(), entry.getValue(), formatFileSize));
×
326
                totalEntry.add(entry.getValue());
×
327

328
                if (includeGroupDetails && entry.getValue() instanceof FileGroupCount) {
×
329
                    FileGroupCount group = (FileGroupCount) entry.getValue();
×
330
                    group.getExtensions().stream()
×
331
                        .sorted(comparator)
×
332
                        .forEach(ext -> {
×
NEW
333
                            logger.printLn(" * " + formatEntry(ext.getExtension(), ext, formatFileSize));
×
334
                        });
×
335
                }
336
            });
×
NEW
337
        logger.printLn("");
×
NEW
338
        logger.printLn(formatEntry("Total", totalEntry, formatFileSize));
×
UNCOV
339
    }
×
340

341
    private static <V extends Comparable<V>> Comparator<FileCountEntry> createComparator(
342
                                                                                   Function<FileCountEntry, V> getter,
343
                                                                                   boolean isDescending) {
344
        Comparator<FileCountEntry> comparator = Comparator.comparing(getter);
×
345
        return isDescending ? comparator.reversed() : comparator;
×
346
    }
347

348
    private String formatEntry(String designation, FileCountEntry entry, boolean formatSize) {
349
        String size = formatSize
×
350
            ? FileSizeUtils.formatToHumanReadableSize(entry.getTotalSizeInBytes().longValue())
×
351
            : entry.getTotalSizeInBytes().toPlainString();
×
352
        return designation + ": " + entry.getCount() + " (" + size + ")";
×
353
    }
354

355
    private void outputHelp() {
NEW
356
        logger.printLn("- Use 'sort' to output results: sort <size/count> [desc], e.g.");
×
NEW
357
        logger.printLn("  * sort size");
×
NEW
358
        logger.printLn("  * sort count desc");
×
NEW
359
        logger.printLn("- Use 'group' to group extensions together: group <groupName> <extensionList>, e.g.");
×
NEW
360
        logger.printLn("  * group images .jpg,.jpeg,.png");
×
NEW
361
        logger.printLn("  * group text .txt,.html,.md");
×
NEW
362
        logger.printLn("  * Regex possible with 'p:', e.g. group web p:\\.x?html?,.css,p:\\.php\\d?");
×
NEW
363
        logger.printLn("- Use 'groups' to view the definition of groups: groups [name]");
×
NEW
364
        logger.printLn("  * Optionally, provide the group name to see details (e.g. 'groups images')");
×
NEW
365
        logger.printLn("- Use 'details' to show/hide the file extensions of a group in the output");
×
NEW
366
        logger.printLn("- Use 'saveconf' to save the configurations (e.g. group definitions) "
×
367
            + "to the configuration file");
NEW
368
        logger.printLn("- Use 'rmgroup' to remove a group.");
×
NEW
369
        logger.printLn("  * The extensions of the group will be restored as individual entries");
×
NEW
370
        logger.printLn("- Use 'rmgroups' to delete ALL groups and restore the original extensions.");
×
NEW
371
        logger.printLn("- Use 'exit' to quit.");
×
UNCOV
372
    }
×
373
}
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

© 2026 Coveralls, Inc