callgraph/src/main/java/org/netbeans/lib/callgraph/Arguments.java
author Tim Boudreau <tboudreau@netbeans.org>
Sat, 03 Sep 2016 02:41:36 -0400
changeset 18374 04a79821e760
parent 18372 25e1d840480b
child 18400 c87c223efe6a
permissions -rw-r--r--
Eliminate duplicates in graph files
     1 /*
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright (C) 1997-2015 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
    31  * Microsystems, Inc. All Rights Reserved.
    32  *
    33  * If you wish your version of this file to be governed by only the CDDL
    34  * or only the GPL Version 2, indicate your decision by adding
    35  * "[Contributor] elects to include this software in this distribution
    36  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    37  * single choice of license, a recipient has the option to distribute
    38  * your version of this file under either the CDDL, the GPL Version 2 or
    39  * to extend the choice of license to its licensees as provided above.
    40  * However, if you add GPL Version 2 code and therefore, elected the GPL
    41  * Version 2 license, then the option applies only if the new code is
    42  * made subject to such option by the copyright holder.
    43  */
    44 package org.netbeans.lib.callgraph;
    45 
    46 import java.io.File;
    47 import java.io.IOException;
    48 import java.util.Collections;
    49 import java.util.HashSet;
    50 import java.util.Iterator;
    51 import java.util.LinkedList;
    52 import java.util.List;
    53 import java.util.Set;
    54 
    55 /**
    56  * Parses and validates command-line arguments.
    57  *
    58  * @author Tim Boudreau
    59  */
    60 final class Arguments implements CallgraphControl {
    61 
    62     private Set<File> folders = new HashSet<>();
    63     private static final Command[] commands = new Command[]{
    64         new NoSelfReferencesCommand(),
    65         new ShortNamesCommand(),
    66         new ExtendedPropertiesCommand(),
    67         new AntCommand(),
    68         new MavenCommand(),
    69         new GradleCommand(),
    70         new IgnoreCommand(),
    71         new OutfileCommand(),
    72         new ExcludeCommand(),
    73         new PackageGraphFileCommand(),
    74         new ClassGraphFileCommand(),
    75         new OmitAbstractCommand(),
    76         new DisableEightBitStringsCommand(),
    77         new OmitAbstractCommand(),
    78         new QuietCommand(),
    79         new ReverseCommand(),
    80         new AggressiveCommand(),
    81         new VerboseCommand()
    82     };
    83     private boolean noSelfReferences = false;
    84     private boolean shortNames = false;
    85     private boolean maven = false;
    86     private boolean gradle = false;
    87     private boolean ant = false;
    88     private boolean xprop = false;
    89     private boolean aggressive = false;
    90     private File outfile;
    91     private Set<String> exclude = new HashSet<>();
    92     private Set<String> ignore = new HashSet<>();
    93     private boolean quiet;
    94     private File classGraphFile;
    95     private File packageGraphFile;
    96     private boolean verbose;
    97     private boolean omitAbstract;
    98     private boolean disableEightBitStrings;
    99     private boolean reverse;
   100 
   101     Arguments(String... args) throws IOException {
   102         this(true, args);
   103     }
   104 
   105     Arguments(boolean abortIfWillNotOutput, String... args) throws IOException {
   106         List<String> unknownArguments = new LinkedList<>();
   107         List<String> errors = new LinkedList<>();
   108         for (int i = 0; i < args.length;) {
   109             int oldPos = i;
   110             for (Command c : commands) {
   111                 try {
   112                     @SuppressWarnings("LeakingThisInConstructor")
   113                     int increment = c.parse(i, args, this);
   114                     if (increment > 0) {
   115                         i += increment;
   116                         break;
   117                     }
   118                 } catch (IllegalArgumentException ex) {
   119                     errors.add(c.name + ": " + ex.getMessage());
   120                 }
   121             }
   122             if (oldPos == i) {
   123                 unknownArguments.add(args[i]);
   124                 i++;
   125             }
   126         }
   127         Set<String> folders = new HashSet<>();
   128         for (String unknown : unknownArguments) {
   129             if (unknown.startsWith("-")) {
   130                 // bad line switch
   131                 errors.add("Unknown argument '" + unknown + "'");
   132             } else {
   133                 folders.add(unknown);
   134             }
   135         }
   136         if (folders.isEmpty()) {
   137             errors.add("No folders of Java sources specified");
   138         } else {
   139             for (String folderName : folders) {
   140                 File folder = new File(folderName);
   141                 if (!folder.exists()) {
   142                     errors.add("Folder does not exist: " + folderName);
   143                 } else if (!folder.isDirectory()) {
   144                     errors.add("Not a folder: " + folderName);
   145                 } else {
   146                     this.folders.add(folder.getCanonicalFile());
   147                 }
   148             }
   149         }
   150         if ((maven && gradle) || (ant && gradle) || (ant && maven)) {
   151             errors.add("--maven, --ant and --gradle are mutually exclusive");
   152         } else if (maven) {
   153             findMavenSubfolders(errors);
   154         } else if (gradle) {
   155             findGradleSubfolders(errors);
   156         } else if (ant) {
   157             findAntSubfolders(errors);
   158         }
   159         Set<File> toIgnore = new HashSet<>();
   160         for (String ig : ignore) {
   161             File ff = new File(ig);
   162             if (ff.exists() && ff.isDirectory()) {
   163                 ff = ff.getCanonicalFile();
   164                 for (File f : this.folders()) {
   165                     File f1 = f.getCanonicalFile();
   166                     if (f1.equals(ff)) {
   167                         toIgnore.add(f1);
   168                     } else {
   169                         if (f1.getPath().startsWith(ff.getPath())) {
   170                             toIgnore.add(f1);
   171                         }
   172                     }
   173                 }
   174             } else {
   175                 for (String u : unknownArguments) {
   176                     if (!u.startsWith("-")) {
   177                         File f = new File(u);
   178                         if (f.exists()) {
   179                             f = f.getCanonicalFile();
   180                             File maybeIgnore = new File(f, ig);
   181                             if (maybeIgnore.exists() && maybeIgnore.isDirectory()) {
   182                                 maybeIgnore = maybeIgnore.getCanonicalFile();
   183                                 for (File fld : this.folders()) {
   184                                     if (fld.equals(maybeIgnore)) {
   185                                         toIgnore.add(fld);
   186                                     } else if (fld.getAbsolutePath().startsWith(maybeIgnore.getAbsolutePath())) {
   187                                         toIgnore.add(fld);
   188                                     }
   189                                 }
   190                             }
   191                         }
   192                     }
   193                 }
   194             }
   195         }
   196         if (verbose && !toIgnore.isEmpty()) {
   197             System.err.println("Ignoring the following projects:");
   198             for (File ti : toIgnore) {
   199                 System.err.println(" - " + ti.getAbsolutePath());
   200             }
   201         }
   202         this.folders.removeAll(toIgnore);
   203         if (verbose && !this.folders.isEmpty()) {
   204             System.err.println("Will scan the following source roots:");
   205             for (File f : folders()) {
   206                 System.err.println("  " + f.getAbsolutePath());
   207             }
   208         }
   209 
   210         if (packageGraphFile != null) {
   211             File parent = packageGraphFile.getParentFile();
   212             if (!parent.exists() || !parent.isDirectory()) {
   213                 errors.add("Parent folder for package graph output file does not exist: " + parent);
   214             }
   215         }
   216         if (classGraphFile != null) {
   217             File parent = classGraphFile.getParentFile();
   218             if (!parent.exists() || !parent.isDirectory()) {
   219                 errors.add("Parent folder for class graph output file does not exist: " + parent);
   220             }
   221         }
   222         if (outfile != null) {
   223             File parent = outfile.getParentFile();
   224             if (!parent.exists() || !parent.isDirectory()) {
   225                 errors.add("Parent folder for output file does not exist: " + parent);
   226             }
   227         } else if (abortIfWillNotOutput && quiet && packageGraphFile == null && classGraphFile == null) {
   228             errors.add("-q or --quiet specified, but no output file specified - would not produce any output at all");
   229         }
   230         // XXX check if any folders are children of each other?
   231         if (!errors.isEmpty()) {
   232             throw new InvalidArgumentsException(help(errors), errors);
   233         }
   234     }
   235 
   236     private void findMavenSubfolders(List<String> errors) {
   237         Set<File> flds = new HashSet<>(this.folders);
   238         this.folders.clear();
   239         for (File f : flds) {
   240             recurseSubfoldersLookingForMavenProjects(f);
   241         }
   242         if (this.folders.isEmpty()) {
   243             errors.add("Did not find any maven projects (looked for pom.xml and src/main/java in all subfolders of folder list)");
   244         }
   245     }
   246 
   247     private void recurseSubfoldersLookingForMavenProjects(File file) {
   248         if (file.isDirectory()) {
   249             if (hasPom(file)) {
   250                 File sources = srcMainJavaFolder(file);
   251                 if (sources != null) {
   252                     this.folders.add(sources);
   253                 }
   254             }
   255             for (File child : file.listFiles()) {
   256                 recurseSubfoldersLookingForMavenProjects(child);
   257             }
   258         }
   259     }
   260 
   261     private boolean hasPom(File fld) {
   262         File pom = new File(fld, "pom.xml");
   263         return pom.exists() && pom.isFile() && pom.canRead();
   264     }
   265 
   266     void findAntSubfolders(List<String> errors) {
   267         Set<File> flds = new HashSet<>(this.folders);
   268         this.folders.clear();
   269         for (File f : flds) {
   270             recurseSubfoldersLookingForAntProjects(f);
   271         }
   272         if (this.folders.isEmpty()) {
   273             errors.add("Did not find any ant projects (looked for build.xml and src/ in all subfolders of folder list)");
   274         }
   275     }
   276 
   277     private void recurseSubfoldersLookingForAntProjects(File file) {
   278         if (file.isDirectory()) {
   279             if (hasBuildXml(file)) {
   280                 File sources = new File(file, "src");
   281                 if (sources.exists() && sources.isDirectory()) {
   282                     this.folders.add(sources);
   283                 }
   284             }
   285             for (File child : file.listFiles()) {
   286                 recurseSubfoldersLookingForAntProjects(child);
   287             }
   288         }
   289     }
   290 
   291     private boolean hasBuildXml(File fld) {
   292         File pom = new File(fld, "build.xml");
   293         return pom.exists() && pom.isFile() && pom.canRead();
   294     }
   295 
   296     void findGradleSubfolders(List<String> errors) {
   297         Set<File> flds = new HashSet<>(this.folders);
   298         this.folders.clear();
   299         for (File f : flds) {
   300             recurseSubfoldersLookingForGradleProjects(f);
   301         }
   302         if (this.folders.isEmpty()) {
   303             errors.add("Did not find any gradle projects (looked for *.gradle and src/main/java in all subfolders of folder list)");
   304         }
   305     }
   306 
   307     private void recurseSubfoldersLookingForGradleProjects(File file) {
   308         if (file.isDirectory()) {
   309             if (hasGradleFile(file)) {
   310                 File sources = srcMainJavaFolder(file);
   311                 if (sources != null) {
   312                     this.folders.add(sources);
   313                 }
   314             }
   315             for (File child : file.listFiles()) {
   316                 recurseSubfoldersLookingForGradleProjects(child);
   317             }
   318         }
   319     }
   320 
   321     boolean hasGradleFile(File fld) {
   322         return fld.listFiles((File pathname) -> {
   323             return pathname.getName().endsWith(".gradle") && pathname.isFile() && pathname.canRead();
   324         }).length > 0;
   325     }
   326 
   327     public boolean isGradle() {
   328         return gradle;
   329     }
   330 
   331     private File srcMainJavaFolder(File projectFolder) {
   332         File src = new File(projectFolder, "src");
   333         File main = new File(src, "main");
   334         File java = new File(main, "java");
   335         if (java.exists() && java.isDirectory()) {
   336             return java;
   337         }
   338         return null;
   339     }
   340 
   341     public boolean isVerbose() {
   342         return verbose;
   343     }
   344 
   345     public boolean isDisableEightBitStrings() {
   346         return disableEightBitStrings;
   347     }
   348 
   349     public boolean isReverse() {
   350         return reverse;
   351     }
   352 
   353     @Override
   354     public File methodGraphFile() {
   355         return outfile;
   356     }
   357 
   358     @Override
   359     public Set<File> folders() {
   360         return Collections.unmodifiableSet(folders);
   361     }
   362 
   363     @Override
   364     public boolean isSelfReferences() {
   365         return noSelfReferences == false;
   366     }
   367 
   368     @Override
   369     public boolean isShortNames() {
   370         return shortNames;
   371     }
   372 
   373     @Override
   374     public boolean isMaven() {
   375         return maven;
   376     }
   377 
   378     @Override
   379     public boolean isAnt() {
   380         return ant;
   381     }
   382 
   383     @Override
   384     public boolean isExtendedProperties() {
   385         return xprop;
   386     }
   387 
   388     public boolean isAggressive() {
   389         return aggressive;
   390     }
   391 
   392     @Override
   393     public File classGraphFile() {
   394         return classGraphFile;
   395     }
   396 
   397     @Override
   398     public File packageGraphFile() {
   399         return packageGraphFile;
   400     }
   401 
   402     @Override
   403     public Iterator<File> iterator() {
   404         return folders().iterator();
   405     }
   406 
   407     @Override
   408     public Set<String> excludePrefixes() {
   409         return Collections.unmodifiableSet(exclude);
   410     }
   411 
   412     public boolean isOmitAbstract() {
   413         return omitAbstract;
   414     }
   415 
   416     public boolean isExcluded(String qname) {
   417         for (String ex : exclude) {
   418             if (qname.startsWith(ex)) {
   419                 return true;
   420             }
   421         }
   422         return false;
   423     }
   424 
   425     @Override
   426     public boolean isQuiet() {
   427         return quiet;
   428     }
   429 
   430     private String help(List<String> errors) {
   431         StringBuilder sb = new StringBuilder("Callgraph prints a graph of things that call each other in a tree of Java sources,\n"
   432                 + "and can output graphs of what methods / classes / packages (or all of the above) call each other\nwithin that"
   433                 + "source tree."
   434                 + "\n\nUsage:\njava -jar callgraph.jar ");
   435         for (Command c : commands) {
   436             if (c.optional) {
   437                 sb.append('[');
   438             }
   439             sb.append("--").append(c.name).append(" | -").append(c.shortcut);
   440             if (c.takesArgument) {
   441                 sb.append(" ").append(c.name);
   442             }
   443             if (c.optional) {
   444                 sb.append(']');
   445             }
   446             sb.append(' ');
   447         }
   448         sb.append("dir1 [dir2 dir3 ...]");
   449         sb.append('\n');
   450         for (Command c : commands) {
   451             sb.append("\n\t");
   452             sb.append("--").append(c.name).append(" / -").append(c.shortcut).append(" :\t").append(c.help());
   453         }
   454         if (!errors.isEmpty()) {
   455             sb.append("\nErrors:\n");
   456             for (String err : errors) {
   457                 sb.append('\t').append(err).append('\n');
   458             }
   459         }
   460         return sb.toString();
   461     }
   462 
   463     private static abstract class Command {
   464 
   465         protected final String name;
   466         protected final String shortcut;
   467         protected final boolean optional;
   468         private final boolean takesArgument;
   469 
   470         Command(String name, String shortcut, boolean optional, boolean takesArgument) {
   471             this.name = name;
   472             this.shortcut = shortcut;
   473             this.optional = optional;
   474             this.takesArgument = takesArgument;
   475         }
   476 
   477         public int parse(int position, String[] args, Arguments toSet) {
   478             boolean match = ("-" + shortcut).equals(args[position])
   479                     || ("--" + name).equals(args[position]);
   480             if (!match) {
   481                 return 0;
   482             }
   483             return doParse(position, args, toSet);
   484         }
   485 
   486         protected abstract int doParse(int i, String[] args, Arguments toSet);
   487 
   488         protected abstract String help();
   489 
   490         public String toString() {
   491             return name;
   492         }
   493     }
   494 
   495     private static final class NoSelfReferencesCommand extends Command {
   496 
   497         NoSelfReferencesCommand() {
   498             super(CMD_NOSELF, "n", true, false);
   499         }
   500 
   501         @Override
   502         protected int doParse(int i, String[] args, Arguments toSet) {
   503             toSet.noSelfReferences = true;
   504             return 1;
   505         }
   506 
   507         @Override
   508         protected String help() {
   509             return "Hide intra-class calls (i.e. if Foo.bar() calls Foo.baz(), don't include it)";
   510         }
   511     }
   512 
   513     private static final class DisableEightBitStringsCommand extends Command {
   514 
   515         DisableEightBitStringsCommand() {
   516             super(CMD_DISABLE_EIGHT_BIT_STRINGS, "u", true, false);
   517         }
   518 
   519         @Override
   520         protected int doParse(int i, String[] args, Arguments toSet) {
   521             toSet.noSelfReferences = true;
   522             return 1;
   523         }
   524 
   525         @Override
   526         protected String help() {
   527             return "Disable string memory optimizations - runs faster and supports unicode class names, but may run out of memory";
   528         }
   529     }
   530 
   531     private static final class ReverseCommand extends Command {
   532 
   533         ReverseCommand() {
   534             super(CMD_REVERSE, "r", true, false);
   535         }
   536 
   537         @Override
   538         protected int doParse(int i, String[] args, Arguments toSet) {
   539             toSet.reverse = true;
   540             return 1;
   541         }
   542 
   543         @Override
   544         protected String help() {
   545             return "Reverse the edges of graph nodes";
   546         }
   547     }
   548 
   549     private static final class AggressiveCommand extends Command {
   550 
   551         AggressiveCommand() {
   552             super(CMD_AGGRESSIVE_MEMORY, "z", true, false);
   553         }
   554 
   555         @Override
   556         protected int doParse(int i, String[] args, Arguments toSet) {
   557             toSet.aggressive = true;
   558             return 1;
   559         }
   560 
   561         @Override
   562         protected String help() {
   563             return "Aggressively optimize the 8-bit string intern table "
   564                     + "for large graphs, sacrificing performace for space";
   565         }
   566     }
   567 
   568     private static final class ShortNamesCommand extends Command {
   569 
   570         ShortNamesCommand() {
   571             super(CMD_SIMPLE, "s", true, false);
   572         }
   573 
   574         @Override
   575         protected int doParse(int i, String[] args, Arguments toSet) {
   576             toSet.shortNames = true;
   577             return 1;
   578         }
   579 
   580         @Override
   581         protected String help() {
   582             return "Use simple class names without the package (may confuse results if two classes have the same name)";
   583         }
   584     }
   585 
   586     private static final class ExtendedPropertiesCommand extends Command {
   587 
   588         ExtendedPropertiesCommand() {
   589             super(CMD_EXTENDED_PROPERTIES, "x", true, false);
   590         }
   591 
   592         @Override
   593         protected int doParse(int i, String[] args, Arguments toSet) {
   594             toSet.xprop = true;
   595             return 1;
   596         }
   597 
   598         @Override
   599         protected String help() {
   600             return "Find all maven projects that are children of the passed folders, and scan their src/main/java subfolders";
   601         }
   602     }
   603 
   604     private static final class AntCommand extends Command {
   605 
   606         AntCommand() {
   607             super(CMD_ANT, "a", true, false);
   608         }
   609 
   610         @Override
   611         protected int doParse(int i, String[] args, Arguments toSet) {
   612             toSet.ant = true;
   613             return 1;
   614         }
   615 
   616         @Override
   617         protected String help() {
   618             return "Find all ant projects that are children of the passed folders, and scan their src/ subfolders";
   619         }
   620     }
   621 
   622     private static final class MavenCommand extends Command {
   623 
   624         MavenCommand() {
   625             super(CMD_MAVEN, "m", true, false);
   626         }
   627 
   628         @Override
   629         protected int doParse(int i, String[] args, Arguments toSet) {
   630             toSet.maven = true;
   631             return 1;
   632         }
   633 
   634         @Override
   635         protected String help() {
   636             return "Find all maven projects that are children of the passed folders, and scan their src/main/java subfolders";
   637         }
   638     }
   639 
   640     private static final class GradleCommand extends Command {
   641 
   642         GradleCommand() {
   643             super(CMD_GRADLE, "g", true, false);
   644         }
   645 
   646         @Override
   647         protected int doParse(int i, String[] args, Arguments toSet) {
   648             toSet.gradle = true;
   649             return 1;
   650         }
   651 
   652         @Override
   653         protected String help() {
   654             return "Find all gradle projects that are children of the passed folders, and scan their src/main/java subfolders";
   655         }
   656     }
   657 
   658     private static final class OmitAbstractCommand extends Command {
   659 
   660         OmitAbstractCommand() {
   661             super(CMD_OMIT_ABSTRACT, "a", true, false);
   662         }
   663 
   664         @Override
   665         protected int doParse(int i, String[] args, Arguments toSet) {
   666             toSet.omitAbstract = true;
   667             return 1;
   668         }
   669 
   670         @Override
   671         protected String help() {
   672             return "Don't emit calls to abstract methods";
   673         }
   674     }
   675 
   676     private static final class VerboseCommand extends Command {
   677 
   678         VerboseCommand() {
   679             super(CMD_VERBOSE, "v", true, false);
   680         }
   681 
   682         @Override
   683         protected int doParse(int i, String[] args, Arguments toSet) {
   684             toSet.verbose = true;
   685             return 1;
   686         }
   687 
   688         @Override
   689         protected String help() {
   690             return "Print output about what is being processed";
   691         }
   692     }
   693 
   694     private static final class QuietCommand extends Command {
   695 
   696         QuietCommand() {
   697             super(CMD_QUIET, "q", true, false);
   698         }
   699 
   700         @Override
   701         protected int doParse(int i, String[] args, Arguments toSet) {
   702             toSet.quiet = true;
   703             return 1;
   704         }
   705 
   706         @Override
   707         protected String help() {
   708             return "Supress writing the graph to the standard output";
   709         }
   710     }
   711 
   712     private static final class OutfileCommand extends Command {
   713 
   714         OutfileCommand() {
   715             super(CMD_METHODGRAPH, "o", true, true);
   716         }
   717 
   718         @Override
   719         protected int doParse(int index, String[] args, Arguments toSet) {
   720             if (args.length == index + 1) {
   721                 throw new IllegalArgumentException("--outfile or -o present but no output file specified");
   722             }
   723             toSet.outfile = new File(args[index + 1]);
   724             return 2;
   725         }
   726 
   727         @Override
   728         protected String help() {
   729             return "Set the output file for the method call graph";
   730         }
   731     }
   732 
   733     private static final class ClassGraphFileCommand extends Command {
   734 
   735         ClassGraphFileCommand() {
   736             super(CMD_CLASSGRAPH, "c", true, true);
   737         }
   738 
   739         @Override
   740         protected int doParse(int index, String[] args, Arguments toSet) {
   741             if (args.length == index + 1) {
   742                 throw new IllegalArgumentException("--outfile or -o present but no output file specified");
   743             }
   744             toSet.classGraphFile = new File(args[index + 1]);
   745             return 2;
   746         }
   747 
   748         @Override
   749         protected String help() {
   750             return "Set the output file for the class call graph";
   751         }
   752     }
   753 
   754     private static final class PackageGraphFileCommand extends Command {
   755 
   756         PackageGraphFileCommand() {
   757             super(CMD_PACKAGEGRAPH, "p", true, true);
   758         }
   759 
   760         @Override
   761         protected int doParse(int index, String[] args, Arguments toSet) {
   762             if (args.length == index + 1) {
   763                 throw new IllegalArgumentException("--outfile or -o present but no output file specified");
   764             }
   765             toSet.packageGraphFile = new File(args[index + 1]);
   766             return 2;
   767         }
   768 
   769         @Override
   770         protected String help() {
   771             return "Set the output file for the package call graph";
   772         }
   773     }
   774 
   775     private static final class ExcludeCommand extends Command {
   776 
   777         ExcludeCommand() {
   778             super("exclude", "e", true, true);
   779         }
   780 
   781         @Override
   782         protected int doParse(int i, String[] args, Arguments toSet) {
   783             if (args.length == i + 1) {
   784                 throw new IllegalArgumentException("--exclude or -e present but no exclusion list present");
   785             }
   786             for (String s : args[i + 1].split(",")) {
   787                 toSet.exclude.add(s);
   788             }
   789             return 2;
   790         }
   791 
   792         @Override
   793         protected String help() {
   794             return "Exclude any relationships where the fully qualified class name starts with any pattern in this comma-delimited list of strings, e.g. -e foo.bar,foo.baz";
   795         }
   796     }
   797 
   798     private static final class IgnoreCommand extends Command {
   799 
   800         IgnoreCommand() {
   801             super(CMD_IGNORE, "i", true, true);
   802         }
   803 
   804         @Override
   805         protected int doParse(int i, String[] args, Arguments toSet) {
   806             if (args.length == i + 1) {
   807                 throw new IllegalArgumentException("--exclude or -e present but no exclusion list present");
   808             }
   809             for (String s : args[i + 1].split(",")) {
   810                 toSet.ignore.add(s);
   811             }
   812             return 2;
   813         }
   814 
   815         @Override
   816         protected String help() {
   817             return "Comma delimited list of folders or subfolders to ignore, absolute or relative to the base directory.";
   818         }
   819     }
   820 
   821     static final class InvalidArgumentsException extends IllegalArgumentException {
   822 
   823         private final List<String> errors;
   824 
   825         InvalidArgumentsException(String msg, List<String> errors) {
   826             super(msg);
   827             this.errors = errors;
   828         }
   829 
   830         public List<String> errors() {
   831             return errors;
   832         }
   833 
   834         public boolean errorContains(String test) { //for tests
   835             for (String err : errors) {
   836                 if (err.contains(test)) {
   837                     return true;
   838                 }
   839             }
   840             return false;
   841         }
   842     }
   843 }