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