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