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 system 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 system 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.getSystemRepository();
 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.getSystemRepository());
 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.getSystemRepository());
 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         return rc;
 301     }
 302 
 303     private static String getMAIText(ModuleArchiveInfo mai) {
 304         StringWriter sw = new StringWriter();
 305         PrintWriter pw = new PrintWriter(sw);
 306         pw.printf(MAIFormat, mai.getName(), mai.getVersion());
 307         if (verboseFlag.isEnabled()) {
 308             long t = mai.getLastModified();
 309             String lastMod = null;
 310             if (t != 0) {
 311                 lastMod = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date(t));
 312             }
 313             pw.printf(MAIFormatVerbose,
 314                 mai.getPlatform() == null ? "generic" : mai.getPlatform(),
 315                 mai.getArch() == null ? "generic" : mai.getArch(),
 316                 lastMod == null ? "n/a" : lastMod,
 317                 mai.getFileName() == null ? "n/a" : mai.getFileName()
 318                 );
 319         }
 320         return sw.toString();
 321     }
 322 
 323     private static String getMText(Module m) {
 324         return getMDText(m.getModuleDefinition());
 325     }
 326     
 327     private static String getMDText(ModuleDefinition md) {
 328         StringWriter sw = new StringWriter();
 329         PrintWriter pw = new PrintWriter(sw);
 330         pw.printf(MDFormat, md.getName(), md.getVersion());
 331         if (verboseFlag.isEnabled()) {
 332             pw.printf(MDFormatVerbose,
 333                       md.getRepository() == Repository.getBootstrapRepository()
 334                       ? "bootstrap"
 335                       : md.getRepository().getSourceLocation().toString());
 336         }
 337         return sw.toString();
 338     }
 339 
 340     void usageError() {
 341         usageError(msg);
 342     }
 343 
 344     static void usageError(Messenger msg) {
 345         if (usage == null) {
 346             StringBuilder ub = new StringBuilder(
 347                 "Usage: jrepo <command>\nwhere <command> includes:"); // XXX i18n
 348             for (Command c : commands.values()) {
 349                 String u = c.usage();
 350                 if (u != null) {
 351                     ub.append("\n    ").append(c.usage());
 352                 }
 353             }
 354             usage = ub.toString();
 355         }
 356         msg.error(usage);
 357     }
 358 
 359     /**
 360      * Represents a flag given on the command line.  Flags always are 2
 361      * characters long, and start with a '-'.  They can have additional
 362      * associated arguments (cf {@link #RepositoryFlag}).
 363      */
 364     private static class Flag {
 365         private final char name;
 366 
 367         private boolean enabled = false;
 368 
 369         private static final Map<Character, Flag> flags = new HashMap<Character, Flag>();
 370 
 371         Flag(char name) {
 372             this.name = name;
 373             flags.put(new Character(name), this);
 374         }
 375 
 376         void register(Map<Character, Flag> registry) {
 377             registry.put(new Character(name), this);
 378         }
 379 
 380         char getName() {
 381             return name;
 382         }
 383 
 384         /**
 385          * @return the number of arguments consumed by this Flag
 386          * @throws IllegalArgumentException if invalid args are given
 387          */
 388         int set(String[] args, int pos) throws IllegalArgumentException {
 389             enabled = true;
 390             return 1;
 391         }
 392 
 393         void reset() {
 394             enabled = false;
 395         }
 396 
 397         boolean isEnabled() {
 398             return enabled;
 399         }
 400 
 401         static Flag get(char c) {
 402             return flags.get(new Character(c));
 403         }
 404     }
 405 
 406     private static class RepositoryFlag extends Flag {
 407         String location = null;
 408 
 409         RepositoryFlag() {
 410             super('r');
 411         }
 412 
 413         @Override
 414         int set(String[] args, int pos) throws IllegalArgumentException {
 415             int rc = super.set(args, pos);
 416             location = args[pos + 1];
 417             return rc + 1;
 418         }
 419 
 420         String getLocation() {
 421             return location;
 422         }
 423 
 424         @Override
 425         void reset() {
 426             super.reset();
 427             location = null;
 428         }
 429     }
 430 
 431     private static class BindingFlag extends Flag {
 432         String platform;
 433         String arch;
 434 
 435         BindingFlag() {
 436             super('b');
 437         }
 438 
 439         @Override
 440         int set(String[] args, int pos) throws IllegalArgumentException {
 441             int rc = super.set(args, pos);
 442             String[] binding = args[pos + 1].split("-");
 443             if (binding.length != 2) {
 444                 throw new IllegalArgumentException(
 445                     "Must 2 and only 2 elements for platform-arch");
 446             }
 447             platform = binding[0];
 448             arch = binding[1];
 449             return rc + 1;
 450         }
 451 
 452         String getPlatform() {
 453             return platform;
 454         }
 455 
 456         String getArch() {
 457             return arch;
 458         }
 459 
 460         @Override
 461         void reset() {
 462             super.reset();
 463             platform = null;
 464             arch = null;
 465         }
 466     }
 467 
 468     /*
 469      * Command types: An abstract base class, plus one concrete class for
 470      * each Command.
 471      */
 472 
 473     /*
 474      * Represents a Command.
 475      */
 476     private static abstract class Command {
 477         private final String name;
 478 
 479         Command(String name) {
 480             this.name = name;
 481         }
 482 
 483         /** Adds this command to the given registry. */
 484         void register(Map<String, Command> registry) {
 485             registry.put(name, this);
 486         }
 487 
 488         void reset() {
 489             // Empty; subclasses can implement
 490         }
 491 
 492         // Flags differ from arguments in that they have the form "-X [opt]".
 493         // Flags appear in a command line before arguments.
 494 
 495         /** Parses the arguments particular to this command. */
 496         abstract boolean parseArgs(String[] args, Messenger msg);
 497 
 498         /**
 499          * Parse the Flags for this command.
 500          * @return number of flags found.
 501          * @throws IllegalArgumentException if an invalid flag is given.
 502          */
 503         int parseFlags(String[] args, Map<Character, Flag> flags) {
 504             int rc = 0;
 505             int i = 0;
 506             while (i < args.length) {
 507                 String s = args[i];
 508                 if (s.length() == 2 && s.charAt(0) == '-') {
 509                     Flag f = flags.get(s.charAt(1));
 510                     if (f != null) {
 511                         int numConsumed = f.set(args, i);
 512                         i += numConsumed;  // Increases at each iteration.
 513                         rc += numConsumed; // Increases only when the arg is a flag.
 514                     } else {
 515                         throw new IllegalArgumentException("unrecognized flag: " // XXX i18n
 516                                                            + args[i]);
 517                     }
 518                 } else {
 519                     i++;
 520                 }
 521             }
 522             return rc;
 523         }
 524 
 525         /** Represents the actual behavior of the command. */
 526         abstract boolean run(Repository repo, Messenger msg);
 527 
 528         /** Returns a usage string describing this command, or null if the
 529          * command is a synonym for another command.
 530          */
 531         abstract String usage();
 532 
 533         @Override
 534         public String toString() { return name; }
 535 
 536         /**
 537          * RepositoryVisitor types walk a parent chain of repositories, invoking
 538          * {@code doit} in each one.  The abstract base class provides the recursion;
 539          * each concrete subclass provides the per-repository behavior.  The
 540          * recursion is such that the bootstrap repository is visited first,
 541          * and the system repository is last.
 542          */
 543         abstract class RepositoryVisitor {
 544             abstract void doit(Repository repo, Messenger msg);
 545 
 546             void preVisit(Messenger msg) { }
 547 
 548             void postVisit(Messenger msg) { }
 549 
 550             final void run(Repository repo, Messenger msg) {
 551                 visit(repo, msg);
 552             }
 553 
 554             private final void visit(Repository repo, Messenger msg) {
 555                 Repository parent = parentFlag.isEnabled() ? repo.getParent() : null;
 556                 if (parent != null) {
 557                     visit(parent, msg);
 558                 }
 559                 preVisit(msg);
 560                 doit(repo, msg);
 561                 postVisit(msg);
 562             }
 563         }
 564     }
 565 
 566 
 567     /** Lists dependencies of a module. */
 568     private class DependenciesCommand extends Command {
 569         @SuppressWarnings("unchecked")
 570         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
 571 
 572         private String name;
 573         private Version version;
 574 
 575         // For printing module name, version.
 576         private String moduleString;
 577 
 578         DependenciesCommand() {
 579             super("dependencies");
 580             javaseFlag.register(myFlags);
 581             bindingFlag.register(myFlags);
 582         }
 583 
 584         @Override
 585         void reset() {
 586             javaseFlag.reset();
 587             bindingFlag.reset();
 588         }
 589         
 590         @Override
 591         int parseFlags(String[] args, Map<Character, Flag> flags) {
 592             return super.parseFlags(args, myFlags);
 593         }
 594 
 595         boolean parseArgs(String[] args, Messenger msg) {
 596             if (args.length > 0) {
 597                 name = args[0];
 598             }
 599             if (args.length > 1) {
 600                 try {
 601                     version = Version.valueOf(args[1]);
 602                 } catch (IllegalArgumentException ex) {
 603                     return false;
 604                 }
 605             }
 606             if (args.length > 2) {
 607                 return false;
 608             }
 609             
 610             if (DEBUG) debug("name: " + name + " ver: " + version
 611                              + " plat: " + bindingFlag.getPlatform()
 612                              + " arch: " + bindingFlag.getArch());
 613             return true;
 614         }
 615 
 616         boolean run(Repository repo, Messenger msg) {
 617             boolean rc = true;
 618             if (name != null) {
 619                 VersionConstraint vc = (version == null
 620                                         ? VersionConstraint.DEFAULT
 621                                         : version.toVersionConstraint());
 622                 printHeader(name, vc);
 623                 rc = depend(repo, name, vc,
 624                             bindingFlag.getPlatform(), bindingFlag.getArch());
 625                 printTrailer(null);
 626             } else {
 627                 for (ModuleArchiveInfo mai : repo.list()) {
 628                     reset();
 629                     String maiName = mai.getName();
 630                     VersionConstraint vc = mai.getVersion().toVersionConstraint();
 631                     printHeader(maiName, vc);
 632                     rc &= depend(repo, maiName, vc,
 633                                  mai.getPlatform(), mai.getArch());
 634                     printTrailer("\n");
 635                 }
 636             }
 637             return rc;
 638         }
 639 
 640         boolean depend(Repository repo, String name, VersionConstraint constraint,
 641                        String platform, String arch) {
 642             ImportTraverser traverser = new ImportTraverser();
 643             ImportTraverser.Visitor visitor = new ImportVisitor(traverser, msg);
 644             try {
 645                 traverser.traverse(visitor, repo, name, constraint, platform, arch);
 646                 if (traverser.traversedAny()) {
 647                     return true;
 648                 }
 649                 if (verboseFlag.isEnabled()) {
 650                     msg.error("Cannot find module " + name + " in " // XXX i18n
 651                               + getRepositoryText(repo));
 652                 }
 653                 return false;
 654             } catch (ModuleInitializationException ex) {
 655                 msg.error("Cannot instantiate module for " + name // XXX i18n
 656                           + ": " + ex);
 657                 return false;
 658             }
 659         }
 660 
 661         
 662         void printHeader(String name, VersionConstraint vc) {
 663             if (verboseFlag.isEnabled()) {
 664                 msg.println("Dependencies for " // XXXi18n
 665                             + name + "-" + vc + ":");
 666             }
 667         }
 668 
 669         void printTrailer(String s) {
 670             if (verboseFlag.isEnabled()) {
 671                 msg.print(s);
 672             }
 673         }
 674         
 675         // TBD support platform binding.  Ideally, want to allow specifying
 676         // version, or binding, or both.
 677         String usage() {
 678             return "dependencies [-v] [-r repositoryLocation] [-b platform-arch]"// XXX i18n
 679                 + " [moduleName [moduleVersion] ]\n"
 680                 + "        Lists all modules on which identified modules depend.\n"
 681                 + "        If no moduleName is given, lists dependencies of all"
 682                 + " module archives in the repository\n"
 683                 + "        which are instantiable on the current platform.";
 684         }
 685     }
 686             
 687 
 688     private static class ImportVisitor extends ImportTraverser.Visitor {
 689         private final Messenger msg;
 690         
 691         private static final String INDENT = "    ";
 692         private static final int INDENT_LENGTH = INDENT.length();
 693         
 694         private String indent = "";
 695 
 696         ImportVisitor(ImportTraverser traverser, Messenger msg) {
 697             super(traverser);
 698             this.msg = msg;
 699         }
 700 
 701         @Override
 702         protected void init(Module m) {
 703             printModule(m);
 704         }
 705 
 706         @Override
 707         protected boolean preVisit(Module m) {
 708             if (javaseFlag.isEnabled() == false
 709                 && m.getModuleDefinition().getName().startsWith("java.se")) {
 710                 return false;
 711             } else {
 712                 indent += INDENT;
 713                 return true;
 714             }
 715         }
 716 
 717         @Override
 718         protected void visit(Module m) {
 719             printModule(m);
 720         }
 721 
 722         @Override
 723         protected void postVisit(Module m) {
 724             if (javaseFlag.isEnabled() == false
 725                 && m.getModuleDefinition().getName().startsWith("java.se")) {
 726                 // empty
 727             } else {
 728                 indent = indent.substring(INDENT_LENGTH);
 729             }
 730         }
 731 
 732         void printModule(Module m) {
 733             msg.println(indent + getMText(m));
 734         }
 735     }
 736 
 737     /** Installs a JAM into a repository. */
 738     private class InstallCommand extends Command {
 739         private String jamName;
 740 
 741         InstallCommand() {
 742             super("install");
 743         }
 744 
 745         boolean parseArgs(String[] args, Messenger msg) {
 746             boolean rc = false;
 747             if (!parentFlag.isEnabled()
 748                     && repositoryFlag.getLocation() != null
 749                     && args.length == 1) {
 750                 jamName = args[0];
 751                 rc = true;
 752             }
 753             return rc;
 754         }
 755 
 756         boolean run(Repository repo, Messenger msg) {
 757             if (repo != null) {
 758                 String jamURL = null;
 759                 File f = new File(jamName);
 760                 if (f.canRead()) {
 761                     try {
 762                         String path = f.getCanonicalPath();
 763                         // Ensure that path starts with a "/" (it does not on
 764                         // some systems, e.g. Windows).
 765                         if (!path.startsWith("/")) {
 766                             path = "/" + path;
 767                         }
 768                         jamURL = "file://" + path;
 769                     } catch (IOException ex) {
 770                         msg.error("Cannot install " + jamName + ": " + ex.getMessage());
 771                         return false;
 772                     }
 773                 } else {
 774                     jamURL = jamName;
 775                 }
 776                 try {
 777                     ModuleArchiveInfo mai = repo.install(new URL(jamURL).toURI());
 778                     if (verboseFlag.isEnabled()) {
 779                         msg.println("Installed " + jamName + ": " + getMAIText(mai));
 780                     }
 781                     return true;
 782                 } catch (URISyntaxException ex) {
 783                     msg.error("Cannot install " + jamName + ": no such file, or malformed URI");
 784                 } catch (MalformedURLException ex) {
 785                     msg.error("Cannot install " + jamName + ": no such file, or malformed URL");
 786                 } catch (IOException ex) {
 787                     msg.error("Cannot install " + jamName + ": " + ex.getMessage());
 788                 }
 789             }
 790             return false;
 791         }
 792 
 793         String usage() {
 794             return "install [-v] -r repositoryLocation jamFile | jamURL\n" // XXX i18n
 795                 +  "        installs a module into a repository";
 796         }
 797     }
 798 
 799     /** Uninstalls a module from a repository. */
 800     private class UninstallCommand extends Command {
 801         @SuppressWarnings("unchecked")
 802         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
 803 
 804         private Flag forceFlag = new Flag('f');
 805 
 806         private Flag interactiveFlag = new Flag('i');
 807 
 808         private String moduleName;
 809 
 810         private Version version;
 811 
 812         private String platformBinding;
 813 
 814         UninstallCommand() {
 815             super("uninstall");
 816             forceFlag.register(myFlags);
 817             interactiveFlag.register(myFlags);
 818         }
 819 
 820         @Override
 821         void reset() {
 822             version = null;
 823             platformBinding = null;
 824             forceFlag.reset();
 825             interactiveFlag.reset();
 826         }
 827 
 828         @Override
 829         int parseFlags(String[] args, Map<Character, Flag> flags) {
 830             return super.parseFlags(args, myFlags);
 831         }
 832 
 833         boolean parseArgs(String[] args, Messenger msg) {
 834             if (repositoryFlag.getLocation() == null) {
 835                 return false;
 836             }
 837 
 838             if (forceFlag.isEnabled() && interactiveFlag.isEnabled()) {
 839                 // Doesn't make sense for these to both be enabled
 840                 msg.error("uninstall cannot simultaneously use both -i and -f: choose one or the other"); // XXX i81n
 841                 return false;
 842             }
 843 
 844             if (args.length >= 1) {
 845                 moduleName = args[0];
 846                 if (args.length >= 2 && args.length < 4) {
 847                     try {
 848                         version = Version.valueOf(args[1]);
 849                     } catch (IllegalArgumentException ex) {
 850                         msg.error(ex.getMessage());
 851                         return false;
 852                     }
 853                 }
 854                 if (args.length == 3) {
 855                     platformBinding = args[2];
 856                 }
 857                 return true;
 858             } else {
 859                 return false;
 860             }
 861         }
 862 
 863         boolean run(Repository repo, Messenger msg) {
 864             boolean rc = false;
 865 
 866             if (DEBUG) debug(
 867                 "force=" + forceFlag.isEnabled()
 868                 + " interactive=" + interactiveFlag.isEnabled()
 869                 + " verbose=" + verboseFlag.isEnabled()
 870                 + " repository=" + repositoryFlag.getLocation()
 871                 + " moduleName=" + moduleName
 872                 + " version=" + version
 873                 + " plat/arch=" + platformBinding);
 874 
 875             List<ModuleArchiveInfo> found = new ArrayList<ModuleArchiveInfo>();
 876             for (ModuleArchiveInfo mai : repo.list()) {
 877                 if (match(mai)) {
 878                     found.add(mai);
 879                 }
 880             }
 881             if (DEBUG) debug("found.size=" + found.size());
 882             if (found.size() == 1) {
 883                 ModuleArchiveInfo mai = found.get(0);
 884                 rc = uninstall(repo, mai, msg);
 885             } else if (found.size() == 0) {
 886                 if (verboseFlag.isEnabled()) {
 887                     msg.error("Could not find a module matching " + getInfo());
 888                 }
 889             } else { // multiple matches
 890                 if (!forceFlag.isEnabled() && !interactiveFlag.isEnabled()) {
 891                     msg.error("Cannot uninstall: multiple modules match " + getInfo());
 892                     if (verboseFlag.isEnabled()) {
 893                         for (ModuleArchiveInfo mai : found) {
 894                             msg.error(getMAIText(mai));
 895                         }
 896                     }
 897                 } else if (forceFlag.isEnabled()) {
 898                     if (DEBUG) debug("forced uninstall of multiple matches");
 899                     rc = true;
 900                     for (ModuleArchiveInfo mai : found) {
 901                         if (!uninstall(repo, mai, msg)) {
 902                             if (DEBUG) debug("uninstall failed for " + getMAIText(mai) + " in " + repo.getSourceLocation());
 903                             rc = false;
 904                             break;
 905                         }
 906                     }
 907                 } else if (interactiveFlag.isEnabled()) {
 908                     rc = uninstallInteractive(repo, found, msg);
 909                 }
 910             }
 911             return rc;
 912         }
 913 
 914         String usage() {
 915             return "uninstall [-v] [-f | -i] -r repositoryLocation moduleName"// XXX i18n
 916                 + " [moduleVersion] [modulePlatformBinding]\n"
 917                 +  "        removes a module from a repository, along with associated files.";
 918         }
 919 
 920         /** Uninstall the ModuleArchiveInfo from the Repository. */
 921         private boolean uninstall(Repository repo, ModuleArchiveInfo mai, Messenger msg) {
 922             boolean rc = false;
 923             if (DEBUG) debug("Uninstalling " + getMAIText(mai));
 924             try {
 925                 rc = repo.uninstall(mai);
 926                 if (verboseFlag.isEnabled()) {
 927                     if (rc) {
 928                         msg.println("Uninstalled " + getMAIText(mai)); // XXX i18n
 929                     } else {
 930                         msg.error("Failed to uninstall " + getMAIText(mai)); // XXX i18n
 931                     }
 932                 }
 933             } catch (Exception ex) {
 934                 msg.error("Exception while uninstalling " + getInfo()
 935                           + ": " + ex.getMessage());
 936             }
 937             return rc;
 938         }
 939 
 940         /** Uninstall one of the ModuleArchiveInfos from the Repository. */
 941         private boolean uninstallInteractive(
 942                 Repository repo,
 943                 List<ModuleArchiveInfo> found,
 944                 Messenger msg) {
 945             boolean rc = false;
 946 
 947             String spaces = "          ";
 948             String fmt = "%" + found.size() + "d %s\n";
 949 
 950             StringWriter sw = new StringWriter();
 951             PrintWriter pw = new PrintWriter(sw);
 952             pw.println("Multiple matches for module found.  Choose one of the below by index:");
 953             boolean saveVerbose = verboseFlag.isEnabled();
 954             verboseFlag.set(new String[0], 0);
 955             int count = 0;
 956             for (ModuleArchiveInfo m : found) {
 957                 pw.printf(fmt, count++, getMAIText(m));
 958             }
 959             pw.print("\nIndex? ");
 960             pw.close();
 961             String choiceMessage = sw.toString();
 962             boolean done = false;
 963             while (!done) {
 964                 msg.print(choiceMessage);
 965                 String input = null;
 966                 try {
 967                     input = msg.readLine().trim();
 968                     int index = Integer.parseInt(input);
 969                     if (index >= 0 && index < found.size()) {
 970                         uninstall(repo, found.get(index), msg);
 971                         done = true;
 972                         rc = true;
 973                     } else {
 974                         msg.error("Invalid input " + input + "; try again");
 975                     }
 976                 } catch (NumberFormatException ex) {
 977                     msg.error("Invalid input '" + input + "'; try again");
 978                 } catch (Exception ex) {
 979                     if (DEBUG) debug("msg.readLine threw " + ex);
 980                     done = true;
 981                 }
 982             }
 983 
 984             // Restore if necessary.
 985             if (!saveVerbose) {
 986                 verboseFlag.reset();
 987             }
 988 
 989             return rc;
 990         }
 991 
 992         /**
 993          * @return true iff moduleName matches mai.getName(), and if version
 994          * and platformBinding are set, they also match.
 995          */
 996         private boolean match(ModuleArchiveInfo mai) {
 997             boolean rc = false;
 998             if (DEBUG) debug("attempting to match " + getMAIText(mai));
 999             if (moduleName.equals(mai.getName())) {
1000                 if (version == null) {
1001                     rc = true;
1002                 } else if (version.equals(mai.getVersion())) {
1003                     if (platformBinding == null) {
1004                         rc = true;
1005                     } else {
1006                         String pb = mai.getPlatform() + "-" + mai.getArch();
1007                         if (platformBinding.equals(pb)) {
1008                             rc = true;
1009                         }
1010                     }
1011                 }
1012             }
1013             return rc;
1014         }
1015 
1016         private String getInfo() {
1017             String s = moduleName;
1018             if (version != null) {
1019                 s += " with version " + version;
1020             }
1021             if (platformBinding != null) {
1022                 s += " and platform-binding of " + platformBinding;
1023             }
1024             return s;
1025         }
1026     }
1027 
1028     /** Prints information about modules found in repositories. */
1029     private class ListCommand extends Command {
1030         @SuppressWarnings("unchecked")
1031         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
1032 
1033         ListCommand() {
1034             super("list");
1035             parentFlag.register(myFlags);
1036         }
1037 
1038         @Override
1039         void reset() {
1040             parentFlag.reset();
1041         }
1042 
1043         @Override
1044         int parseFlags(String[] args, Map<Character, Flag> flags) {
1045             return super.parseFlags(args, myFlags);
1046         }
1047 
1048         boolean parseArgs(String[] args, Messenger msg){
1049             boolean rc = true;
1050             if (args.length == 0) {
1051                 moduleName = null;
1052             } else if (args.length == 1) {
1053                 moduleName = args[0];
1054             } else {
1055                 rc = false;
1056             }
1057             return rc;
1058         }
1059 
1060         boolean run(Repository repo, Messenger msg) {
1061             ListRepositoryVisitor visitor = new ListRepositoryVisitor();
1062             visitor.run(repo, msg);
1063             boolean found = visitor.wasFound();
1064             if (verboseFlag.isEnabled() && !found) {
1065                 if (moduleName != null) {
1066                     msg.error("Could not find module name starting with '"// 
1067                                                                           // XXX i18n
1068                               + moduleName + "'");
1069                 } else {
1070                     msg.error("Could not find any modules"); // XXX i18n
1071                 }
1072             }
1073             return found;
1074         }
1075 
1076         String usage() {
1077             return "list [-v] [-p] [-r repositoryLocation] moduleName\n"// 
1078                                                                         // 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 }