blob: b4b505c034ba477d004abee2578033a102e2b6a5 [file] [log] [blame]
package org.apache.maven.shared.dependency.analyzer;
/*
* 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.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
/**
* <p>DefaultProjectDependencyAnalyzer class.</p>
*
* @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
*/
@Component( role = ProjectDependencyAnalyzer.class )
public class DefaultProjectDependencyAnalyzer
implements ProjectDependencyAnalyzer
{
/**
* ClassAnalyzer
*/
@Requirement
private ClassAnalyzer classAnalyzer;
/**
* DependencyAnalyzer
*/
@Requirement
private DependencyAnalyzer dependencyAnalyzer;
/** {@inheritDoc} */
public ProjectDependencyAnalysis analyze( MavenProject project )
throws ProjectDependencyAnalyzerException
{
try
{
Map<Artifact, Set<String>> artifactClassMap = buildArtifactClassMap( project );
Set<String> dependencyClasses = buildDependencyClasses( project );
Set<String> testOnlyDependencyClasses = buildTestDependencyClasses( project );
Set<Artifact> declaredArtifacts = buildDeclaredArtifacts( project );
Set<Artifact> usedArtifacts = buildUsedArtifacts( artifactClassMap, dependencyClasses );
Set<Artifact> testOnlyArtifacts = buildUsedArtifacts( artifactClassMap, testOnlyDependencyClasses );
Set<Artifact> usedDeclaredArtifacts = new LinkedHashSet<>( declaredArtifacts );
usedDeclaredArtifacts.retainAll( usedArtifacts );
Set<Artifact> usedUndeclaredArtifacts = new LinkedHashSet<>( usedArtifacts );
usedUndeclaredArtifacts = removeAll( usedUndeclaredArtifacts, declaredArtifacts );
Set<Artifact> unusedDeclaredArtifacts = new LinkedHashSet<>( declaredArtifacts );
unusedDeclaredArtifacts = removeAll( unusedDeclaredArtifacts, usedArtifacts );
Set<Artifact> testArtifactsWithNonTestScope = getTestArtifactsWithNonTestScope( testOnlyArtifacts );
return new ProjectDependencyAnalysis( usedDeclaredArtifacts, usedUndeclaredArtifacts,
unusedDeclaredArtifacts, testArtifactsWithNonTestScope );
}
catch ( IOException exception )
{
throw new ProjectDependencyAnalyzerException( "Cannot analyze dependencies", exception );
}
}
/**
* This method defines a new way to remove the artifacts by using the conflict id. We don't care about the version
* here because there can be only 1 for a given artifact anyway.
*
* @param start initial set
* @param remove set to exclude
* @return set with remove excluded
*/
private Set<Artifact> removeAll( Set<Artifact> start, Set<Artifact> remove )
{
Set<Artifact> results = new LinkedHashSet<>( start.size() );
for ( Artifact artifact : start )
{
boolean found = false;
for ( Artifact artifact2 : remove )
{
if ( artifact.getDependencyConflictId().equals( artifact2.getDependencyConflictId() ) )
{
found = true;
break;
}
}
if ( !found )
{
results.add( artifact );
}
}
return results;
}
private Set<Artifact> getTestArtifactsWithNonTestScope( Set<Artifact> testOnlyArtifacts )
{
Set<Artifact> nonTestScopeArtifacts = new LinkedHashSet<>();
for ( Artifact artifact : testOnlyArtifacts )
{
if ( artifact.getScope().equals( "compile" ) )
{
nonTestScopeArtifacts.add( artifact );
}
}
return nonTestScopeArtifacts;
}
private Map<Artifact, Set<String>> buildArtifactClassMap( MavenProject project )
throws IOException
{
Map<Artifact, Set<String>> artifactClassMap = new LinkedHashMap<>();
@SuppressWarnings( "unchecked" )
Set<Artifact> dependencyArtifacts = project.getArtifacts();
for ( Artifact artifact : dependencyArtifacts )
{
File file = artifact.getFile();
if ( file != null && file.getName().endsWith( ".jar" ) )
{
// optimized solution for the jar case
try ( JarFile jarFile = new JarFile( file ) )
{
Enumeration<JarEntry> jarEntries = jarFile.entries();
Set<String> classes = new HashSet<>();
while ( jarEntries.hasMoreElements() )
{
String entry = jarEntries.nextElement().getName();
if ( entry.endsWith( ".class" ) )
{
String className = entry.replace( '/', '.' );
className = className.substring( 0, className.length() - ".class".length() );
classes.add( className );
}
}
artifactClassMap.put( artifact, classes );
}
}
else if ( file != null && file.isDirectory() )
{
URL url = file.toURI().toURL();
Set<String> classes = classAnalyzer.analyze( url );
artifactClassMap.put( artifact, classes );
}
}
return artifactClassMap;
}
private Set<String> buildTestDependencyClasses( MavenProject project ) throws IOException
{
Set<String> testOnlyDependencyClasses = new HashSet<>();
String outputDirectory = project.getBuild().getOutputDirectory();
Set<String> nonTestDependencyClasses = new HashSet<>( buildDependencyClasses( outputDirectory ) );
String testOutputDirectory = project.getBuild().getTestOutputDirectory();
Set<String> testDependencyClasses = new HashSet<>( buildDependencyClasses( testOutputDirectory ) );
for ( String testString : testDependencyClasses )
{
if ( !nonTestDependencyClasses.contains( testString ) )
{
testOnlyDependencyClasses.add( testString );
}
}
return testOnlyDependencyClasses;
}
private Set<String> buildDependencyClasses( MavenProject project )
throws IOException
{
String outputDirectory = project.getBuild().getOutputDirectory();
Set<String> dependencyClasses = new HashSet<>( buildDependencyClasses( outputDirectory ) );
String testOutputDirectory = project.getBuild().getTestOutputDirectory();
dependencyClasses.addAll( buildDependencyClasses( testOutputDirectory ) );
return dependencyClasses;
}
private Set<String> buildDependencyClasses( String path )
throws IOException
{
URL url = new File( path ).toURI().toURL();
return dependencyAnalyzer.analyze( url );
}
private Set<Artifact> buildDeclaredArtifacts( MavenProject project )
{
@SuppressWarnings( "unchecked" )
Set<Artifact> declaredArtifacts = project.getDependencyArtifacts();
if ( declaredArtifacts == null )
{
declaredArtifacts = Collections.emptySet();
}
return declaredArtifacts;
}
private Set<Artifact> buildUsedArtifacts( Map<Artifact, Set<String>> artifactClassMap,
Set<String> dependencyClasses )
{
Set<Artifact> usedArtifacts = new HashSet<>();
for ( String className : dependencyClasses )
{
Artifact artifact = findArtifactForClassName( artifactClassMap, className );
if ( artifact != null )
{
usedArtifacts.add( artifact );
}
}
return usedArtifacts;
}
private Artifact findArtifactForClassName( Map<Artifact, Set<String>> artifactClassMap, String className )
{
for ( Map.Entry<Artifact, Set<String>> entry : artifactClassMap.entrySet() )
{
if ( entry.getValue().contains( className ) )
{
return entry.getKey();
}
}
return null;
}
}