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