1 /* 2 * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.java.util.jar.pack; 27 28 import java.io.BufferedInputStream; 29 import java.io.BufferedOutputStream; 30 import java.io.File; 31 import java.io.FileInputStream; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.io.PrintStream; 37 import java.text.MessageFormat; 38 import java.nio.file.Files; 39 import java.nio.file.Path; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.HashMap; 43 import java.util.Iterator; 44 import java.util.List; 45 import java.util.ListIterator; 46 import java.util.Map; 47 import java.util.Properties; 48 import java.util.ResourceBundle; 49 import java.util.SortedMap; 50 import java.util.TreeMap; 51 import java.util.jar.JarFile; 52 import java.util.jar.JarOutputStream; 53 import java.util.jar.Pack200; 54 import java.util.zip.GZIPInputStream; 55 import java.util.zip.GZIPOutputStream; 56 57 /** Command line interface for Pack200. 58 */ 59 60 @SuppressWarnings({"removal"}) 61 class Driver { 62 private static final ResourceBundle RESOURCE = 63 ResourceBundle.getBundle("com.sun.java.util.jar.pack.DriverResource"); 64 private static boolean suppressDeprecateMsg = false; 65 66 public static void main(String[] ava) throws IOException { 67 List<String> av = new ArrayList<>(Arrays.asList(ava)); 68 69 boolean doPack = true; 70 boolean doUnpack = false; 71 boolean doRepack = false; 72 boolean doZip = true; 73 suppressDeprecateMsg = av.remove("-XDsuppress-tool-removal-message"); 74 String logFile = null; 75 String verboseProp = Utils.DEBUG_VERBOSE; 76 77 { 78 // Non-standard, undocumented "--unpack" switch enables unpack mode. 79 String arg0 = av.isEmpty() ? "" : av.get(0); 80 switch (arg0) { 81 case "--pack": 82 av.remove(0); 83 break; 84 case "--unpack": 85 av.remove(0); 86 doPack = false; 87 doUnpack = true; 88 break; 89 } 90 } 91 92 if (!suppressDeprecateMsg) { 93 printDeprecateWarning(doPack, System.out); 94 } 95 96 // Collect engine properties here: 97 Map<String,String> engProps = new HashMap<>(); 98 engProps.put(verboseProp, System.getProperty(verboseProp)); 99 100 String optionMap; 101 String[] propTable; 102 if (doPack) { 103 optionMap = PACK200_OPTION_MAP; 104 propTable = PACK200_PROPERTY_TO_OPTION; 105 } else { 106 optionMap = UNPACK200_OPTION_MAP; 107 propTable = UNPACK200_PROPERTY_TO_OPTION; 108 } 109 110 // Collect argument properties here: 111 Map<String,String> avProps = new HashMap<>(); 112 try { 113 for (;;) { 114 String state = parseCommandOptions(av, optionMap, avProps); 115 // Translate command line options to Pack200 properties: 116 eachOpt: 117 for (Iterator<String> opti = avProps.keySet().iterator(); 118 opti.hasNext(); ) { 119 String opt = opti.next(); 120 String prop = null; 121 for (int i = 0; i < propTable.length; i += 2) { 122 if (opt.equals(propTable[1+i])) { 123 prop = propTable[0+i]; 124 break; 125 } 126 } 127 if (prop != null) { 128 String val = avProps.get(opt); 129 opti.remove(); // remove opt from avProps 130 if (!prop.endsWith(".")) { 131 // Normal string or boolean. 132 if (!(opt.equals("--verbose") 133 || opt.endsWith("="))) { 134 // Normal boolean; convert to T/F. 135 boolean flag = (val != null); 136 if (opt.startsWith("--no-")) 137 flag = !flag; 138 val = flag? "true": "false"; 139 } 140 engProps.put(prop, val); 141 } else if (prop.contains(".attribute.")) { 142 for (String val1 : val.split("\0")) { 143 String[] val2 = val1.split("=", 2); 144 engProps.put(prop+val2[0], val2[1]); 145 } 146 } else { 147 // Collection property: pack.pass.file.cli.NNN 148 int idx = 1; 149 for (String val1 : val.split("\0")) { 150 String prop1; 151 do { 152 prop1 = prop+"cli."+(idx++); 153 } while (engProps.containsKey(prop1)); 154 engProps.put(prop1, val1); 155 } 156 } 157 } 158 } 159 160 // See if there is any other action to take. 161 if ("--config-file=".equals(state)) { 162 String propFile = av.remove(0); 163 Properties fileProps = new Properties(); 164 try (InputStream propIn = new FileInputStream(propFile)) { 165 fileProps.load(propIn); 166 } 167 if (engProps.get(verboseProp) != null) 168 fileProps.list(System.out); 169 for (Map.Entry<Object,Object> me : fileProps.entrySet()) { 170 engProps.put((String) me.getKey(), (String) me.getValue()); 171 } 172 } else if ("--version".equals(state)) { 173 System.out.println(MessageFormat.format(RESOURCE.getString(DriverResource.VERSION), 174 Driver.class.getName(), "1.31, 07/05/05")); 175 return; 176 } else if ("--help".equals(state)) { 177 printUsage(doPack, true, System.out); 178 System.exit(0); 179 return; 180 } else { 181 break; 182 } 183 } 184 } catch (IllegalArgumentException ee) { 185 System.err.println(MessageFormat.format(RESOURCE.getString(DriverResource.BAD_ARGUMENT), ee)); 186 printUsage(doPack, false, System.err); 187 System.exit(2); 188 return; 189 } 190 191 // Deal with remaining non-engine properties: 192 for (String opt : avProps.keySet()) { 193 String val = avProps.get(opt); 194 switch (opt) { 195 case "--repack": 196 doRepack = true; 197 break; 198 case "--no-gzip": 199 doZip = (val == null); 200 break; 201 case "--log-file=": 202 logFile = val; 203 break; 204 default: 205 throw new InternalError(MessageFormat.format( 206 RESOURCE.getString(DriverResource.BAD_OPTION), 207 opt, avProps.get(opt))); 208 } 209 } 210 211 if (logFile != null && !logFile.isEmpty()) { 212 if (logFile.equals("-")) { 213 System.setErr(System.out); 214 } else { 215 OutputStream log = new FileOutputStream(logFile); 216 //log = new BufferedOutputStream(out); 217 System.setErr(new PrintStream(log)); 218 } 219 } 220 221 boolean verbose = (engProps.get(verboseProp) != null); 222 223 String packfile = ""; 224 if (!av.isEmpty()) 225 packfile = av.remove(0); 226 227 String jarfile = ""; 228 if (!av.isEmpty()) 229 jarfile = av.remove(0); 230 231 String newfile = ""; // output JAR file if --repack 232 String bakfile = ""; // temporary backup of input JAR 233 String tmpfile = ""; // temporary file to be deleted 234 if (doRepack) { 235 // The first argument is the target JAR file. 236 // (Note: *.pac is nonstandard, but may be necessary 237 // if a host OS truncates file extensions.) 238 if (packfile.toLowerCase().endsWith(".pack") || 239 packfile.toLowerCase().endsWith(".pac") || 240 packfile.toLowerCase().endsWith(".gz")) { 241 System.err.println(MessageFormat.format( 242 RESOURCE.getString(DriverResource.BAD_REPACK_OUTPUT), 243 packfile)); 244 printUsage(doPack, false, System.err); 245 System.exit(2); 246 } 247 newfile = packfile; 248 // The optional second argument is the source JAR file. 249 if (jarfile.isEmpty()) { 250 // If only one file is given, it is the only JAR. 251 // It serves as both input and output. 252 jarfile = newfile; 253 } 254 tmpfile = createTempFile(newfile, ".pack").getPath(); 255 packfile = tmpfile; 256 doZip = false; // no need to zip the temporary file 257 } 258 259 if (!av.isEmpty() 260 // Accept jarfiles ending with .jar or .zip. 261 // Accept jarfile of "-" (stdout), but only if unpacking. 262 || !(jarfile.toLowerCase().endsWith(".jar") 263 || jarfile.toLowerCase().endsWith(".zip") 264 || (jarfile.equals("-") && !doPack))) { 265 printUsage(doPack, false, System.err); 266 System.exit(2); 267 return; 268 } 269 270 if (doRepack) 271 doPack = doUnpack = true; 272 else if (doPack) 273 doUnpack = false; 274 275 Pack200.Packer jpack = Pack200.newPacker(); 276 Pack200.Unpacker junpack = Pack200.newUnpacker(); 277 278 jpack.properties().putAll(engProps); 279 junpack.properties().putAll(engProps); 280 if (doRepack && newfile.equals(jarfile)) { 281 String zipc = getZipComment(jarfile); 282 if (verbose && zipc.length() > 0) 283 System.out.println(MessageFormat.format(RESOURCE.getString(DriverResource.DETECTED_ZIP_COMMENT), zipc)); 284 if (zipc.indexOf(Utils.PACK_ZIP_ARCHIVE_MARKER_COMMENT) >= 0) { 285 System.out.println(MessageFormat.format(RESOURCE.getString(DriverResource.SKIP_FOR_REPACKED), jarfile)); 286 doPack = false; 287 doUnpack = false; 288 doRepack = false; 289 } 290 } 291 292 try { 293 294 if (doPack) { 295 // Mode = Pack. 296 JarFile in = new JarFile(new File(jarfile)); 297 OutputStream out; 298 // Packfile must be -, *.gz, *.pack, or *.pac. 299 if (packfile.equals("-")) { 300 out = System.out; 301 // Send warnings, etc., to stderr instead of stdout. 302 System.setOut(System.err); 303 } else if (doZip) { 304 if (!packfile.endsWith(".gz")) { 305 System.err.println(MessageFormat.format(RESOURCE.getString(DriverResource.WRITE_PACK_FILE), packfile)); 306 printUsage(doPack, false, System.err); 307 System.exit(2); 308 } 309 out = new FileOutputStream(packfile); 310 out = new BufferedOutputStream(out); 311 out = new GZIPOutputStream(out); 312 } else { 313 if (!packfile.toLowerCase().endsWith(".pack") && 314 !packfile.toLowerCase().endsWith(".pac")) { 315 System.err.println(MessageFormat.format(RESOURCE.getString(DriverResource.WRITE_PACKGZ_FILE),packfile)); 316 printUsage(doPack, false, System.err); 317 System.exit(2); 318 } 319 out = new FileOutputStream(packfile); 320 out = new BufferedOutputStream(out); 321 } 322 jpack.pack(in, out); 323 //in.close(); // p200 closes in but not out 324 out.close(); 325 } 326 327 if (doRepack && newfile.equals(jarfile)) { 328 // If the source and destination are the same, 329 // we will move the input JAR aside while regenerating it. 330 // This allows us to restore it if something goes wrong. 331 File bakf = createTempFile(jarfile, ".bak"); 332 // On Windows target must be deleted see 4017593 333 bakf.delete(); 334 boolean okBackup = new File(jarfile).renameTo(bakf); 335 if (!okBackup) { 336 throw new Error(MessageFormat.format(RESOURCE.getString(DriverResource.SKIP_FOR_MOVE_FAILED),bakfile)); 337 } else { 338 // Open jarfile recovery bracket. 339 bakfile = bakf.getPath(); 340 } 341 } 342 343 if (doUnpack) { 344 // Mode = Unpack. 345 InputStream in; 346 if (packfile.equals("-")) 347 in = System.in; 348 else 349 in = new FileInputStream(new File(packfile)); 350 BufferedInputStream inBuf = new BufferedInputStream(in); 351 in = inBuf; 352 if (Utils.isGZIPMagic(Utils.readMagic(inBuf))) { 353 in = new GZIPInputStream(in); 354 } 355 String outfile = newfile.isEmpty()? jarfile: newfile; 356 OutputStream fileOut; 357 if (outfile.equals("-")) 358 fileOut = System.out; 359 else 360 fileOut = new FileOutputStream(outfile); 361 fileOut = new BufferedOutputStream(fileOut); 362 try (JarOutputStream out = new JarOutputStream(fileOut)) { 363 junpack.unpack(in, out); 364 // p200 closes in but not out 365 } 366 // At this point, we have a good jarfile (or newfile, if -r) 367 } 368 369 if (!bakfile.isEmpty()) { 370 // On success, abort jarfile recovery bracket. 371 new File(bakfile).delete(); 372 bakfile = ""; 373 } 374 375 } finally { 376 // Close jarfile recovery bracket. 377 if (!bakfile.isEmpty()) { 378 File jarFile = new File(jarfile); 379 jarFile.delete(); // Win32 requires this, see above 380 new File(bakfile).renameTo(jarFile); 381 } 382 // In all cases, delete temporary *.pack. 383 if (!tmpfile.isEmpty()) 384 new File(tmpfile).delete(); 385 } 386 } 387 388 private static 389 File createTempFile(String basefile, String suffix) throws IOException { 390 File base = new File(basefile); 391 String prefix = base.getName(); 392 if (prefix.length() < 3) prefix += "tmp"; 393 394 File where = (base.getParentFile() == null && suffix.equals(".bak")) 395 ? new File(".").getAbsoluteFile() 396 : base.getParentFile(); 397 398 Path tmpfile = (where == null) 399 ? Files.createTempFile(prefix, suffix) 400 : Files.createTempFile(where.toPath(), prefix, suffix); 401 402 return tmpfile.toFile(); 403 } 404 405 private static 406 void printDeprecateWarning(boolean doPack, PrintStream out) { 407 String prog = doPack ? "pack200" : "unpack200"; 408 out.println(MessageFormat.format(RESOURCE.getString(DriverResource.DEPRECATED), prog)); 409 } 410 411 private static 412 void printUsage(boolean doPack, boolean full, PrintStream out) { 413 String prog = doPack ? "pack200" : "unpack200"; 414 String[] packUsage = (String[])RESOURCE.getObject(DriverResource.PACK_HELP); 415 String[] unpackUsage = (String[])RESOURCE.getObject(DriverResource.UNPACK_HELP); 416 String[] usage = doPack? packUsage: unpackUsage; 417 for (int i = 0; i < usage.length; i++) { 418 out.println(usage[i]); 419 if (!full) { 420 out.println(MessageFormat.format(RESOURCE.getString(DriverResource.MORE_INFO), prog)); 421 break; 422 } 423 } 424 // Print a warning at the end 425 // The full help page is long, the beginning warning could be out of sight 426 if (full && !suppressDeprecateMsg) { 427 printDeprecateWarning(doPack, out); 428 } 429 } 430 431 private static 432 String getZipComment(String jarfile) throws IOException { 433 byte[] tail = new byte[1000]; 434 long filelen = new File(jarfile).length(); 435 if (filelen <= 0) return ""; 436 long skiplen = Math.max(0, filelen - tail.length); 437 try (InputStream in = new FileInputStream(new File(jarfile))) { 438 in.skip(skiplen); 439 in.read(tail); 440 for (int i = tail.length-4; i >= 0; i--) { 441 if (tail[i+0] == 'P' && tail[i+1] == 'K' && 442 tail[i+2] == 5 && tail[i+3] == 6) { 443 // Skip sig4, disks4, entries4, clen4, coff4, cmt2 444 i += 4+4+4+4+4+2; 445 if (i < tail.length) 446 return new String(tail, i, tail.length-i, "UTF8"); 447 return ""; 448 } 449 } 450 return ""; 451 } 452 } 453 454 private static final String PACK200_OPTION_MAP = 455 ("" 456 +"--repack $ \n -r +>- @--repack $ \n" 457 +"--no-gzip $ \n -g +>- @--no-gzip $ \n" 458 +"--strip-debug $ \n -G +>- @--strip-debug $ \n" 459 +"--no-keep-file-order $ \n -O +>- @--no-keep-file-order $ \n" 460 +"--segment-limit= *> = \n -S +> @--segment-limit= = \n" 461 +"--effort= *> = \n -E +> @--effort= = \n" 462 +"--deflate-hint= *> = \n -H +> @--deflate-hint= = \n" 463 +"--modification-time= *> = \n -m +> @--modification-time= = \n" 464 +"--pass-file= *> &\0 \n -P +> @--pass-file= &\0 \n" 465 +"--unknown-attribute= *> = \n -U +> @--unknown-attribute= = \n" 466 +"--class-attribute= *> &\0 \n -C +> @--class-attribute= &\0 \n" 467 +"--field-attribute= *> &\0 \n -F +> @--field-attribute= &\0 \n" 468 +"--method-attribute= *> &\0 \n -M +> @--method-attribute= &\0 \n" 469 +"--code-attribute= *> &\0 \n -D +> @--code-attribute= &\0 \n" 470 +"--config-file= *> . \n -f +> @--config-file= . \n" 471 472 // Negative options as required by CLIP: 473 +"--no-strip-debug !--strip-debug \n" 474 +"--gzip !--no-gzip \n" 475 +"--keep-file-order !--no-keep-file-order \n" 476 477 // Non-Standard Options 478 +"--verbose $ \n -v +>- @--verbose $ \n" 479 +"--quiet !--verbose \n -q +>- !--verbose \n" 480 +"--log-file= *> = \n -l +> @--log-file= = \n" 481 //+"--java-option= *> = \n -J +> @--java-option= = \n" 482 +"--version . \n -V +> @--version . \n" 483 +"--help . \n -? +> @--help . \n -h +> @--help . \n" 484 485 // Termination: 486 +"-- . \n" // end option sequence here 487 +"- +? >- . \n" // report error if -XXX present; else use stdout 488 ); 489 // Note: Collection options use "\0" as a delimiter between arguments. 490 491 // For Java version of unpacker (used for testing only): 492 private static final String UNPACK200_OPTION_MAP = 493 ("" 494 +"--deflate-hint= *> = \n -H +> @--deflate-hint= = \n" 495 +"--verbose $ \n -v +>- @--verbose $ \n" 496 +"--quiet !--verbose \n -q +>- !--verbose \n" 497 +"--remove-pack-file $ \n -r +>- @--remove-pack-file $ \n" 498 +"--log-file= *> = \n -l +> @--log-file= = \n" 499 +"--config-file= *> . \n -f +> @--config-file= . \n" 500 501 // Termination: 502 +"-- . \n" // end option sequence here 503 +"- +? >- . \n" // report error if -XXX present; else use stdin 504 +"--version . \n -V +> @--version . \n" 505 +"--help . \n -? +> @--help . \n -h +> @--help . \n" 506 ); 507 508 private static final String[] PACK200_PROPERTY_TO_OPTION = { 509 Pack200.Packer.SEGMENT_LIMIT, "--segment-limit=", 510 Pack200.Packer.KEEP_FILE_ORDER, "--no-keep-file-order", 511 Pack200.Packer.EFFORT, "--effort=", 512 Pack200.Packer.DEFLATE_HINT, "--deflate-hint=", 513 Pack200.Packer.MODIFICATION_TIME, "--modification-time=", 514 Pack200.Packer.PASS_FILE_PFX, "--pass-file=", 515 Pack200.Packer.UNKNOWN_ATTRIBUTE, "--unknown-attribute=", 516 Pack200.Packer.CLASS_ATTRIBUTE_PFX, "--class-attribute=", 517 Pack200.Packer.FIELD_ATTRIBUTE_PFX, "--field-attribute=", 518 Pack200.Packer.METHOD_ATTRIBUTE_PFX, "--method-attribute=", 519 Pack200.Packer.CODE_ATTRIBUTE_PFX, "--code-attribute=", 520 //Pack200.Packer.PROGRESS, "--progress=", 521 Utils.DEBUG_VERBOSE, "--verbose", 522 Utils.COM_PREFIX+"strip.debug", "--strip-debug", 523 }; 524 525 private static final String[] UNPACK200_PROPERTY_TO_OPTION = { 526 Pack200.Unpacker.DEFLATE_HINT, "--deflate-hint=", 527 //Pack200.Unpacker.PROGRESS, "--progress=", 528 Utils.DEBUG_VERBOSE, "--verbose", 529 Utils.UNPACK_REMOVE_PACKFILE, "--remove-pack-file", 530 }; 531 532 /*-* 533 * Remove a set of command-line options from args, 534 * storing them in the map in a canonicalized form. 535 * <p> 536 * The options string is a newline-separated series of 537 * option processing specifiers. 538 */ 539 private static 540 String parseCommandOptions(List<String> args, 541 String options, 542 Map<String,String> properties) { 543 //System.out.println(args+" // "+properties); 544 545 String resultString = null; 546 547 // Convert options string into optLines dictionary. 548 TreeMap<String,String[]> optmap = new TreeMap<>(); 549 loadOptmap: 550 for (String optline : options.split("\n")) { 551 String[] words = optline.split("\\p{Space}+"); 552 if (words.length == 0) continue loadOptmap; 553 String opt = words[0]; 554 words[0] = ""; // initial word is not a spec 555 if (opt.length() == 0 && words.length >= 1) { 556 opt = words[1]; // initial "word" is empty due to leading ' ' 557 words[1] = ""; 558 } 559 if (opt.length() == 0) continue loadOptmap; 560 String[] prevWords = optmap.put(opt, words); 561 if (prevWords != null) 562 throw new RuntimeException(MessageFormat.format(RESOURCE.getString(DriverResource.DUPLICATE_OPTION), optline.trim())); 563 } 564 565 // State machine for parsing a command line. 566 ListIterator<String> argp = args.listIterator(); 567 ListIterator<String> pbp = new ArrayList<String>().listIterator(); 568 doArgs: 569 for (;;) { 570 // One trip through this loop per argument. 571 // Multiple trips per option only if several options per argument. 572 String arg; 573 if (pbp.hasPrevious()) { 574 arg = pbp.previous(); 575 pbp.remove(); 576 } else if (argp.hasNext()) { 577 arg = argp.next(); 578 } else { 579 // No more arguments at all. 580 break doArgs; 581 } 582 tryOpt: 583 for (int optlen = arg.length(); ; optlen--) { 584 // One time through this loop for each matching arg prefix. 585 String opt; 586 // Match some prefix of the argument to a key in optmap. 587 findOpt: 588 for (;;) { 589 opt = arg.substring(0, optlen); 590 if (optmap.containsKey(opt)) break findOpt; 591 if (optlen == 0) break tryOpt; 592 // Decide on a smaller prefix to search for. 593 SortedMap<String,String[]> pfxmap = optmap.headMap(opt); 594 // pfxmap.lastKey is no shorter than any prefix in optmap. 595 int len = pfxmap.isEmpty() ? 0 : pfxmap.lastKey().length(); 596 optlen = Math.min(len, optlen - 1); 597 opt = arg.substring(0, optlen); 598 // (Note: We could cut opt down to its common prefix with 599 // pfxmap.lastKey, but that wouldn't save many cycles.) 600 } 601 opt = opt.intern(); 602 assert(arg.startsWith(opt)); 603 assert(opt.length() == optlen); 604 String val = arg.substring(optlen); // arg == opt+val 605 606 // Execute the option processing specs for this opt. 607 // If no actions are taken, then look for a shorter prefix. 608 boolean didAction = false; 609 boolean isError = false; 610 611 int pbpMark = pbp.nextIndex(); // in case of backtracking 612 String[] specs = optmap.get(opt); 613 eachSpec: 614 for (String spec : specs) { 615 if (spec.length() == 0) continue eachSpec; 616 if (spec.startsWith("#")) break eachSpec; 617 int sidx = 0; 618 char specop = spec.charAt(sidx++); 619 620 // Deal with '+'/'*' prefixes (spec conditions). 621 boolean ok; 622 switch (specop) { 623 case '+': 624 // + means we want an non-empty val suffix. 625 ok = (val.length() != 0); 626 specop = spec.charAt(sidx++); 627 break; 628 case '*': 629 // * means we accept empty or non-empty 630 ok = true; 631 specop = spec.charAt(sidx++); 632 break; 633 default: 634 // No condition prefix means we require an exact 635 // match, as indicated by an empty val suffix. 636 ok = (val.length() == 0); 637 break; 638 } 639 if (!ok) continue eachSpec; 640 641 String specarg = spec.substring(sidx); 642 switch (specop) { 643 case '.': // terminate the option sequence 644 resultString = (specarg.length() != 0)? specarg.intern(): opt; 645 break doArgs; 646 case '?': // abort the option sequence 647 resultString = (specarg.length() != 0)? specarg.intern(): arg; 648 isError = true; 649 break eachSpec; 650 case '@': // change the effective opt name 651 opt = specarg.intern(); 652 break; 653 case '>': // shift remaining arg val to next arg 654 pbp.add(specarg + val); // push a new argument 655 val = ""; 656 break; 657 case '!': // negation option 658 String negopt = (specarg.length() != 0)? specarg.intern(): opt; 659 properties.remove(negopt); 660 properties.put(negopt, null); // leave placeholder 661 didAction = true; 662 break; 663 case '$': // normal "boolean" option 664 String boolval; 665 if (specarg.length() != 0) { 666 // If there is a given spec token, store it. 667 boolval = specarg; 668 } else { 669 String old = properties.get(opt); 670 if (old == null || old.length() == 0) { 671 boolval = "1"; 672 } else { 673 // Increment any previous value as a numeral. 674 boolval = ""+(1+Integer.parseInt(old)); 675 } 676 } 677 properties.put(opt, boolval); 678 didAction = true; 679 break; 680 case '=': // "string" option 681 case '&': // "collection" option 682 // Read an option. 683 boolean append = (specop == '&'); 684 String strval; 685 if (pbp.hasPrevious()) { 686 strval = pbp.previous(); 687 pbp.remove(); 688 } else if (argp.hasNext()) { 689 strval = argp.next(); 690 } else { 691 resultString = arg + " ?"; 692 isError = true; 693 break eachSpec; 694 } 695 if (append) { 696 String old = properties.get(opt); 697 if (old != null) { 698 // Append new val to old with embedded delim. 699 String delim = specarg; 700 if (delim.length() == 0) delim = " "; 701 strval = old + specarg + strval; 702 } 703 } 704 properties.put(opt, strval); 705 didAction = true; 706 break; 707 default: 708 throw new RuntimeException(MessageFormat.format(RESOURCE.getString(DriverResource.BAD_SPEC),opt, spec)); 709 } 710 } 711 712 // Done processing specs. 713 if (didAction && !isError) { 714 continue doArgs; 715 } 716 717 // The specs should have done something, but did not. 718 while (pbp.nextIndex() > pbpMark) { 719 // Remove anything pushed during these specs. 720 pbp.previous(); 721 pbp.remove(); 722 } 723 724 if (isError) { 725 throw new IllegalArgumentException(resultString); 726 } 727 728 if (optlen == 0) { 729 // We cannot try a shorter matching option. 730 break tryOpt; 731 } 732 } 733 734 // If we come here, there was no matching option. 735 // So, push back the argument, and return to caller. 736 pbp.add(arg); 737 break doArgs; 738 } 739 // Report number of arguments consumed. 740 args.subList(0, argp.nextIndex()).clear(); 741 // Report any unconsumed partial argument. 742 while (pbp.hasPrevious()) { 743 args.add(0, pbp.previous()); 744 } 745 //System.out.println(args+" // "+properties+" -> "+resultString); 746 return resultString; 747 } 748 }