com.ning.maven.plugins.duplicatefinder.DuplicateFinderMojo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of maven-duplicate-finder-plugin Show documentation
Show all versions of maven-duplicate-finder-plugin Show documentation
The maven-duplicate-class-finder-plugin is a plugin that will search for classes with the same name, as well as resources with the same path,
in the classpaths of a maven project. More specifically, it will check the compile, runtime, and test classpaths for
* Classes with the same qualified name in the current project and all dependencies relevant for that classpath
* Files that are not class files, with the same resource path (i.e. as if it would be accessed via the classloader) in the current project and all dependencies relevant for that
(Note that at the moment, the plugin does not check if the files are actually the same or not, it only looks for the same file/class name.)
/*
* Copyright 2010 Ning, Inc.
*
* Ning licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.ning.maven.plugins.duplicatefinder;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.pyx4j.log4j.MavenLogAppender;
/**
* Finds duplicate classes/resources.
*
* @goal check
* @phase verify
* @requiresDependencyResolution test
* @see Mojo Developer Cookbook
* @author Ning, Inc.
* @author kreyssel
*/
public class DuplicateFinderMojo extends AbstractMojo
{
protected final Logger LOG = LoggerFactory.getLogger(this.getClass());
// the constants for conflicts
private final static int NO_CONFLICT = 0;
private final static int CONFLICT_CONTENT_EQUAL = 1;
private final static int CONFLICT_CONTENT_DIFFERENT = 2;
/**
* The maven project (effective pom).
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* Whether the mojo should print files that are the same as per sha256 from the output.
* @parameter default-value="false"
* @since 1.0.6
*/
private boolean printEqualFiles = false;
/**
* Whether the mojo should fail the build if a conflict with content different elements was found.
* @parameter default-value="false"
* @since 1.0.3
*/
private boolean failBuildInCaseOfDifferentContentConflict;
/**
* Whether the mojo should fail the build if a conflict with content equal elements was found.
* @parameter default-value="false"
* @since 1.0.3
*/
private boolean failBuildInCaseOfEqualContentConflict;
/**
* Whether the mojo should fail the build if a conflict was found.
* @parameter default-value="false"
*/
private boolean failBuildInCaseOfConflict;
/**
* Whether the mojo should use the default resource ignore list.
* @parameter default-value="true"
*/
private boolean useDefaultResourceIgnoreList = true;
/**
* Additional resources that should be ignored.
* @parameter alias="ignoredResources"
*/
private String [] ignoredResources;
/**
* A set of artifacts with expected and resolved versions that are to be except from the check.
* @parameter alias="exceptions"
*/
private Exception[] exceptions;
/**
* A set of dependecies that should be completely ignored in the check.
* @parameter property="ignoredDependencies"
*/
private DependencyWrapper[] ignoredDependencies;
/**
* Check the compile classpath. On by default.
* @parameter default-value="true"
*/
private boolean checkCompileClasspath = true;
/**
* Check the runtime classpath. On by default.
* @parameter default-value="true"
*/
private boolean checkRuntimeClasspath = true;
/**
* Check the test classpath. On by default.
* @parameter default-value="true"
*/
private boolean checkTestClasspath = true;
/**
* Skip the plugin execution.
*
*
*
* true
*
*
*
* @parameter default-value="false"
*/
protected boolean skip = false;
public void setIgnoredDependencies(Dependency[] ignoredDependencies) throws InvalidVersionSpecificationException
{
this.ignoredDependencies = new DependencyWrapper[ignoredDependencies.length];
for (int idx = 0; idx < ignoredDependencies.length; idx++) {
this.ignoredDependencies[idx] = new DependencyWrapper(ignoredDependencies[idx]);
}
}
public void execute() throws MojoExecutionException
{
MavenLogAppender.startPluginLog(this);
try {
if (skip) {
LOG.debug("Skipping execution!");
}
else {
if (checkCompileClasspath) {
checkCompileClasspath();
}
if (checkRuntimeClasspath) {
checkRuntimeClasspath();
}
if (checkTestClasspath) {
checkTestClasspath();
}
}
}
finally {
MavenLogAppender.endPluginLog(this);
}
}
private void checkCompileClasspath() throws MojoExecutionException
{
try {
LOG.info("Checking compile classpath");
Map artifactsByFile = createArtifactsByFileMap(project.getCompileArtifacts());
addOutputDirectory(artifactsByFile);
checkClasspath(project.getCompileClasspathElements(), artifactsByFile);
}
catch (DependencyResolutionRequiredException ex) {
throw new MojoExecutionException("Could not resolve dependencies", ex);
}
}
private void checkRuntimeClasspath() throws MojoExecutionException
{
try {
LOG.info("Checking runtime classpath");
Map artifactsByFile = createArtifactsByFileMap(project.getRuntimeArtifacts());
addOutputDirectory(artifactsByFile);
checkClasspath(project.getRuntimeClasspathElements(), artifactsByFile);
}
catch (DependencyResolutionRequiredException ex) {
throw new MojoExecutionException("Could not resolve dependencies", ex);
}
}
private void checkTestClasspath() throws MojoExecutionException
{
try {
LOG.info("Checking test classpath");
Map artifactsByFile = createArtifactsByFileMap(project.getTestArtifacts());
addOutputDirectory(artifactsByFile);
addTestOutputDirectory(artifactsByFile);
checkClasspath(project.getTestClasspathElements(), artifactsByFile);
}
catch (DependencyResolutionRequiredException ex) {
throw new MojoExecutionException("Could not resolve dependencies", ex);
}
}
private void checkClasspath(List classpathElements, Map artifactsByFile) throws MojoExecutionException
{
ClasspathDescriptor classpathDesc = createClasspathDescriptor(classpathElements);
int foundDuplicateClassesConflict = checkForDuplicateClasses(classpathDesc, artifactsByFile);
int foundDuplicateResourcesConflict = checkForDuplicateResources(classpathDesc, artifactsByFile);
int maxConflict = Math.max(foundDuplicateClassesConflict, foundDuplicateResourcesConflict);
if ( (failBuildInCaseOfConflict && maxConflict > NO_CONFLICT) ||
(failBuildInCaseOfDifferentContentConflict && maxConflict == CONFLICT_CONTENT_DIFFERENT) ||
(failBuildInCaseOfEqualContentConflict && maxConflict >= CONFLICT_CONTENT_EQUAL) ) {
throw new MojoExecutionException("Found duplicate classes/resources");
}
}
private int checkForDuplicateClasses(ClasspathDescriptor classpathDesc, Map artifactsByFile) throws MojoExecutionException
{
Map classDifferentConflictsByArtifactNames = new TreeMap(new ToStringComparator());
Map classEqualConflictsByArtifactNames = new TreeMap(new ToStringComparator());
for (Iterator classNameIt = classpathDesc.getClasss().iterator(); classNameIt.hasNext();) {
String className = (String)classNameIt.next();
Set elements = classpathDesc.getElementsHavingClass(className);
if (elements.size() > 1) {
Set artifacts = getArtifactsForElements(elements, artifactsByFile);
filterIgnoredDependencies(artifacts);
if ((artifacts.size() < 2) || isExceptedClass(className, artifacts)) {
continue;
}
Map conflictsByArtifactNames;
if (isAllElementsAreEqual(elements, className.replace('.', '/') + ".class"))
{
conflictsByArtifactNames = classEqualConflictsByArtifactNames;
}
else {
conflictsByArtifactNames = classDifferentConflictsByArtifactNames;
}
String artifactNames = getArtifactsToString(artifacts);
List classNames = (List)conflictsByArtifactNames.get(artifactNames);
if (classNames == null) {
classNames = new ArrayList();
conflictsByArtifactNames.put(artifactNames, classNames);
}
classNames.add(className);
}
}
int conflict = NO_CONFLICT;
if (!classEqualConflictsByArtifactNames.isEmpty()) {
if (printEqualFiles ||
failBuildInCaseOfConflict ||
failBuildInCaseOfEqualContentConflict) {
printWarningMessage(classEqualConflictsByArtifactNames, "(but equal)", "classes");
}
conflict = CONFLICT_CONTENT_EQUAL;
}
if (!classDifferentConflictsByArtifactNames.isEmpty()) {
printWarningMessage(classDifferentConflictsByArtifactNames, "and different", "classes");
conflict = CONFLICT_CONTENT_DIFFERENT;
}
return conflict;
}
private int checkForDuplicateResources(ClasspathDescriptor classpathDesc, Map artifactsByFile) throws MojoExecutionException
{
Map resourceDifferentConflictsByArtifactNames = new TreeMap(new ToStringComparator());
Map resourceEqualConflictsByArtifactNames = new TreeMap(new ToStringComparator());
for (Iterator resourceIt = classpathDesc.getResources().iterator(); resourceIt.hasNext();) {
String resource = (String)resourceIt.next();
Set elements = classpathDesc.getElementsHavingResource(resource);
if (elements.size() > 1) {
Set artifacts = getArtifactsForElements(elements, artifactsByFile);
filterIgnoredDependencies(artifacts);
if ((artifacts.size() < 2) || isExceptedResource(resource, artifacts)) {
continue;
}
Map conflictsByArtifactNames;
if (isAllElementsAreEqual(elements, resource)) {
conflictsByArtifactNames = resourceEqualConflictsByArtifactNames;
}
else {
conflictsByArtifactNames = resourceDifferentConflictsByArtifactNames;
}
String artifactNames = getArtifactsToString(artifacts);
List resources = (List)conflictsByArtifactNames.get(artifactNames);
if (resources == null) {
resources = new ArrayList();
conflictsByArtifactNames.put(artifactNames, resources);
}
resources.add(resource);
}
}
int conflict = NO_CONFLICT;
if (!resourceEqualConflictsByArtifactNames.isEmpty()) {
if (printEqualFiles ||
failBuildInCaseOfConflict ||
failBuildInCaseOfEqualContentConflict) {
printWarningMessage(resourceEqualConflictsByArtifactNames, "(but equal)", "resources");
}
conflict = CONFLICT_CONTENT_EQUAL;
}
if (!resourceDifferentConflictsByArtifactNames.isEmpty()) {
printWarningMessage(resourceDifferentConflictsByArtifactNames, "and different", "resources");
conflict = CONFLICT_CONTENT_DIFFERENT;
}
return conflict;
}
/**
* Prints the conflict messages.
*
* @param conflictsByArtifactNames the Map of conflicts (Artifactnames, List of classes)
* @param hint hint with the type of the conflict ("all equal" or "content different")
* @param type type of conflict (class or resource)
*/
private void printWarningMessage(Map conflictsByArtifactNames, String hint, String type)
{
for (Iterator conflictIt = conflictsByArtifactNames.entrySet().iterator(); conflictIt.hasNext();) {
Map.Entry entry = (Map.Entry)conflictIt.next();
String artifactNames = (String)entry.getKey();
List classNames = (List)entry.getValue();
LOG.warn("Found duplicate " + hint + " " + type + " in " + artifactNames + " :");
for (Iterator classNameIt = classNames.iterator(); classNameIt.hasNext();) {
LOG.warn(" " + classNameIt.next());
}
}
}
/**
* Detects class/resource differences via SHA256 hash comparsion.
*
* @param resourcePath the class or resource path that has duplicates in classpath
* @param elements the files contains the duplicates
* @return true if all classes are "byte equal" and false if any class differ
*/
private boolean isAllElementsAreEqual(final Set elements, final String resourcePath)
{
File firstFile = null;
String firstSHA256 = null;
for (Iterator it = elements.iterator(); it.hasNext();)
{
File file = (File)it.next();
try {
String newSHA256 = getSHA256HexOfElement(file, resourcePath);
if (firstSHA256 == null) {
// save sha256 hash from the first element
firstSHA256 = newSHA256;
firstFile = file;
}
else if (!newSHA256.equals(firstSHA256)) {
LOG.debug("Found different SHA256 hashs for elements " + resourcePath + " in file " + firstFile + " and " + file);
return false;
}
}
catch (IOException ex) {
LOG.warn("Could not read content from file " + file + "!", ex);
}
}
return true;
}
/**
* Calculates the SHA256 Hash of a class in a file.
*
* @param file the archive contains the class
* @param resourcePath the name of the class
* @return the MD% Hash as Hex-Value
* @throws IOException if any error occurs on reading class in archive
*/
private String getSHA256HexOfElement(final File file, final String resourcePath) throws IOException
{
ZipFile zip = null;
InputStream in;
if (file.isDirectory()) {
File resourceFile = new File(file, resourcePath);
in = new BufferedInputStream(new FileInputStream(resourceFile));
}
else {
zip = new ZipFile(file);
ZipEntry zipEntry = zip.getEntry(resourcePath);
if (zipEntry == null) {
throw new IOException("Could not find " + resourcePath + " in archive " + file);
}
in = zip.getInputStream(zipEntry);
}
try {
return DigestUtils.sha256Hex(in);
}
finally {
IOUtils.closeQuietly(in);
if (zip != null) {
try {
zip.close();
} catch (IOException ioe) {
// swallow exception
}
}
}
}
private void filterIgnoredDependencies(final Set artifacts)
{
if (ignoredDependencies != null) {
for (int idx = 0; idx < ignoredDependencies.length; idx++) {
for (Iterator artifactIt = artifacts.iterator(); artifactIt.hasNext();) {
Artifact artifact = (Artifact)artifactIt.next();
if (ignoredDependencies[idx].matches(artifact)) {
artifactIt.remove();
}
}
}
}
}
private boolean isExceptedClass(final String className, final Collection artifacts)
{
List exceptions = getExceptionsFor(artifacts);
for (Iterator it = exceptions.iterator(); it.hasNext();) {
Exception exception = (Exception)it.next();
if (exception.containsClass(className)) {
return true;
}
}
return false;
}
private boolean isExceptedResource(String resource, Collection artifacts)
{
List exceptions = getExceptionsFor(artifacts);
for (Iterator it = exceptions.iterator(); it.hasNext();) {
Exception exception = (Exception)it.next();
if (exception.containsResource(resource)) {
return true;
}
}
return false;
}
private List getExceptionsFor(Collection artifacts)
{
List result = new ArrayList();
if (exceptions != null) {
for (int idx = 0; idx < exceptions.length; idx++) {
if (exceptions[idx].isForArtifacts(artifacts, project.getArtifact())) {
result.add(exceptions[idx]);
}
}
}
return result;
}
private Set getArtifactsForElements(Collection elements, Map artifactsByFile)
{
Set artifacts = new TreeSet();
for (Iterator elementUrlIt = elements.iterator(); elementUrlIt.hasNext();) {
File element = (File)elementUrlIt.next();
Artifact artifact = (Artifact)artifactsByFile.get(element);
if (artifact == null) {
artifact = project.getArtifact();
}
artifacts.add(artifact);
}
return artifacts;
}
private String getArtifactsToString(Collection artifacts)
{
StringBuffer result = new StringBuffer();
result.append("[");
for (Iterator it = artifacts.iterator(); it.hasNext();) {
if (result.length() > 1) {
result.append(",");
}
result.append(getQualifiedName((Artifact)it.next()));
}
result.append("]");
return result.toString();
}
private ClasspathDescriptor createClasspathDescriptor(List classpathElements) throws MojoExecutionException
{
ClasspathDescriptor classpathDesc = new ClasspathDescriptor();
classpathDesc.setUseDefaultResourceIgnoreList(useDefaultResourceIgnoreList);
classpathDesc.setIgnoredResources(ignoredResources);
for (Iterator elementIt = classpathElements.iterator(); elementIt.hasNext();) {
String element = (String)elementIt.next();
try {
classpathDesc.add(new File(element));
}
catch (FileNotFoundException ex) {
LOG.debug("Could not access classpath element " + element);
}
catch (IOException ex) {
throw new MojoExecutionException("Error trying to access element " + element, ex);
}
}
return classpathDesc;
}
private Map createArtifactsByFileMap(List artifacts) throws DependencyResolutionRequiredException
{
Map artifactsByFile = new HashMap(artifacts.size());
for (Iterator artifactIt = artifacts.iterator(); artifactIt.hasNext();) {
Artifact artifact = (Artifact)artifactIt.next();
File localPath = getLocalProjectPath(artifact);
File repoPath = artifact.getFile();
if ((localPath == null) && (repoPath == null)) {
throw new DependencyResolutionRequiredException(artifact);
}
if (localPath != null) {
artifactsByFile.put(localPath, artifact);
}
if (repoPath != null) {
artifactsByFile.put(repoPath, artifact);
}
}
return artifactsByFile;
}
private File getLocalProjectPath(Artifact artifact) throws DependencyResolutionRequiredException
{
String refId = artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion();
MavenProject owningProject = (MavenProject)project.getProjectReferences().get(refId);
if (owningProject != null) {
if (artifact.getType().equals("test-jar")) {
File testOutputDir = new File(owningProject.getBuild().getTestOutputDirectory());
if (testOutputDir.exists()) {
return testOutputDir;
}
}
else {
return new File(project.getBuild().getOutputDirectory());
}
}
return null;
}
private void addOutputDirectory(Map artifactsByFile)
{
File outputDir = new File(project.getBuild().getOutputDirectory());
if (outputDir.exists()) {
artifactsByFile.put(outputDir, null);
}
}
private void addTestOutputDirectory(Map artifactsByFile)
{
File outputDir = new File(project.getBuild().getOutputDirectory());
if (outputDir.exists()) {
artifactsByFile.put(outputDir, null);
}
}
private String getQualifiedName(Artifact artifact)
{
String result = artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion();
if ((artifact.getType() != null) && !"jar".equals(artifact.getType())) {
result = result + ":" + artifact.getType();
}
if ((artifact.getClassifier() != null) && (!"tests".equals(artifact.getClassifier()) || !"test-jar".equals(artifact.getType()))) {
result = result + ":" + artifact.getClassifier();
}
return result;
}
}