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