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 ) ); + } +}