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