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