View Javadoc
1   package org.apache.maven.plugin.checkstyle;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.OutputStream;
28  import java.util.ArrayList;
29  import java.util.Calendar;
30  import java.util.Collections;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.ResourceBundle;
35  
36  import org.apache.maven.artifact.Artifact;
37  import org.apache.maven.doxia.tools.SiteTool;
38  import org.apache.maven.model.Dependency;
39  import org.apache.maven.model.Plugin;
40  import org.apache.maven.model.PluginManagement;
41  import org.apache.maven.model.ReportPlugin;
42  import org.apache.maven.model.Resource;
43  import org.apache.maven.plugin.MojoExecution;
44  import org.apache.maven.plugin.checkstyle.exec.CheckstyleExecutor;
45  import org.apache.maven.plugin.checkstyle.exec.CheckstyleExecutorException;
46  import org.apache.maven.plugin.checkstyle.exec.CheckstyleExecutorRequest;
47  import org.apache.maven.plugin.checkstyle.exec.CheckstyleResults;
48  import org.apache.maven.plugin.checkstyle.rss.CheckstyleRssGenerator;
49  import org.apache.maven.plugin.checkstyle.rss.CheckstyleRssGeneratorRequest;
50  import org.apache.maven.plugin.descriptor.PluginDescriptor;
51  import org.apache.maven.plugins.annotations.Component;
52  import org.apache.maven.plugins.annotations.Parameter;
53  import org.apache.maven.reporting.AbstractMavenReport;
54  import org.apache.maven.reporting.MavenReportException;
55  import org.codehaus.plexus.resource.ResourceManager;
56  import org.codehaus.plexus.resource.loader.FileResourceLoader;
57  import org.codehaus.plexus.util.FileUtils;
58  import org.codehaus.plexus.util.PathTool;
59  import org.codehaus.plexus.util.StringUtils;
60  
61  import com.puppycrawl.tools.checkstyle.DefaultLogger;
62  import com.puppycrawl.tools.checkstyle.XMLLogger;
63  import com.puppycrawl.tools.checkstyle.api.AuditListener;
64  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
65  
66  /**
67   * Base abstract class for Checkstyle reports.
68   *
69   * @version $Id: AbstractCheckstyleReport.java 1655882 2015-01-29 23:20:52Z hboutemy $
70   */
71  public abstract class AbstractCheckstyleReport
72      extends AbstractMavenReport
73  {
74      public static final String PLUGIN_RESOURCES = "org/apache/maven/plugin/checkstyle";
75  
76      protected static final String JAVA_FILES = "**\\/*.java";
77  
78      /**
79       * Specifies the cache file used to speed up Checkstyle on successive runs.
80       */
81      @Parameter( defaultValue = "${project.build.directory}/checkstyle-cachefile" )
82      protected String cacheFile;
83  
84      /**
85       * <p>
86       * Specifies the location of the XML configuration to use.
87       * </p>
88       * <p/>
89       * <p>
90       * Potential values are a filesystem path, a URL, or a classpath resource.
91       * This parameter expects that the contents of the location conform to the
92       * xml format (Checkstyle <a
93       * href="http://checkstyle.sourceforge.net/config.html#Modules">Checker
94       * module</a>) configuration of rulesets.
95       * </p>
96       * <p/>
97       * <p>
98       * This parameter is resolved as resource, URL, then file. If successfully
99       * resolved, the contents of the configuration is copied into the
100      * <code>${project.build.directory}/checkstyle-configuration.xml</code>
101      * file before being passed to Checkstyle as a configuration.
102      * </p>
103      * <p/>
104      * <p>
105      * There are 3 predefined rulesets included in Maven Checkstyle Plugin:
106      * </p>
107      * <ul>
108      * <li><code>config/sun_checks.xml</code>: Sun Checks.</li>
109      * <li><code>config/turbine_checks.xml</code>: Turbine Checks.</li>
110      * <li><code>config/maven_checks.xml</code>: Maven Source Checks.</li>
111      * </ul>
112      */
113     @Parameter( property = "checkstyle.config.location", defaultValue = "config/sun_checks.xml" )
114     protected String configLocation;
115 
116     /**
117      * Output errors to console.
118      */
119     @Parameter( property = "checkstyle.consoleOutput", defaultValue = "false" )
120     protected boolean consoleOutput;
121 
122     /**
123      * The file encoding to use when reading the source files. If the property <code>project.build.sourceEncoding</code>
124      * is not set, the platform default encoding is used. <strong>Note:</strong> This parameter always overrides the
125      * property <code>charset</code> from Checkstyle's <code>TreeWalker</code> module.
126      *
127      * @since 2.2
128      */
129     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
130     protected String encoding;
131 
132     /**
133      * Specifies if the build should fail upon a violation.
134      */
135     @Parameter( defaultValue = "false" )
136     protected boolean failsOnError;
137 
138     /**
139      * <p>
140      * Specifies the location of the License file (a.k.a. the header file) that
141      * can be used by Checkstyle to verify that source code has the correct
142      * license header.
143      * </p>
144      * <p>
145      * You need to use ${checkstyle.header.file} in your Checkstyle xml
146      * configuration to reference the name of this header file.
147      * </p>
148      * <p>
149      * For instance:
150      * </p>
151      * <p>
152      * <code>
153      * &lt;module name="RegexpHeader">
154      * &lt;property name="headerFile" value="${checkstyle.header.file}"/>
155      * &lt;/module>
156      * </code>
157      * </p>
158      *
159      * @since 2.0-beta-2
160      */
161     @Parameter( property = "checkstyle.header.file", defaultValue = "LICENSE.txt" )
162     protected String headerLocation;
163 
164     /**
165      * Skip entire check.
166      *
167      * @since 2.2
168      */
169     @Parameter( property = "checkstyle.skip", defaultValue = "false" )
170     protected boolean skip;
171 
172     /**
173      * Specifies the path and filename to save the Checkstyle output. The format
174      * of the output file is determined by the <code>outputFileFormat</code>
175      * parameter.
176      */
177     @Parameter( property = "checkstyle.output.file", defaultValue = "${project.build.directory}/checkstyle-result.xml" )
178     private File outputFile;
179 
180     /**
181      * <p>
182      * Specifies the location of the properties file.
183      * </p>
184      * <p/>
185      * <p>
186      * This parameter is resolved as URL, File then resource. If successfully
187      * resolved, the contents of the properties location is copied into the
188      * <code>${project.build.directory}/checkstyle-checker.properties</code>
189      * file before being passed to Checkstyle for loading.
190      * </p>
191      * <p/>
192      * <p>
193      * The contents of the <code>propertiesLocation</code> will be made
194      * available to Checkstyle for specifying values for parameters within the
195      * xml configuration (specified in the <code>configLocation</code>
196      * parameter).
197      * </p>
198      *
199      * @since 2.0-beta-2
200      */
201     @Parameter( property = "checkstyle.properties.location" )
202     protected String propertiesLocation;
203 
204     /**
205      * Allows for specifying raw property expansion information.
206      */
207     @Parameter
208     protected String propertyExpansion;
209 
210     /**
211      * Specifies the location of the resources to be used for Checkstyle.
212      *
213      * @since 2.10
214      */
215     @Parameter( defaultValue = "${project.resources}", readonly = true )
216     protected List<Resource> resources;
217 
218     /**
219      * Specifies the location of the test resources to be used for Checkstyle.
220      *
221      * @since 2.11
222      */
223     @Parameter( defaultValue = "${project.testResources}", readonly = true )
224     protected List<Resource> testResources;
225 
226     /**
227      * Specifies the names filter of the source files to be used for Checkstyle.
228      */
229     @Parameter( property = "checkstyle.includes", defaultValue = JAVA_FILES, required = true )
230     protected String includes;
231 
232     /**
233      * Specifies the names filter of the source files to be excluded for
234      * Checkstyle.
235      */
236     @Parameter( property = "checkstyle.excludes" )
237     protected String excludes;
238 
239     /**
240      * Specifies the names filter of the resource files to be used for Checkstyle.
241      * @since 2.11
242      */
243     @Parameter( property = "checkstyle.resourceIncludes", defaultValue = "**/*.properties", required = true )
244     protected String resourceIncludes;
245 
246     /**
247      * Specifies the names filter of the resource files to be excluded for
248      * Checkstyle.
249      * @since 2.11
250      */
251     @Parameter( property = "checkstyle.resourceExcludes" )
252     protected String resourceExcludes;
253 
254     /**
255      * Specifies whether to include the resource directories in the check.
256      * @since 2.11
257      */
258     @Parameter( property = "checkstyle.includeResources", defaultValue = "true", required = true )
259     protected boolean includeResources;
260 
261     /**
262      * Specifies whether to include the test resource directories in the check.
263      * @since 2.11
264      */
265     @Parameter( property = "checkstyle.includeTestResources", defaultValue = "true", required = true )
266     protected boolean includeTestResources;
267 
268     /**
269      * Specifies the location of the source directory to be used for Checkstyle.
270      * 
271      * @deprecated instead use {@link #sourceDirectories}
272      */
273     @Deprecated
274     @Parameter
275     private File sourceDirectory;
276 
277     /**
278      * Specifies the location of the source directories to be used for Checkstyle.
279      * @since 2.13
280      */
281     @Parameter( defaultValue = "${project.compileSourceRoots}" )
282     private List<String> sourceDirectories;
283     
284     /**
285      * Specifies the location of the test source directory to be used for
286      * Checkstyle.
287      *
288      * @since 2.2
289      * @deprecated instead use {@link #testSourceDirectories}
290      */
291     @Parameter
292     @Deprecated
293     private File testSourceDirectory;
294     
295     /**
296      * Specifies the location of the test source directories to be used for Checkstyle.
297      * @since 2.13
298      */
299     @Parameter( defaultValue = "${project.testCompileSourceRoots}" )
300     private List<String> testSourceDirectories;
301 
302     /**
303      * Include or not the test source directory/directories to be used for Checkstyle.
304      *
305      * @since 2.2
306      */
307     @Parameter( defaultValue = "false" )
308     protected boolean includeTestSourceDirectory;
309 
310     /**
311      * The key to be used in the properties for the suppressions file.
312      *
313      * @since 2.1
314      */
315     @Parameter( property = "checkstyle.suppression.expression", defaultValue = "checkstyle.suppressions.file" )
316     protected String suppressionsFileExpression;
317 
318     /**
319      * <p>
320      * Specifies the location of the suppressions XML file to use.
321      * </p>
322      * <p/>
323      * <p>
324      * This parameter is resolved as resource, URL, then file. If successfully
325      * resolved, the contents of the suppressions XML is copied into the
326      * <code>${project.build.directory}/checkstyle-supressions.xml</code> file
327      * before being passed to Checkstyle for loading.
328      * </p>
329      * <p/>
330      * <p>
331      * See <code>suppressionsFileExpression</code> for the property that will
332      * be made available to your Checkstyle configuration.
333      * </p>
334      *
335      * @since 2.0-beta-2
336      */
337     @Parameter( property = "checkstyle.suppressions.location" )
338     protected String suppressionsLocation;
339 
340     /**
341      * If <code>null</code>, the Checkstyle plugin will display violations on stdout.
342      * Otherwise, a text file will be created with the violations.
343      */
344     @Parameter
345     private File useFile;
346 
347     /**
348      * Specifies the format of the output to be used when writing to the output
349      * file. Valid values are "<code>plain</code>" and "<code>xml</code>".
350      */
351     @Parameter( property = "checkstyle.output.format", defaultValue = "xml" )
352     private String outputFileFormat;
353 
354     /**
355      * Specifies if the Rules summary should be enabled or not.
356      */
357     @Parameter( property = "checkstyle.enable.rules.summary", defaultValue = "true" )
358     private boolean enableRulesSummary;
359 
360     /**
361      * Specifies if the Severity summary should be enabled or not.
362      */
363     @Parameter( property = "checkstyle.enable.severity.summary", defaultValue = "true" )
364     private boolean enableSeveritySummary;
365 
366     /**
367      * Specifies if the Files summary should be enabled or not.
368      */
369     @Parameter( property = "checkstyle.enable.files.summary", defaultValue = "true" )
370     private boolean enableFilesSummary;
371 
372     /**
373      * Specifies if the RSS should be enabled or not.
374      */
375     @Parameter( property = "checkstyle.enable.rss", defaultValue = "true" )
376     private boolean enableRSS;
377 
378     /**
379      * SiteTool.
380      *
381      * @since 2.2
382      */
383     @Component( role = SiteTool.class )
384     protected SiteTool siteTool;
385 
386     /**
387      * The Plugin Descriptor
388      */
389     @Parameter( defaultValue = "${plugin}", readonly = true, required = true )
390     private PluginDescriptor plugin;
391 
392     // remove when requiring Maven 3.x, just use #plugin 
393     @Parameter( defaultValue = "${mojoExecution}", readonly = true, required = true )
394     private MojoExecution mojoExecution;
395     
396     /**
397      * Link the violation line numbers to the source xref. Will link
398      * automatically if Maven JXR plugin is being used.
399      *
400      * @since 2.1
401      */
402     @Parameter( property = "linkXRef", defaultValue = "true" )
403     private boolean linkXRef;
404 
405     /**
406      * Location of the Xrefs to link to.
407      */
408     @Parameter( defaultValue = "${project.reporting.outputDirectory}/xref" )
409     private File xrefLocation;
410 
411     /**
412      * When using custom treeWalkers, specify their names here so the checks
413      * inside the treeWalker end up the the rule-summary.
414      * 
415      * @since 2.11
416      */
417     @Parameter
418     private List<String> treeWalkerNames;
419 
420     /**
421      */
422     @Component
423     protected ResourceManager locator;
424 
425     /**
426      * CheckstyleRssGenerator.
427      *
428      * @since 2.4
429      */
430     @Component( role = CheckstyleRssGenerator.class, hint = "default" )
431     protected CheckstyleRssGenerator checkstyleRssGenerator;
432 
433     /**
434      * @since 2.5
435      */
436     @Component( role = CheckstyleExecutor.class, hint = "default" )
437     protected CheckstyleExecutor checkstyleExecutor;
438 
439     protected ByteArrayOutputStream stringOutputStream;
440 
441     /** {@inheritDoc} */
442     public String getName( Locale locale )
443     {
444         return getBundle( locale ).getString( "report.checkstyle.name" );
445     }
446 
447     /** {@inheritDoc} */
448     public String getDescription( Locale locale )
449     {
450         return getBundle( locale ).getString( "report.checkstyle.description" );
451     }
452 
453     /** {@inheritDoc} */
454     public void executeReport( Locale locale )
455         throws MavenReportException
456     {
457         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
458         locator.addSearchPath( "url", "" );
459 
460         locator.setOutputDirectory( new File( project.getBuild().getDirectory() ) );
461 
462         // for when we start using maven-shared-io and maven-shared-monitor...
463         // locator = new Locator( new MojoLogMonitorAdaptor( getLog() ) );
464 
465         // locator = new Locator( getLog(), new File( project.getBuild().getDirectory() ) );
466 
467         ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
468 
469         try
470         {
471             CheckstyleExecutorRequest request = createRequest().setLicenseArtifacts( collectArtifacts( "license" ) )
472                             .setConfigurationArtifacts( collectArtifacts( "configuration" ) );
473 
474             CheckstyleResults results = checkstyleExecutor.executeCheckstyle( request );
475 
476             ResourceBundle bundle = getBundle( locale );
477             generateReportStatics();
478             generateMainReport( results, bundle );
479             if ( enableRSS )
480             {
481                 CheckstyleRssGeneratorRequest checkstyleRssGeneratorRequest =
482                     new CheckstyleRssGeneratorRequest( this.project, this.getCopyright(), outputDirectory, getLog() );
483                 checkstyleRssGenerator.generateRSS( results, checkstyleRssGeneratorRequest );
484             }
485 
486         }
487         catch ( CheckstyleException e )
488         {
489             throw new MavenReportException( "Failed during checkstyle configuration", e );
490         }
491         catch ( CheckstyleExecutorException e )
492         {
493             throw new MavenReportException( "Failed during checkstyle execution", e );
494         }
495         finally
496         {
497             //be sure to restore original context classloader
498             Thread.currentThread().setContextClassLoader( currentClassLoader );
499         }
500     }
501 
502     /**
503      * Create the Checkstyle executor request.
504      *
505      * @return The executor request.
506      * @throws MavenReportException If something goes wrong during creation.
507      */
508     protected abstract CheckstyleExecutorRequest createRequest()
509             throws MavenReportException;
510 
511     @SuppressWarnings( "unchecked" )
512     private List<Artifact> collectArtifacts( String hint )
513     {
514         if ( plugin == null || plugin.getGroupId() == null )
515         {
516             // Maven 2.x workaround
517             plugin = mojoExecution.getMojoDescriptor().getPluginDescriptor();
518         }
519         
520         List<Artifact> artifacts = new ArrayList<Artifact>();
521 
522         PluginManagement pluginManagement = project.getBuild().getPluginManagement();
523         if ( pluginManagement != null )
524         {
525             artifacts.addAll( getCheckstylePluginDependenciesAsArtifacts( pluginManagement.getPluginsAsMap(), hint ) );
526         }
527 
528         artifacts.addAll( getCheckstylePluginDependenciesAsArtifacts( project.getBuild().getPluginsAsMap(), hint ) );
529 
530         return artifacts;
531     }
532 
533     private List<Artifact> getCheckstylePluginDependenciesAsArtifacts( Map<String, Plugin> plugins, String hint )
534     {
535         List<Artifact> artifacts = new ArrayList<Artifact>();
536         
537         Plugin checkstylePlugin = plugins.get( plugin.getGroupId() + ":" + plugin.getArtifactId() );
538         if ( checkstylePlugin != null )
539         {
540             for ( Dependency dep : checkstylePlugin.getDependencies() )
541             {
542              // @todo if we can filter on hints, it should be done here...
543                 String depKey = dep.getGroupId() + ":" + dep.getArtifactId();
544                 artifacts.add( (Artifact) plugin.getArtifactMap().get( depKey ) );
545             }
546         }
547         return artifacts;
548     }
549 
550     /**
551      * Creates and returns the report generation listener.
552      *
553      * @return The audit listener.
554      * @throws MavenReportException If something goes wrong.
555      */
556     protected AuditListener getListener()
557         throws MavenReportException
558     {
559         AuditListener listener = null;
560 
561         if ( StringUtils.isNotEmpty( outputFileFormat ) )
562         {
563             File resultFile = outputFile;
564 
565             OutputStream out = getOutputStream( resultFile );
566 
567             if ( "xml".equals( outputFileFormat ) )
568             {
569                 listener = new XMLLogger( out, true );
570             }
571             else if ( "plain".equals( outputFileFormat ) )
572             {
573                 listener = new DefaultLogger( out, true );
574             }
575             else
576             {
577                 // TODO: failure if not a report
578                 throw new MavenReportException( "Invalid output file format: (" + outputFileFormat
579                     + "). Must be 'plain' or 'xml'." );
580             }
581         }
582 
583         return listener;
584     }
585 
586     private OutputStream getOutputStream( File file )
587         throws MavenReportException
588     {
589         File parentFile = file.getAbsoluteFile().getParentFile();
590 
591         if ( !parentFile.exists() )
592         {
593             parentFile.mkdirs();
594         }
595 
596         FileOutputStream fileOutputStream;
597         try
598         {
599             fileOutputStream = new FileOutputStream( file );
600         }
601         catch ( FileNotFoundException e )
602         {
603             throw new MavenReportException( "Unable to create output stream: " + file, e );
604         }
605         return fileOutputStream;
606     }
607 
608     /**
609      * Creates and returns the console listener.
610      *
611      * @return The console listener.
612      * @throws MavenReportException If something goes wrong.
613      */
614     protected DefaultLogger getConsoleListener()
615         throws MavenReportException
616     {
617         DefaultLogger consoleListener;
618 
619         if ( useFile == null )
620         {
621             stringOutputStream = new ByteArrayOutputStream();
622             consoleListener = new DefaultLogger( stringOutputStream, false );
623         }
624         else
625         {
626             OutputStream out = getOutputStream( useFile );
627 
628             consoleListener = new DefaultLogger( out, true );
629         }
630 
631         return consoleListener;
632     }
633 
634     private void generateReportStatics()
635         throws MavenReportException
636     {
637         ReportResource rresource = new ReportResource( PLUGIN_RESOURCES, outputDirectory );
638         try
639         {
640             rresource.copy( "images/rss.png" );
641         }
642         catch ( IOException e )
643         {
644             throw new MavenReportException( "Unable to copy static resources.", e );
645         }
646     }
647 
648 
649     private String getCopyright()
650     {
651         String copyright;
652         int currentYear = Calendar.getInstance().get( Calendar.YEAR );
653         if ( StringUtils.isNotEmpty( project.getInceptionYear() )
654             && !String.valueOf( currentYear ).equals( project.getInceptionYear() ) )
655         {
656             copyright = project.getInceptionYear() + " - " + currentYear;
657         }
658         else
659         {
660             copyright = String.valueOf( currentYear );
661         }
662 
663         if ( ( project.getOrganization() != null ) && StringUtils.isNotEmpty( project.getOrganization().getName() ) )
664         {
665             copyright = copyright + " " + project.getOrganization().getName();
666         }
667         return copyright;
668     }
669 
670     private void generateMainReport( CheckstyleResults results, ResourceBundle bundle )
671     {
672         CheckstyleReportGenerator generator =
673             new CheckstyleReportGenerator( getSink(), bundle, project.getBasedir(), siteTool, configLocation );
674 
675         generator.setLog( getLog() );
676         generator.setEnableRulesSummary( enableRulesSummary );
677         generator.setEnableSeveritySummary( enableSeveritySummary );
678         generator.setEnableFilesSummary( enableFilesSummary );
679         generator.setEnableRSS( enableRSS );
680         generator.setCheckstyleConfig( results.getConfiguration() );
681         if ( linkXRef )
682         {
683             String relativePath = PathTool.getRelativePath( getOutputDirectory(), xrefLocation.getAbsolutePath() );
684             if ( StringUtils.isEmpty( relativePath ) )
685             {
686                 relativePath = ".";
687             }
688             relativePath = relativePath + "/" + xrefLocation.getName();
689             if ( xrefLocation.exists() )
690             {
691                 // XRef was already generated by manual execution of a lifecycle
692                 // binding
693                 generator.setXrefLocation( relativePath );
694             }
695             else
696             {
697                 // Not yet generated - check if the report is on its way
698                 for ( ReportPlugin report : (Iterable<ReportPlugin>) getProject().getReportPlugins() )
699                 {
700                     String artifactId = report.getArtifactId();
701                     if ( "maven-jxr-plugin".equals( artifactId ) || "jxr-maven-plugin".equals( artifactId ) )
702                     {
703                         generator.setXrefLocation( relativePath );
704                     }
705                 }
706             }
707 
708             if ( generator.getXrefLocation() == null && results.getFileCount() > 0 )
709             {
710                 getLog().warn( "Unable to locate Source XRef to link to - DISABLED" );
711             }
712         }
713         if ( treeWalkerNames != null )
714         {
715             generator.setTreeWalkerNames( treeWalkerNames );
716         }
717         generator.generateReport( results );
718     }
719 
720     private static ResourceBundle getBundle( Locale locale )
721     {
722         return ResourceBundle.getBundle( "checkstyle-report", locale, AbstractCheckstyleReport.class.getClassLoader() );
723     }
724 
725     protected List<File> getSourceDirectories()
726     {
727         List<File> sourceDirs = null;
728         // if sourceDirectory is explicitly set, use it
729         if ( sourceDirectory != null )
730         {
731             sourceDirs = Collections.singletonList( sourceDirectory );
732         }
733         else
734         {
735             sourceDirs = new ArrayList<File>( sourceDirectories.size() );
736             for ( String sourceDir : sourceDirectories )
737             {
738                 sourceDirs.add( FileUtils.resolveFile( project.getBasedir(), sourceDir ) );
739             }
740         }
741         
742         return sourceDirs;
743     }
744 
745     protected List<File> getTestSourceDirectories()
746     {
747         List<File> testSourceDirs = null;
748         // if testSourceDirectory is explicitly set, use it
749         if ( testSourceDirectory != null )
750         {
751             testSourceDirs = Collections.singletonList( testSourceDirectory );
752         }
753         // probably null-check only required due to MavenProjectStubs
754         else if ( testSourceDirectories != null )
755         {
756             testSourceDirs = new ArrayList<File>( testSourceDirectories.size() );
757             for ( String testSourceDir : testSourceDirectories )
758             {
759                 testSourceDirs.add( FileUtils.resolveFile( project.getBasedir(), testSourceDir ) );
760             }
761         }
762         
763         return testSourceDirs;
764     }
765 }