View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package org.basepom.mojo.duplicatefinder;
16  
17  import static com.google.common.base.Preconditions.checkArgument;
18  import static com.google.common.base.Preconditions.checkNotNull;
19  import static com.google.common.base.Preconditions.checkState;
20  import static java.lang.String.format;
21  import static org.apache.maven.artifact.Artifact.SCOPE_COMPILE;
22  import static org.apache.maven.artifact.Artifact.SCOPE_PROVIDED;
23  import static org.apache.maven.artifact.Artifact.SCOPE_RUNTIME;
24  import static org.apache.maven.artifact.Artifact.SCOPE_SYSTEM;
25  import static org.basepom.mojo.duplicatefinder.ConflictState.CONFLICT_CONTENT_DIFFERENT;
26  import static org.basepom.mojo.duplicatefinder.ConflictState.CONFLICT_CONTENT_EQUAL;
27  import static org.basepom.mojo.duplicatefinder.ConflictType.CLASS;
28  import static org.basepom.mojo.duplicatefinder.ConflictType.RESOURCE;
29  import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.getOutputDirectory;
30  import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.getTestOutputDirectory;
31  
32  import org.basepom.mojo.duplicatefinder.ResultCollector.ConflictResult;
33  import org.basepom.mojo.duplicatefinder.artifact.ArtifactFileResolver;
34  import org.basepom.mojo.duplicatefinder.artifact.MavenCoordinates;
35  import org.basepom.mojo.duplicatefinder.classpath.ClasspathDescriptor;
36  
37  import java.io.BufferedInputStream;
38  import java.io.File;
39  import java.io.IOException;
40  import java.io.InputStream;
41  import java.nio.file.Files;
42  import java.util.AbstractMap.SimpleImmutableEntry;
43  import java.util.Arrays;
44  import java.util.Collection;
45  import java.util.EnumSet;
46  import java.util.Map;
47  import java.util.Map.Entry;
48  import java.util.Set;
49  import java.util.SortedSet;
50  import java.util.zip.ZipEntry;
51  import java.util.zip.ZipFile;
52  import javax.xml.stream.XMLStreamException;
53  
54  import com.google.common.base.Strings;
55  import com.google.common.collect.ImmutableMap;
56  import com.google.common.collect.ImmutableSet;
57  import com.google.common.collect.Maps;
58  import com.google.common.collect.Multimap;
59  import com.google.common.hash.HashFunction;
60  import com.google.common.hash.Hashing;
61  import com.google.common.io.ByteStreams;
62  import com.google.common.io.Closer;
63  import org.apache.maven.artifact.Artifact;
64  import org.apache.maven.artifact.DependencyResolutionRequiredException;
65  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
66  import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
67  import org.apache.maven.model.Dependency;
68  import org.apache.maven.plugin.AbstractMojo;
69  import org.apache.maven.plugin.MojoExecutionException;
70  import org.apache.maven.plugin.MojoFailureException;
71  import org.apache.maven.plugins.annotations.LifecyclePhase;
72  import org.apache.maven.plugins.annotations.Mojo;
73  import org.apache.maven.plugins.annotations.Parameter;
74  import org.apache.maven.plugins.annotations.ResolutionScope;
75  import org.apache.maven.project.MavenProject;
76  import org.codehaus.stax2.XMLOutputFactory2;
77  import org.codehaus.staxmate.SMOutputFactory;
78  import org.codehaus.staxmate.out.SMOutputDocument;
79  import org.codehaus.staxmate.out.SMOutputElement;
80  import org.slf4j.Logger;
81  import org.slf4j.LoggerFactory;
82  
83  /**
84   * Finds duplicate classes/resources on the classpath.
85   */
86  @Mojo(name = "check", requiresProject = true, threadSafe = true, defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.TEST)
87  public final class DuplicateFinderMojo extends AbstractMojo {
88  
89      private static final Logger LOG = LoggerFactory.getLogger(DuplicateFinderMojo.class);
90  
91      private static final int SAVE_FILE_VERSION = 1;
92  
93      private static final HashFunction SHA_256 = Hashing.sha256();
94  
95      private static final Set<String> COMPILE_SCOPE = ImmutableSet.of(SCOPE_COMPILE, SCOPE_PROVIDED, SCOPE_SYSTEM);
96      private static final Set<String> RUNTIME_SCOPE = ImmutableSet.of(SCOPE_COMPILE, SCOPE_RUNTIME);
97      private static final Set<String> TEST_SCOPE = ImmutableSet.of(); // Empty == all scopes
98  
99      /**
100      * The maven project (effective pom).
101      */
102     @Parameter(defaultValue = "${project}", readonly = true)
103     public MavenProject project;
104 
105     /**
106      * Report files that have the same sha256 has value.
107      *
108      * @since 1.0.6
109      */
110     @Parameter(defaultValue = "false", property = "duplicate-finder.printEqualFiles")
111     public boolean printEqualFiles = false;
112 
113     /**
114      * Fail the build if files with the same name but different content are detected.
115      *
116      * @since 1.0.3
117      */
118     @Parameter(defaultValue = "false", property = "duplicate-finder.failBuildInCaseOfDifferentContentConflict")
119     public boolean failBuildInCaseOfDifferentContentConflict = false;
120 
121     /**
122      * Fail the build if files with the same name and the same content are detected.
123      *
124      * @since 1.0.3
125      */
126     @Parameter(defaultValue = "false", property = "duplicate-finder.failBuildInCaseOfEqualContentConflict")
127     public boolean failBuildInCaseOfEqualContentConflict = false;
128 
129     /**
130      * Fail the build if any files with the same name are found.
131      */
132     @Parameter(defaultValue = "false", property = "duplicate-finder.failBuildInCaseOfConflict")
133     public boolean failBuildInCaseOfConflict = false;
134 
135     /**
136      * Use the default resource ignore list.
137      */
138     @Parameter(defaultValue = "true", property = "duplicate-finder.useDefaultResourceIgnoreList")
139     public boolean useDefaultResourceIgnoreList = true;
140 
141     /**
142      * Use the default class ignore list.
143      *
144      * @since 1.2.1
145      */
146     @Parameter(defaultValue = "true", property = "duplicate-finder.useDefaultClassIgnoreList")
147     public boolean useDefaultClassIgnoreList = true;
148 
149     /**
150      * Ignored resources, which are not checked for multiple occurences.
151      */
152     @Parameter
153     public String[] ignoredResourcePatterns = new String[0];
154 
155     /**
156      * Ignored classes, which are not checked for multiple occurences.
157      *
158      * @since 1.2.1
159      */
160     @Parameter
161     public String[] ignoredClassPatterns = new String[0];
162 
163     /**
164      * Artifacts with expected and resolved versions that are checked.
165      */
166     @Parameter(alias = "exceptions")
167     public ConflictingDependency[] conflictingDependencies = new ConflictingDependency[0];
168 
169     /**
170      * Dependencies that should not be checked at all.
171      */
172     @Parameter(alias = "ignoredDependencies")
173     MavenCoordinates[] ignoredDependencies = new MavenCoordinates[0];
174 
175     /**
176      * Check resources and classes on the compile class path.
177      */
178     @Parameter(defaultValue = "true", property = "duplicate-finder.checkCompileClasspath")
179     public boolean checkCompileClasspath = true;
180 
181     /**
182      * Check resources and classes on the runtime class path.
183      */
184     @Parameter(defaultValue = "true", property = "duplicate-finder.checkRuntimeClasspath")
185     public boolean checkRuntimeClasspath = true;
186 
187     /**
188      * Check resources and classes on the test class path.
189      */
190     @Parameter(defaultValue = "true", property = "duplicate-finder.checkTestClasspath")
191     public boolean checkTestClasspath = true;
192 
193     /**
194      * Skips the plugin execution.
195      */
196     @Parameter(defaultValue = "false", property = "duplicate-finder.skip")
197     public boolean skip = false;
198 
199     /**
200      * Quiets the plugin (report only errors).
201      *
202      * @since 1.1.0
203      * @deprecated Maven logging controls the log level now.
204      */
205     @Parameter(defaultValue = "false", property = "duplicate-finder.quiet")
206     @Deprecated
207     public boolean quiet = false;
208 
209     /**
210      * Whether existing local directories with classes or existing artifacts are preferred.
211      *
212      * @since 1.1.0
213      */
214     @Parameter(defaultValue = "true", property = "duplicate-finder.preferLocal")
215     public boolean preferLocal = true;
216 
217     /**
218      * Output file for the result of the plugin.
219      *
220      * @since 1.1.0
221      */
222     @Parameter(defaultValue = "${project.build.directory}/duplicate-finder-result.xml", property = "duplicate-finder.resultFile")
223     public File resultFile;
224 
225     /**
226      * Write result to output file.
227      *
228      * @since 1.1.0
229      */
230     @Parameter(defaultValue = "true", property = "duplicate-finder.useResultFile")
231     public boolean useResultFile = true;
232 
233     /**
234      * Minimum occurences on the class path to be listed in the result file.
235      *
236      * @since 1.1.0
237      */
238     @Parameter(defaultValue = "2", property = "duplicate-finder.resultFileMinClasspathCount")
239     public int resultFileMinClasspathCount = 2;
240 
241     /**
242      * Include the boot class path in duplicate detection. This will find duplicates with the JDK internal classes (e.g. the classes in rt.jar).
243      *
244      * @since 1.1.1
245      * @deprecated Inspecting the boot classpath is no longer supported in Java 9+
246      */
247     @Parameter(defaultValue = "false", property = "duplicate-finder.includeBootClasspath")
248     @Deprecated
249     public boolean includeBootClasspath = false;
250 
251     /**
252      * System property that contains the boot class path.
253      *
254      * @since 1.1.1
255      * @deprecated Inspecting the boot classpath is no longer supported in Java 9+
256      */
257     @Parameter(property = "duplicate-finder.bootClasspathProperty")
258     @Deprecated
259     public String bootClasspathProperty = null;
260 
261     /**
262      * Include POM projects in validation.
263      *
264      * @since 1.2.0
265      */
266     @Parameter(defaultValue = "false", property = "duplicate-finder.includePomProjects")
267     public boolean includePomProjects = false;
268 
269     private final EnumSet<ConflictState> printState = EnumSet.of(CONFLICT_CONTENT_DIFFERENT);
270     private final EnumSet<ConflictState> failState = EnumSet.noneOf(ConflictState.class);
271 
272     // called by maven
273     public void setIgnoredDependencies(final Dependency... dependencies) throws InvalidVersionSpecificationException {
274         checkArgument(dependencies != null);
275 
276         this.ignoredDependencies = new MavenCoordinates[dependencies.length];
277         for (int idx = 0; idx < dependencies.length; idx++) {
278             this.ignoredDependencies[idx] = new MavenCoordinates(dependencies[idx]);
279         }
280     }
281 
282     @Override
283     public void execute() throws MojoExecutionException, MojoFailureException {
284         if (skip) {
285             LOG.info("Skipping duplicate-finder execution!");
286         } else if (!includePomProjects && "pom".equals(project.getArtifact().getType())) {
287             LOG.info("Ignoring POM project!");
288         } else {
289             if (printEqualFiles) {
290                 printState.add(CONFLICT_CONTENT_EQUAL);
291             }
292 
293             if (failBuildInCaseOfConflict || failBuildInCaseOfEqualContentConflict) {
294                 printState.add(CONFLICT_CONTENT_EQUAL);
295 
296                 failState.add(CONFLICT_CONTENT_EQUAL);
297                 failState.add(CONFLICT_CONTENT_DIFFERENT);
298             }
299 
300             if (failBuildInCaseOfDifferentContentConflict) {
301                 failState.add(CONFLICT_CONTENT_DIFFERENT);
302             }
303 
304             if (includeBootClasspath) {
305                 LOG.warn("<includeBootClasspath> is no longer supported and will be ignored!");
306             }
307             if (bootClasspathProperty != null) {
308                 LOG.warn("<bootClasspathProperty> is no longer supported and will be ignored!");
309             }
310 
311             if (quiet) {
312                 LOG.warn("<quiet> is no longer supported and will be ignored!");
313             }
314 
315             try {
316                 // Prep conflicting dependencies
317                 MavenCoordinates projectCoordinates = new MavenCoordinates(project.getArtifact());
318 
319                 for (ConflictingDependency conflictingDependency : conflictingDependencies) {
320                     conflictingDependency.addProjectMavenCoordinates(projectCoordinates);
321                 }
322 
323                 final ArtifactFileResolver artifactFileResolver = new ArtifactFileResolver(project, preferLocal);
324                 final ImmutableMap.Builder<String, Entry<ResultCollector, ClasspathDescriptor>> classpathResultBuilder = ImmutableMap.builder();
325 
326                 if (checkCompileClasspath) {
327                     LOG.info("Checking compile classpath");
328                     final ResultCollector resultCollector = new ResultCollector(printState, failState);
329                     final ClasspathDescriptor classpathDescriptor = checkClasspath(resultCollector, artifactFileResolver, COMPILE_SCOPE,
330                             getOutputDirectory(project));
331                     classpathResultBuilder.put("compile", new SimpleImmutableEntry<>(resultCollector, classpathDescriptor));
332                 }
333 
334                 if (checkRuntimeClasspath) {
335                     LOG.info("Checking runtime classpath");
336                     final ResultCollector resultCollector = new ResultCollector(printState, failState);
337                     final ClasspathDescriptor classpathDescriptor = checkClasspath(resultCollector, artifactFileResolver, RUNTIME_SCOPE,
338                             getOutputDirectory(project));
339                     classpathResultBuilder.put("runtime", new SimpleImmutableEntry<>(resultCollector, classpathDescriptor));
340                 }
341 
342                 if (checkTestClasspath) {
343                     LOG.info("Checking test classpath");
344                     final ResultCollector resultCollector = new ResultCollector(printState, failState);
345                     final ClasspathDescriptor classpathDescriptor = checkClasspath(resultCollector,
346                             artifactFileResolver,
347                             TEST_SCOPE,
348                             getOutputDirectory(project),
349                             getTestOutputDirectory(project));
350                     classpathResultBuilder.put("test", new SimpleImmutableEntry<>(resultCollector, classpathDescriptor));
351                 }
352 
353                 final ImmutableMap<String, Entry<ResultCollector, ClasspathDescriptor>> classpathResults = classpathResultBuilder.build();
354 
355                 if (useResultFile) {
356                     checkState(resultFile != null, "resultFile must be set if useResultFile is true");
357                     writeResultFile(resultFile, classpathResults);
358                 }
359 
360                 boolean failed = false;
361 
362                 for (Map.Entry<String, Entry<ResultCollector, ClasspathDescriptor>> classpathEntry : classpathResults.entrySet()) {
363                     String classpathName = classpathEntry.getKey();
364                     ResultCollector resultCollector = classpathEntry.getValue().getKey();
365 
366                     for (final ConflictState state : printState) {
367                         for (final ConflictType type : ConflictType.values()) {
368                             if (resultCollector.hasConflictsFor(type, state)) {
369                                 final Map<String, Collection<ConflictResult>> results = resultCollector.getResults(type, state);
370                                 for (final Map.Entry<String, Collection<ConflictResult>> entry : results.entrySet()) {
371                                     final String artifactNames = entry.getKey();
372                                     final Collection<ConflictResult> conflictResults = entry.getValue();
373 
374                                     LOG.warn(format("Found duplicate %s %s in [%s]:", state.getHint(), type.getType(), artifactNames));
375                                     for (final ConflictResult conflictResult : conflictResults) {
376                                         LOG.warn(format("  %s", conflictResult.getName()));
377                                     }
378                                 }
379                             }
380                         }
381                     }
382 
383                     failed |= resultCollector.isFailed();
384 
385                     if (resultCollector.isFailed()) {
386                         LOG.warn(format("Found duplicate classes/resources in %s classpath.", classpathName));
387                     }
388                 }
389 
390                 if (failed) {
391                     throw new MojoExecutionException("Found duplicate classes/resources!");
392                 }
393             } catch (final DependencyResolutionRequiredException e) {
394                 throw new MojoFailureException("Could not resolve dependencies", e);
395             } catch (final InvalidVersionSpecificationException e) {
396                 throw new MojoFailureException("Invalid version specified", e);
397             } catch (final OverConstrainedVersionException e) {
398                 throw new MojoFailureException("Version too constrained", e);
399             } catch (final IOException e) {
400                 throw new MojoExecutionException("While loading artifacts", e);
401             }
402         }
403     }
404 
405     private ImmutableSet<String> getIgnoredResourcePatterns() {
406         ImmutableSet.Builder<String> builder = ImmutableSet.builder();
407         builder.add(ignoredResourcePatterns);
408 
409         return builder.build();
410     }
411 
412     private ImmutableSet<String> getIgnoredClassPatterns() {
413         ImmutableSet.Builder<String> builder = ImmutableSet.builder();
414         builder.add(ignoredClassPatterns);
415 
416         return builder.build();
417     }
418 
419     /**
420      * Checks the maven classpath for a given set of scopes whether it contains duplicates. In addition to the artifacts on the classpath, one or more
421      * additional project folders are added.
422      */
423     private ClasspathDescriptor checkClasspath(final ResultCollector resultCollector,
424             final ArtifactFileResolver artifactFileResolver,
425             final Set<String> scopes,
426             final File... projectFolders)
427             throws MojoExecutionException, InvalidVersionSpecificationException, OverConstrainedVersionException, DependencyResolutionRequiredException {
428 
429         // Map of files to artifacts. Depending on the type of build, referenced projects in a multi-module build
430         // may be local folders in the project instead of repo jar references.
431         final Multimap<File, Artifact> fileToArtifactMap = artifactFileResolver.resolveArtifactsForScopes(scopes);
432 
433         final ClasspathDescriptor classpathDescriptor = ClasspathDescriptor.createClasspathDescriptor(project,
434                 fileToArtifactMap,
435                 getIgnoredResourcePatterns(),
436                 getIgnoredClassPatterns(),
437                 Arrays.asList(ignoredDependencies),
438                 useDefaultResourceIgnoreList,
439                 useDefaultClassIgnoreList,
440                 projectFolders);
441 
442         // Now a scope specific classpath descriptor (scope relevant artifacts and project folders) and the global artifact resolver
443         // are primed. Run conflict resolution for classes and resources.
444         checkForDuplicates(CLASS, resultCollector, classpathDescriptor, artifactFileResolver);
445         checkForDuplicates(RESOURCE, resultCollector, classpathDescriptor, artifactFileResolver);
446         return classpathDescriptor;
447     }
448 
449     private void checkForDuplicates(final ConflictType type, final ResultCollector resultCollector, final ClasspathDescriptor classpathDescriptor,
450             final ArtifactFileResolver artifactFileResolver)
451             throws OverConstrainedVersionException {
452         // only look at entries with a size > 1.
453         final Map<String, Collection<File>> filteredMap = ImmutableMap.copyOf(Maps.filterEntries(classpathDescriptor.getClasspathElementLocations(type),
454                 entry -> {
455                     checkNotNull(entry, "entry is null");
456                     checkState(entry.getValue() != null, "Entry '%s' is invalid", entry);
457 
458                     return entry.getValue().size() > 1;
459                 }));
460 
461         for (final Map.Entry<String, Collection<File>> entry : filteredMap.entrySet()) {
462             final String name = entry.getKey();
463             final Collection<File> elements = entry.getValue();
464 
465             // Map which contains a printable name for the conflicting entry (which is either the printable name for an artifact or
466             // a folder name for a project folder) as keys and a classpath element as value.
467             final SortedSet<ClasspathElement> conflictingClasspathElements = artifactFileResolver.getClasspathElementsForElements(elements);
468 
469             ImmutableSet.Builder<Artifact> artifactBuilder = ImmutableSet.builder();
470 
471             for (ClasspathElement conflictingClasspathElement : conflictingClasspathElements) {
472                 if (conflictingClasspathElement.hasArtifact()) {
473                     artifactBuilder.add(conflictingClasspathElement.getArtifact());
474                 } else if (conflictingClasspathElement.isLocalFolder()) {
475                     artifactBuilder.add(project.getArtifact());
476                 }
477             }
478 
479             final boolean excepted = isExcepted(type, name, artifactBuilder.build());
480             final ConflictState conflictState = DuplicateFinderMojo.determineConflictState(type, name, elements);
481 
482             resultCollector.addConflict(type, name, conflictingClasspathElements, excepted, conflictState);
483         }
484     }
485 
486     /**
487      * Detects class/resource differences via SHA256 hash comparison.
488      */
489     private static ConflictState determineConflictState(final ConflictType type, final String name, final Iterable<File> elements) {
490         File firstFile = null;
491         String firstSHA256 = null;
492 
493         final String resourcePath = type == ConflictType.CLASS ? name.replace('.', '/') + ".class" : name;
494 
495         for (final File element : elements) {
496             try {
497                 final String newSHA256 = getSHA256HexOfElement(element, resourcePath);
498 
499                 if (firstSHA256 == null) {
500                     // save sha256 hash from the first element
501                     firstSHA256 = newSHA256;
502                     firstFile = element;
503                 } else if (!newSHA256.equals(firstSHA256)) {
504                     LOG.debug(format("Found different SHA256 hashes for elements %s in file %s and %s", resourcePath, firstFile, element));
505                     return ConflictState.CONFLICT_CONTENT_DIFFERENT;
506                 }
507             } catch (final IOException ex) {
508                 LOG.warn(format("Could not read content from file %s!", element), ex);
509             }
510         }
511 
512         return ConflictState.CONFLICT_CONTENT_EQUAL;
513     }
514 
515     /**
516      * Calculates the SHA256 Hash of a class in a file.
517      *
518      * @param file         the archive contains the class
519      * @param resourcePath the name of the class
520      * @return the MD% Hash as Hex-Value
521      * @throws IOException if any error occurs on reading class in archive
522      */
523     private static String getSHA256HexOfElement(final File file, final String resourcePath) throws IOException {
524 
525         try (Closer closer = Closer.create()) {
526             InputStream in;
527 
528             if (file.isDirectory()) {
529                 final File resourceFile = new File(file, resourcePath);
530                 in = closer.register(new BufferedInputStream(Files.newInputStream(resourceFile.toPath())));
531             } else {
532                 final ZipFile zip = new ZipFile(file);
533 
534                 closer.register(zip::close);
535 
536                 final ZipEntry zipEntry = zip.getEntry(resourcePath);
537 
538                 if (zipEntry == null) {
539                     throw new IOException(format("Could not find %s in archive %s", resourcePath, file));
540                 }
541 
542                 in = zip.getInputStream(zipEntry);
543             }
544 
545             return SHA_256.newHasher().putBytes(ByteStreams.toByteArray(in)).hash().toString();
546         }
547     }
548 
549     private boolean isExcepted(final ConflictType type, final String name, final Set<Artifact> artifacts)
550             throws OverConstrainedVersionException {
551         final ImmutableSet.Builder<ConflictingDependency> conflictBuilder = ImmutableSet.builder();
552         checkState(conflictingDependencies != null, "conflictingDependencies is null");
553 
554         // Find all exception definitions from the configuration that match these artifacts.
555         for (final ConflictingDependency conflictingDependency : conflictingDependencies) {
556             if (conflictingDependency.isForArtifacts(artifacts)) {
557                 conflictBuilder.add(conflictingDependency);
558             }
559         }
560 
561         // If any of the possible candidates covers this class or resource, then the conflict is excepted.
562         for (final ConflictingDependency conflictingDependency : conflictBuilder.build()) {
563             if (type == ConflictType.CLASS && conflictingDependency.containsClass(name)) {
564                 return true;
565             } else if (type == ConflictType.RESOURCE && conflictingDependency.containsResource(name)) {
566                 return true;
567             }
568         }
569         return false;
570     }
571 
572     private void writeResultFile(File resultFile, ImmutableMap<String, Entry<ResultCollector, ClasspathDescriptor>> results)
573             throws MojoExecutionException, InvalidVersionSpecificationException, OverConstrainedVersionException {
574         File parent = resultFile.getParentFile();
575         if (!parent.exists()) {
576             if (!parent.mkdirs()) {
577                 throw new MojoExecutionException("Could not create parent folders for " + parent.getAbsolutePath());
578             }
579         }
580         if (!parent.isDirectory() || !parent.canWrite()) {
581             throw new MojoExecutionException("Can not create result file in " + parent.getAbsolutePath());
582         }
583 
584         try {
585             SMOutputFactory factory = new SMOutputFactory(XMLOutputFactory2.newFactory());
586             SMOutputDocument resultDocument = factory.createOutputDocument(resultFile);
587             resultDocument.setIndentation("\n" + Strings.repeat(" ", 64), 1, 4);
588 
589             SMOutputElement rootElement = resultDocument.addElement("duplicate-finder-result");
590             XMLWriterUtils.addAttribute(rootElement, "version", SAVE_FILE_VERSION);
591 
592             XMLWriterUtils.addProjectInformation(rootElement, project);
593 
594             addConfiguration(rootElement);
595 
596             SMOutputElement resultsElement = rootElement.addElement("results");
597             for (Map.Entry<String, Entry<ResultCollector, ClasspathDescriptor>> entry : results.entrySet()) {
598                 SMOutputElement resultElement = resultsElement.addElement("result");
599                 XMLWriterUtils.addAttribute(resultElement, "name", entry.getKey());
600                 XMLWriterUtils.addResultCollector(resultElement, entry.getValue().getKey());
601                 XMLWriterUtils.addClasspathDescriptor(resultElement, resultFileMinClasspathCount, entry.getValue().getValue());
602             }
603 
604             resultDocument.closeRootAndWriter();
605         } catch (XMLStreamException e) {
606             throw new MojoExecutionException("While writing result file", e);
607         }
608     }
609 
610     private void addConfiguration(SMOutputElement rootElement)
611             throws XMLStreamException {
612         SMOutputElement prefs = XMLWriterUtils.addElement(rootElement, "configuration", null);
613         // Simple configuration options
614         XMLWriterUtils.addAttribute(prefs, "skip", skip);
615         XMLWriterUtils.addAttribute(prefs, "checkCompileClasspath", checkCompileClasspath);
616         XMLWriterUtils.addAttribute(prefs, "checkRuntimeClasspath", checkRuntimeClasspath);
617         XMLWriterUtils.addAttribute(prefs, "checkTestClasspath", checkTestClasspath);
618         XMLWriterUtils.addAttribute(prefs, "failBuildInCaseOfDifferentContentConflict", failBuildInCaseOfDifferentContentConflict);
619         XMLWriterUtils.addAttribute(prefs, "failBuildInCaseOfEqualContentConflict", failBuildInCaseOfEqualContentConflict);
620         XMLWriterUtils.addAttribute(prefs, "failBuildInCaseOfConflict", failBuildInCaseOfConflict);
621         XMLWriterUtils.addAttribute(prefs, "printEqualFiles", printEqualFiles);
622         XMLWriterUtils.addAttribute(prefs, "preferLocal", preferLocal);
623         XMLWriterUtils.addAttribute(prefs, "includePomProjects", includePomProjects);
624         // Ignoring Dependencies and resources
625         XMLWriterUtils.addAttribute(prefs, "useDefaultResourceIgnoreList", useDefaultResourceIgnoreList);
626         XMLWriterUtils.addAttribute(prefs, "useDefaultClassIgnoreList", useDefaultClassIgnoreList);
627         // Result file options
628         XMLWriterUtils.addAttribute(prefs, "useResultFile", useResultFile);
629         XMLWriterUtils.addAttribute(prefs, "resultFileMinClasspathCount", resultFileMinClasspathCount);
630         XMLWriterUtils.addAttribute(prefs, "resultFile", resultFile.getAbsolutePath());
631 
632         SMOutputElement ignoredResourcesElement = prefs.addElement("ignoredResourcePatterns");
633         for (String ignoredResource : getIgnoredResourcePatterns()) {
634             XMLWriterUtils.addElement(ignoredResourcesElement, "ignoredResourcePattern", ignoredResource);
635         }
636 
637         SMOutputElement ignoredClassElement = prefs.addElement("ignoredClassPatterns");
638         for (String ignoredClass : getIgnoredClassPatterns()) {
639             XMLWriterUtils.addElement(ignoredClassElement, "ignoredClassPattern", ignoredClass);
640         }
641 
642         SMOutputElement conflictingDependenciesElement = prefs.addElement("conflictingDependencies");
643         for (ConflictingDependency conflictingDependency : conflictingDependencies) {
644             XMLWriterUtils.addConflictingDependency(conflictingDependenciesElement, "conflictingDependency", conflictingDependency);
645         }
646 
647         SMOutputElement ignoredDependenciesElement = prefs.addElement("ignoredDependencies");
648         for (MavenCoordinates ignoredDependency : ignoredDependencies) {
649             XMLWriterUtils.addMavenCoordinate(ignoredDependenciesElement, "dependency", ignoredDependency);
650         }
651     }
652 }
653