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}