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         long startLoadRules = System.nanoTime();
218         RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.getRulesetFactory(configuration);
219 
220         RuleSets ruleSets = RulesetsFactoryUtils.getRuleSets(configuration.getRuleSets(), ruleSetFactory,
221                 startLoadRules);
222         if (ruleSets == null)
223             return;
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 
241             processFiles(configuration, ruleSetFactory, files, ctx, renderers);
242 
243             reportStart = System.nanoTime();
244             renderer.end();
245             renderer.flush();
246         } catch (Exception e) {
247             String message = e.getMessage();
248             if (message != null) {
249                 LOG.severe(message);
250             } else {
251                 LOG.log(Level.SEVERE, "Exception during processing", e);
252             }
253             LOG.log(Level.FINE, "Exception during processing", e);
254             LOG.info(PMDCommandLineInterface.buildUsageText());
255         } finally {
256             Benchmarker.mark(Benchmark.Reporting, System.nanoTime() - reportStart, 0);
257         }
258     }
259 
260     /**
261      * Creates a new rule context, initialized with a new, empty report.
262      *
263      * @param sourceCodeFilename the source code filename
264      * @param sourceCodeFile the source code file
265      * @return the rule context
266      */
267     public static RuleContext newRuleContext(String sourceCodeFilename, File sourceCodeFile) {
268 
269         RuleContext context = new RuleContext();
270         context.setSourceCodeFile(sourceCodeFile);
271         context.setSourceCodeFilename(sourceCodeFilename);
272         context.setReport(new Report());
273         return context;
274     }
275 
276     /**
277      * A callback that would be implemented by IDEs keeping track of PMD's
278      * progress as it evaluates a set of files.
279      * 
280      * @author Brian Remedios
281      */
282     public interface ProgressMonitor {
283         /**
284          * A status update reporting on current progress. Implementers will
285          * return true if it is to continue, false otherwise.
286          * 
287          * @param total total number of files to be analyzed
288          * @param totalDone number of files, that have been done analyzing.
289          * @return <code>true</code> if the execution of PMD should continue, <code>false</code> if the execution
290          * should be cancelled/terminated.
291          */
292         boolean status(int total, int totalDone);
293     }
294 
295     /**
296      * An entry point that would typically be used by IDEs intent on providing
297      * ongoing feedback and the ability to terminate it at will.
298      * 
299      * @param configuration the PMD configuration to use
300      * @param ruleSetFactory ruleset factory
301      * @param files the files to analyze
302      * @param ctx the rule context to use for the execution
303      * @param monitor PMD informs about the progress through this progress monitor. It provides also
304      * the ability to terminate/cancel the execution.
305      */
306     public static void processFiles(PMDConfiguration configuration, RuleSetFactory ruleSetFactory,
307             Collection<File> files, RuleContext ctx, ProgressMonitor monitor) {
308 
309         // TODO
310         // call the main processFiles with just the new monitor and a single
311         // logRenderer
312     }
313 
314     /**
315      * Run PMD on a list of files using multiple threads - if more than one is
316      * available
317      * 
318      * @param configuration
319      *            Configuration
320      * @param ruleSetFactory
321      *            RuleSetFactory
322      * @param files
323      *            List<DataSource>
324      * @param ctx
325      *            RuleContext
326      * @param renderers
327      *            List<Renderer>
328      */
329     public static void processFiles(final PMDConfiguration configuration, final RuleSetFactory ruleSetFactory,
330             final List<DataSource> files, final RuleContext ctx, final List<Renderer> renderers) {
331 
332         sortFiles(configuration, files);
333 
334         /*
335          * Check if multithreaded support is available. ExecutorService can also
336          * be disabled if threadCount is not positive, e.g. using the
337          * "-threads 0" command line option.
338          */
339         if (SystemUtils.MT_SUPPORTED && configuration.getThreads() > 0) {
340             new MultiThreadProcessor(configuration).processFiles(ruleSetFactory, files, ctx, renderers);
341         } else {
342             new MonoThreadProcessor(configuration).processFiles(ruleSetFactory, files, ctx, renderers);
343         }
344     }
345 
346     private static void sortFiles(final PMDConfiguration configuration, final List<DataSource> files) {
347         if (configuration.isStressTest()) {
348             // randomize processing order
349             Collections.shuffle(files);
350         } else {
351             final boolean useShortNames = configuration.isReportShortNames();
352             final String inputPaths = configuration.getInputPaths();
353             Collections.sort(files, new Comparator<DataSource>() {
354                 public int compare(DataSource left, DataSource right) {
355                     String leftString = left.getNiceFileName(useShortNames, inputPaths);
356                     String rightString = right.getNiceFileName(useShortNames, inputPaths);
357                     return leftString.compareTo(rightString);
358                 }
359             });
360         }
361     }
362 
363     /**
364      * Determines all the files, that should be analyzed by PMD.
365      * @param configuration contains either the file path or the DB URI, from where to load the files
366      * @param languages used to filter by file extension
367      * @return List<DataSource> of files
368      */
369     public static List<DataSource> getApplicableFiles(PMDConfiguration configuration, Set<Language> languages) {
370         long startFiles = System.nanoTime();
371         LanguageFilenameFilter fileSelector = new LanguageFilenameFilter(languages);
372         List<DataSource> files = new ArrayList<DataSource>();
373 
374         if (null != configuration.getInputPaths()) {
375             files.addAll(FileUtil.collectFiles(configuration.getInputPaths(), fileSelector));
376         }
377 
378         if (null != configuration.getInputUri()) {
379             String uriString = configuration.getInputUri();
380             try {
381                 List<DataSource> dataSources = getURIDataSources(uriString);
382 
383                 files.addAll(dataSources);
384             } catch (PMDException ex) {
385                 LOG.log(Level.SEVERE, "Problem with Input URI", ex);
386                 throw new RuntimeException("Problem with DBURI: " + uriString, ex);
387             }
388         }
389         long endFiles = System.nanoTime();
390         Benchmarker.mark(Benchmark.CollectFiles, endFiles - startFiles, 0);
391         return files;
392     }
393 
394     private static Set<Language> getApplicableLanguages(PMDConfiguration configuration, RuleSets ruleSets) {
395         Set<Language> languages = new HashSet<Language>();
396         LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer();
397 
398         for (Rule rule : ruleSets.getAllRules()) {
399             Language language = rule.getLanguage();
400             if (languages.contains(language))
401                 continue;
402             LanguageVersion version = discoverer.getDefaultLanguageVersion(language);
403             if (RuleSet.applies(rule, version)) {
404                 languages.add(language);
405                 LOG.fine("Using " + language.getShortName() + " version: " + version.getShortName());
406             }
407         }
408         return languages;
409     }
410 
411     /**
412      * Entry to invoke PMD as command line tool
413      * 
414      * @param args command line arguments
415      */
416     public static void main(String[] args) {
417         PMDCommandLineInterface.run(args);
418     }
419 
420     /**
421      * Parses the command line arguments and executes PMD.
422      * @param args command line arguments
423      * @return the exit code, where <code>0</code> means successful execution.
424      */
425     public static int run(String[] args) {
426         int status = 0;
427         long start = System.nanoTime();
428         final PMDParameters params = PMDCommandLineInterface.extractParameters(new PMDParameters(), args, "pmd");
429         final PMDConfiguration configuration = PMDParameters.transformParametersIntoConfiguration(params);
430 
431         final Level logLevel = params.isDebug() ? Level.FINER : Level.INFO;
432         final Handler logHandler = new ConsoleLogHandler();
433         final ScopedLogHandlersManager logHandlerManager = new ScopedLogHandlersManager(logLevel, logHandler);
434         final Level oldLogLevel = LOG.getLevel();
435         LOG.setLevel(logLevel); // Need to do this, since the static logger has
436                                 // already been initialized at this point
437         try {
438             PMD.doPMD(configuration);
439         } catch (Exception e) {
440             PMDCommandLineInterface.buildUsageText();
441             System.out.println(e.getMessage());
442             status = PMDCommandLineInterface.ERROR_STATUS;
443         } finally {
444             logHandlerManager.close();
445             LOG.setLevel(oldLogLevel);
446             if (params.isBenchmark()) {
447                 long end = System.nanoTime();
448                 Benchmarker.mark(Benchmark.TotalPMD, end - start, 0);
449 
450                 TextReport report = new TextReport(); // TODO get specified
451                                                       // report format from
452                                                       // config
453                 report.generate(Benchmarker.values(), System.err);
454             }
455         }
456         return status;
457     }
458 
459     /**
460      * Constant that contains always the current version of PMD.
461      */
462     public static final String VERSION;
463     /**
464      * Determines the version from maven's generated pom.properties file.
465      */
466     static {
467         String pmdVersion = null;
468         InputStream stream = PMD.class.getResourceAsStream("/META-INF/maven/net.sourceforge.pmd/pmd/pom.properties");
469         if (stream != null) {
470             try {
471                 Properties properties = new Properties();
472                 properties.load(stream);
473                 pmdVersion = properties.getProperty("version");
474             } catch (IOException e) {
475                 LOG.log(Level.FINE, "Couldn't determine version of PMD", e);
476             }
477         }
478         if (pmdVersion == null) {
479             pmdVersion = "unknown";
480         }
481         VERSION = pmdVersion;
482     }
483 }