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.Annotations.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            if (jarFilesArr.length == 0)
335            {
336                System.out.println("There are no Files to Jar.  It's going to complain");
337                throw new BuildError();
338            }
339
340            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
341            // Run the '.jar' Command
342            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
343            //
344            // Now just build the Operating-System 'jar' Command.
345            // All that needs to happen is to concatenate the 2 command switches and the list of
346            // files from the jar-files String[]-Array that was just created previously.
347
348            String[] JAR_COMMAND = new String[2 + jarFilesArr.length];
349
350            JAR_COMMAND[0] = firstIteration ? "-cvf" : "-uvf";
351            JAR_COMMAND[1] = JAR_FILE_NAME_ABSOLUTE;
352            System.arraycopy(jarFilesArr, 0, JAR_COMMAND, 2, jarFilesArr.length);
353
354            if (cpStrLen > 0)
355            {
356                // REMEMBER: shell.osExtras is re-assigned null after each and every invocation
357                //           shell.printAndRun().  This resetting to null is done INSIDE class
358                //           OSCommands.
359                //
360                // Note that, here, we are currently inside of a for-loop, and this instruction
361                // will be executed each time the for-loop iterates - and, of course, when 
362                // 'cpStrLen' is greater than zero (meaning that the current 'BuildPackage'
363                // being iterated by the loop isn't in the CWD from whence the BuilderRecord class
364                // was executed, but rather some sub-directory, which is the whole reason an
365                // 'osExtras' instance is necessary)
366
367                osExtras.currentWorkingDirectory = new File(bp.classPathLocation);
368                shell.osExtras = osExtras;
369            }
370
371
372            // Run the Command using class Shell.  "CHECK" will actually halt the entire JRE using
373            // System.exit if there were any errors (and print a thoughtfully worded error-message)
374
375            CHECK(shell.COMMAND("jar", JAR_COMMAND));
376
377            // This prints a total of how many files were just inserted into the jar
378            printTotals(jarFilesArr, logAndScreen);
379
380            logOnly.append(Printing.TAR_JAR_DIVIDER);
381
382            firstIteration = false;
383        }
384
385
386        // This chunk-o-stuff was relocated to its own dedicated method.  The method simply
387        // iterates the "jarIncludes" ReadOnlyList, and adds each directory listed in the list
388        // directly into the Jar-File.  A "JarInclude" is stuff like the "META-INF" - but it may be
389        // essentially anything else that needs to be placed into the '.jar'
390        //
391        // The user controls / configures this idea by instantiating some "JarInclude's", and
392        // putting into the BuilderRecord's Configuration-Constructor class "Config"
393
394        if ((brec.jarIncludes != null) && (brec.jarIncludes.size() > 0))
395
396            addJarExtras(
397                brec.jarIncludes, JAR_FILE_NAME_ABSOLUTE,
398                shell, osExtras, logAndScreen, logOnly
399            );
400    }
401
402
403    // ********************************************************************************************
404    // ********************************************************************************************
405    // HELPERS FOR: "Build JAR-File"
406    // ********************************************************************************************
407    // ********************************************************************************************
408
409
410    private static final FilenameFilter DEFAULT_DATA_DIR_FILTER = (File dir, String name) ->
411        StrCmpr.endsWithNAND(name, ".java", ".class");
412
413    // Each 'BuildPackage' that is included by the 'BuilderRecord' may contain a `data-files`
414    // directory.  When there is a `data-files` directory, each file in that directory ALSO NEEDS
415    // TO BE ADDED into the JAR.
416    //
417    // If the user so chooses to provide a DataFilesList.txt file inside his Package's data-files/
418    // directory, he may do so.  All this file is intended to do is specify which files inside that
419    // directory actually need to be inserted into the JAR.
420    //
421    // If there is no 'DataFilesList.txt' file, then **ALL** files in the data-files/ subdirectory
422    // are inserted into the '.jar'
423
424    private static void addDataFilesDir(
425            final String                    fDataDirName,
426            final Stream.Builder<String>    b,
427            final AppendableSafe            logAndScreen
428        )
429        throws IOException
430    {
431        logAndScreen.append
432            ("    Adding '" + BCYAN + "data-files/" + RESET + "' Directory-Contents\n");
433
434        final String    fDataFilesListFileName  = fDataDirName + "DataFilesList.txt";
435        final File      fDataFilesListFile      = new File(fDataFilesListFileName);
436
437        // Check if there is a src-package/data-files/DataFilesList.txt file
438        // If there is a "DataFilesList.txt" file, convert that into a File-Name List.
439
440        if (fDataFilesListFile.exists() && fDataFilesListFile.isFile()) FileRW
441            .loadFileToStream(fDataFilesListFileName, false)
442            .map((String line) -> line.trim())
443            .filter((String line) -> line.length() > 0)
444            .filter((String line) -> ! line.startsWith("#"))
445            .map((String fileName) -> fDataDirName + fileName)
446            .forEachOrdered((String dataFileName) -> b.accept(dataFileName));
447
448        else FileNode
449            .createRoot(fDataDirName)
450            .loadTree(-1, DEFAULT_DATA_DIR_FILTER, null)
451            .flattenJustFiles(RTC.FULLPATH_STREAM_BUILDER(b));
452    }
453
454
455    // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
456    // Add the '.jar' File-Extras.  (Uses the "JarInclude" User Data-Configuration Class)
457    // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
458    //
459    // Some examples of "Jar-Extras" can (hopefully / probably) be seen with the Jar-Includes from
460    // Java-HTML.  These are things like the "META-INF" directories, and the Data-File Directory.
461    //
462    //  final FileNodeFilter FILTER = (FileNode fn) ->
463    //      StrCmpr.endsWithNAND(fn.name, ".java", ".class");
464    //
465    //  return new JarInclude()
466    //      .add("", "Torello/Data/", true, FILTER, TRUE_FILTER)
467    //      .add("Torello/BuildJAR/IncludeFiles/", "META-INF/", true, TRUE_FILTER, TRUE_FILTER)
468    //      .add("Torello/etc/External/", "META-INF/", true, TRUE_FILTER, TRUE_FILTER);
469
470    private static void addJarExtras(
471            final Iterable<JarInclude.Descriptor> includes,
472            final String            JAR_FILE_NAME_ABSOLUTE,
473            final Shell             shell,
474            final OSExtras          osExtras,
475            final AppendableSafe    logAndScreen,
476            final StringBuilder     logOnly
477        )
478        throws IOException
479    {
480        for (JarInclude.Descriptor jid : includes)
481        {
482            logAndScreen.append("Adding Jar-Include: " + jid.toString() + '\n');
483
484            final int cpStrLen = jid.workingDirectory.length();
485
486            final FileNode tempFN = FileNode
487                .createRoot(jid.workingDirectory + jid.subDirectory)
488                .loadTree((jid.traverseTree ? -1 : 0), null, null);
489
490            final String[] extraFiles = (cpStrLen == 0)
491
492                ? tempFN.flatten
493                    (RTC.FULLPATH_ARRAY(), -1, jid.fileFilter, true, jid.dirFilter, false)
494
495                : tempFN
496                    .flatten(RTC.FULLPATH_STREAM(), -1, jid.fileFilter, true, jid.dirFilter, false)
497                    .map((String fileName) -> fileName.substring(cpStrLen))
498                    .toArray(String[]::new);
499
500            String[] JAR_COMMAND = new String[2 + extraFiles.length];
501
502            JAR_COMMAND[0] = "-uvf";
503            JAR_COMMAND[1] = JAR_FILE_NAME_ABSOLUTE;
504            System.arraycopy(extraFiles, 0, JAR_COMMAND, 2, extraFiles.length);
505
506            if (cpStrLen > 0)
507            {
508                osExtras.currentWorkingDirectory = new File(jid.workingDirectory);
509                shell.osExtras = osExtras;
510            }
511    
512            CHECK(shell.COMMAND("jar", JAR_COMMAND));
513
514            printTotals(extraFiles, logAndScreen);
515
516            logOnly.append(Printing.TAR_JAR_DIVIDER);
517        }
518    }
519
520
521    // ********************************************************************************************
522    // ********************************************************************************************
523    // JAR-File: MORE HELPER's.  Simpler ones
524    // ********************************************************************************************
525    // ********************************************************************************************
526
527
528    private static void CHECK(OSResponse osr)
529    {
530        if (osr.errorOutput.length() > 0)
531        {
532            System.out.println
533                (BRED + "\nTEXT PRINTED TO STANDARD ERROR:\n" + RESET + osr.errorOutput);
534
535            Util.ERROR_EXIT("Build Tar-Jar (using Regular Shell)");
536        }
537    }
538
539    // This allows for '.class' files, but  also includes anything else in the directory - for
540    // instance ".properties" files, or other stuff that may or may not have been left there.  It
541    // seems a little questionable, but I cannot think of any generalized rule for this.  So, in
542    // the end, it just makes sure to leave out the '.java' file
543    //
544    // Remember: javax.json has a ".properties" file...  That's really the SINGLE-ONLY REASON that
545    //           this doesn't just say instead (more specifically): name.endsWith(".class")
546
547    private static final FilenameFilter JAR_FILENAME_FILTER =
548        (File file, String name) -> ! name.endsWith(".java");
549
550    // This isn't used at all - EXCEPT when a 'BuildPackage' claims to have "Sub-packages"
551    // "SubPackages" means that the entire directory-tree rooted at bp.pkgRootDirectory (and all of
552    // its '.class' files) needs to be included in the '.jar'
553    //
554    // NOTE: This essentially talking about the Packages: JDUInternal and BuildJAR
555    // FURTHERMORE: Since 'BuildJAR' isn't put into the '.jar', this is only used for JDUInternal
556    //
557    // The last addition to this thing was to make it print out the directories that match so that
558    // the build script tells the user all of the sub-packages.  It looks nice, however, whereas
559    // this used to be a 'FileFilter' **FIELD**, it is now a **METHOD** that returns a 'FileFilter'
560
561    private static final FileFilter getSubPackageDirFilter(AppendableSafe logAndScreen)
562    {
563        // This is creating a Lambda/Predicate that **ALSO** prints out the directories that match
564        return (File file) ->
565        {
566            final String path = file.getPath();
567
568            final boolean ret = ! StrCmpr.containsOR
569                (path, "upgrade-files", "doc-files", "package-source");
570
571            if (ret) logAndScreen.append
572                ("    Adding Sub-Package Files from: " + BCYAN + path + RESET + '\n');
573
574            return ret;
575        };
576    }
577
578    // Just prints out a count of how many files (and the file types) that were just inserted
579    // on the most recent loop-iteration, into the JAR.
580
581    private static void printTotals(String[] files, Appendable a) throws IOException
582    {
583        TreeMap<String, Integer> tm = new TreeMap<>();
584
585        for (String fileName : files)
586        {
587            int slashPos    = fileName.lastIndexOf(FS);
588            int dotPos      = fileName.lastIndexOf('.');
589
590            final String fileExt = ((dotPos == -1) || (slashPos > dotPos))
591                ? "NOEXT"
592                : fileName.substring(dotPos);
593
594            tm.compute(
595                fileExt,
596                (String dummy, Integer count) -> (count == null) ? 1 : (count+1)
597            );
598        }
599
600        for (String fileExt : tm.keySet()) a.append(
601            "    Added " +
602            BGREEN + tm.get(fileExt) + RESET +
603            (fileExt.equals("NOEXT")
604                ? (BRED + " Other " + RESET)
605                : (" '" + BYELLOW + fileExt + RESET + "' ")) +
606            "Files.\n"
607        );
608
609        // for (String fileName : files) System.out.print(fileName + ", ");
610        // System.out.println();
611
612        a.append('\n');
613    }
614}