From 764ba34805489d2f20f0f2ed64a15b3e4ee209bf Mon Sep 17 00:00:00 2001
From: Brad Cupit <106213+bradcupit@users.noreply.github.com>
Date: Fri, 15 Jun 2018 06:58:19 -0400
Subject: [PATCH] let BanDuplicateClasses ignore when the bytecode matches
exactly (#50)
Fixed #54 - let ban duplicate classes ignore when the bytecode matches exactly
* fix checkstyle warnings and errors
* update documentation with new BanDuplicateClasses option
---
pom.xml | 5 +
.../invoker.properties | 2 +
.../pom.xml | 64 +++++
.../verify.groovy | 6 +
.../invoker.properties | 2 +
.../pom.xml | 63 ++++
.../verify.groovy | 6 +
.../plugins/enforcer/BanDuplicateClasses.java | 135 ++++-----
.../maven/plugins/enforcer/ClassFile.java | 92 ++++++
.../plugins/enforcer/ClassesWithSameName.java | 248 ++++++++++++++++
.../apache/maven/plugins/enforcer/Hasher.java | 139 +++++++++
.../maven/plugins/enforcer/JarUtils.java | 38 +++
src/site/apt/banDuplicateClasses.apt.vm | 3 +
.../plugins/enforcer/ArtifactBuilder.java | 98 +++++++
.../plugins/enforcer/ClassFileHelper.java | 126 ++++++++
.../maven/plugins/enforcer/ClassFileTest.java | 38 +++
.../enforcer/ClassesWithSameNameTest.java | 270 ++++++++++++++++++
.../maven/plugins/enforcer/HasherTest.java | 53 ++++
.../maven/plugins/enforcer/JarUtilsTest.java | 46 +++
19 files changed, 1369 insertions(+), 65 deletions(-)
create mode 100644 src/it/banduplicate-classes-fail-when-not-identical/invoker.properties
create mode 100644 src/it/banduplicate-classes-fail-when-not-identical/pom.xml
create mode 100644 src/it/banduplicate-classes-fail-when-not-identical/verify.groovy
create mode 100644 src/it/banduplicate-classes-ignore-when-identical/invoker.properties
create mode 100644 src/it/banduplicate-classes-ignore-when-identical/pom.xml
create mode 100644 src/it/banduplicate-classes-ignore-when-identical/verify.groovy
create mode 100644 src/main/java/org/apache/maven/plugins/enforcer/ClassFile.java
create mode 100644 src/main/java/org/apache/maven/plugins/enforcer/ClassesWithSameName.java
create mode 100644 src/main/java/org/apache/maven/plugins/enforcer/Hasher.java
create mode 100644 src/main/java/org/apache/maven/plugins/enforcer/JarUtils.java
create mode 100644 src/test/java/org/apache/maven/plugins/enforcer/ArtifactBuilder.java
create mode 100644 src/test/java/org/apache/maven/plugins/enforcer/ClassFileHelper.java
create mode 100644 src/test/java/org/apache/maven/plugins/enforcer/ClassFileTest.java
create mode 100644 src/test/java/org/apache/maven/plugins/enforcer/ClassesWithSameNameTest.java
create mode 100644 src/test/java/org/apache/maven/plugins/enforcer/HasherTest.java
create mode 100644 src/test/java/org/apache/maven/plugins/enforcer/JarUtilsTest.java
diff --git a/pom.xml b/pom.xml
index e37163af..d629ff48 100644
--- a/pom.xml
+++ b/pom.xml
@@ -117,6 +117,11 @@
plexus-container-default
1.0-alpha-9
+
+ commons-codec
+ commons-codec
+ 1.11
+
junit
junit
diff --git a/src/it/banduplicate-classes-fail-when-not-identical/invoker.properties b/src/it/banduplicate-classes-fail-when-not-identical/invoker.properties
new file mode 100644
index 00000000..38b185e5
--- /dev/null
+++ b/src/it/banduplicate-classes-fail-when-not-identical/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = enforcer:enforce
+invoker.buildResult = failure
diff --git a/src/it/banduplicate-classes-fail-when-not-identical/pom.xml b/src/it/banduplicate-classes-fail-when-not-identical/pom.xml
new file mode 100644
index 00000000..b539c5b0
--- /dev/null
+++ b/src/it/banduplicate-classes-fail-when-not-identical/pom.xml
@@ -0,0 +1,64 @@
+
+ 4.0.0
+
+ org.apache.maven
+ banduplicate-classes-ignore-when-identical
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+
+
+
+
+
+ maven-enforcer-plugin
+ @enforcerPluginVersion@
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+
+
+
+
+
+ true
+
+
+
+
+
+ compile
+
+
+
+
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.9.0
+ runtime
+
+
+
+ commons-logging
+ commons-logging
+ 1.2
+
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+ 1.7.25
+
+
+
+
diff --git a/src/it/banduplicate-classes-fail-when-not-identical/verify.groovy b/src/it/banduplicate-classes-fail-when-not-identical/verify.groovy
new file mode 100644
index 00000000..818608de
--- /dev/null
+++ b/src/it/banduplicate-classes-fail-when-not-identical/verify.groovy
@@ -0,0 +1,6 @@
+File file = new File( basedir, "build.log" )
+assert file.exists()
+
+String text = file.getText( "utf-8" )
+
+return true
diff --git a/src/it/banduplicate-classes-ignore-when-identical/invoker.properties b/src/it/banduplicate-classes-ignore-when-identical/invoker.properties
new file mode 100644
index 00000000..a30e3767
--- /dev/null
+++ b/src/it/banduplicate-classes-ignore-when-identical/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = enforcer:enforce
+invoker.buildResult = success
diff --git a/src/it/banduplicate-classes-ignore-when-identical/pom.xml b/src/it/banduplicate-classes-ignore-when-identical/pom.xml
new file mode 100644
index 00000000..bb21c91b
--- /dev/null
+++ b/src/it/banduplicate-classes-ignore-when-identical/pom.xml
@@ -0,0 +1,63 @@
+
+ 4.0.0
+
+ org.apache.maven
+ banduplicate-classes-ignore-when-identical
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+
+
+
+
+
+ maven-enforcer-plugin
+ @enforcerPluginVersion@
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+ compile
+
+
+
+
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.9.0
+ runtime
+
+
+
+ org.mockito
+ mockito-core
+ 1.10.19
+
+
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+
+
+
+
diff --git a/src/it/banduplicate-classes-ignore-when-identical/verify.groovy b/src/it/banduplicate-classes-ignore-when-identical/verify.groovy
new file mode 100644
index 00000000..818608de
--- /dev/null
+++ b/src/it/banduplicate-classes-ignore-when-identical/verify.groovy
@@ -0,0 +1,6 @@
+File file = new File( basedir, "build.log" )
+assert file.exists()
+
+String text = file.getText( "utf-8" )
+
+return true
diff --git a/src/main/java/org/apache/maven/plugins/enforcer/BanDuplicateClasses.java b/src/main/java/org/apache/maven/plugins/enforcer/BanDuplicateClasses.java
index 5f29b34f..c1cb0238 100644
--- a/src/main/java/org/apache/maven/plugins/enforcer/BanDuplicateClasses.java
+++ b/src/main/java/org/apache/maven/plugins/enforcer/BanDuplicateClasses.java
@@ -25,7 +25,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.LinkedHashSet;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -38,6 +38,8 @@
import org.codehaus.mojo.enforcer.Dependency;
import org.codehaus.plexus.util.FileUtils;
+import static org.apache.maven.plugins.enforcer.JarUtils.isJarFile;
+
/**
* Bans duplicate classes on the classpath.
*/
@@ -51,7 +53,7 @@ public class BanDuplicateClasses
* also contained several times.
*/
private static final String[] DEFAULT_CLASSES_IGNORES = { "module-info", "META-INF/versions/*/module-info" };
-
+
/**
* The failure message
*/
@@ -75,6 +77,12 @@ public class BanDuplicateClasses
*/
private List scopes;
+ /**
+ * If {@code true} do not fail the build when duplicate classes exactly match each other. In other words, ignore
+ * duplication if the bytecode in the class files match. Default is {@code false}.
+ */
+ private boolean ignoreWhenIdentical;
+
@Override
protected void handleArtifacts( Set artifacts ) throws EnforcerRuleException
{
@@ -114,9 +122,9 @@ protected void handleArtifacts( Set artifacts ) throws EnforcerRuleExc
ignorableDependencies.add( ignorableDependency );
}
}
-
- Map classNames = new HashMap();
- Map> duplicates = new HashMap>();
+
+ Map classesSeen = new HashMap();
+ Set duplicateClassNames = new HashSet();
for ( Artifact o : artifacts )
{
if( scopes != null && !scopes.contains( o.getScope() ) )
@@ -140,7 +148,7 @@ else if ( file.isDirectory() )
for ( String name : FileUtils.getFileNames( file, null, null, false ) )
{
getLog().debug( " " + name );
- checkAndAddName( o, name, classNames, duplicates, ignorableDependencies );
+ checkAndAddName( o, name, classesSeen, duplicateClassNames, ignorableDependencies );
}
}
catch ( IOException e )
@@ -149,7 +157,7 @@ else if ( file.isDirectory() )
"Unable to process dependency " + o.toString() + " due to " + e.getLocalizedMessage(), e );
}
}
- else if ( file.isFile() && "jar".equals( o.getType() ) )
+ else if ( isJarFile( o ) )
{
try
{
@@ -159,7 +167,8 @@ else if ( file.isFile() && "jar".equals( o.getType() ) )
{
for ( JarEntry entry : Collections.list( jar.entries() ) )
{
- checkAndAddName( o, entry.getName(), classNames, duplicates, ignorableDependencies );
+ String fileName = entry.getName();
+ checkAndAddName( o, fileName, classesSeen, duplicateClassNames, ignorableDependencies );
}
}
finally
@@ -181,18 +190,21 @@ else if ( file.isFile() && "jar".equals( o.getType() ) )
}
}
}
- if ( !duplicates.isEmpty() )
+ if ( !duplicateClassNames.isEmpty() )
{
Map, List> inverted = new HashMap, List>();
- for ( Map.Entry> entry : duplicates.entrySet() )
+ for ( String className : duplicateClassNames )
{
- List s = inverted.get( entry.getValue() );
+ ClassesWithSameName classesWithSameName = classesSeen.get( className );
+ Set artifactsOfDuplicateClass = classesWithSameName.getAllArtifactsThisClassWasFoundIn();
+
+ List s = inverted.get( artifactsOfDuplicateClass );
if ( s == null )
{
s = new ArrayList();
}
- s.add( entry.getKey() );
- inverted.put( entry.getValue(), s );
+ s.add( classesWithSameName.toOutputString( ignoreWhenIdentical ) );
+ inverted.put( artifactsOfDuplicateClass, s );
}
StringBuilder buf = new StringBuilder( message == null ? "Duplicate classes found:" : message );
buf.append( '\n' );
@@ -205,10 +217,10 @@ else if ( file.isFile() && "jar".equals( o.getType() ) )
buf.append( a );
}
buf.append( "\n Duplicate classes:" );
- for ( String className : entry.getValue() )
+ for ( String classNameWithDuplicationInfo : entry.getValue() )
{
buf.append( "\n " );
- buf.append( className );
+ buf.append( classNameWithDuplicationInfo );
}
buf.append( '\n' );
}
@@ -217,73 +229,66 @@ else if ( file.isFile() && "jar".equals( o.getType() ) )
}
- private void checkAndAddName( Artifact artifact, String name, Map classNames,
- Map> duplicates, Collection ignores )
+ private void checkAndAddName( Artifact artifact, String pathToClassFile, Map classesSeen, Set duplicateClasses,
+ Collection ignores )
throws EnforcerRuleException
{
- if ( !name.endsWith( ".class" ) )
+ if ( !pathToClassFile.endsWith( ".class" ) )
{
return;
}
-
+
for ( IgnorableDependency c : ignores )
{
- if ( c.matchesArtifact( artifact ) && c.matches( name ) )
+ if ( c.matchesArtifact( artifact ) && c.matches( pathToClassFile ) )
{
- if( classNames.containsKey( name ) )
+ if ( classesSeen.containsKey( pathToClassFile ) )
{
- getLog().debug( "Ignoring excluded class " + name );
+ getLog().debug( "Ignoring excluded class " + pathToClassFile );
}
return;
}
}
- if ( classNames.containsKey( name ) )
+ ClassesWithSameName classesWithSameName = classesSeen.get( pathToClassFile );
+ boolean isFirstTimeSeeingThisClass = ( classesWithSameName == null );
+ ClassFile classFile = new ClassFile( pathToClassFile, artifact );
+
+ if ( isFirstTimeSeeingThisClass )
{
- Artifact dup = classNames.put( name, artifact );
- if ( !( findAllDuplicates && duplicates.containsKey( name ) ) )
- {
- for ( IgnorableDependency c : ignores )
- {
- if ( c.matchesArtifact( artifact ) && c.matches(name) )
- {
- getLog().debug( "Ignoring duplicate class " + name );
- return;
- }
- }
- }
-
- if ( findAllDuplicates )
- {
- Set dups = duplicates.get( name );
- if ( dups == null )
- {
- dups = new LinkedHashSet();
- dups.add( dup );
- }
- dups.add( artifact );
- duplicates.put( name, dups );
- }
- else
- {
- StringBuilder buf = new StringBuilder( message == null ? "Duplicate class found:" : message );
- buf.append( '\n' );
- buf.append( "\n Found in:" );
- buf.append( "\n " );
- buf.append( dup );
- buf.append( "\n " );
- buf.append( artifact );
- buf.append( "\n Duplicate classes:" );
- buf.append( "\n " );
- buf.append( name );
- buf.append( '\n' );
- buf.append( "There may be others but was set to false, so failing fast" );
- throw new EnforcerRuleException( buf.toString() );
- }
+ classesSeen.put( pathToClassFile, new ClassesWithSameName( getLog(), classFile ) );
+ return;
}
- else
+
+ classesWithSameName.add( classFile );
+
+ if ( !classesWithSameName.hasDuplicates( ignoreWhenIdentical ) )
{
- classNames.put( name, artifact );
+ return;
+ }
+
+ if ( findAllDuplicates )
+ {
+ duplicateClasses.add( pathToClassFile );
+ }
+ else
+ {
+ Artifact previousArtifact = classesWithSameName.previous().getArtifactThisClassWasFoundIn();
+
+ StringBuilder buf = new StringBuilder( message == null ? "Duplicate class found:" : message );
+ buf.append( '\n' );
+ buf.append( "\n Found in:" );
+ buf.append( "\n " );
+ buf.append( previousArtifact );
+ buf.append( "\n " );
+ buf.append( artifact );
+ buf.append( "\n Duplicate classes:" );
+ buf.append( "\n " );
+ buf.append( pathToClassFile );
+ buf.append( '\n' );
+ buf.append( "There may be others but was set to false, so failing fast" );
+ throw new EnforcerRuleException( buf.toString() );
}
}
}
diff --git a/src/main/java/org/apache/maven/plugins/enforcer/ClassFile.java b/src/main/java/org/apache/maven/plugins/enforcer/ClassFile.java
new file mode 100644
index 00000000..64798207
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/enforcer/ClassFile.java
@@ -0,0 +1,92 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import org.apache.maven.artifact.Artifact;
+
+/**
+ * This class represents a binary class file.
+ *
+ * The path to the class file should be a relative, file system path to the
+ * actual file. Examples:
+ *
+ * - CORRECT: org/apache/maven/Stuff.class
+ * - NO: /org/apache/maven/Stuff.class
+ * - NO: org.apache.maven.Stuff
+ * - NO: maven.jar!org.apache.maven.Stuff
+ * - NO: maven.jar!/org/apache/maven/Stuff.class
+ * - NO: /path/to/some/directory/org.apache.maven.Stuff
+ * - NO: /path/to/some/directory/org/apache/maven/Stuff.class
+ *
+ * The file must exist in either a directory or a jar file, but the path
+ * of the directory/jar is not included in the class file path. Rather,
+ * it's included in the Artifact. See {@link Artifact#getFile()}
+ */
+public class ClassFile
+{
+ /** the path to the .class file. Example: org/apache/maven/Stuff.class */
+ private final String classFilePath;
+ private final Artifact artifactThisClassWasFoundIn;
+ private final Hasher hasher;
+ private String lazilyComputedHash;
+
+ /**
+ * Constructor.
+ * @param classFilePath path to the class file. Example: org/apache/maven/Stuff.class
+ * @param artifactThisClassWasFoundIn the maven artifact the class appeared in (example: a jar file)
+ */
+ public ClassFile( String classFilePath, Artifact artifactThisClassWasFoundIn )
+ {
+ this.classFilePath = classFilePath;
+ this.artifactThisClassWasFoundIn = artifactThisClassWasFoundIn;
+ this.hasher = new Hasher( classFilePath );
+ }
+
+ /**
+ * @return the path to the .class file. Example: org/apache/maven/Stuff.class
+ */
+ public String getClassFilePath()
+ {
+ return classFilePath;
+ }
+
+ /**
+ * @return the maven artifact the class appeared in (example: a jar file)
+ */
+ public Artifact getArtifactThisClassWasFoundIn()
+ {
+ return artifactThisClassWasFoundIn;
+ }
+
+ /**
+ * @return a hash or checksum of the binary file. If two files have the same hash
+ * then they are the same binary file.
+ */
+ public String getHash()
+ {
+ if ( lazilyComputedHash == null )
+ {
+ lazilyComputedHash = hasher.generateHash( artifactThisClassWasFoundIn );
+ }
+
+ return lazilyComputedHash;
+ }
+
+}
diff --git a/src/main/java/org/apache/maven/plugins/enforcer/ClassesWithSameName.java b/src/main/java/org/apache/maven/plugins/enforcer/ClassesWithSameName.java
new file mode 100644
index 00000000..7e6d843a
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/enforcer/ClassesWithSameName.java
@@ -0,0 +1,248 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.logging.Log;
+
+/**
+ * Represents one or more class files that have the same exact name.
+ *
+ * In this case the class name is a relative, file system path to the
+ * class file. For example: org/apache/maven/Stuff.class
+ *
+ * Example of how we can have two of the same class:
+ * - mockito-core-1.9.5.jar contains org/mockito/Mockito.class
+ * - mockito-all-1.9.5.jar contains org/mockito/Mockito.class
+ *
+ * With that example you're not supposed to have both on the classpath. Typically
+ * you'd choose the maven way (mockito-core) or the convenient-for-non-maven-users
+ * way (mockito-all) but not both.
+ */
+public class ClassesWithSameName
+{
+ private final Log log;
+ /** the path to the .class file. Example: org/apache/maven/Stuff.class */
+ private final String classFilePath;
+ private final List list = new ArrayList();
+
+ /**
+ * @param log (required) the logger
+ * @param initialClassFile (required) we require at least one class file. Splitting this param from the
+ * next one lets us require at least one at compile time (instead of runtime).
+ * @param additionalClassFiles (optional) additional class files
+ */
+ public ClassesWithSameName( Log log, ClassFile initialClassFile, ClassFile... additionalClassFiles )
+ {
+ this.log = log;
+ classFilePath = initialClassFile.getClassFilePath();
+ list.add( initialClassFile );
+
+ for ( ClassFile classFile : additionalClassFiles )
+ {
+ throwIfClassNameDoesNotMatch( classFile, classFilePath );
+ list.add( classFile );
+ }
+ }
+
+ /**
+ * @return the previous ClassFile, meaning, the one added before the most recent one. Psuedo-code:
+ * add("Class1.class")
+ * add("Class2.class")
+ * previous() // returns "Class1.class"
+ */
+ public ClassFile previous()
+ {
+ if ( list.size() > 1 )
+ {
+ int lastIndex = list.size() - 2;
+ return list.get( lastIndex );
+ }
+ else
+ {
+ throw new IllegalArgumentException( "there was only " + list.size()
+ + " element(s) in the list, so there is no 2nd-to-last element to retrieve " );
+ }
+ }
+
+ /**
+ * Add a new .class file with the same exact path and name as the other classes this file represents
+ * (though the artifact can be different).
+ * @param classFile The path to the .class file. Example: org/apache/maven/Stuff.class
+ */
+ public void add( ClassFile classFile )
+ {
+ throwIfClassNameDoesNotMatch( classFile, classFilePath );
+ list.add( classFile );
+ }
+
+ /**
+ * @return Return a Set rather than a List so we can use this as the key in another Map.
+ * List.of(3,2,1) doesn't equal List.of(1,2,3) but Set.of(3,2,1) equals Set.of(1,2,3)
+ */
+ public Set getAllArtifactsThisClassWasFoundIn()
+ {
+ Set result = new HashSet();
+
+ for ( ClassFile classFile : list )
+ {
+ result.add( classFile.getArtifactThisClassWasFoundIn() );
+ }
+
+ return result;
+ }
+
+ /**
+ * Main logic to determine if this object represents more than one of the exact same class
+ * on the classpath.
+ * @param ignoreWhenIdentical True if we should ignore two or more classes when they have the
+ * exact same bytecode; false means fail whenever there's more than
+ * one of the same class, regardless of bytecode.
+ * @return true if there are duplicates, false if not.
+ */
+ public boolean hasDuplicates( boolean ignoreWhenIdentical )
+ {
+ boolean compareJustClassNames = !ignoreWhenIdentical;
+ if ( compareJustClassNames )
+ {
+ return list.size() > 1;
+ }
+
+ if ( list.size() <= 1 )
+ {
+ return false;
+ }
+
+ String previousHash = list.get( 0 ).getHash();
+ for ( int i = 1; i < list.size(); i++ )
+ {
+ String currentHash = list.get( i ).getHash();
+ if ( !previousHash.equals( currentHash ) )
+ {
+ return true;
+ }
+ }
+
+ log.debug( "ignoring duplicates of class " + classFilePath + " since the bytecode matches exactly" );
+
+ return false;
+ }
+
+ /**
+ * @param ignoreWhenIdentical True if we should ignore two or more classes when they have the
+ * exact same bytecode; false means fail whenever there's more than
+ * one of the same class, regardless of bytecode.
+ * @return the output string displayed on the command line when there are duplicate classes.
+ *
+ * Example (ignoreWhenIdentical = false):
+ * org/apache/maven/Stuff.class
+ *
+ * Example (ignoreWhenIdentical = true):
+ * org/apache/maven/Stuff.class -- the bytecode exactly matches in these: a.jar and b.jar
+ */
+ public String toOutputString( boolean ignoreWhenIdentical )
+ {
+ String result = classFilePath;
+
+ if ( list.size() >= 2 && ignoreWhenIdentical )
+ {
+ StringBuilder duplicationInfo = new StringBuilder();
+ for ( Set groupedArtifacts : groupArtifactsWhoseClassesAreExactMatch().values() )
+ {
+ if ( groupedArtifacts.size() <= 1 )
+ {
+ continue;
+ }
+
+ if ( duplicationInfo.length() == 0 )
+ {
+ duplicationInfo.append( " -- the bytecode exactly matches in these: " );
+ }
+ else
+ {
+ duplicationInfo.append( "; and more exact matches in these: " );
+ }
+
+ duplicationInfo.append( joinWithSeparator( groupedArtifacts, " and " ) );
+ }
+
+ result += duplicationInfo.toString();
+ }
+
+ return result;
+ }
+
+ private static void throwIfClassNameDoesNotMatch( ClassFile classFile, String otherClassFilePath )
+ {
+ if ( !classFile.getClassFilePath().equals( otherClassFilePath ) )
+ {
+ throw new IllegalArgumentException( "Expected class " + otherClassFilePath
+ + " but got " + classFile.getClassFilePath() );
+ }
+ }
+
+ private String joinWithSeparator( Set artifacts, String separator )
+ {
+ StringBuilder result = new StringBuilder();
+ boolean first = true;
+ for ( Artifact artifact : artifacts )
+ {
+ if ( first )
+ {
+ first = false;
+ }
+ else
+ {
+ result.append( separator );
+ }
+
+ result.append( artifact );
+ }
+
+ return result.toString();
+ }
+
+ private Map> groupArtifactsWhoseClassesAreExactMatch()
+ {
+ Map> groupedArtifacts = new LinkedHashMap>();
+
+ for ( ClassFile classFile : list )
+ {
+ Set artifacts = groupedArtifacts.get( classFile.getHash() );
+ if ( artifacts == null )
+ {
+ artifacts = new LinkedHashSet();
+ }
+ artifacts.add( classFile.getArtifactThisClassWasFoundIn() );
+
+ groupedArtifacts.put( classFile.getHash(), artifacts );
+ }
+
+ return groupedArtifacts;
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/enforcer/Hasher.java b/src/main/java/org/apache/maven/plugins/enforcer/Hasher.java
new file mode 100644
index 00000000..471f1e9e
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/enforcer/Hasher.java
@@ -0,0 +1,139 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.jar.JarFile;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.maven.artifact.Artifact;
+
+import static org.apache.maven.plugins.enforcer.JarUtils.isJarFile;
+
+/**
+ * Utility class to generate hashes/checksums for binary files.
+ * Typically used to generate a hashes for .class files to compare
+ * those files for equality.
+ */
+public class Hasher
+{
+ /** the path to the .class file. Example: org/apache/maven/Stuff.class */
+ private final String classFilePath;
+
+ /**
+ * Constructor.
+ * @param classFilePath The path to the .class file. This is the file we'll generate a hash for.
+ * Example: org/apache/maven/Stuff.class
+ */
+ public Hasher( String classFilePath )
+ {
+ this.classFilePath = classFilePath;
+ }
+
+ /**
+ * @param artifact The artifact (example: jar file) which contains the {@link #classFilePath}.
+ * We'll generate a hash for the class file inside this artifact.
+ * @return generate a hash/checksum for the .class file in the provided artifact.
+ */
+ public String generateHash( Artifact artifact )
+ {
+ File artifactFile = artifact.getFile();
+ try
+ {
+ if ( artifactFile.isDirectory() )
+ {
+ return hashForFileInDirectory( artifactFile );
+ }
+ else if ( isJarFile( artifact ) )
+ {
+ return hashForFileInJar( artifactFile );
+ }
+ else
+ {
+ throw new IllegalArgumentException(
+ "Expected either a directory or a jar file, but instead received: " + artifactFile );
+ }
+ }
+ catch ( IOException e )
+ {
+ throw new RuntimeException( "Problem calculating hash for " + artifact + " " + classFilePath, e );
+ }
+ }
+
+ private String hashForFileInDirectory( File artifactFile ) throws IOException
+ {
+ File classFile = new File( artifactFile, classFilePath );
+ InputStream inputStream = new FileInputStream( classFile );
+ try
+ {
+ return DigestUtils.md5Hex( inputStream );
+ }
+ finally
+ {
+ closeAll( inputStream );
+ }
+ }
+
+ private String hashForFileInJar( File artifactFile ) throws IOException
+ {
+ JarFile jar = new JarFile( artifactFile );
+ InputStream inputStream = jar.getInputStream( jar.getEntry( classFilePath ) );
+ try
+ {
+ return DigestUtils.md5Hex( inputStream );
+ }
+ finally
+ {
+ closeAll( inputStream, jar );
+ }
+ }
+
+ private void closeAll( Closeable... closeables ) throws IOException
+ {
+ IOException firstException = null;
+
+ for ( Closeable closeable : closeables )
+ {
+ if ( closeable != null )
+ {
+ try
+ {
+ closeable.close();
+ }
+ catch ( IOException exception )
+ {
+ if ( firstException == null )
+ {
+ firstException = exception;
+ }
+ }
+ }
+ }
+
+ if ( firstException != null )
+ {
+ throw firstException;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/enforcer/JarUtils.java b/src/main/java/org/apache/maven/plugins/enforcer/JarUtils.java
new file mode 100644
index 00000000..60c851cf
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/enforcer/JarUtils.java
@@ -0,0 +1,38 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import org.apache.maven.artifact.Artifact;
+
+/**
+ * Utility methods for working with Java jar files.
+ */
+public class JarUtils
+{
+ /**
+ * @param artifact the artifact to check (could be a jar file, directory, etc.)
+ * @return true if the artifact is a jar file, false if it's something else (like a directory)
+ */
+ public static boolean isJarFile( Artifact artifact )
+ {
+ return artifact.getFile().isFile() && "jar".equals( artifact.getType() );
+ }
+
+}
diff --git a/src/site/apt/banDuplicateClasses.apt.vm b/src/site/apt/banDuplicateClasses.apt.vm
index 882cad2c..25a1a9f7 100644
--- a/src/site/apt/banDuplicateClasses.apt.vm
+++ b/src/site/apt/banDuplicateClasses.apt.vm
@@ -34,6 +34,8 @@ Ban Duplicate Classes
* findAllDuplicates - a boolean to indicate whether the rule should find all duplicates or fail fast at the first duplicate. Defaults to <<>>.
+ * ignoreWhenIdentical - when <<>> indicates duplicate classes don't fail the build when their bytecode exactly matches each other. Defaults to <<>>.
+
* message - an optional message to provide when duplicates are found.
* dependencies - a list of dependencies for which you want to ignore specific classes.
@@ -69,6 +71,7 @@ Ban Duplicate Classes
org.apache.commons.logging.*
true
+ true
true
diff --git a/src/test/java/org/apache/maven/plugins/enforcer/ArtifactBuilder.java b/src/test/java/org/apache/maven/plugins/enforcer/ArtifactBuilder.java
new file mode 100644
index 00000000..436a57f9
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/enforcer/ArtifactBuilder.java
@@ -0,0 +1,98 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.versioning.VersionRange;
+
+/**
+ * Test helper for working with {@link Artifact}s.
+ */
+public class ArtifactBuilder
+{
+ private String groupId = "groupId";
+ private String artifactId = "artifactId";
+ private VersionRange versionRange = VersionRange.createFromVersion( "1.0" );
+ private String scope = "scope";
+ private String type = "type";
+ private String classifier = "classifier";
+ private File fileOrDirectory = getAnyFile();
+
+ public static ArtifactBuilder newBuilder()
+ {
+ return new ArtifactBuilder();
+ }
+
+ public ArtifactBuilder withVersion( String version )
+ {
+ versionRange = VersionRange.createFromVersion( version );
+ return this;
+ }
+
+ public ArtifactBuilder withType( String type )
+ {
+ this.type = type;
+ return this;
+ }
+
+ public ArtifactBuilder withAnyDirectory()
+ {
+ fileOrDirectory = getAnyDirectory();
+ return this;
+ }
+
+ public ArtifactBuilder withFileOrDirectory( File directory )
+ {
+ fileOrDirectory = directory;
+ return this;
+ }
+
+ public Artifact build()
+ {
+ Artifact artifact = new DefaultArtifact( groupId, artifactId, versionRange, scope, type, classifier, null );
+ artifact.setFile( fileOrDirectory );
+
+ return artifact;
+ }
+
+ private static File getAnyFile()
+ {
+ // the actual file isn't important, just so long as it exists
+ URL url = ArtifactBuilder.class.getResource( "/utf8.txt" );
+ try
+ {
+ return new File( url.toURI() );
+ }
+ catch ( URISyntaxException exception )
+ {
+ throw new RuntimeException( exception );
+ }
+ }
+
+ private File getAnyDirectory()
+ {
+ return getAnyFile().getParentFile();
+ }
+}
diff --git a/src/test/java/org/apache/maven/plugins/enforcer/ClassFileHelper.java b/src/test/java/org/apache/maven/plugins/enforcer/ClassFileHelper.java
new file mode 100644
index 00000000..b61e67d1
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/enforcer/ClassFileHelper.java
@@ -0,0 +1,126 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.apache.maven.artifact.Artifact;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Test utility to make writing tests with {@link ClassFile}s easier.
+ */
+public class ClassFileHelper
+{
+ private static int uniqueId = 0;
+
+ private final TemporaryFolder temporaryFolder;
+
+ public ClassFileHelper( TemporaryFolder temporaryFolder )
+ {
+ this.temporaryFolder = temporaryFolder;
+ }
+
+ public ClassFile createWithContent( String pathToClassFile, String fileContents ) throws IOException
+ {
+ uniqueId++;
+ String uniqueIdStr = Integer.toString( uniqueId );
+
+ File tempDirectory = createTempDirectory( uniqueIdStr );
+ createClassFile( tempDirectory, pathToClassFile, fileContents );
+
+ Artifact artifact = ArtifactBuilder.newBuilder()
+ .withFileOrDirectory( tempDirectory )
+ .withVersion( uniqueIdStr )
+ .withType( "some type that isn't 'jar' so our code assumes it's a directory" )
+ .build();
+
+ return new ClassFile( pathToClassFile, artifact );
+ }
+
+ public ClassFile createJarWithContent( String jarFileName, String pathToClassFile, String fileContents )
+ throws IOException
+ {
+ uniqueId++;
+ String uniqueIdStr = Integer.toString( uniqueId );
+
+ File tempDirectory = createTempDirectory( uniqueIdStr );
+ File tempJarFile = new File( tempDirectory, jarFileName );
+
+ JarOutputStream outStream = new JarOutputStream( new FileOutputStream( tempJarFile ) );
+ try
+ {
+ outStream.putNextEntry( new JarEntry( pathToClassFile ) );
+ outStream.write( fileContents.getBytes( "UTF-8" ) );
+ }
+ finally
+ {
+ outStream.close();
+ }
+
+ Artifact artifact = ArtifactBuilder.newBuilder()
+ .withFileOrDirectory( tempJarFile )
+ .withVersion( uniqueIdStr )
+ .withType( "jar" )
+ .build();
+
+ return new ClassFile( pathToClassFile, artifact );
+ }
+
+ private File createTempDirectory( String uniqueIdStr )
+ {
+ try
+ {
+ return temporaryFolder.newFolder( uniqueIdStr );
+ }
+ catch ( IOException exception )
+ {
+ throw new RuntimeException( "unable to create temporary folder", exception );
+ }
+ }
+
+ private void createClassFile( File directory, String pathToClassFile, String fileContents ) throws IOException
+ {
+ File file = new File( directory, pathToClassFile );
+
+ boolean madeDirs = file.getParentFile().mkdirs();
+ if ( !madeDirs )
+ {
+ throw new RuntimeException( "unable to create parent directories for " + file );
+ }
+
+ file.createNewFile();
+
+ FileWriter writer = new FileWriter( file );
+ try
+ {
+ writer.write( fileContents );
+ }
+ finally
+ {
+ writer.close();
+ }
+ }
+}
diff --git a/src/test/java/org/apache/maven/plugins/enforcer/ClassFileTest.java b/src/test/java/org/apache/maven/plugins/enforcer/ClassFileTest.java
new file mode 100644
index 00000000..9f2f4b04
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/enforcer/ClassFileTest.java
@@ -0,0 +1,38 @@
+package org.apache.maven.plugins.enforcer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertEquals;
+
+public class ClassFileTest
+{
+ private static final String PATH_TO_CLASS_FILE = ClassFileTest.class.getName().replace( '.', '/' ) + ".class";
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private final ClassFileHelper classFileHelper = new ClassFileHelper( tempFolder );
+
+ @Test
+ public void getHashComputesHashOfFile() throws Exception
+ {
+ ClassFile classFile = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "the content of the file" );
+
+ assertEquals( "7e47820975c51a762e63caa95cc76e45", classFile.getHash() );
+ }
+
+ @Test
+ public void getHashReturnsConsistentHashWhenInvokedTwice() throws Exception
+ {
+ ClassFile classFile = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content" );
+
+ String hash1 = classFile.getHash();
+ String hash2 = classFile.getHash();
+
+ assertEquals( "d10b4c3ff123b26dc068d43a8bef2d23", hash1 );
+ assertEquals( hash1, hash2 );
+ }
+
+}
diff --git a/src/test/java/org/apache/maven/plugins/enforcer/ClassesWithSameNameTest.java b/src/test/java/org/apache/maven/plugins/enforcer/ClassesWithSameNameTest.java
new file mode 100644
index 00000000..ddb9f393
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/enforcer/ClassesWithSameNameTest.java
@@ -0,0 +1,270 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import java.util.Set;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.monitor.logging.DefaultLog;
+import org.apache.maven.plugin.logging.Log;
+import org.codehaus.plexus.logging.console.ConsoleLogger;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ClassesWithSameNameTest
+{
+ /** logging thresholds are: DEBUG=0, INFO=1, WARNING=2, ERROR=3, FATAL ERROR=4, DISABLED=5 */
+ private static final int LOGGING_THRESHOLD = 5;
+ private static final String PATH_TO_CLASS_FILE = ClassesWithSameNameTest.class.getName().replace( '.', '/' ) + ".class";
+
+ /** this is an alias to make the code read better */
+ private static final boolean DETERMINE_DUPLICATES_BY_NAME_AND_BYTECODE = true;
+
+ /** this is an alias to make the code read better */
+ private static final boolean DETERMINE_DUPLICATES_BY_NAME = false;
+
+ private static final Log log = new DefaultLog( new ConsoleLogger( LOGGING_THRESHOLD, "test" ) );
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private ClassFileHelper classFileHelper;
+
+ @Before
+ public void beforeEachTest()
+ {
+ classFileHelper = new ClassFileHelper( temporaryFolder );
+ }
+
+ /**
+ * Verify the method returns true when there's a simple duplication (meaning, the names match).
+ * This check is only concerned if we found a duplicate. It should still fail even if the two *.class
+ * files are exactly the same.
+ */
+ @Test
+ public void hasDuplicatesShouldReturnTrueWhenClassNameIsDuplicate() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+
+ boolean result = classesWithSameName.hasDuplicates( DETERMINE_DUPLICATES_BY_NAME );
+
+ assertTrue( result );
+ }
+
+ @Test
+ public void hasDuplicatesShouldReturnFalseWhenClassNameIsDuplicateButBytecodeIsIdentical() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "content matches in both" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "content matches in both" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+
+ boolean result = classesWithSameName.hasDuplicates( DETERMINE_DUPLICATES_BY_NAME_AND_BYTECODE );
+
+ assertFalse( result );
+ }
+
+ @Test
+ public void hasDuplicatesShouldReturnFalseWhenClassHasNoDuplicates() throws Exception
+ {
+ ClassFile classFile = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile );
+
+ boolean result = classesWithSameName.hasDuplicates( DETERMINE_DUPLICATES_BY_NAME );
+
+ assertFalse( result );
+ }
+
+ /**
+ * This test compares two files with the same exact relative path (so they look like the same file)
+ * but they exist in two different folders and their bytecode doesn't match. This should be considered
+ * a duplicate.
+ *
+ * We set the test up so it fails if it finds the same class name/path twice (meaning, it does not compare
+ * bytecode).
+ */
+ @Test
+ public void hasDuplicatesShouldReturnTrueWhenClassNameIsDuplicateButBytecodeDiffers() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "1" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "2" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+
+ boolean result = classesWithSameName.hasDuplicates( DETERMINE_DUPLICATES_BY_NAME );
+
+ assertTrue( result );
+ }
+
+ /**
+ * This test compares two files with the same exact relative path (so they look like the same file)
+ * but they exist in two different folders and their bytecode doesn't match. This should be considered
+ * a duplicate.
+ *
+ * We set the test up so it finds duplicates only if the bytecode differs.
+ */
+ @Test
+ public void hasDuplicatesShouldReturnFalseWhenClassNameIsDuplicateAndBytecodeDiffers() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "1" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "2" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+
+ boolean result = classesWithSameName.hasDuplicates( DETERMINE_DUPLICATES_BY_NAME_AND_BYTECODE );
+
+ assertTrue( result );
+ }
+
+ /**
+ * This tests the normal condition where we just output the class file path.
+ */
+ @Test
+ public void toOutputStringOutputsPlainArtifactWhenJustNamesAreDuplicate() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+
+ String actualOutput = classesWithSameName.toOutputString( DETERMINE_DUPLICATES_BY_NAME );
+
+ assertEquals( PATH_TO_CLASS_FILE, actualOutput );
+ }
+
+ /**
+ * Verify the output string contains all the information, specifically, it should list which artifacts
+ * were an exact match (meaning, the bytecode of the .class files were identical). This helps users
+ * determine which artifacts they can ignore when fix the BanDuplicateClasses error.
+ */
+ @Test
+ public void toOutputStringOutputsTwoArtifactsWhereBytecodeIsExactMatch() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "content matches in both" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "content matches in both" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+
+ String actualOutput = classesWithSameName.toOutputString( DETERMINE_DUPLICATES_BY_NAME_AND_BYTECODE );
+
+ String expectedOutput = PATH_TO_CLASS_FILE + " -- the bytecode exactly matches in these: " +
+ classFile1.getArtifactThisClassWasFoundIn() + " and " + classFile2.getArtifactThisClassWasFoundIn();
+ assertEquals( expectedOutput, actualOutput );
+ }
+
+ /**
+ * This verifies the output string contains all the information, specifically, it should list
+ * which artifacts were an exact match. In this case we have 4 artifacts: 1, 2, 3, and 4.
+ * The bytecode of 1 and 2 match each other, the bytecode of 3 and 4 match each other, but
+ * 1 and 2 don't match 3 and 4.
+ */
+ @Test
+ public void toOutputStringOutputsFourArtifactsWhereBytecodeIsExactMatchInTwoAndExactMatchInOtherTwo() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content of 1 and 2" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content of 1 and 2" );
+ ClassFile classFile3 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content of 3 and 4" );
+ ClassFile classFile4 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content of 3 and 4" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2, classFile3, classFile4 );
+
+ String actualOutput = classesWithSameName.toOutputString( DETERMINE_DUPLICATES_BY_NAME_AND_BYTECODE );
+
+ String expectedOutput = PATH_TO_CLASS_FILE + " -- the bytecode exactly matches in these: " +
+ classFile1.getArtifactThisClassWasFoundIn() + " and " + classFile2.getArtifactThisClassWasFoundIn() +
+ "; and more exact matches in these: " +
+ classFile3.getArtifactThisClassWasFoundIn() + " and " + classFile4.getArtifactThisClassWasFoundIn();
+ assertEquals( expectedOutput, actualOutput );
+ }
+
+ /**
+ * The method should return the 2nd-to-last element in the last, but if there's only 1 element
+ * there's no 2nd-to-last element to return.
+ */
+ @Test( expected = IllegalArgumentException.class )
+ public void previousShouldThrowIfOnlyOneArtifact() throws Exception
+ {
+ ClassFile classFile = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content of 1 and 2" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile );
+
+ classesWithSameName.previous();
+ }
+
+ @Test
+ public void previousShouldReturn2ndToLastElement() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content of 1 and 2" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "file content of 1 and 2" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+
+ ClassFile previous = classesWithSameName.previous();
+
+ assertEquals( classFile1, previous );
+ }
+
+ @Test
+ public void addShouldAddArtifact() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1 );
+
+ assertEquals( 1, classesWithSameName.getAllArtifactsThisClassWasFoundIn().size() );
+ classesWithSameName.add( classFile2 );
+ assertEquals( 2, classesWithSameName.getAllArtifactsThisClassWasFoundIn().size() );
+ }
+
+ @Test( expected = IllegalArgumentException.class )
+ public void addShouldThrowWhenClassNameDoesNotMatch() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassFile classFile2 = classFileHelper.createWithContent( "some/other/path.class", "" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1 );
+
+ classesWithSameName.add( classFile2 );
+ }
+
+ @Test( expected = IllegalArgumentException.class )
+ public void constructorShouldThrowWhenClassNameDoesNotMatch() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassFile classFile2 = classFileHelper.createWithContent( "some/other/path.class", "" );
+
+ new ClassesWithSameName( log, classFile1, classFile2 );
+ }
+
+ @Test
+ public void getAllArtifactsThisClassWasFoundInShouldReturnAllArtifacts() throws Exception
+ {
+ ClassFile classFile1 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassFile classFile2 = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "" );
+ ClassesWithSameName classesWithSameName = new ClassesWithSameName( log, classFile1, classFile2 );
+ Artifact artifact1 = classFile1.getArtifactThisClassWasFoundIn();
+ Artifact artifact2 = classFile2.getArtifactThisClassWasFoundIn();
+
+ Set result = classesWithSameName.getAllArtifactsThisClassWasFoundIn();
+
+ assertEquals( 2, result.size() );
+ assertTrue( result.contains( artifact1 ) );
+ assertTrue( result.contains( artifact2 ) );
+ }
+}
diff --git a/src/test/java/org/apache/maven/plugins/enforcer/HasherTest.java b/src/test/java/org/apache/maven/plugins/enforcer/HasherTest.java
new file mode 100644
index 00000000..7abf0318
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/enforcer/HasherTest.java
@@ -0,0 +1,53 @@
+package org.apache.maven.plugins.enforcer;
+
+import org.apache.maven.artifact.Artifact;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestName;
+
+import static org.junit.Assert.assertEquals;
+
+public class HasherTest
+{
+ private static final String PATH_TO_CLASS_FILE = HasherTest.class.getName().replace( '.', '/' ) + ".class";
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Rule
+ public final TestName testName = new TestName();
+
+ private ClassFileHelper classFileHelper;
+
+ @Before
+ public void beforeEachTest()
+ {
+ classFileHelper = new ClassFileHelper( temporaryFolder );
+ }
+
+ @Test
+ public void generateHashReturnsCorrectHashForFileInDirectory() throws Exception
+ {
+ ClassFile classFile = classFileHelper.createWithContent( PATH_TO_CLASS_FILE, "this is the file's contents" );
+ Artifact artifact = classFile.getArtifactThisClassWasFoundIn();
+ Hasher hasher = new Hasher( PATH_TO_CLASS_FILE );
+
+ String hash = hasher.generateHash( artifact );
+
+ assertEquals( "ae23844bc5db9bfad3fbbe5426d89dd3", hash );
+ }
+
+ @Test
+ public void generateHashReturnsCorrectHashForFileInJar() throws Exception
+ {
+ ClassFile classFile = classFileHelper.createJarWithContent( "temp.jar", PATH_TO_CLASS_FILE, "this is the file's contents" );
+ Artifact artifact = classFile.getArtifactThisClassWasFoundIn();
+ Hasher hasher = new Hasher( PATH_TO_CLASS_FILE );
+
+ String hash = hasher.generateHash( artifact );
+
+ assertEquals( "ae23844bc5db9bfad3fbbe5426d89dd3", hash );
+ }
+}
diff --git a/src/test/java/org/apache/maven/plugins/enforcer/JarUtilsTest.java b/src/test/java/org/apache/maven/plugins/enforcer/JarUtilsTest.java
new file mode 100644
index 00000000..101e0d34
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/enforcer/JarUtilsTest.java
@@ -0,0 +1,46 @@
+package org.apache.maven.plugins.enforcer;
+
+import org.apache.maven.artifact.Artifact;
+import org.junit.Test;
+
+import static org.apache.maven.plugins.enforcer.ArtifactBuilder.newBuilder;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class JarUtilsTest
+{
+ /**
+ * "Sunny day" test: the method should return true for a jar artifact.
+ */
+ @Test
+ public void isJarFileShouldReturnTrueForJarFile()
+ {
+ Artifact artifact = newBuilder().withType( "jar" ).build();
+ assertTrue( JarUtils.isJarFile( artifact ) );
+ }
+
+ /**
+ * The method should return false when the artifact is a directory (for example:
+ * a folder with a bunch of packages/class files in it).
+ */
+ @Test
+ public void isJarFileShouldReturnFalseForDirectory()
+ {
+ Artifact artifact = newBuilder()
+ .withType( "jar" )
+ .withAnyDirectory()
+ .build();
+ assertFalse( JarUtils.isJarFile( artifact ) );
+ }
+
+ /**
+ * The method should return false whenever we're passed an artifact who's type is
+ * not "jar". For example: a war or a zip file.
+ */
+ @Test
+ public void isJarFileShouldReturnFalseWhenArtifactTypeIsNotJar()
+ {
+ Artifact artifact = newBuilder().withType( "war" ).build();
+ assertFalse( JarUtils.isJarFile( artifact ) );
+ }
+}