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