package org.fusesource.mvnplugins.uberize.mojo; /* * 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.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; 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.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.metadata.ArtifactMetadataSource; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactCollector; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter; import org.apache.maven.model.Dependency; import org.apache.maven.model.Exclusion; import org.apache.maven.model.Model; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.fusesource.mvnplugins.uberize.Transformer; import org.fusesource.mvnplugins.uberize.Uberizer; import org.fusesource.mvnplugins.uberize.transformer.ManifestEditor; import org.fusesource.mvnplugins.uberize.mojo.ArchiveFilter; import org.fusesource.mvnplugins.uberize.mojo.ArtifactSet; import org.fusesource.mvnplugins.uberize.filter.SimpleFilter; import org.fusesource.mvnplugins.uberize.pom.PomWriter; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectBuilder; import org.apache.maven.project.MavenProjectHelper; import org.apache.maven.project.ProjectBuildingException; import org.apache.maven.shared.dependency.tree.DependencyNode; import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder; import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.WriterFactory; /** * Mojo that creates an uber jar and optionally shades, relocate, * or merges the source jar contents. * * @author Jason van Zyl * @author Mauro Talevi * @author David Blevins * @author <a href="http://hiramchirino.com">Hiram Chirino</a> * * @goal uberize * @phase package * @requiresDependencyResolution runtime */ public class UberizeMojo extends AbstractMojo { /** * @parameter expression="${project}" * @readonly * @required */ private MavenProject project; /** * @component * @required * @readonly */ private MavenProjectHelper projectHelper; /** * @component * @required * @readonly */ private Uberizer uberizer; /** * The dependency tree builder to use. * * @component * @required * @readonly */ private DependencyTreeBuilder dependencyTreeBuilder; /** * ProjectBuilder, needed to create projects from the artifacts. * * @component role="org.apache.maven.project.MavenProjectBuilder" * @required * @readonly */ private MavenProjectBuilder mavenProjectBuilder; /** * The artifact metadata source to use. * * @component * @required * @readonly */ private ArtifactMetadataSource artifactMetadataSource; /** * The artifact collector to use. * * @component * @required * @readonly */ private ArtifactCollector artifactCollector; /** * Remote repositories which will be searched for source attachments. * * @parameter expression="${project.remoteArtifactRepositories}" * @required * @readonly */ protected List remoteArtifactRepositories; /** * Local maven repository. * * @parameter expression="${localRepository}" * @required * @readonly */ protected ArtifactRepository localRepository; /** * Artifact factory, needed to download source jars for inclusion in classpath. * * @component role="org.apache.maven.artifact.factory.ArtifactFactory" * @required * @readonly */ protected ArtifactFactory artifactFactory; /** * Artifact resolver, needed to download source jars for inclusion in classpath. * * @component role="org.apache.maven.artifact.resolver.ArtifactResolver" * @required * @readonly */ protected ArtifactResolver artifactResolver; /** * Artifacts to include/exclude from the final artifact. * * @parameter */ private ArtifactSet artifactSet; /** * Additional transformers to be used. * * @parameter */ private Transformer[] transformers; /** * Archive Filters to be used. Allows you to specify an artifact in the form of * groupId:artifactId and a set of include/exclude file patterns for filtering which * contents of the archive are added to the uber jar. From a logical perspective, * includes are processed before excludes, thus it's possible to use an include to * collect a set of files from the archive then use excludes to further reduce the set. * By default, all files are included and no files are excluded. * * @parameter */ private ArchiveFilter[] filters; /** * The work directory for extracting and modifying with the jar file contents. * * @parameter default-value="${project.build.directory}/uber" */ private File workDirectory; /** * The destination directory for the uber artifact. * * @parameter default-value="${project.build.directory}" */ private File outputDirectory; /** * The name of the uber artifactId * * @parameter expression="${finalName}" */ private String finalName; /** * The name of the uber artifactId. So you may want to use a different artifactId and keep * the standard version. If the original artifactId was "foo" then the final artifact would * be something like foo-1.0.jar. So if you change the artifactId you might have something * like foo-special-1.0.jar. * * @parameter expression="${uberArtifactId}" default-value="${project.artifactId}" */ private String uberArtifactId; /** * If specified, this will include only artifacts which have groupIds which * start with this. * * @parameter expression="${uberGroupFilter}" */ private String uberGroupFilter; /** * Defines whether the uber artifact should be attached as classifier to * the original artifact. If false, the uber jar will be the main artifact * of the project * * @parameter expression="${uberArtifactAttached}" default-value="false" */ private boolean uberArtifactAttached; /** * Flag whether to generate a simplified POM for the uber artifact. If set to <code>true</code>, dependencies that * have been included into the uber JAR will be removed from the <code><dependencies></code> section of the * generated POM. The reduced POM will be named <code>dependency-reduced-pom.xml</code> and is stored into the same * directory as the uber artifact. * * @parameter expression="${createDependencyReducedPom}" default-value="true" */ private boolean createDependencyReducedPom; /** * When true, dependencies are kept in the pom but with scope 'provided'; when false, * the dependency is removed. * * @parameter expression="${keepDependenciesWithProvidedScope}" default-value="false" */ private boolean keepDependenciesWithProvidedScope; /** * When true, transitive deps of removed dependencies are promoted to direct dependencies. * This should allow the drop in replacement of the removed deps with the new uber * jar and everything should still work. * * @parameter expression="${promoteTransitiveDependencies}" default-value="false" */ private boolean promoteTransitiveDependencies; /** * The name of the classifier used in case the uber artifact is attached. * * @parameter expression="${uberClassifierName}" default-value="uber" */ private String uberClassifierName; /** * When true, it will attempt to create a sources jar as well * * @parameter expression="${createSourcesJar}" default-value="false" */ private boolean createSourcesJar; /** * Space separated list of additional scopes of artifacts to include in the uber jar. * Typically used to include system, runtime, or test scoped jar that are not part of * the standard transitive dependeny list. * * @parameter default-value="" */ private String additionalScopes; /** @throws MojoExecutionException */ public void execute() throws MojoExecutionException { Set<String> additionalScopes = getAdditionalScopes(); Set artifacts = new LinkedHashSet(); Set artifactIds = new LinkedHashSet(); Set sourceArtifacts = new LinkedHashSet(); if ( project.getArtifact().getFile() == null ) { getLog().error( "The project main artifact does not exist. This could have the following" ); getLog().error( "reasons:" ); getLog().error( "- You have invoked the goal directly from the command line. This is not" ); getLog().error( " supported. Please add the goal to the default lifecycle via an" ); getLog().error( " <execution> element in your POM and use \"mvn package\" to have it run." ); getLog().error( "- You have bound the goal to a lifecycle phase before \"package\". Please" ); getLog().error( " remove this binding from your POM such that the goal will be run in" ); getLog().error( " the proper phase." ); throw new MojoExecutionException( "Failed to create uber artifact.", new IllegalStateException( "Project main artifact does not exist." ) ); } artifacts.add( project.getArtifact().getFile() ); if ( createSourcesJar ) { File file = uberSourcesArtifactFile(); if ( file.exists() ) { sourceArtifacts.add( file ); } } for ( Iterator it = project.getArtifacts().iterator(); it.hasNext(); ) { Artifact artifact = (Artifact) it.next(); if ( excludeArtifact( artifact ) ) { getLog().info( "Excluding " + artifact.getId() + " from the uber jar." ); continue; } add(artifacts, artifactIds, sourceArtifacts, artifact); } if( !additionalScopes.isEmpty() ) { // Also pick up scope artifacts that are not part of the default transitive deps for ( Iterator it = project.getDependencyArtifacts().iterator(); it.hasNext(); ) { Artifact artifact = (Artifact) it.next(); if( artifactIds.contains( getId( artifact ))) { continue; } for (String scope : additionalScopes) { if( scope.equals(artifact.getScope()) ) { add(artifacts, artifactIds, sourceArtifacts, artifact); } } } } File outputJar = uberArtifactFileWithClassifier(); File sourcesJar = uberSourceArtifactFileWithClassifier(); // Now add our extra resources try { List filters = getFilters(); List<Transformer> transformers = getTransformers(); uberizer.uberize(workDirectory, artifacts, outputJar, filters, transformers); if ( createSourcesJar ) { uberizer.uberize(workDirectory, sourceArtifacts, sourcesJar, filters, transformers); } if ( uberArtifactAttached ) { getLog().info( "Attaching uber artifact." ); projectHelper.attachArtifact( project, project.getArtifact().getType(), uberClassifierName, outputJar ); if ( createSourcesJar ) { projectHelper.attachArtifact( project, "jar", uberClassifierName + "-sources", sourcesJar ); } } else { getLog().info( "Replacing original artifact with uber artifact." ); File file = uberArtifactFile(); replaceFile( file, outputJar ); if ( createSourcesJar ) { file = uberSourcesArtifactFile(); replaceFile( file, sourcesJar ); projectHelper.attachArtifact( project, "jar", "sources", file ); } if ( createDependencyReducedPom ) { createDependencyReducedPom( artifactIds ); } } } catch ( Exception e ) { throw new MojoExecutionException( "Error creating uber jar.", e ); } } private Set<String> getAdditionalScopes() { HashSet<String> rc = new HashSet<String>(); if( additionalScopes!=null && !additionalScopes.trim().isEmpty() ) { String[] strings = additionalScopes.split("\\s"); for (String value : strings) { rc.add(value); } } return rc; } private void add(Set artifacts, Set artifactIds, Set sourceArtifacts, Artifact artifact) { if ( excludeArtifact( artifact ) ) { getLog().info( "Excluding " + artifact.getId() + " from the uber jar." ); } else { getLog().info( "Including " + artifact.getId() + " in the uber jar." ); artifacts.add( artifact.getFile() ); artifactIds.add( getId( artifact ) ); if ( createSourcesJar ) { File file = resolveArtifactSources( artifact ); if ( file != null ) { sourceArtifacts.add( file ); } } } } private void replaceFile( File oldFile, File newFile ) throws MojoExecutionException { getLog().info( "Replacing " + oldFile + " with " + newFile ); File origFile = new File( outputDirectory, "original-" + oldFile.getName() ); if ( oldFile.exists() && !oldFile.renameTo( origFile ) ) { //try a gc to see if an unclosed stream needs garbage collecting System.gc(); System.gc(); if ( !oldFile.renameTo( origFile ) ) { // Still didn't work. We'll do a copy try { FileOutputStream fout = new FileOutputStream( origFile ); FileInputStream fin = new FileInputStream( oldFile ); try { IOUtil.copy( fin, fout ); } finally { IOUtil.close( fin ); IOUtil.close( fout ); } } catch ( IOException ex ) { //kind of ignorable here. We're just trying to save the original getLog().warn( ex ); } } } if ( !newFile.renameTo( oldFile ) ) { //try a gc to see if an unclosed stream needs garbage collecting System.gc(); System.gc(); if ( !newFile.renameTo( oldFile ) ) { // Still didn't work. We'll do a copy try { FileOutputStream fout = new FileOutputStream( oldFile ); FileInputStream fin = new FileInputStream( newFile ); try { IOUtil.copy( fin, fout ); } finally { IOUtil.close( fin ); IOUtil.close( fout ); } } catch ( IOException ex ) { throw new MojoExecutionException( "Could not replace original artifact with uber artifact!", ex ); } } } } private File resolveArtifactSources( Artifact artifact ) { Artifact resolvedArtifact = artifactFactory.createArtifactWithClassifier( artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), "java-source", "sources" ); try { artifactResolver.resolve( resolvedArtifact, remoteArtifactRepositories, localRepository ); } catch ( ArtifactNotFoundException e ) { // ignore, the jar has not been found } catch ( ArtifactResolutionException e ) { getLog().warn( "Could not get sources for " + artifact ); } if ( resolvedArtifact.isResolved() ) { return resolvedArtifact.getFile(); } return null; } private boolean excludeArtifact( Artifact artifact ) { String id = getId( artifact ); // This is the case where we have only stated artifacts to include and no exclusions // have been listed. We just want what we have asked to include. if ( artifactSet != null && ( artifactSet.getExcludes() == null && artifactSet.getIncludes() != null ) && !includedArtifacts().contains( id ) ) { return true; } if ( excludedArtifacts().contains( id ) ) { return true; } if ( uberGroupFilter != null && !artifact.getGroupId().startsWith( uberGroupFilter ) ) { return true; } return false; } private Set excludedArtifacts() { if ( artifactSet != null && artifactSet.getExcludes() != null ) { return artifactSet.getExcludes(); } return Collections.EMPTY_SET; } private Set includedArtifacts() { if ( artifactSet != null && artifactSet.getIncludes() != null ) { return artifactSet.getIncludes(); } return Collections.EMPTY_SET; } private List<Transformer> getTransformers() { final List<Transformer> list = transformers == null? Collections.EMPTY_LIST : Arrays.asList(transformers); final ArrayList<Transformer> rc = new ArrayList(list); if( !containsTransformer(rc, ManifestEditor.class) ) { rc.add(new ManifestEditor()); } return rc; } private boolean containsTransformer(ArrayList<Transformer> rc, Class clazz) { for (Transformer transformer : rc) { if( clazz.isAssignableFrom(transformer.getClass()) ) { return true; } } return false; } private List getFilters() { List filters = new ArrayList(); if ( this.filters == null ) { return filters; } Map artifacts = new HashMap(); artifacts.put( getId( project.getArtifact() ), project.getArtifact().getFile() ); for ( Iterator it = project.getArtifacts().iterator(); it.hasNext(); ) { Artifact artifact = (Artifact) it.next(); artifacts.put( getId( artifact ), artifact.getFile() ); } for ( int i = 0; i < this.filters.length; i++ ) { ArchiveFilter f = this.filters[i]; File jar = (File) artifacts.get( f.getArtifact() ); if ( jar == null ) { getLog().info( "No artifact matching filter " + f.getArtifact() ); continue; } filters.add( new SimpleFilter( jar, f.getIncludes(), f.getExcludes() ) ); } return filters; } private File uberArtifactFileWithClassifier() { Artifact artifact = project.getArtifact(); final String uberName = uberArtifactId + "-" + artifact.getVersion() + "-" + uberClassifierName + "." + artifact.getArtifactHandler().getExtension(); return new File( outputDirectory, uberName ); } private File uberSourceArtifactFileWithClassifier() { Artifact artifact = project.getArtifact(); final String uberName = uberArtifactId + "-" + artifact.getVersion() + "-" + uberClassifierName + "-sources." + artifact.getArtifactHandler().getExtension(); return new File( outputDirectory, uberName ); } private File uberArtifactFile() { Artifact artifact = project.getArtifact(); String uberName; if ( finalName != null ) { uberName = finalName + "." + artifact.getArtifactHandler().getExtension(); } else { uberName = uberArtifactId + "-" + artifact.getVersion() + "." + artifact.getArtifactHandler().getExtension(); } return new File( outputDirectory, uberName ); } private File uberSourcesArtifactFile() { Artifact artifact = project.getArtifact(); String uberName; if ( finalName != null ) { uberName = finalName + "-sources." + artifact.getArtifactHandler().getExtension(); } else { uberName = uberArtifactId + "-" + artifact.getVersion() + "-sources." + artifact.getArtifactHandler().getExtension(); } return new File( outputDirectory, uberName ); } // We need to find the direct dependencies that have been included in the uber JAR so that we can modify the // POM accordingly. private void createDependencyReducedPom( Set artifactsToRemove ) throws IOException, DependencyTreeBuilderException, ProjectBuildingException { Model model = project.getOriginalModel(); List dependencies = new ArrayList(); boolean modified = false; List transitiveDeps = new ArrayList(); for ( Iterator it = project.getArtifacts().iterator(); it.hasNext(); ) { Artifact artifact = (Artifact) it.next(); //promote Dependency dep = new Dependency(); dep.setArtifactId( artifact.getArtifactId() ); if ( artifact.hasClassifier() ) { dep.setClassifier( artifact.getClassifier() ); } dep.setGroupId( artifact.getGroupId() ); dep.setOptional( artifact.isOptional() ); dep.setScope( artifact.getScope() ); dep.setType( artifact.getType() ); dep.setVersion( artifact.getVersion() ); //we'll figure out the exclusions in a bit. transitiveDeps.add( dep ); } List origDeps = project.getDependencies(); if ( promoteTransitiveDependencies ) { origDeps = transitiveDeps; } for ( Iterator i = origDeps.iterator(); i.hasNext(); ) { Dependency d = (Dependency) i.next(); dependencies.add( d ); String id = d.getGroupId() + ":" + d.getArtifactId(); if ( artifactsToRemove.contains( id ) ) { modified = true; if ( keepDependenciesWithProvidedScope ) { d.setScope( "provided" ); } else { dependencies.remove( d ); } } } // Check to see if we have a reduction and if so rewrite the POM. if ( modified ) { while ( modified ) { model.setDependencies( dependencies ); File f = new File( outputDirectory, "dependency-reduced-pom.xml" ); if ( f.exists() ) { f.delete(); } Writer w = WriterFactory.newXmlWriter( f ); PomWriter.write( w, model, true ); w.close(); MavenProject p2 = mavenProjectBuilder.build( f, localRepository, null ); modified = updateExcludesInDeps( p2, dependencies, transitiveDeps ); } //copy the dependecy-reduced-pom.xml to the basedir where //we'll set the file for the project to it. We cannot set //it to the real version in "target" as then ${basedir} gets //messed up. We'll delete this file on exit to make //sure it gets cleaned up. File f = new File( project.getBasedir(), "dependency-reduced-pom.xml" ); File f2 = new File( outputDirectory, "dependency-reduced-pom.xml" ); if ( f.exists() ) { f.delete(); } FileUtils.copyFile( f2, f ); FileUtils.forceDeleteOnExit( f ); project.setFile( f ); } } private String getId( Artifact artifact ) { if ( artifact.getClassifier() == null || "jar".equals( artifact.getClassifier() ) ) { return artifact.getGroupId() + ":" + artifact.getArtifactId(); } else { return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getClassifier(); } } public boolean updateExcludesInDeps( MavenProject project, List dependencies, List transitiveDeps ) throws DependencyTreeBuilderException { DependencyNode node = dependencyTreeBuilder.buildDependencyTree( project, localRepository, artifactFactory, artifactMetadataSource, null, artifactCollector ); boolean modified = false; Iterator it = node.getChildren().listIterator(); while ( it.hasNext() ) { DependencyNode n2 = (DependencyNode) it.next(); Iterator it2 = n2.getChildren().listIterator(); while ( it2.hasNext() ) { DependencyNode n3 = (DependencyNode) it2.next(); //anything two levels deep that is marked "included" //is stuff that was excluded by the original poms, make sure it //remains excluded IF promoting transitives. if ( n3.getState() == DependencyNode.INCLUDED ) { //check if it really isn't in the list of original dependencies. Maven //prior to 2.0.8 may grab versions from transients instead of //from the direct deps in which case they would be marked included //instead of OMITTED_FOR_DUPLICATE //also, if not promoting the transitives, level 2's would be included boolean found = false; for ( int x = 0; x < transitiveDeps.size(); x++ ) { Dependency dep = (Dependency) transitiveDeps.get( x ); if ( dep.getArtifactId().equals( n3.getArtifact().getArtifactId() ) && dep.getGroupId().equals( n3.getArtifact().getGroupId() ) ) { found = true; } } if ( !found ) { for ( int x = 0; x < dependencies.size(); x++ ) { Dependency dep = (Dependency) dependencies.get( x ); if ( dep.getArtifactId().equals( n2.getArtifact().getArtifactId() ) && dep.getGroupId().equals( n2.getArtifact().getGroupId() ) ) { Exclusion exclusion = new Exclusion(); exclusion.setArtifactId( n3.getArtifact().getArtifactId() ); exclusion.setGroupId( n3.getArtifact().getGroupId() ); dep.addExclusion( exclusion ); modified = true; break; } } } } } } return modified; } }