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