View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd;
5   
6   import java.io.File;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.net.URISyntaxException;
10  import java.sql.SQLException;
11  import java.util.ArrayList;
12  import java.util.Collection;
13  import java.util.Collections;
14  import java.util.Comparator;
15  import java.util.HashSet;
16  import java.util.LinkedList;
17  import java.util.List;
18  import java.util.Properties;
19  import java.util.Set;
20  import java.util.logging.Handler;
21  import java.util.logging.Level;
22  import java.util.logging.Logger;
23  
24  import net.sourceforge.pmd.benchmark.Benchmark;
25  import net.sourceforge.pmd.benchmark.Benchmarker;
26  import net.sourceforge.pmd.benchmark.TextReport;
27  import net.sourceforge.pmd.cli.PMDCommandLineInterface;
28  import net.sourceforge.pmd.cli.PMDParameters;
29  import net.sourceforge.pmd.lang.Language;
30  import net.sourceforge.pmd.lang.LanguageFilenameFilter;
31  import net.sourceforge.pmd.lang.LanguageVersion;
32  import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
33  import net.sourceforge.pmd.lang.LanguageVersionHandler;
34  import net.sourceforge.pmd.lang.Parser;
35  import net.sourceforge.pmd.lang.ParserOptions;
36  import net.sourceforge.pmd.processor.MonoThreadProcessor;
37  import net.sourceforge.pmd.processor.MultiThreadProcessor;
38  import net.sourceforge.pmd.renderers.Renderer;
39  import net.sourceforge.pmd.util.FileUtil;
40  import net.sourceforge.pmd.util.IOUtil;
41  import net.sourceforge.pmd.util.SystemUtils;
42  import net.sourceforge.pmd.util.database.DBMSMetadata;
43  import net.sourceforge.pmd.util.database.DBURI;
44  import net.sourceforge.pmd.util.database.SourceObject;
45  import net.sourceforge.pmd.util.datasource.DataSource;
46  import net.sourceforge.pmd.util.datasource.ReaderDataSource;
47  import net.sourceforge.pmd.util.log.ConsoleLogHandler;
48  import net.sourceforge.pmd.util.log.ScopedLogHandlersManager;
49  
50  /**
51   * This is the main class for interacting with PMD. The primary flow of all Rule
52   * process is controlled via interactions with this class. A command line
53   * interface is supported, as well as a programmatic API for integrating PMD
54   * with other software such as IDEs and Ant.
55   */
56  public class PMD {
57  
58      private static final Logger LOG = Logger.getLogger(PMD.class.getName());
59  
60      /** The line delimiter used by PMD in outputs. Usually the platform specific line separator. */
61      public static final String EOL = System.getProperty("line.separator", "\n");
62  
63      /** The default suppress marker string. */
64      public static final String SUPPRESS_MARKER = "NOPMD";
65  
66      /**
67       * Parses the given string as a database uri and returns a list of datasources.
68       * @param uriString the URI to parse
69       * @return list of data sources
70       * @throws PMDException if the URI couldn't be parsed
71       * @see DBURI
72       */
73      public static List<DataSource> getURIDataSources(String uriString) throws PMDException {
74          List<DataSource> dataSources = new ArrayList<DataSource>();
75  
76          try {
77              DBURI dbUri = new DBURI(uriString);
78              DBMSMetadata dbmsMetadata = new DBMSMetadata(dbUri);
79              LOG.log(Level.FINE, "DBMSMetadata retrieved");
80              List<SourceObject> sourceObjectList = dbmsMetadata.getSourceObjectList();
81              LOG.log(Level.FINE, "Located {0} database source objects", sourceObjectList.size());
82              for (SourceObject sourceObject : sourceObjectList) {
83                  String falseFilePath = sourceObject.getPseudoFileName();
84                  LOG.log(Level.FINEST, "Adding database source object {0}", falseFilePath);
85  
86                  try {
87                      dataSources.add(new ReaderDataSource(dbmsMetadata.getSourceCode(sourceObject), falseFilePath));
88                  } catch (SQLException ex) {
89                      LOG.log(Level.WARNING, "Cannot get SourceCode for " + falseFilePath + "  - skipping ...", ex);
90                  }
91              }
92          } catch (URISyntaxException e) {
93              throw new PMDException("Cannot get DataSources from DBURI - \"" + uriString + "\"", e);
94          } catch (SQLException e) {
95              throw new PMDException("Cannot get DataSources from DBURI, couldn't access the database - \"" + uriString
96                      + "\"", e);
97          } catch (ClassNotFoundException e) {
98              throw new PMDException("Cannot get DataSources from DBURI, probably missing database jdbc driver - \""
99                      + uriString + "\"", e);
100         } catch (Exception e) {
101             throw new PMDException("Encountered unexpected problem with URI \""
102                     + uriString + "\"", e);
103         }
104         return dataSources;
105     }
106 
107     /** Contains the configuration with which this PMD instance has been created. */
108     protected final PMDConfiguration configuration;
109 
110     private final SourceCodeProcessor rulesetsFileProcessor;
111 
112     /**
113      * Helper method to get a configured parser for the requested language. The parser is
114      * configured based on the given {@link PMDConfiguration}.
115      * @param languageVersion the requested language
116      * @param configuration the given configuration
117      * @return the pre-configured parser
118      */
119     public static Parser parserFor(LanguageVersion languageVersion, PMDConfiguration configuration) {
120 
121         // TODO Handle Rules having different parser options.
122         LanguageVersionHandler languageVersionHandler = languageVersion.getLanguageVersionHandler();
123         ParserOptions options = languageVersionHandler.getDefaultParserOptions();
124         if (configuration != null)
125             options.setSuppressMarker(configuration.getSuppressMarker());
126         return languageVersionHandler.getParser(options);
127     }
128 
129     /**
130      * Create a report, filter out any defective rules, and keep a record of
131      * them.
132      * 
133      * @param rs the rules
134      * @param ctx the rule context
135      * @param fileName the filename of the source file, which should appear in the report
136      * @return the Report
137      */
138     public static Report setupReport(RuleSets rs, RuleContext ctx, String fileName) {
139 
140         Set<Rule> brokenRules = removeBrokenRules(rs);
141         Report report = Report.createReport(ctx, fileName);
142 
143         for (Rule rule : brokenRules) {
144             report.addConfigError(new Report.RuleConfigurationError(rule, rule.dysfunctionReason()));
145         }
146 
147         return report;
148     }
149 
150     /**
151      * Remove and return the misconfigured rules from the rulesets and log them
152      * for good measure.
153      * 
154      * @param ruleSets
155      *            RuleSets
156      * @return Set<Rule>
157      */
158     private static Set<Rule> removeBrokenRules(RuleSets ruleSets) {
159 
160         Set<Rule> brokenRules = new HashSet<Rule>();
161         ruleSets.removeDysfunctionalRules(brokenRules);
162 
163         for (Rule rule : brokenRules) {
164             LOG.log(Level.WARNING,
165                     "Removed misconfigured rule: " + rule.getName() + "  cause: " + rule.dysfunctionReason());
166         }
167 
168         return brokenRules;
169     }
170 
171     /**
172      * Create a PMD instance using a default Configuration. Changes to the
173      * configuration may be required.
174      */
175     public PMD() {
176         this(new PMDConfiguration());
177     }
178 
179     /**
180      * Create a PMD instance using the specified Configuration.
181      * 
182      * @param configuration
183      *            The runtime Configuration of PMD to use.
184      */
185     public PMD(PMDConfiguration configuration) {
186         this.configuration = configuration;
187         this.rulesetsFileProcessor = new SourceCodeProcessor(configuration);
188     }
189 
190     /**
191      * Get the runtime configuration. The configuration can be modified to
192      * affect how PMD behaves.
193      * 
194      * @return The configuration.
195      * @see PMDConfiguration
196      */
197     public PMDConfiguration getConfiguration() {
198         return configuration;
199     }
200 
201     /**
202      * Gets the source code processor.
203      * @return SourceCodeProcessor
204      */
205     public SourceCodeProcessor getSourceCodeProcessor() {
206         return rulesetsFileProcessor;
207     }
208 
209     /**
210      * This method is the main entry point for command line usage.
211      * 
212      * @param configuration the configure to use
213      */
214     public static void doPMD(PMDConfiguration configuration) {
215 
216         // Load the RuleSets
217         RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.getRulesetFactory(configuration);
218         RuleSets ruleSets = RulesetsFactoryUtils.getRuleSetsWithBenchmark(configuration.getRuleSets(), ruleSetFactory);
219         if (ruleSets == null)
220             return;
221 
222         Set<Language> languages = getApplicableLanguages(configuration, ruleSets);
223         List<DataSource> files = getApplicableFiles(configuration, languages);
224 
225         long reportStart = System.nanoTime();
226         try {
227             Renderer renderer = configuration.createRenderer();
228             List<Renderer> renderers = new LinkedList<Renderer>();
229             renderers.add(renderer);
230 
231             renderer.setWriter(IOUtil.createWriter(configuration.getReportFile()));
232             renderer.start();
233 
234             Benchmarker.mark(Benchmark.Reporting, System.nanoTime() - reportStart, 0);
235 
236             RuleContext ctx = new RuleContext();
237 
238             processFiles(configuration, ruleSetFactory, files, ctx, renderers);
239 
240             reportStart = System.nanoTime();
241             renderer.end();
242             renderer.flush();
243         } catch (Exception e) {
244             String message = e.getMessage();
245             if (message != null) {
246                 LOG.severe(message);
247             } else {
248                 LOG.log(Level.SEVERE, "Exception during processing", e);
249             }
250             LOG.log(Level.FINE, "Exception during processing", e);
251             LOG.info(PMDCommandLineInterface.buildUsageText());
252         } finally {
253             Benchmarker.mark(Benchmark.Reporting, System.nanoTime() - reportStart, 0);
254         }
255     }
256 
257     /**
258      * Creates a new rule context, initialized with a new, empty report.
259      *
260      * @param sourceCodeFilename the source code filename
261      * @param sourceCodeFile the source code file
262      * @return the rule context
263      */
264     public static RuleContext newRuleContext(String sourceCodeFilename, File sourceCodeFile) {
265 
266         RuleContext context = new RuleContext();
267         context.setSourceCodeFile(sourceCodeFile);
268         context.setSourceCodeFilename(sourceCodeFilename);
269         context.setReport(new Report());
270         return context;
271     }
272 
273     /**
274      * A callback that would be implemented by IDEs keeping track of PMD's
275      * progress as it evaluates a set of files.
276      * 
277      * @author Brian Remedios
278      */
279     public interface ProgressMonitor {
280         /**
281          * A status update reporting on current progress. Implementers will
282          * return true if it is to continue, false otherwise.
283          * 
284          * @param total total number of files to be analyzed
285          * @param totalDone number of files, that have been done analyzing.
286          * @return <code>true</code> if the execution of PMD should continue, <code>false</code> if the execution
287          * should be cancelled/terminated.
288          */
289         boolean status(int total, int totalDone);
290     }
291 
292     /**
293      * An entry point that would typically be used by IDEs intent on providing
294      * ongoing feedback and the ability to terminate it at will.
295      * 
296      * @param configuration the PMD configuration to use
297      * @param ruleSetFactory ruleset factory
298      * @param files the files to analyze
299      * @param ctx the rule context to use for the execution
300      * @param monitor PMD informs about the progress through this progress monitor. It provides also
301      * the ability to terminate/cancel the execution.
302      */
303     public static void processFiles(PMDConfiguration configuration, RuleSetFactory ruleSetFactory,
304             Collection<File> files, RuleContext ctx, ProgressMonitor monitor) {
305 
306         // TODO
307         // call the main processFiles with just the new monitor and a single
308         // logRenderer
309     }
310 
311     /**
312      * Run PMD on a list of files using multiple threads - if more than one is
313      * available
314      * 
315      * @param configuration
316      *            Configuration
317      * @param ruleSetFactory
318      *            RuleSetFactory
319      * @param files
320      *            List<DataSource>
321      * @param ctx
322      *            RuleContext
323      * @param renderers
324      *            List<Renderer>
325      */
326     public static void processFiles(final PMDConfiguration configuration, final RuleSetFactory ruleSetFactory,
327             final List<DataSource> files, final RuleContext ctx, final List<Renderer> renderers) {
328 
329         sortFiles(configuration, files);
330 
331         /*
332          * Check if multithreaded support is available. ExecutorService can also
333          * be disabled if threadCount is not positive, e.g. using the
334          * "-threads 0" command line option.
335          */
336         if (SystemUtils.MT_SUPPORTED && configuration.getThreads() > 0) {
337             new MultiThreadProcessor(configuration).processFiles(ruleSetFactory, files, ctx, renderers);
338         } else {
339             new MonoThreadProcessor(configuration).processFiles(ruleSetFactory, files, ctx, renderers);
340         }
341     }
342 
343     private static void sortFiles(final PMDConfiguration configuration, final List<DataSource> files) {
344         if (configuration.isStressTest()) {
345             // randomize processing order
346             Collections.shuffle(files);
347         } else {
348             final boolean useShortNames = configuration.isReportShortNames();
349             final String inputPaths = configuration.getInputPaths();
350             Collections.sort(files, new Comparator<DataSource>() {
351                 public int compare(DataSource left, DataSource right) {
352                     String leftString = left.getNiceFileName(useShortNames, inputPaths);
353                     String rightString = right.getNiceFileName(useShortNames, inputPaths);
354                     return leftString.compareTo(rightString);
355                 }
356             });
357         }
358     }
359 
360     /**
361      * Determines all the files, that should be analyzed by PMD.
362      * @param configuration contains either the file path or the DB URI, from where to load the files
363      * @param languages used to filter by file extension
364      * @return List<DataSource> of files
365      */
366     public static List<DataSource> getApplicableFiles(PMDConfiguration configuration, Set<Language> languages) {
367         long startFiles = System.nanoTime();
368         LanguageFilenameFilter fileSelector = new LanguageFilenameFilter(languages);
369         List<DataSource> files = new ArrayList<DataSource>();
370 
371         if (null != configuration.getInputPaths()) {
372             files.addAll(FileUtil.collectFiles(configuration.getInputPaths(), fileSelector));
373         }
374 
375         if (null != configuration.getInputUri()) {
376             String uriString = configuration.getInputUri();
377             try {
378                 List<DataSource> dataSources = getURIDataSources(uriString);
379 
380                 files.addAll(dataSources);
381             } catch (PMDException ex) {
382                 LOG.log(Level.SEVERE, "Problem with Input URI", ex);
383                 throw new RuntimeException("Problem with DBURI: " + uriString, ex);
384             }
385         }
386         long endFiles = System.nanoTime();
387         Benchmarker.mark(Benchmark.CollectFiles, endFiles - startFiles, 0);
388         return files;
389     }
390 
391     private static Set<Language> getApplicableLanguages(PMDConfiguration configuration, RuleSets ruleSets) {
392         Set<Language> languages = new HashSet<Language>();
393         LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer();
394 
395         for (Rule rule : ruleSets.getAllRules()) {
396             Language language = rule.getLanguage();
397             if (languages.contains(language))
398                 continue;
399             LanguageVersion version = discoverer.getDefaultLanguageVersion(language);
400             if (RuleSet.applies(rule, version)) {
401                 languages.add(language);
402                 LOG.fine("Using " + language.getShortName() + " version: " + version.getShortName());
403             }
404         }
405         return languages;
406     }
407 
408     /**
409      * Entry to invoke PMD as command line tool
410      * 
411      * @param args command line arguments
412      */
413     public static void main(String[] args) {
414         PMDCommandLineInterface.run(args);
415     }
416 
417     /**
418      * Parses the command line arguments and executes PMD.
419      * @param args command line arguments
420      * @return the exit code, where <code>0</code> means successful execution.
421      */
422     public static int run(String[] args) {
423         int status = 0;
424         long start = System.nanoTime();
425         final PMDParameters params = PMDCommandLineInterface.extractParameters(new PMDParameters(), args, "pmd");
426         final PMDConfiguration configuration = PMDParameters.transformParametersIntoConfiguration(params);
427 
428         final Level logLevel = params.isDebug() ? Level.FINER : Level.INFO;
429         final Handler logHandler = new ConsoleLogHandler();
430         final ScopedLogHandlersManager logHandlerManager = new ScopedLogHandlersManager(logLevel, logHandler);
431         final Level oldLogLevel = LOG.getLevel();
432         LOG.setLevel(logLevel); // Need to do this, since the static logger has
433                                 // already been initialized at this point
434         try {
435             PMD.doPMD(configuration);
436         } catch (Exception e) {
437             System.out.println(PMDCommandLineInterface.buildUsageText());
438             System.out.println();
439             System.out.println(e.getMessage());
440             status = PMDCommandLineInterface.ERROR_STATUS;
441         } finally {
442             logHandlerManager.close();
443             LOG.setLevel(oldLogLevel);
444             if (params.isBenchmark()) {
445                 long end = System.nanoTime();
446                 Benchmarker.mark(Benchmark.TotalPMD, end - start, 0);
447 
448                 TextReport report = new TextReport(); // TODO get specified
449                                                       // report format from
450                                                       // config
451                 report.generate(Benchmarker.values(), System.err);
452             }
453         }
454         return status;
455     }
456 
457     /**
458      * Constant that contains always the current version of PMD.
459      */
460     public static final String VERSION;
461     /**
462      * Determines the version from maven's generated pom.properties file.
463      */
464     static {
465         String pmdVersion = null;
466         InputStream stream = PMD.class.getResourceAsStream("/META-INF/maven/net.sourceforge.pmd/pmd/pom.properties");
467         if (stream != null) {
468             try {
469                 Properties properties = new Properties();
470                 properties.load(stream);
471                 pmdVersion = properties.getProperty("version");
472             } catch (IOException e) {
473                 LOG.log(Level.FINE, "Couldn't determine version of PMD", e);
474             }
475         }
476         if (pmdVersion == null) {
477             pmdVersion = "unknown";
478         }
479         VERSION = pmdVersion;
480     }
481 }