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 }