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