1 /*
   2  * Copyright 2007-2008 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 sun.module.tools;
  27 
  28 import java.io.BufferedReader;
  29 import java.io.File;
  30 import java.io.FileNotFoundException;
  31 import java.io.IOException;
  32 import java.io.InputStreamReader;
  33 import java.io.OutputStream;
  34 import java.io.PrintStream;
  35 import java.io.PrintWriter;
  36 import java.io.StringWriter;
  37 import java.net.MalformedURLException;
  38 import java.net.URI;
  39 import java.net.URL;
  40 import java.net.URISyntaxException;
  41 import java.module.*;
  42 import java.module.annotation.ImportPolicyClass;
  43 import java.security.AccessController;
  44 import java.security.PrivilegedAction;
  45 import java.text.DateFormat;
  46 import java.util.Arrays;
  47 import java.util.ArrayList;
  48 import java.util.Collections;
  49 import java.util.Date;
  50 import java.util.HashMap;
  51 import java.util.HashSet;
  52 import java.util.LinkedHashMap;
  53 import java.util.List;
  54 import java.util.Map;
  55 import java.util.Set;
  56 import sun.module.repository.RepositoryConfig;
  57 import sun.security.action.GetPropertyAction;
  58 import sun.tools.jar.CommandLine;
  59 
  60 /**
  61  * Java Modules Repository Management Tool
  62  * @since 1.7
  63  */
  64 public class JRepo {
  65     public static final boolean DEBUG;
  66 
  67     static {
  68         DEBUG = (AccessController.doPrivileged(new GetPropertyAction("sun.module.tools.debug")) != null);
  69     }
  70 
  71     private static void debug(String s) {
  72         System.err.println(s);
  73     }
  74 
  75     /** For printing user output. */
  76     private final Messenger msg;
  77 
  78     /** Optional command line argument used by {@code list()}. */
  79     private String moduleName;
  80 
  81     /** Map from this tool's command names to the commands themselves. */
  82     private static final Map<String, Command> commands = new LinkedHashMap<String, Command>();
  83 
  84     /** Usage message created from each command's usage. */
  85     private static String usage = null;
  86 
  87     /** Format for ModuleArchiveInfo name & version. */
  88     private static final String MAIFormat = "%-20s %-20s";
  89 
  90     /** Format for additional/verbose ModuleArchiveInfo details. */
  91     private static final String MAIFormatVerbose = " %-9s %-7s %-17s %s";
  92 
  93     /** Format for ModuleDefinition name & version. */
  94     private static final String MDFormat = "%s-%s";
  95 
  96     /** Format for additional/verbose ModuleDefinition details. */
  97     private static final String MDFormatVerbose = " %s";
  98 
  99     /** String containing column headings for name & version. */
 100     private static final String maiHeading;
 101 
 102     /** String containing column headings for additional/verbose module information. */
 103     private static final String maiHeadingVerbose;
 104 
 105     /** Indicates that parent repositories should be used by a command. */
 106     private static final Flag parentFlag = new Flag('p');
 107 
 108     /** Indicates that command output should be verbose. */
 109     private static final Flag verboseFlag = new Flag('v');
 110 
 111     /** Location of repository; if not given uses application repository. */
 112     private static final RepositoryFlag repositoryFlag = new RepositoryFlag();
 113 
 114     /** Indicates dependencies command should display info on core modules. */
 115     private static final Flag javaseFlag = new Flag('j');
 116 
 117     /** Provides way to specify platform binding fo dependencies command. */
 118     private static final BindingFlag bindingFlag = new BindingFlag();
 119 
 120     /** Contains the flags that are common to all commands. */
 121     private static final HashMap<Character, Flag> commonFlags = new HashMap<Character, Flag>();
 122 
 123     static {
 124         repositoryFlag.register(commonFlags);
 125         verboseFlag.register(commonFlags);
 126 
 127         StringWriter sw = new StringWriter();
 128         PrintWriter pw = new PrintWriter(sw);
 129         pw.printf(MAIFormat, "Name", "Version"); // XXX i18n
 130         maiHeading = sw.toString();
 131 
 132         sw = new StringWriter();
 133         pw = new PrintWriter(sw);
 134         pw.printf(MAIFormat, "Name", "Version"); // XXX i18n
 135         pw.printf(MAIFormatVerbose, "Platform", "Arch", "Modified", "Filename"); // XXX i18n
 136         maiHeadingVerbose = sw.toString();
 137     }
 138 
 139     public JRepo(OutputStream out, OutputStream err, BufferedReader reader) {
 140         this(new PrintStream(out), new PrintStream(err), reader);
 141     }
 142 
 143     public JRepo(PrintStream out, PrintStream err, BufferedReader reader) {
 144         msg = new Messenger("jrepo", out, err, reader);
 145         synchronized(commands) {
 146             if (commands.isEmpty()) {
 147                 new ListCommand().register(commands);
 148                 new InstallCommand().register(commands);
 149                 new UninstallCommand().register(commands);
 150                 new DependenciesCommand().register(commands);
 151             }
 152         }
 153         reset();
 154     }
 155 
 156     public static void main(String[] args) {
 157         JRepo jrepo = new JRepo(
 158             System.out, System.err,
 159             new BufferedReader(new InputStreamReader(System.in)));
 160         System.exit(jrepo.run(args) ? 0 : 1);
 161     }
 162 
 163     public synchronized boolean run(String[] args) {
 164         reset();
 165 
 166         if (DEBUG) { for (String s : args) debug("arg: '" + s + "'"); }
 167         Command cmd = parseArgs(args);
 168         if (DEBUG && cmd != null) debug("running " + cmd);
 169         if (cmd == null) {
 170             return false;
 171         }
 172 
 173         Repository repo = null;
 174         try {
 175              repo = getRepository();
 176              repo.shutdownOnExit(true);
 177         } catch (IOException ex) {
 178             msg.fatalError(ex.getMessage());
 179             return false;
 180         }
 181 
 182         boolean rc = cmd.run(repo, msg);
 183         if (DEBUG) debug(cmd + " returned " + rc);
 184         return rc;
 185 
 186     }
 187 
 188     /**
 189      * Gets the repository based on command line flags.
 190      * @return Reposistory based on the value from {@code repositoryFlag} if
 191      * that is non-null, else the application repository.
 192      */
 193     private Repository getRepository() throws IOException {
 194         Repository rc = null;
 195 
 196         String repositoryLocation = repositoryFlag.getLocation();
 197 
 198         if (repositoryLocation == null) {
 199             rc = Repository.getApplicationRepository();
 200         } else {
 201             // If repositoryLocation is a URL use URLRepository else LocalRepository.
 202             try {
 203                 URL u = new URL(repositoryLocation);
 204                 rc = Modules.newURLRepository(
 205                     "jrepo", u, null, RepositoryConfig.getApplicationRepository());
 206             } catch (MalformedURLException ex) {
 207                 File f = new File(repositoryLocation);
 208                 if (f.exists() && f.canRead()) {
 209                     rc = Modules.newLocalRepository(
 210                         "jrepo",
 211                         f.getCanonicalFile(), null,
 212                         RepositoryConfig.getApplicationRepository());
 213                 } else {
 214                     throw new IOException("Cannot access repository at " // XXX i18n
 215                                           + repositoryLocation);
 216                 }
 217             }
 218         }
 219         return rc;
 220     }
 221 
 222     /** Reset instance state to default values. */
 223     private void reset() {
 224         moduleName = null;
 225         parentFlag.reset();
 226         repositoryFlag.reset();
 227         verboseFlag.reset();
 228         for (Command cmd : commands.values()) {
 229             cmd.reset();
 230         }
 231     }
 232 
 233 
 234     /** Parse command line arguments.*/
 235     private Command parseArgs(String[] args) {
 236         /* Preprocess and expand @file arguments */
 237         try {
 238             args = CommandLine.parse(args);
 239         } catch (FileNotFoundException e) {
 240             msg.fatalError(msg.formatMsg("error.cant.open", e.getMessage()));
 241             return null;
 242         } catch (IOException e) {
 243             msg.fatalError(msg.formatMsg("caught.exception", e.getMessage()));
 244             return null;
 245         }
 246 
 247         if (args.length < 1) {
 248             usageError();
 249             return null;
 250         }
 251 
 252         Command cmd = null;
 253         for (Map.Entry<String, Command> entry : commands.entrySet()) {
 254             if (entry.getKey().startsWith(args[0])) {
 255                 cmd = entry.getValue();
 256                 break;
 257             }
 258         }
 259         if (DEBUG) debug("found command " + cmd);
 260         if (cmd == null) {
 261             usageError();
 262             return null;
 263         }
 264 
 265         try {
 266             int numFlags = cmd.parseFlags(args, commonFlags);
 267             args = Arrays.copyOfRange(args, numFlags + 1, args.length);
 268             try {
 269                 if (cmd.parseArgs(args, msg)) {
 270                     return cmd;
 271                 } else {
 272                     usageError();
 273                     return null;
 274                 }
 275             } catch (ArrayIndexOutOfBoundsException e) {
 276                 usageError();
 277             }
 278         } catch (IllegalArgumentException ex) {
 279             msg.error(ex.getMessage());
 280             usageError();
 281             return null;
 282         }
 283         return cmd;
 284     }
 285 
 286 
 287     /** Returns a user-grokkable description of the repository. */
 288     private static String getRepositoryText(Repository repo) {
 289         String rc;
 290 /*        URI u = repo.getSourceLocation();
 291         if (u == null) {
 292             rc = "Bootstrap repository";
 293         } else {
 294             try {
 295                 rc = "Repository " + u.toURL().toExternalForm();
 296             } catch (MalformedURLException ex) {
 297                 rc = "Repository unknown";
 298             }
 299         }
 300  */
 301         rc = "[" + repo.toString() + "]";
 302 
 303         return rc;
 304     }
 305 
 306     private static String getMAIText(ModuleArchiveInfo mai) {
 307         StringWriter sw = new StringWriter();
 308         PrintWriter pw = new PrintWriter(sw);
 309         pw.printf(MAIFormat, mai.getName(), mai.getVersion());
 310         if (verboseFlag.isEnabled()) {
 311             long t = mai.getLastModified();
 312             String lastMod = null;
 313             if (t != 0) {
 314                 lastMod = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date(t));
 315             }
 316             pw.printf(MAIFormatVerbose,
 317                 mai.getPlatform() == null ? "generic" : mai.getPlatform(),
 318                 mai.getArch() == null ? "generic" : mai.getArch(),
 319                 lastMod == null ? "n/a" : lastMod,
 320                 mai.getFileName() == null ? "n/a" : mai.getFileName()
 321                 );
 322         }
 323         return sw.toString();
 324     }
 325 
 326     private static String getMText(Module m) {
 327         return getMDText(m.getModuleDefinition());
 328     }
 329 
 330     private static String getMDText(ModuleDefinition md) {
 331         StringWriter sw = new StringWriter();
 332         PrintWriter pw = new PrintWriter(sw);
 333         pw.printf(MDFormat, md.getName(), md.getVersion());
 334         if (verboseFlag.isEnabled()) {
 335             pw.printf(MDFormatVerbose,
 336 //                      md.getRepository() == Repository.getBootstrapRepository()
 337 //                      ? "bootstrap"
 338 //                      : md.getRepository().getSourceLocation().toString());
 339                      md.getRepository().getName());
 340         }
 341         return sw.toString();
 342     }
 343 
 344     void usageError() {
 345         usageError(msg);
 346     }
 347 
 348     static void usageError(Messenger msg) {
 349         if (usage == null) {
 350             StringBuilder ub = new StringBuilder(
 351                 "Usage: jrepo <command>\nwhere <command> includes:"); // XXX i18n
 352             for (Command c : commands.values()) {
 353                 String u = c.usage();
 354                 if (u != null) {
 355                     ub.append("\n    ").append(c.usage());
 356                 }
 357             }
 358             usage = ub.toString();
 359         }
 360         msg.error(usage);
 361     }
 362 
 363     /**
 364      * Represents a flag given on the command line.  Flags always are 2
 365      * characters long, and start with a '-'.  They can have additional
 366      * associated arguments (cf {@link #RepositoryFlag}).
 367      */
 368     private static class Flag {
 369         private final char name;
 370 
 371         private boolean enabled = false;
 372 
 373         private static final Map<Character, Flag> flags = new HashMap<Character, Flag>();
 374 
 375         Flag(char name) {
 376             this.name = name;
 377             flags.put(new Character(name), this);
 378         }
 379 
 380         void register(Map<Character, Flag> registry) {
 381             registry.put(new Character(name), this);
 382         }
 383 
 384         char getName() {
 385             return name;
 386         }
 387 
 388         /**
 389          * @return the number of arguments consumed by this Flag
 390          * @throws IllegalArgumentException if invalid args are given
 391          */
 392         int set(String[] args, int pos) throws IllegalArgumentException {
 393             enabled = true;
 394             return 1;
 395         }
 396 
 397         void reset() {
 398             enabled = false;
 399         }
 400 
 401         boolean isEnabled() {
 402             return enabled;
 403         }
 404 
 405         static Flag get(char c) {
 406             return flags.get(new Character(c));
 407         }
 408     }
 409 
 410     private static class RepositoryFlag extends Flag {
 411         String location = null;
 412 
 413         RepositoryFlag() {
 414             super('r');
 415         }
 416 
 417         @Override
 418         int set(String[] args, int pos) throws IllegalArgumentException {
 419             int rc = super.set(args, pos);
 420             location = args[pos + 1];
 421             return rc + 1;
 422         }
 423 
 424         String getLocation() {
 425             return location;
 426         }
 427 
 428         @Override
 429         void reset() {
 430             super.reset();
 431             location = null;
 432         }
 433     }
 434 
 435     private static class BindingFlag extends Flag {
 436         String platform;
 437         String arch;
 438 
 439         BindingFlag() {
 440             super('b');
 441         }
 442 
 443         @Override
 444         int set(String[] args, int pos) throws IllegalArgumentException {
 445             int rc = super.set(args, pos);
 446             String[] binding = args[pos + 1].split("-");
 447             if (binding.length != 2) {
 448                 throw new IllegalArgumentException(
 449                     "Must 2 and only 2 elements for platform-arch");
 450             }
 451             platform = binding[0];
 452             arch = binding[1];
 453             return rc + 1;
 454         }
 455 
 456         String getPlatform() {
 457             return platform;
 458         }
 459 
 460         String getArch() {
 461             return arch;
 462         }
 463 
 464         @Override
 465         void reset() {
 466             super.reset();
 467             platform = null;
 468             arch = null;
 469         }
 470     }
 471 
 472     /*
 473      * Command types: An abstract base class, plus one concrete class for
 474      * each Command.
 475      */
 476 
 477     /*
 478      * Represents a Command.
 479      */
 480     private static abstract class Command {
 481         private final String name;
 482 
 483         Command(String name) {
 484             this.name = name;
 485         }
 486 
 487         /** Adds this command to the given registry. */
 488         void register(Map<String, Command> registry) {
 489             registry.put(name, this);
 490         }
 491 
 492         void reset() {
 493             // Empty; subclasses can implement
 494         }
 495 
 496         // Flags differ from arguments in that they have the form "-X [opt]".
 497         // Flags appear in a command line before arguments.
 498 
 499         /** Parses the arguments particular to this command. */
 500         abstract boolean parseArgs(String[] args, Messenger msg);
 501 
 502         /**
 503          * Parse the Flags for this command.
 504          * @return number of flags found.
 505          * @throws IllegalArgumentException if an invalid flag is given.
 506          */
 507         int parseFlags(String[] args, Map<Character, Flag> flags) {
 508             int rc = 0;
 509             int i = 0;
 510             while (i < args.length) {
 511                 String s = args[i];
 512                 if (s.length() == 2 && s.charAt(0) == '-') {
 513                     Flag f = flags.get(s.charAt(1));
 514                     if (f != null) {
 515                         int numConsumed = f.set(args, i);
 516                         i += numConsumed;  // Increases at each iteration.
 517                         rc += numConsumed; // Increases only when the arg is a flag.
 518                     } else {
 519                         throw new IllegalArgumentException("unrecognized flag: " // XXX i18n
 520                                                            + args[i]);
 521                     }
 522                 } else {
 523                     i++;
 524                 }
 525             }
 526             return rc;
 527         }
 528 
 529         /** Represents the actual behavior of the command. */
 530         abstract boolean run(Repository repo, Messenger msg);
 531 
 532         /** Returns a usage string describing this command, or null if the
 533          * command is a synonym for another command.
 534          */
 535         abstract String usage();
 536 
 537         @Override
 538         public String toString() { return name; }
 539 
 540         /**
 541          * RepositoryVisitor types walk a parent chain of repositories, invoking
 542          * {@code doit} in each one.  The abstract base class provides the recursion;
 543          * each concrete subclass provides the per-repository behavior.  The
 544          * recursion is such that the bootstrap repository is visited first,
 545          * and the application repository is last.
 546          */
 547         abstract class RepositoryVisitor {
 548             abstract void doit(Repository repo, Messenger msg);
 549 
 550             void preVisit(Messenger msg) { }
 551 
 552             void postVisit(Messenger msg) { }
 553 
 554             final void run(Repository repo, Messenger msg) {
 555                 visit(repo, msg);
 556             }
 557 
 558             private final void visit(Repository repo, Messenger msg) {
 559                 Repository parent = parentFlag.isEnabled() ? repo.getParent() : null;
 560                 if (parent != null) {
 561                     visit(parent, msg);
 562                 }
 563                 preVisit(msg);
 564                 doit(repo, msg);
 565                 postVisit(msg);
 566             }
 567         }
 568     }
 569 
 570 
 571     /** Lists dependencies of a module. */
 572     private class DependenciesCommand extends Command {
 573         @SuppressWarnings("unchecked")
 574         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
 575 
 576         private String name;
 577         private Version version;
 578 
 579         // For printing module name, version.
 580         private String moduleString;
 581 
 582         DependenciesCommand() {
 583             super("dependencies");
 584             javaseFlag.register(myFlags);
 585             bindingFlag.register(myFlags);
 586         }
 587 
 588         @Override
 589         void reset() {
 590             javaseFlag.reset();
 591             bindingFlag.reset();
 592         }
 593 
 594         @Override
 595         int parseFlags(String[] args, Map<Character, Flag> flags) {
 596             return super.parseFlags(args, myFlags);
 597         }
 598 
 599         boolean parseArgs(String[] args, Messenger msg) {
 600             if (args.length > 0) {
 601                 name = args[0];
 602             }
 603             if (args.length > 1) {
 604                 try {
 605                     version = Version.valueOf(args[1]);
 606                 } catch (IllegalArgumentException ex) {
 607                     return false;
 608                 }
 609             }
 610             if (args.length > 2) {
 611                 return false;
 612             }
 613 
 614             if (DEBUG) debug("name: " + name + " ver: " + version
 615                              + " plat: " + bindingFlag.getPlatform()
 616                              + " arch: " + bindingFlag.getArch());
 617             return true;
 618         }
 619 
 620         boolean run(Repository repo, Messenger msg) {
 621             boolean rc = true;
 622             if (name != null) {
 623                 VersionConstraint vc = (version == null
 624                                         ? VersionConstraint.DEFAULT
 625                                         : version.toVersionConstraint());
 626                 printHeader(name, vc);
 627                 rc = depend(repo, name, vc,
 628                             bindingFlag.getPlatform(), bindingFlag.getArch());
 629                 printTrailer(null);
 630             } else {
 631                 for (ModuleArchiveInfo mai : repo.list()) {
 632                     reset();
 633                     String maiName = mai.getName();
 634                     VersionConstraint vc = mai.getVersion().toVersionConstraint();
 635                     printHeader(maiName, vc);
 636                     rc &= depend(repo, maiName, vc,
 637                                  mai.getPlatform(), mai.getArch());
 638                     printTrailer("\n");
 639                 }
 640             }
 641             return rc;
 642         }
 643 
 644         boolean depend(Repository repo, String name, VersionConstraint constraint,
 645                        String platform, String arch) {
 646             ImportTraverser traverser = new ImportTraverser();
 647             ImportTraverser.Visitor visitor = new ImportVisitor(traverser, msg);
 648             try {
 649                 traverser.traverse(visitor, repo, name, constraint, platform, arch);
 650                 if (traverser.traversedAny()) {
 651                     return true;
 652                 }
 653                 if (verboseFlag.isEnabled()) {
 654                     msg.error("Cannot find module " + name + " in " // XXX i18n
 655                               + getRepositoryText(repo));
 656                 }
 657                 return false;
 658             } catch (ModuleInitializationException ex) {
 659                 msg.error("Cannot instantiate module for " + name // XXX i18n
 660                           + ": " + ex);
 661                 return false;
 662             }
 663         }
 664 
 665 
 666         void printHeader(String name, VersionConstraint vc) {
 667             if (verboseFlag.isEnabled()) {
 668                 msg.println("Dependencies for " // XXXi18n
 669                             + name + "-" + vc + ":");
 670             }
 671         }
 672 
 673         void printTrailer(String s) {
 674             if (verboseFlag.isEnabled()) {
 675                 msg.print(s);
 676             }
 677         }
 678 
 679         String usage() {
 680             return "dependencies [-v] [-r repositoryLocation] [-b platform-arch]"// XXX i18n
 681                 + " [moduleName [moduleVersion] ]\n"
 682                 + "        Lists all modules on which identified modules depend.\n"
 683                 + "        If no moduleName is given, lists dependencies of all"
 684                 + " module archives in the repository\n"
 685                 + "        which are instantiable on the current platform.";
 686         }
 687     }
 688 
 689 
 690     private static class ImportVisitor extends ImportTraverser.Visitor {
 691         private final Messenger msg;
 692 
 693         private static final String INDENT = "    ";
 694         private static final int INDENT_LENGTH = INDENT.length();
 695 
 696         private String indent = "";
 697 
 698         ImportVisitor(ImportTraverser traverser, Messenger msg) {
 699             super(traverser);
 700             this.msg = msg;
 701         }
 702 
 703         @Override
 704         protected void init(Module m) {
 705             printModule(m);
 706         }
 707 
 708         @Override
 709         protected boolean preVisit(Module m) {
 710             if (javaseFlag.isEnabled() == false
 711                 && m.getModuleDefinition().getName().startsWith("java.se")) {
 712                 return false;
 713             } else {
 714                 indent += INDENT;
 715                 return true;
 716             }
 717         }
 718 
 719         @Override
 720         protected void visit(Module m) {
 721             printModule(m);
 722         }
 723 
 724         @Override
 725         protected void postVisit(Module m) {
 726             if (javaseFlag.isEnabled() == false
 727                 && m.getModuleDefinition().getName().startsWith("java.se")) {
 728                 // empty
 729             } else {
 730                 indent = indent.substring(INDENT_LENGTH);
 731             }
 732         }
 733 
 734         void printModule(Module m) {
 735             msg.println(indent + getMText(m));
 736         }
 737     }
 738 
 739     /** Installs a JAM into a repository. */
 740     private class InstallCommand extends Command {
 741         private String jamName;
 742 
 743         InstallCommand() {
 744             super("install");
 745         }
 746 
 747         boolean parseArgs(String[] args, Messenger msg) {
 748             boolean rc = false;
 749             if (!parentFlag.isEnabled()
 750                     && repositoryFlag.getLocation() != null
 751                     && args.length == 1) {
 752                 jamName = args[0];
 753                 rc = true;
 754             }
 755             return rc;
 756         }
 757 
 758         boolean run(Repository repo, Messenger msg) {
 759             if (repo != null) {
 760                 String jamURL = null;
 761                 File f = new File(jamName);
 762                 if (f.canRead()) {
 763                     try {
 764                         String path = f.getCanonicalPath();
 765                         // Ensure that path starts with a "/" (it does not on
 766                         // some systems, e.g. Windows).
 767                         if (!path.startsWith("/")) {
 768                             path = "/" + path;
 769                         }
 770                         jamURL = "file://" + path;
 771                     } catch (IOException ex) {
 772                         msg.error("Cannot install " + jamName + ": " + ex.getMessage());
 773                         return false;
 774                     }
 775                 } else {
 776                     jamURL = jamName;
 777                 }
 778                 try {
 779                     ModuleArchiveInfo mai = repo.install(new URL(jamURL).toURI());
 780                     if (verboseFlag.isEnabled()) {
 781                         msg.println("Installed " + jamName + ": " + getMAIText(mai));
 782                     }
 783                     return true;
 784                 } catch (URISyntaxException ex) {
 785                     msg.error("Cannot install " + jamName + ": no such file, or malformed URI");
 786                 } catch (MalformedURLException ex) {
 787                     msg.error("Cannot install " + jamName + ": no such file, or malformed URL");
 788                 } catch (IOException ex) {
 789                     msg.error("Cannot install " + jamName + ": " + ex.getMessage());
 790                 }
 791             }
 792             return false;
 793         }
 794 
 795         String usage() {
 796             return "install [-v] -r repositoryLocation jamFile | jamURL\n" // XXX i18n
 797                 +  "        installs a module into a repository";
 798         }
 799     }
 800 
 801     /** Uninstalls a module from a repository. */
 802     private class UninstallCommand extends Command {
 803         @SuppressWarnings("unchecked")
 804         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
 805 
 806         private Flag forceFlag = new Flag('f');
 807 
 808         private Flag interactiveFlag = new Flag('i');
 809 
 810         private String moduleName;
 811 
 812         private Version version;
 813 
 814         private String platformBinding;
 815 
 816         UninstallCommand() {
 817             super("uninstall");
 818             forceFlag.register(myFlags);
 819             interactiveFlag.register(myFlags);
 820         }
 821 
 822         @Override
 823         void reset() {
 824             version = null;
 825             platformBinding = null;
 826             forceFlag.reset();
 827             interactiveFlag.reset();
 828         }
 829 
 830         @Override
 831         int parseFlags(String[] args, Map<Character, Flag> flags) {
 832             return super.parseFlags(args, myFlags);
 833         }
 834 
 835         boolean parseArgs(String[] args, Messenger msg) {
 836             if (repositoryFlag.getLocation() == null) {
 837                 return false;
 838             }
 839 
 840             if (forceFlag.isEnabled() && interactiveFlag.isEnabled()) {
 841                 // Doesn't make sense for these to both be enabled
 842                 msg.error("uninstall cannot simultaneously use both -i and -f: choose one or the other"); // XXX i81n
 843                 return false;
 844             }
 845 
 846             if (args.length >= 1) {
 847                 moduleName = args[0];
 848                 if (args.length >= 2 && args.length < 4) {
 849                     try {
 850                         version = Version.valueOf(args[1]);
 851                     } catch (IllegalArgumentException ex) {
 852                         msg.error(ex.getMessage());
 853                         return false;
 854                     }
 855                 }
 856                 if (args.length == 3) {
 857                     platformBinding = args[2];
 858                 }
 859                 return true;
 860             } else {
 861                 return false;
 862             }
 863         }
 864 
 865         boolean run(Repository repo, Messenger msg) {
 866             boolean rc = false;
 867 
 868             if (DEBUG) debug(
 869                 "force=" + forceFlag.isEnabled()
 870                 + " interactive=" + interactiveFlag.isEnabled()
 871                 + " verbose=" + verboseFlag.isEnabled()
 872                 + " repository=" + repositoryFlag.getLocation()
 873                 + " moduleName=" + moduleName
 874                 + " version=" + version
 875                 + " plat/arch=" + platformBinding);
 876 
 877             List<ModuleArchiveInfo> found = new ArrayList<ModuleArchiveInfo>();
 878             for (ModuleArchiveInfo mai : repo.list()) {
 879                 if (match(mai)) {
 880                     found.add(mai);
 881                 }
 882             }
 883             if (DEBUG) debug("found.size=" + found.size());
 884             if (found.size() == 1) {
 885                 ModuleArchiveInfo mai = found.get(0);
 886                 rc = uninstall(repo, mai, msg);
 887             } else if (found.size() == 0) {
 888                 if (verboseFlag.isEnabled()) {
 889                     msg.error("Could not find a module matching " + getInfo());
 890                 }
 891             } else { // multiple matches
 892                 if (!forceFlag.isEnabled() && !interactiveFlag.isEnabled()) {
 893                     msg.error("Cannot uninstall: multiple modules match " + getInfo());
 894                     if (verboseFlag.isEnabled()) {
 895                         for (ModuleArchiveInfo mai : found) {
 896                             msg.error(getMAIText(mai));
 897                         }
 898                     }
 899                 } else if (forceFlag.isEnabled()) {
 900                     if (DEBUG) debug("forced uninstall of multiple matches");
 901                     rc = true;
 902                     for (ModuleArchiveInfo mai : found) {
 903                         if (!uninstall(repo, mai, msg)) {
 904                             if (DEBUG) debug("uninstall failed for " + getMAIText(mai) + " in " + repo.toString());
 905                             rc = false;
 906                             break;
 907                         }
 908                     }
 909                 } else if (interactiveFlag.isEnabled()) {
 910                     rc = uninstallInteractive(repo, found, msg);
 911                 }
 912             }
 913             return rc;
 914         }
 915 
 916         String usage() {
 917             return "uninstall [-v] [-f | -i] -r repositoryLocation moduleName"// XXX i18n
 918                 + " [moduleVersion] [modulePlatformBinding]\n"
 919                 +  "        removes a module from a repository, along with associated files.";
 920         }
 921 
 922         /** Uninstall the ModuleArchiveInfo from the Repository. */
 923         private boolean uninstall(Repository repo, ModuleArchiveInfo mai, Messenger msg) {
 924             boolean rc = false;
 925             if (DEBUG) debug("Uninstalling " + getMAIText(mai));
 926             try {
 927                 rc = repo.uninstall(mai);
 928                 if (verboseFlag.isEnabled()) {
 929                     if (rc) {
 930                         msg.println("Uninstalled " + getMAIText(mai)); // XXX i18n
 931                     } else {
 932                         msg.error("Failed to uninstall " + getMAIText(mai)); // XXX i18n
 933                     }
 934                 }
 935             } catch (Exception ex) {
 936                 msg.error("Exception while uninstalling " + getInfo()
 937                           + ": " + ex.getMessage());
 938             }
 939             return rc;
 940         }
 941 
 942         /** Uninstall one of the ModuleArchiveInfos from the Repository. */
 943         private boolean uninstallInteractive(
 944                 Repository repo,
 945                 List<ModuleArchiveInfo> found,
 946                 Messenger msg) {
 947             boolean rc = false;
 948 
 949             String spaces = "          ";
 950             String fmt = "%" + found.size() + "d %s\n";
 951 
 952             StringWriter sw = new StringWriter();
 953             PrintWriter pw = new PrintWriter(sw);
 954             pw.println("Multiple matches for module found.  Choose one of the below by index:");
 955             boolean saveVerbose = verboseFlag.isEnabled();
 956             verboseFlag.set(new String[0], 0);
 957             int count = 0;
 958             for (ModuleArchiveInfo m : found) {
 959                 pw.printf(fmt, count++, getMAIText(m));
 960             }
 961             pw.print("\nIndex? ");
 962             pw.close();
 963             String choiceMessage = sw.toString();
 964             boolean done = false;
 965             while (!done) {
 966                 msg.print(choiceMessage);
 967                 String input = null;
 968                 try {
 969                     input = msg.readLine().trim();
 970                     int index = Integer.parseInt(input);
 971                     if (index >= 0 && index < found.size()) {
 972                         uninstall(repo, found.get(index), msg);
 973                         done = true;
 974                         rc = true;
 975                     } else {
 976                         msg.error("Invalid input " + input + "; try again");
 977                     }
 978                 } catch (NumberFormatException ex) {
 979                     msg.error("Invalid input '" + input + "'; try again");
 980                 } catch (Exception ex) {
 981                     if (DEBUG) debug("msg.readLine threw " + ex);
 982                     done = true;
 983                 }
 984             }
 985 
 986             // Restore if necessary.
 987             if (!saveVerbose) {
 988                 verboseFlag.reset();
 989             }
 990 
 991             return rc;
 992         }
 993 
 994         /**
 995          * @return true iff moduleName matches mai.getName(), and if version
 996          * and platformBinding are set, they also match.
 997          */
 998         private boolean match(ModuleArchiveInfo mai) {
 999             boolean rc = false;
1000             if (DEBUG) debug("attempting to match " + getMAIText(mai));
1001             if (moduleName.equals(mai.getName())) {
1002                 if (version == null) {
1003                     rc = true;
1004                 } else if (version.equals(mai.getVersion())) {
1005                     if (platformBinding == null) {
1006                         rc = true;
1007                     } else {
1008                         String pb = mai.getPlatform() + "-" + mai.getArch();
1009                         if (platformBinding.equals(pb)) {
1010                             rc = true;
1011                         }
1012                     }
1013                 }
1014             }
1015             return rc;
1016         }
1017 
1018         private String getInfo() {
1019             String s = moduleName;
1020             if (version != null) {
1021                 s += " with version " + version;
1022             }
1023             if (platformBinding != null) {
1024                 s += " and platform-binding of " + platformBinding;
1025             }
1026             return s;
1027         }
1028     }
1029 
1030     /** Prints information about modules found in repositories. */
1031     private class ListCommand extends Command {
1032         @SuppressWarnings("unchecked")
1033         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
1034 
1035         ListCommand() {
1036             super("list");
1037             parentFlag.register(myFlags);
1038         }
1039 
1040         @Override
1041         void reset() {
1042             parentFlag.reset();
1043         }
1044 
1045         @Override
1046         int parseFlags(String[] args, Map<Character, Flag> flags) {
1047             return super.parseFlags(args, myFlags);
1048         }
1049 
1050         boolean parseArgs(String[] args, Messenger msg){
1051             boolean rc = true;
1052             if (args.length == 0) {
1053                 moduleName = null;
1054             } else if (args.length == 1) {
1055                 moduleName = args[0];
1056             } else {
1057                 rc = false;
1058             }
1059             return rc;
1060         }
1061 
1062         boolean run(Repository repo, Messenger msg) {
1063             ListRepositoryVisitor visitor = new ListRepositoryVisitor();
1064             visitor.run(repo, msg);
1065             boolean found = visitor.wasFound();
1066             if (verboseFlag.isEnabled() && !found) {
1067                 if (moduleName != null) {
1068                     msg.error("Could not find module name starting with '"// XXX i18n
1069                               + moduleName + "'");
1070                 } else {
1071                     msg.error("Could not find any modules"); // XXX i18n
1072                 }
1073             }
1074             return found;
1075         }
1076 
1077         String usage() {
1078             return "list [-v] [-p] [-r repositoryLocation] moduleName\n"// XXX i18n
1079                 +  "        lists the modules in the repository";
1080         }
1081 
1082         class ListRepositoryVisitor extends RepositoryVisitor {
1083             private boolean found = false;
1084 
1085             boolean wasFound() { return found; }
1086 
1087             void doit(Repository repo, Messenger msg) {
1088                 boolean printedHeader = false;
1089                 List<ModuleArchiveInfo> maiList = repo.list();
1090                 if (maiList.size() == 0 && verboseFlag.isEnabled()) {
1091                     msg.println(getRepositoryText(repo));
1092                     msg.println("   empty");
1093                 } else {
1094                     for (ModuleArchiveInfo mai : repo.list()) {
1095                         if (moduleName == null || mai.getName().startsWith(moduleName)) {
1096                             if (!printedHeader) {
1097                                 msg.println(getRepositoryText(repo));
1098                                 msg.println(verboseFlag.isEnabled() ? maiHeadingVerbose : maiHeading);
1099                                 printedHeader = true;
1100                             }
1101                             msg.println(getMAIText(mai));
1102                             found = true;
1103                         }
1104                     }
1105                 }
1106             }
1107         }
1108     }
1109 }