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