001package Torello.Java.Build;
002
003import Torello.Java.Shell;
004import Torello.Java.OSResponse;
005import Torello.Java.OSExtras;
006import Torello.Java.FileNode;
007import Torello.Java.StrCmpr;
008import Torello.Java.FileRW;
009import Torello.Java.RTC;
010
011import Torello.Java.Additional.BiAppendable;
012import Torello.Java.Additional.AppendableSafe;
013import Torello.Java.ReadOnly.ROArrayListBuilder;
014import Torello.Java.ReadOnly.ReadOnlyList;
015
016import static Torello.Java.C.*;
017
018import java.io.IOException;
019import java.io.File;
020import java.io.FilenameFilter;
021import java.io.FileFilter;
022
023import java.util.TreeMap;
024import java.util.stream.Stream;
025
026/**
027 * This is the fourth Build-Stage, and it runs the UNIX-Shell utility {@code 'tar'} and the Java 
028 * utility {@code 'jar'}.  These operations are performed via {@link Shell Torello.Java.Shell}.
029 * 
030 * <EMBED CLASS=external-html DATA-FILE-ID=STAGE_PRIVATE_NOTE>
031 * <EMBED CLASS='external-html' DATA-FILE-ID=S04_TAR_JAR>
032 */
033@Torello.JavaDoc.StaticFunctional
034public class S04_TarJar
035{
036    // Completely irrelevant, and the 'private' modifier keeps it off of JavaDoc
037    private S04_TarJar() { }
038
039    private static final String FS = File.separator;
040
041
042    // ********************************************************************************************
043    // ********************************************************************************************
044    // This class MAIN METHOD
045    // ********************************************************************************************
046    // ********************************************************************************************
047
048
049    public static void compress(final BuilderRecord brec) throws IOException
050    {
051        brec.timers.startStage04();
052
053        final StringBuilder SB_TAR = new StringBuilder();
054        final StringBuilder SB_JAR = new StringBuilder();
055
056        Printing.startStep(4);
057
058
059        // If the user selects "-4", he has the option of using the Auxilliary-Switch
060        // -JFO, --jarFileOnly.  In such cases, only the '.jar' will be built
061        //
062        // NOTE: (as a reminder) Here is the "Data-Record Explanation"
063        // 
064        // brec: BuilderRecord - Top-of-Tree Data-Record for executing a build, Contains everything
065        // cli:  (Command-Line-Interface) - All info gathered from user-input to "String[] argv"
066        // aor:  Auxiliary-Options Record - A Record containing only final / constant booleans,
067        //       whose values are all exactly equal to whether or not each of the 6 or 7 Auxiliary
068        //       options were passed, by the user, to "String[] argv" at the command line.
069
070        if (brec.cli.aor.JAR_FILE_ONLY_SWITCH)
071        {
072            jarFile(brec, SB_JAR);
073
074
075            // IMPORTANT NOTE: Have a moment of clarity, you are going to see the light now.
076            // 
077            // Field bred.JAR_FILE_NAME is a User-Provided Configuration that allows the user
078            // to tell the Build-Tool where to store the '.jar' File.  It is not the same as
079            // the Auto-Generated name of the '.jar' File that is initially constructed.  The
080            // user can request it be copied somehwere into his File-System, and he can also
081            // change the name to whatever he wants.  
082    
083            if (brec.JAR_FILE_NAME != null)
084            {
085                System.out.println(
086                    "Moving " + BYELLOW + brec.JAR_FILE + RESET + " to " + 
087                    BYELLOW + brec.JAR_FILE_NAME + RESET + '\n'
088                );
089    
090                FileRW.moveFile(brec.JAR_FILE, brec.JAR_FILE_NAME, false);
091            }
092        }
093
094        else 
095        {
096            twoTarFiles(brec, SB_TAR);
097
098            System.out.println();
099
100            jarFile(brec, SB_JAR);
101
102            brec.logs.write_S04_LOGS(SB_TAR.toString(), SB_JAR.toString());
103        }
104
105        brec.timers.endStage04();
106    }
107
108
109    // ********************************************************************************************
110    // ********************************************************************************************
111    // Build the 2 TAR-Files, Build the 1 JAR-File
112    // ********************************************************************************************
113    // ********************************************************************************************
114
115
116    private static void twoTarFiles
117        (final BuilderRecord brec, final StringBuilder SB_TAR)
118        throws IOException
119    {
120        // NOTE: It is OK to print the entire command directly to standard output, because the
121        //       '.tar' Files are very short commands, that only tar-up a single directory
122        //
123        // MAIN-TAR       ==> Root-Source
124        // JAVA-DOC-TAR   ==> 'javadoc/'
125        //
126        // Shell-Constructor Paramters used:
127        // (outputAppendable, commandStrAppendable, standardOutput, errorOutput)
128
129        Shell shell = new Shell(SB_TAR, new BiAppendable(SB_TAR, System.out), null, null);
130
131        // JavaHTML-1.x.tar.gz
132        CHECK(
133            shell.COMMAND(
134                "tar",
135                new String[] { "-cvzf", brec.TAR_FILE, brec.TAR_SOURCE_DIR }
136            ));
137
138        SB_TAR.append(Printing.TAR_JAR_DIVIDER);
139
140        // JavaHTML-javadoc-1.x.tar
141        CHECK(
142            shell.COMMAND(
143                "tar",
144                new String[] { "-cvf", brec.JAVADOC_TAR_FILE, brec.LOCAL_JAVADOC_DIR }
145            ));
146    }
147
148
149    // ********************************************************************************************
150    // ********************************************************************************************
151    // Build the JAR-File
152    // ********************************************************************************************
153    // ********************************************************************************************
154
155
156    private static void jarFile(final BuilderRecord brec, final StringBuilder logOnly)
157        throws IOException
158    {
159        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
160        // Setup some variables / constants for running this class
161        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
162        // 
163        // Uses Shell-Contructor:
164        // (outputAppendable, commandStrAppendable, standardOutput, errorOutput)
165
166        final Shell shell = new Shell(logOnly, logOnly, null, null);
167
168        final AppendableSafe logAndScreen = new AppendableSafe(
169            new BiAppendable(System.out, logOnly),
170            AppendableSafe.USE_APPENDABLE_ERROR
171        );
172
173
174        // The first loop-iteration uses a different command-switch to the 'jar' command
175        //  *** First jar-command uses:         "-cvf" (Create, Verbose, Files)
176        //  *** Successive jar-commands use:    "-uvf" (Update, Verbose, Files)
177
178        boolean firstIteration = true;
179
180
181        // The Shell 'jar' command must be executed from the File-System directory set to the
182        // "Current Working Directory" - or else the location inside the '.jar' will just be all
183        // messed up!
184        //
185        // NOTE: This cute little thing here was A LOT of work, not just a little bit.  The
186        //       "Relative Path" within the jar cannot include extranneous class-path directory
187        //       stuff.  In the end, using it just 3 lines of code!
188
189        final OSExtras osExtras = new OSExtras();
190
191
192        // This creates a 'FileFilter' that is **ALSO** to configured to print out directories that
193        // match - into the provided StringBuilder - 'logAndScreen'
194        // 
195        // Prior to printing out the directories it accepts, "getSubPackageDirFilter" was a private 
196        // static final constant named "PACKAGE_DIR_FILTER" (or something like that - it wasn't a
197        // method is the only point).
198
199        final FileFilter JAR_SUB_PACKAGES_DIR_FILTER = getSubPackageDirFilter(logAndScreen);
200
201
202        // Since the Shell & OSExtras combo is being used - where the command is actually executed
203        // from a separate directory, the Jar-File needs to be referenced using its absolute path
204        // name
205
206        final String JAR_FILE_NAME_ABSOLUTE = brec.HOME_DIR + brec.JAR_FILE;
207
208
209        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
210        // Loop all Packages in the Project
211        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
212
213        for (BuildPackage bp : brec.packageList)
214        {
215            // The user is allowed to specify (via the "BuildPackage" Constructor and one of that 
216            // classes flags), to avoid / skip putting some packages into the '.jar'
217
218            if (bp.doNotJAR) continue;
219
220            // Classes considered to be "still in early development" are also not put into the jar
221            if (bp.earlyDevelopment) continue;
222
223            // Since this is just **TOO** long, use a "System.out" that just says what it's doing
224            final String note =
225                "Add to '.jar' File: " + bp.fullName +
226                (bp.hasSubPackages ? (" & " + BCYAN + "Sub-Packages" + RESET) : "") +
227                 '\n';
228
229            logAndScreen.append(note);
230
231
232            // This is how to PROPERLY insert files into the JAR - making-sure that their actual
233            // File-Name DOES-NOT include the Class-Path part of the directory-location, *AND* 
234            // DOES include the Java-Package part of the diretory-location.
235            //
236            // For "MyProjects/src/main/My/Java/Package/SourceFile.java"
237            // The Name in the JAR for any '.class' File whose package is named:
238            //      "My.Java.Package.SomeSourceFile"
239            //
240            // SHOULD INCLUDE:      "My/Java/Package/SomeSourceFile.class"
241            // SHOULD NOT INCLUDE:  "MyProjects/src/main/"
242            //
243            // That is all this "cpStrLen" and "classPathLocationStr-Length" and "map(substring)"
244            // stuff is actually trying to acheive.  (And doing it very well, I might add)
245            // I am choosing to call this "JAR File Integrity"
246
247            final int cpStrLen = bp.classPathLocation.length();
248
249            /*
250            System.out.println(
251                "bp.classPathLocation: [" + bp.classPathLocation + ']' + '\n' +
252                "bp.cpl.length():       " + bp.classPathLocation.length()
253            );
254            */
255
256
257            // Unlike the TAR-Files, when creating the JAR-File, the actual files being inserted
258            // have to be NAMED, EXPLICITLY.  As a result, class FileNode needs to be used.
259            // Also note that because of this reason, the jar-commands will not be printed to the
260            // user's terminal (because they grow very long and unreadable / ugly).
261
262            final Stream.Builder<String> b = FileNode
263                .createRoot(bp.pkgRootDirectory)
264                .loadTree(
265                    bp.hasSubPackages ? -1 : 0,
266                    JAR_FILENAME_FILTER,
267                    bp.hasSubPackages ? JAR_SUB_PACKAGES_DIR_FILTER : null
268                )
269                .flattenJustFiles(RTC.FULLPATH_STREAM_BUILDER(Stream.builder()));
270
271
272            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
273            // Any & All User-Provided / User-Specified "Helper Package" Sub-Directories
274            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
275            // 
276            // NOTE: the "BuildPackage.helperPackages" is just a User-Provided Configuration that
277            //       allows the user to add some brief sub-packages / sub-directories that remain
278            //       undocumented (and, therefore, invisible) - but are still imported into the JAR
279            //
280            // The "Helper Packages" are very few/rare.  As of August 2024, only the "api"
281            // sub-directory of the Glass-Fish JSON Parser is a helper package, as is a small
282            // "hidden" package associated with Torello.JavaDoc
283            //
284            // Stuff inside of a Helper Package wasn't important enough to include it in the
285            // JAR-API, and it is not documented (not included in Java-Doc).  The classes and
286            // methods they export are, indeed, public - but they just aren't mentioned anywhere
287            // because the end-users wouldn't be able to do anything with them.
288            // 
289            // They cannot simply be declared "Package-Private" because they are used in other
290            // packages within this JAR Library.
291
292            final RTC<Stream.Builder<String>> rtcb = RTC.FULLPATH_STREAM_BUILDER(b);
293
294            for (String helperPkgDirName : bp.helperPackages)
295            {
296                logAndScreen.append(
297                    "    Adding Helper-Package Files: " +
298                    BCYAN + helperPkgDirName + RESET + '\n'
299                );
300
301                FileNode
302                    .createRoot(helperPkgDirName)
303                    .loadTree(0, JAR_FILENAME_FILTER, null)
304                    .flattenJustFiles(rtcb);
305            }
306
307
308            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
309            // The "data-files/" directory
310            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
311            // 
312            // If there is a "data-files/" sub-directory of a Package-Directory, then all of it's
313            // contents also need to be added to the '.jar'
314
315            final String    fDataDirName    = bp.pkgRootDirectory + "data-files" + FS;
316            final File      fDataDir        = new File(fDataDirName);
317
318            // FIRST: Check if there is a package/data-files/ directory
319            if (fDataDir.exists() && fDataDir.isDirectory())
320                addDataFilesDir(fDataDirName, b, logAndScreen);
321
322
323            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
324            // Now Convert the Stream.Builder<String> into a String[] array
325            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
326            //
327            // In order to actually utilize the Class-Path / cpStrLen-Substring stuff, the building
328            // of these '.class' File String[]-Arrays is done using the ? : syntax thingy...
329
330            final String[] jarFilesArr = (cpStrLen == 0)
331                ? b.build().toArray(String[]::new)
332                : b.build().map(fileName -> fileName.substring(cpStrLen)).toArray(String[]::new);
333
334
335            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
336            // Run the '.jar' Command
337            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
338            //
339            // Now just build the Operating-System 'jar' Command.
340            // All that needs to happen is to concatenate the 2 command switches and the list of
341            // files from the jar-files String[]-Array that was just created previously.
342
343            String[] JAR_COMMAND = new String[2 + jarFilesArr.length];
344
345            JAR_COMMAND[0] = firstIteration ? "-cvf" : "-uvf";
346            JAR_COMMAND[1] = JAR_FILE_NAME_ABSOLUTE;
347            System.arraycopy(jarFilesArr, 0, JAR_COMMAND, 2, jarFilesArr.length);
348
349            if (cpStrLen > 0)
350            {
351                // REMEMBER: shell.osExtras is re-assigned null after each and every invocation
352                //           shell.printAndRun().  This resetting to null is done INSIDE class
353                //           OSCommands.
354                //
355                // Note that, here, we are currently inside of a for-loop, and this instruction
356                // will be executed each time the for-loop iterates - and, of course, when 
357                // 'cpStrLen' is greater than zero (meaning that the current 'BuildPackage'
358                // being iterated by the loop isn't in the CWD from whence the BuilderRecord class
359                // was executed, but rather some sub-directory, which is the whole reason an
360                // 'osExtras' instance is necessary)
361
362                osExtras.currentWorkingDirectory = new File(bp.classPathLocation);
363                shell.osExtras = osExtras;
364            }
365
366
367            // Run the Command using class Shell.  "CHECK" will actually halt the entire JRE using
368            // System.exit if there were any errors (and print a thoughtfully worded error-message)
369
370            CHECK(shell.COMMAND("jar", JAR_COMMAND));
371
372            // This prints a total of how many files were just inserted into the jar
373            printTotals(jarFilesArr, logAndScreen);
374
375            logOnly.append(Printing.TAR_JAR_DIVIDER);
376
377            firstIteration = false;
378        }
379
380
381        // This chunk-o-stuff was relocated to its own dedicated method.  The method simply
382        // iterates the "jarIncludes" ReadOnlyList, and adds each directory listed in the list
383        // directly into the Jar-File.  A "JarInclude" is stuff like the "META-INF" - but it may be
384        // essentially anything else that needs to be placed into the '.jar'
385        //
386        // The user controls / configures this idea by instantiating some "JarInclude's", and
387        // putting into the BuilderRecord's Configuration-Constructor class "Config"
388
389        if ((brec.jarIncludes != null) && (brec.jarIncludes.size() > 0))
390
391            addJarExtras(
392                brec.jarIncludes, JAR_FILE_NAME_ABSOLUTE,
393                shell, osExtras, logAndScreen, logOnly
394            );
395    }
396
397
398    // ********************************************************************************************
399    // ********************************************************************************************
400    // HELPERS FOR: "Build JAR-File"
401    // ********************************************************************************************
402    // ********************************************************************************************
403
404
405    private static final FilenameFilter DEFAULT_DATA_DIR_FILTER = (File dir, String name) ->
406        StrCmpr.endsWithNAND(name, ".java", ".class");
407
408    // Each 'BuildPackage' that is included by the 'BuilderRecord' may contain a `data-files`
409    // directory.  When there is a `data-files` directory, each file in that directory ALSO NEEDS
410    // TO BE ADDED into the JAR.
411    //
412    // If the user so chooses to provide a DataFilesList.txt file inside his Package's data-files/
413    // directory, he may do so.  All this file is intended to do is specify which files inside that
414    // directory actually need to be inserted into the JAR.
415    //
416    // If there is no 'DataFilesList.txt' file, then **ALL** files in the data-files/ subdirectory
417    // are inserted into the '.jar'
418
419    private static void addDataFilesDir(
420            final String                    fDataDirName,
421            final Stream.Builder<String>    b,
422            final AppendableSafe            logAndScreen
423        )
424        throws IOException
425    {
426        logAndScreen.append
427            ("    Adding '" + BCYAN + "data-files/" + RESET + "' Directory-Contents\n");
428
429        final String    fDataFilesListFileName  = fDataDirName + "DataFilesList.txt";
430        final File      fDataFilesListFile      = new File(fDataFilesListFileName);
431
432        // Check if there is a src-package/data-files/DataFilesList.txt file
433        // If there is a "DataFilesList.txt" file, convert that into a File-Name List.
434
435        if (fDataFilesListFile.exists() && fDataFilesListFile.isFile()) FileRW
436            .loadFileToStream(fDataFilesListFileName, false)
437            .map((String line) -> line.trim())
438            .filter((String line) -> line.length() > 0)
439            .filter((String line) -> ! line.startsWith("#"))
440            .map((String fileName) -> fDataDirName + fileName)
441            .forEachOrdered((String dataFileName) -> b.accept(dataFileName));
442
443        else FileNode
444            .createRoot(fDataDirName)
445            .loadTree(-1, DEFAULT_DATA_DIR_FILTER, null)
446            .flattenJustFiles(RTC.FULLPATH_STREAM_BUILDER(b));
447    }
448
449
450    // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
451    // Add the '.jar' File-Extras.  (Uses the "JarInclude" User Data-Configuration Class)
452    // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
453    //
454    // Some examples of "Jar-Extras" can (hopefully / probably) be seen with the Jar-Includes from
455    // Java-HTML.  These are things like the "META-INF" directories, and the Data-File Directory.
456    //
457    //  final FileNodeFilter FILTER = (FileNode fn) ->
458    //      StrCmpr.endsWithNAND(fn.name, ".java", ".class");
459    //
460    //  return new JarInclude()
461    //      .add("", "Torello/Data/", true, FILTER, TRUE_FILTER)
462    //      .add("Torello/BuildJAR/IncludeFiles/", "META-INF/", true, TRUE_FILTER, TRUE_FILTER)
463    //      .add("Torello/etc/External/", "META-INF/", true, TRUE_FILTER, TRUE_FILTER);
464
465    private static void addJarExtras(
466            final Iterable<JarInclude.Descriptor> includes,
467            final String            JAR_FILE_NAME_ABSOLUTE,
468            final Shell             shell,
469            final OSExtras          osExtras,
470            final AppendableSafe    logAndScreen,
471            final StringBuilder     logOnly
472        )
473        throws IOException
474    {
475        for (JarInclude.Descriptor jid : includes)
476        {
477            logAndScreen.append("Adding Jar-Include: " + jid.toString() + '\n');
478
479            final int cpStrLen = jid.workingDirectory.length();
480
481            final FileNode tempFN = FileNode
482                .createRoot(jid.workingDirectory + jid.subDirectory)
483                .loadTree((jid.traverseTree ? -1 : 0), null, null);
484
485            final String[] extraFiles = (cpStrLen == 0)
486
487                ? tempFN.flatten
488                    (RTC.FULLPATH_ARRAY(), -1, jid.fileFilter, true, jid.dirFilter, false)
489
490                : tempFN
491                    .flatten(RTC.FULLPATH_STREAM(), -1, jid.fileFilter, true, jid.dirFilter, false)
492                    .map((String fileName) -> fileName.substring(cpStrLen))
493                    .toArray(String[]::new);
494
495            String[] JAR_COMMAND = new String[2 + extraFiles.length];
496
497            JAR_COMMAND[0] = "-uvf";
498            JAR_COMMAND[1] = JAR_FILE_NAME_ABSOLUTE;
499            System.arraycopy(extraFiles, 0, JAR_COMMAND, 2, extraFiles.length);
500
501            if (cpStrLen > 0)
502            {
503                osExtras.currentWorkingDirectory = new File(jid.workingDirectory);
504                shell.osExtras = osExtras;
505            }
506    
507            CHECK(shell.COMMAND("jar", JAR_COMMAND));
508
509            printTotals(extraFiles, logAndScreen);
510
511            logOnly.append(Printing.TAR_JAR_DIVIDER);
512        }
513    }
514
515
516    // ********************************************************************************************
517    // ********************************************************************************************
518    // JAR-File: MORE HELPER's.  Simpler ones
519    // ********************************************************************************************
520    // ********************************************************************************************
521
522
523    private static void CHECK(OSResponse osr)
524    {
525        if (osr.errorOutput.length() > 0)
526        {
527            System.out.println
528                (BRED + "\nTEXT PRINTED TO STANDARD ERROR:\n" + RESET + osr.errorOutput);
529
530            Util.ERROR_EXIT("Build Tar-Jar (using Regular Shell)");
531        }
532    }
533
534    // This allows for '.class' files, but  also includes anything else in the directory - for
535    // instance ".properties" files, or other stuff that may or may not have been left there.  It
536    // seems a little questionable, but I cannot think of any generalized rule for this.  So, in
537    // the end, it just makes sure to leave out the '.java' file
538    //
539    // Remember: javax.json has a ".properties" file...  That's really the SINGLE-ONLY REASON that
540    //           this doesn't just say instead (more specifically): name.endsWith(".class")
541
542    private static final FilenameFilter JAR_FILENAME_FILTER =
543        (File file, String name) -> ! name.endsWith(".java");
544
545    // This isn't used at all - EXCEPT when a 'BuildPackage' claims to have "Sub-packages"
546    // "SubPackages" means that the entire directory-tree rooted at bp.pkgRootDirectory (and all of
547    // its '.class' files) needs to be included in the '.jar'
548    //
549    // NOTE: This essentially talking about the Packages: JDUInternal and BuildJAR
550    // FURTHERMORE: Since 'BuildJAR' isn't put into the '.jar', this is only used for JDUInternal
551    //
552    // The last addition to this thing was to make it print out the directories that match so that
553    // the build script tells the user all of the sub-packages.  It looks nice, however, whereas
554    // this used to be a 'FileFilter' **FIELD**, it is now a **METHOD** that returns a 'FileFilter'
555
556    private static final FileFilter getSubPackageDirFilter(AppendableSafe logAndScreen)
557    {
558        // This is creating a Lambda/Predicate that **ALSO** prints out the directories that match
559        return (File file) ->
560        {
561            final String path = file.getPath();
562
563            final boolean ret = ! StrCmpr.containsOR
564                (path, "upgrade-files", "doc-files", "package-source");
565
566            if (ret) logAndScreen.append
567                ("    Adding Sub-Package Files from: " + BCYAN + path + RESET + '\n');
568
569            return ret;
570        };
571    }
572
573    // Just prints out a count of how many files (and the file types) that were just inserted
574    // on the most recent loop-iteration, into the JAR.
575
576    private static void printTotals(String[] files, Appendable a) throws IOException
577    {
578        TreeMap<String, Integer> tm = new TreeMap<>();
579
580        for (String fileName : files)
581        {
582            int slashPos    = fileName.lastIndexOf(FS);
583            int dotPos      = fileName.lastIndexOf('.');
584
585            final String fileExt = ((dotPos == -1) || (slashPos > dotPos))
586                ? "NOEXT"
587                : fileName.substring(dotPos);
588
589            tm.compute(
590                fileExt,
591                (String dummy, Integer count) -> (count == null) ? 1 : (count+1)
592            );
593        }
594
595        for (String fileExt : tm.keySet()) a.append(
596            "    Added " +
597            BGREEN + tm.get(fileExt) + RESET +
598            (fileExt.equals("NOEXT")
599                ? (BRED + " Other " + RESET)
600                : (" '" + BYELLOW + fileExt + RESET + "' ")) +
601            "Files.\n"
602        );
603
604        // for (String fileName : files) System.out.print(fileName + ", ");
605        // System.out.println();
606
607        a.append('\n');
608    }
609}