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.PrintStream;
  34 import java.io.PrintWriter;
  35 import java.io.StringWriter;
  36 import java.net.MalformedURLException;
  37 import java.net.URI;
  38 import java.net.URL;
  39 import java.net.URISyntaxException;
  40 import java.module.*;
  41 import java.security.AccessController;
  42 import java.text.DateFormat;
  43 import java.util.Arrays;
  44 import java.util.ArrayList;
  45 import java.util.Date;
  46 import java.util.HashMap;
  47 import java.util.LinkedHashMap;
  48 import java.util.List;
  49 import java.util.Map;
  50 import sun.module.JamUtils;
  51 import sun.module.repository.RepositoryConfig;
  52 import sun.security.action.GetPropertyAction;
  53 import sun.tools.jar.CommandLine;
  54 
  55 /**
  56  * Java Modules Repository Management Tool
  57  * @since 1.7
  58  */
  59 public class JRepo {
  60     public static final boolean DEBUG;
  61 
  62     static {
  63         DEBUG = (AccessController.doPrivileged(new GetPropertyAction("sun.module.tools.debug")) != null);
  64     }
  65 
  66     private static void debug(String s) {
  67         System.err.println(s);
  68     }
  69 
  70     /** For printing user output. */
  71     private final Messenger msg;
  72 
  73     /** Optional command line argument used by {@code list()}. */
  74     private String moduleName;
  75 
  76     /** Map from this tool's command names to the commands themselves. */
  77     private static final Map<String, Command> commands = new LinkedHashMap<String, Command>();
  78 
  79     /** Usage message created from each command's usage. */
  80     private static String usage = null;
  81 
  82     /** Format for module name & version. */
  83     private static final String MAIFormat = "%-20s %-20s";
  84 
  85     /** Format for additional/verbose module information. */
  86     private static final String MAIFormatVerbose = " %-9s %-7s %-17s %s";
  87 
  88     /** String containing column headings for name & version. */
  89     private static final String maiHeading;
  90 
  91     /** String containing column headings for additional/verbose module information. */
  92     private static final String maiHeadingVerbose;
  93 
  94     /** Indicates that parent repositories should be used by a command. */
  95     private static final Flag parentFlag = new Flag('p');
  96 
  97     /** Indicates that command output should be verbose. */
  98     private static final Flag verboseFlag = new Flag('v');
  99 
 100     /** Location of repository; if not given uses system repository. */
 101     private static final RepositoryFlag repositoryFlag = new RepositoryFlag();
 102 
 103     /** Contains the flags that are common to all commands. */
 104     private static final HashMap<Character, Flag> commonFlags = new HashMap<Character, Flag>();
 105 
 106     static {
 107         repositoryFlag.register(commonFlags);
 108         verboseFlag.register(commonFlags);
 109 
 110         StringWriter sw = new StringWriter();
 111         PrintWriter pw = new PrintWriter(sw);
 112         pw.printf(MAIFormat, "Name", "Version"); // XXX i18n
 113         maiHeading = sw.toString();
 114 
 115         sw = new StringWriter();
 116         pw = new PrintWriter(sw);
 117         pw.printf(MAIFormat, "Name", "Version"); // XXX i18n
 118         pw.printf(MAIFormatVerbose, "Platform", "Arch", "Modified", "Filename"); // XXX i18n
 119         maiHeadingVerbose = sw.toString();
 120     }
 121 
 122     public JRepo(PrintStream out, PrintStream err, BufferedReader reader) {
 123         msg = new Messenger("jrepo", out, err, reader);
 124         synchronized(commands) {
 125             if (commands.isEmpty()) {
 126                 new ListCommand().register(commands);
 127                 new InstallCommand().register(commands);
 128                 new UninstallCommand().register(commands);
 129             }
 130         }
 131         reset();
 132     }
 133 
 134     public static void main(String[] args) {
 135         JRepo jrepo = new JRepo(
 136             System.out, System.err,
 137             new BufferedReader(new InputStreamReader(System.in)));
 138         System.exit(jrepo.run(args) ? 0 : 1);
 139     }
 140 
 141     public synchronized boolean run(String[] args) {
 142         reset();
 143 
 144         if (DEBUG) { for (String s : args) debug("arg: '" + s + "'"); }
 145         Command cmd = parseArgs(args);
 146         if (DEBUG && cmd != null) debug("running " + cmd);
 147         if (cmd == null) {
 148             return false;
 149         }
 150 
 151         Repository repo = null;
 152         try {
 153              repo = getRepository();
 154              repo.shutdownOnExit(true);
 155         } catch (IOException ex) {
 156             msg.fatalError(ex.getMessage());
 157             return false;
 158         }
 159 
 160         boolean rc = cmd.run(repo, msg);
 161         if (DEBUG) debug(cmd + " returned " + rc);
 162         return rc;
 163 
 164     }
 165 
 166     /**
 167      * Gets the repository based on command line flags.
 168      * @return Reposistory based on the value from {@code repositoryFlag} if
 169      * that is non-null, else the system repository.
 170      */
 171     private Repository getRepository() throws IOException {
 172         Repository rc = null;
 173 
 174         String repositoryLocation = repositoryFlag.getLocation();
 175 
 176         if (repositoryLocation == null) {
 177             rc = Repository.getSystemRepository();
 178         } else {
 179             // If repositoryLocation is a URL use URLRepository else LocalRepository.
 180             try {
 181                 URL u = new URL(repositoryLocation);
 182                 rc = Modules.newURLRepository(
 183                     "jrepo", u, null, RepositoryConfig.getSystemRepository());
 184             } catch (MalformedURLException ex) {
 185                 File f = new File(repositoryLocation);
 186                 if (f.exists() && f.canRead()) {
 187                     rc = Modules.newLocalRepository(
 188                         "jrepo",
 189                         f.getCanonicalFile(), null,
 190                         RepositoryConfig.getSystemRepository());
 191                 } else {
 192                     throw new IOException("Cannot access repository at " + repositoryLocation);
 193                 }
 194             }
 195         }
 196         return rc;
 197     }
 198 
 199     /** Reset instance state to default values. */
 200     private void reset() {
 201         moduleName = null;
 202         parentFlag.reset();
 203         repositoryFlag.reset();
 204         verboseFlag.reset();
 205         for (Command cmd : commands.values()) {
 206             cmd.reset();
 207         }
 208     }
 209 
 210 
 211     /** Parse command line arguments.*/
 212     private Command parseArgs(String[] args) {
 213         /* Preprocess and expand @file arguments */
 214         try {
 215             args = CommandLine.parse(args);
 216         } catch (FileNotFoundException e) {
 217             msg.fatalError(msg.formatMsg("error.cant.open", e.getMessage()));
 218             return null;
 219         } catch (IOException e) {
 220             msg.fatalError(msg.formatMsg("caught.exception", e.getMessage()));
 221             return null;
 222         }
 223 
 224         if (args.length < 1) {
 225             usageError();
 226             return null;
 227         }
 228 
 229         Command cmd = null;
 230         for (Map.Entry<String, Command> entry : commands.entrySet()) {
 231             if (entry.getKey().startsWith(args[0])) {
 232                 cmd = entry.getValue();
 233                 break;
 234             }
 235         }
 236         if (DEBUG) debug("found command " + cmd);
 237         if (cmd == null) {
 238             usageError();
 239             return null;
 240         }
 241 
 242         try {
 243             int numFlags = cmd.parseFlags(args, commonFlags);
 244             args = Arrays.copyOfRange(args, numFlags + 1, args.length);
 245             try {
 246                 if (cmd.parseArgs(args, msg)) {
 247                     return cmd;
 248                 } else {
 249                     usageError();
 250                     return null;
 251                 }
 252             } catch (ArrayIndexOutOfBoundsException e) {
 253                 usageError();
 254             }
 255         } catch (IllegalArgumentException ex) {
 256             msg.error(ex.getMessage());
 257             usageError();
 258             return null;
 259         }
 260         return cmd;
 261     }
 262 
 263 
 264     /** Returns a user-grokkable description of the repository. */
 265     private String getRepositoryText(Repository repo) {
 266         String rc;
 267         URI u = repo.getSourceLocation();
 268         if (u == null) {
 269             rc = "Bootstrap repository";
 270         } else {
 271             try {
 272                 rc = "Repository " + u.toURL().toExternalForm();
 273             } catch (MalformedURLException ex) {
 274                 rc = "Repository unknown";
 275             }
 276         }
 277         return rc;
 278     }
 279 
 280     private String getMAIText(ModuleArchiveInfo mai) {
 281         StringWriter sw = new StringWriter();
 282         PrintWriter pw = new PrintWriter(sw);
 283         pw.printf(MAIFormat, mai.getName(), mai.getVersion());
 284         if (verboseFlag.isEnabled()) {
 285             long t = mai.getLastModified();
 286             String lastMod = null;
 287             if (t != 0) {
 288                 lastMod = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date(t));
 289             }
 290             pw.printf(MAIFormatVerbose,
 291                 mai.getPlatform() == null ? "generic" : mai.getPlatform(),
 292                 mai.getArch() == null ? "generic" : mai.getArch(),
 293                 lastMod == null ? "n/a" : lastMod,
 294                 mai.getFileName() == null ? "n/a" : mai.getFileName()
 295                 );
 296         }
 297         return sw.toString();
 298     }
 299 
 300     void usageError() {
 301         usageError(msg);
 302     }
 303 
 304     static void usageError(Messenger msg) {
 305         if (usage == null) {
 306             StringBuilder ub = new StringBuilder(
 307                 "Usage: jrepo <command>\nwhere <command> includes:");
 308             for (Command c : commands.values()) {
 309                 String u = c.usage();
 310                 if (u != null) {
 311                     ub.append("\n    ").append(c.usage());
 312                 }
 313             }
 314             usage = ub.toString();
 315         }
 316         msg.error(usage);
 317     }
 318 
 319     /**
 320      * Represents a flag given on the command line.  Flags always are 2
 321      * characters long, and start with a '-'.  They can have additional
 322      * associated arguments (cf RepositoryFlag).
 323      */
 324     private static class Flag {
 325         private final char name;
 326 
 327         private boolean enabled = false;
 328 
 329         private static final Map<Character, Flag> flags = new HashMap<Character, Flag>();
 330 
 331         Flag(char name) {
 332             this.name = name;
 333             flags.put(new Character(name), this);
 334         }
 335 
 336         void register(Map<Character, Flag> registry) {
 337             registry.put(new Character(name), this);
 338         }
 339 
 340         char getName() {
 341             return name;
 342         }
 343 
 344         /** @return the number of arguments consumed by this Flag. */
 345         int set(String[] args, int pos) {
 346             enabled = true;
 347             return 1;
 348         }
 349 
 350         void reset() {
 351             enabled = false;
 352         }
 353 
 354         boolean isEnabled() {
 355             return enabled;
 356         }
 357 
 358         static Flag get(char c) {
 359             return flags.get(new Character(c));
 360         }
 361     }
 362 
 363     private static class RepositoryFlag extends Flag {
 364         String location = null;
 365 
 366         RepositoryFlag() {
 367             super('r');
 368         }
 369 
 370         int set(String[] args, int pos) {
 371             int rc = super.set(args, pos);
 372             location = args[pos + 1];
 373             return rc + 1;
 374         }
 375 
 376         String getLocation() {
 377             return location;
 378         }
 379 
 380         void reset() {
 381             super.reset();
 382             location = null;
 383         }
 384     }
 385 
 386     /*
 387      * Command types: An abstract base class, plus one concrete class for
 388      * each Command.
 389      */
 390 
 391     /*
 392      * Represents a Command.
 393      */
 394     private static abstract class Command {
 395         private final String name;
 396 
 397         Command(String name) {
 398             this.name = name;
 399         }
 400 
 401         /** Adds this command to the given registry. */
 402         void register(Map<String, Command> registry) {
 403             registry.put(name, this);
 404         }
 405 
 406         void reset() {
 407             // Empty; subclasses can implement
 408         }
 409 
 410         // Flags differ from arguments in that they have the form "-X [opt]".
 411         // Flags appear in a command line before arguments.
 412 
 413         /** Parses the arguments particular to this command. */
 414         abstract boolean parseArgs(String[] args, Messenger msg);
 415 
 416         /**
 417          * Parse the Flags for this command.
 418          * @return number of flags found.
 419          * @throws IllegalArgumentException if an invalid flag is given.
 420          */
 421         int parseFlags(String[] args, Map<Character, Flag> flags) {
 422             int rc = 0;
 423             int i = 0;
 424             while (i < args.length) {
 425                 String s = args[i];
 426                 if (s.length() == 2 && s.charAt(0) == '-') {
 427                     Flag f = flags.get(s.charAt(1));
 428                     if (f != null) {
 429                         int numConsumed = f.set(args, i);
 430                         i += numConsumed;  // Increases at each iteration.
 431                         rc += numConsumed; // Increases only when the arg is a flag.
 432                     } else {
 433                         throw new IllegalArgumentException("unrecognized flag: " + args[i]);
 434                     }
 435                 } else {
 436                     i++;
 437                 }
 438             }
 439             return rc;
 440         }
 441 
 442         /** Represents the actual behavior of the command. */
 443         abstract boolean run(Repository repo, Messenger msg);
 444 
 445         /** Returns a usage string describing this command, or null if the
 446          * command is a synonym for another command.
 447          */
 448         abstract String usage();
 449 
 450         public String toString() { return name; }
 451 
 452         /**
 453          * RepositoryVisitor types walk a parent chain of repositories, invoking
 454          * {@code doit} in each one.  The abstract base class provides the recursion;
 455          * each concrete subclass provides the per-repository behavior.  The
 456          * recursion is such that the bootstrap repository is visited first,
 457          * and the system repository is last.
 458          */
 459         abstract class RepositoryVisitor {
 460             abstract void doit(Repository repo, Messenger msg);
 461 
 462             void doBefore(Messenger msg) { }
 463 
 464             void doAfter(Messenger msg) { }
 465 
 466             final void run(Repository repo, Messenger msg) {
 467                 visit(repo, msg);
 468             }
 469 
 470             private final void visit(Repository repo, Messenger msg) {
 471                 Repository parent = parentFlag.isEnabled() ? repo.getParent() : null;
 472                 if (parent != null) {
 473                     visit(parent, msg);
 474                 }
 475                 doBefore(msg);
 476                 doit(repo, msg);
 477                 doAfter(msg);
 478             }
 479         }
 480     }
 481 
 482     /** Installs a JAM into a repository. */
 483     private class InstallCommand extends Command {
 484         private String jamName;
 485 
 486         InstallCommand() {
 487             super("install");
 488         }
 489 
 490         boolean parseArgs(String[] args, Messenger msg) {
 491             boolean rc = false;
 492             if (!parentFlag.isEnabled()
 493                     && repositoryFlag.getLocation() != null
 494                     && args.length == 1) {
 495                 jamName = args[0];
 496                 rc = true;
 497             }
 498             return rc;
 499         }
 500 
 501         boolean run(Repository repo, Messenger msg) {
 502             if (repo != null) {
 503                 String jamURL = null;
 504                 File f = new File(jamName);
 505                 if (f.canRead()) {
 506                     try {
 507                         String path = f.getCanonicalPath();
 508                         // Ensure that path starts with a "/" (it does not on
 509                         // some systems, e.g. Windows).
 510                         if (!path.startsWith("/")) {
 511                             path = "/" + path;
 512                         }
 513                         jamURL = "file://" + path;
 514                     } catch (IOException ex) {
 515                         msg.error("Cannot install " + jamName + ": " + ex.getMessage());
 516                         return false;
 517                     }
 518                 } else {
 519                     jamURL = jamName;
 520                 }
 521                 try {
 522                     ModuleArchiveInfo mai = repo.install(new URL(jamURL).toURI());
 523                     if (verboseFlag.isEnabled()) {
 524                         msg.println("Installed " + jamName + ": " + getMAIText(mai));
 525                     }
 526                     return true;
 527                 } catch (URISyntaxException ex) {
 528                     msg.error("Cannot install " + jamName + ": no such file, or malformed URI");
 529                 } catch (MalformedURLException ex) {
 530                     msg.error("Cannot install " + jamName + ": no such file, or malformed URL");
 531                 } catch (IOException ex) {
 532                     msg.error("Cannot install " + jamName + ": " + ex.getMessage());
 533                 }
 534             }
 535             return false;
 536         }
 537 
 538         String usage() {
 539             return "install [-v] -r repositoryLocation jamFile | jamURL\n"
 540                 +  "        installs a module into a repository";
 541         }
 542     }
 543 
 544     /** Uninstalls a module from a repository. */
 545     private class UninstallCommand extends Command {
 546         @SuppressWarnings("unchecked")
 547         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
 548 
 549         private Flag forceFlag = new Flag('f');
 550 
 551         private Flag interactiveFlag = new Flag('i');
 552 
 553         private String moduleName;
 554 
 555         private Version version;
 556 
 557         private String platformBinding;
 558 
 559         UninstallCommand() {
 560             super("uninstall");
 561             forceFlag.register(myFlags);
 562             interactiveFlag.register(myFlags);
 563         }
 564 
 565         void reset() {
 566             version = null;
 567             platformBinding = null;
 568             forceFlag.reset();
 569             interactiveFlag.reset();
 570         }
 571 
 572         int parseFlags(String[] args, Map<Character, Flag> flags) {
 573             return super.parseFlags(args, myFlags);
 574         }
 575 
 576         boolean parseArgs(String[] args, Messenger msg) {
 577             if (repositoryFlag.getLocation() == null) {
 578                 return false;
 579             }
 580 
 581             if (forceFlag.isEnabled() && interactiveFlag.isEnabled()) {
 582                 // Doesn't make sense for these to both be enabled
 583                 msg.error("uninstall cannot simultaneously use both -i and -f: choose one or the other"); // XXX i81n
 584                 return false;
 585             }
 586 
 587             if (args.length >= 1) {
 588                 moduleName = args[0];
 589                 if (args.length >= 2 && args.length < 4) {
 590                     try {
 591                         version = Version.valueOf(args[1]);
 592                     } catch (IllegalArgumentException ex) {
 593                         msg.error(ex.getMessage());
 594                         return false;
 595                     }
 596                 }
 597                 if (args.length == 3) {
 598                     platformBinding = args[2];
 599                 }
 600                 return true;
 601             } else {
 602                 return false;
 603             }
 604         }
 605 
 606         boolean run(Repository repo, Messenger msg) {
 607             boolean rc = false;
 608 
 609             if (DEBUG) debug(
 610                 "force=" + forceFlag.isEnabled()
 611                 + " interactive=" + interactiveFlag.isEnabled()
 612                 + " verbose=" + verboseFlag.isEnabled()
 613                 + " repository=" + repositoryFlag.getLocation()
 614                 + " moduleName=" + moduleName
 615                 + " version=" + version
 616                 + " plat/arch=" + platformBinding);
 617 
 618             List<ModuleArchiveInfo> found = new ArrayList<ModuleArchiveInfo>();
 619             for (ModuleArchiveInfo mai : repo.list()) {
 620                 if (match(mai)) {
 621                     found.add(mai);
 622                 }
 623             }
 624             if (DEBUG) debug("found.size=" + found.size());
 625             if (found.size() == 1) {
 626                 ModuleArchiveInfo mai = found.get(0);
 627                 rc = uninstall(repo, mai, msg);
 628             } else if (found.size() == 0) {
 629                 if (verboseFlag.isEnabled()) {
 630                     msg.error("Could not find a module matching " + getInfo());
 631                 }
 632             } else { // multiple matches
 633                 if (!forceFlag.isEnabled() && !interactiveFlag.isEnabled()) {
 634                     msg.error("Cannot uninstall: multiple modules match " + getInfo());
 635                     if (verboseFlag.isEnabled()) {
 636                         for (ModuleArchiveInfo mai : found) {
 637                             msg.error(getMAIText(mai));
 638                         }
 639                     }
 640                 } else if (forceFlag.isEnabled()) {
 641                     if (DEBUG) debug("forced uninstall of multiple matches");
 642                     rc = true;
 643                     for (ModuleArchiveInfo mai : found) {
 644                         if (!uninstall(repo, mai, msg)) {
 645                             if (DEBUG) debug("uninstall failed for " + getMAIText(mai) + " in " + repo.getSourceLocation());
 646                             rc = false;
 647                             break;
 648                         }
 649                     }
 650                 } else if (interactiveFlag.isEnabled()) {
 651                     rc = uninstallInteractive(repo, found, msg);
 652                 }
 653             }
 654             return rc;
 655         }
 656 
 657         String usage() {
 658             return "uninstall [-v] [-f | -i] -r repositoryLocation moduleName [moduleVersion] [modulePlatformBinding]\n"
 659                 +  "        removes a module from a repository, along with associated files.";
 660         }
 661 
 662         /** Uninstall the ModuleArchiveInfo from the Repository. */
 663         private boolean uninstall(Repository repo, ModuleArchiveInfo mai, Messenger msg) {
 664             boolean rc = false;
 665             if (DEBUG) debug("Uninstalling " + getMAIText(mai));
 666             try {
 667                 rc = repo.uninstall(mai);
 668                 if (verboseFlag.isEnabled()) {
 669                     if (rc) {
 670                         msg.println("Uninstalled " + getMAIText(mai));
 671                     } else {
 672                         msg.error("Failed to uninstall " + getMAIText(mai));
 673                     }
 674                 }
 675             } catch (Exception ex) {
 676                 msg.error("Exception while uninstalling " + getInfo()
 677                           + ": " + ex.getMessage());
 678             }
 679             return rc;
 680         }
 681 
 682         /** Uninstall one of the ModuleArchiveInfos from the Repository. */
 683         private boolean uninstallInteractive(
 684                 Repository repo,
 685                 List<ModuleArchiveInfo> found,
 686                 Messenger msg) {
 687             boolean rc = false;
 688 
 689             String spaces = "          ";
 690             String fmt = "%" + found.size() + "d %s\n";
 691 
 692             StringWriter sw = new StringWriter();
 693             PrintWriter pw = new PrintWriter(sw);
 694             pw.println("Multiple matches for module found.  Choose one of the below by index:");
 695             boolean saveVerbose = verboseFlag.isEnabled();
 696             verboseFlag.set(new String[0], 0);
 697             int count = 0;
 698             for (ModuleArchiveInfo m : found) {
 699                 pw.printf(fmt, count++, getMAIText(m));
 700             }
 701             pw.print("\nIndex? ");
 702             pw.close();
 703             String choiceMessage = sw.toString();
 704             boolean done = false;
 705             while (!done) {
 706                 msg.print(choiceMessage);
 707                 String input = null;
 708                 try {
 709                     input = msg.readLine().trim();
 710                     int index = Integer.parseInt(input);
 711                     if (index >= 0 && index < found.size()) {
 712                         uninstall(repo, found.get(index), msg);
 713                         done = true;
 714                         rc = true;
 715                     } else {
 716                         msg.error("Invalid input " + input + "; try again");
 717                     }
 718                 } catch (NumberFormatException ex) {
 719                     msg.error("Invalid input '" + input + "'; try again");
 720                 } catch (Exception ex) {
 721                     if (DEBUG) debug("msg.readLine threw " + ex);
 722                     done = true;
 723                 }
 724             }
 725 
 726             // Restore if necessary.
 727             if (!saveVerbose) {
 728                 verboseFlag.reset();
 729             }
 730 
 731             return rc;
 732         }
 733 
 734         /**
 735          * @return true iff moduleName matches mai.getName(), and if version
 736          * and platformBinding are set, they also match.
 737          */
 738         private boolean match(ModuleArchiveInfo mai) {
 739             boolean rc = false;
 740             if (DEBUG) debug("attempting to match " + getMAIText(mai));
 741             if (moduleName.equals(mai.getName())) {
 742                 if (version == null) {
 743                     rc = true;
 744                 } else if (version.equals(mai.getVersion())) {
 745                     if (platformBinding == null) {
 746                         rc = true;
 747                     } else {
 748                         String pb = mai.getPlatform() + "-" + mai.getArch();
 749                         if (platformBinding.equals(pb)) {
 750                             rc = true;
 751                         }
 752                     }
 753                 }
 754             }
 755             return rc;
 756         }
 757 
 758         private String getInfo() {
 759             String s = moduleName;
 760             if (version != null) {
 761                 s += " with version " + version;
 762             }
 763             if (platformBinding != null) {
 764                 s += " and platform-binding of " + platformBinding;
 765             }
 766             return s;
 767         }
 768     }
 769 
 770     /** Prints information about modules found in repositories. */
 771     private class ListCommand extends Command {
 772         @SuppressWarnings("unchecked")
 773         private final Map<Character, Flag> myFlags = (Map<Character, Flag>) commonFlags.clone();
 774 
 775         ListCommand() {
 776             super("list");
 777             parentFlag.register(myFlags);
 778         }
 779 
 780         void reset() {
 781             parentFlag.reset();
 782         }
 783 
 784         int parseFlags(String[] args, Map<Character, Flag> flags) {
 785             return super.parseFlags(args, myFlags);
 786         }
 787 
 788         boolean parseArgs(String[] args, Messenger msg){
 789             boolean rc = true;
 790             if (args.length == 0) {
 791                 moduleName = null;
 792             } else if (args.length == 1) {
 793                 moduleName = args[0];
 794             } else {
 795                 rc = false;
 796             }
 797             return rc;
 798         }
 799 
 800         boolean run(Repository repo, Messenger msg) {
 801             ListRepositoryVisitor visitor = new ListRepositoryVisitor();
 802             visitor.run(repo, msg);
 803             boolean found = visitor.wasFound();
 804             if (verboseFlag.isEnabled() && !found) {
 805                 if (moduleName != null) {
 806                     msg.error("Could not find module name starting with '" + moduleName + "'");
 807                 } else {
 808                     msg.error("Could not find any modules");
 809                 }
 810             }
 811             return found;
 812         }
 813 
 814         String usage() {
 815             return "list [-v] [-p] [-r repositoryLocation] moduleName\n"
 816                 +  "        lists the modules in the repository";
 817         }
 818 
 819         class ListRepositoryVisitor extends RepositoryVisitor {
 820             private boolean found = false;
 821 
 822             boolean wasFound() { return found; }
 823 
 824             void doit(Repository repo, Messenger msg) {
 825                 boolean printedHeader = false;
 826                 for (ModuleArchiveInfo mai : repo.list()) {
 827                     if (moduleName == null || mai.getName().startsWith(moduleName)) {
 828                         if (!printedHeader) {
 829                             msg.println(getRepositoryText(repo));
 830                             msg.println(verboseFlag.isEnabled() ? maiHeadingVerbose : maiHeading);
 831                             printedHeader = true;
 832                         }
 833                         msg.println(getMAIText(mai));
 834                         found = true;
 835                     }
 836                 }
 837             }
 838         }
 839     }
 840 }