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}