View Javadoc
1   /*******************************************************************************
2    * Copyright (c) 2008, 2011 Sonatype Inc. and others.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *    Sonatype Inc. - initial API and implementation
10   *******************************************************************************/
11  package com.simpligility.maven.plugins.android.phase_prebuild;
12  
13  import com.simpligility.maven.plugins.android.common.AndroidExtension;
14  import com.simpligility.maven.plugins.android.common.ArtifactResolverHelper;
15  import com.simpligility.maven.plugins.android.common.DependencyResolver;
16  import com.simpligility.maven.plugins.android.common.PomConfigurationHelper;
17  import com.simpligility.maven.plugins.android.common.UnpackedLibHelper;
18  import org.apache.maven.AbstractMavenLifecycleParticipant;
19  import org.apache.maven.MavenExecutionException;
20  import org.apache.maven.artifact.Artifact;
21  import org.apache.maven.artifact.resolver.ArtifactResolver;
22  import org.apache.maven.execution.MavenSession;
23  import org.apache.maven.model.Dependency;
24  import org.apache.maven.plugin.MojoExecutionException;
25  import org.apache.maven.project.MavenProject;
26  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
27  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
28  import org.codehaus.plexus.component.annotations.Component;
29  import org.codehaus.plexus.component.annotations.Requirement;
30  import org.codehaus.plexus.logging.Logger;
31  
32  import java.io.File;
33  import java.io.FileOutputStream;
34  import java.io.IOException;
35  import java.util.Enumeration;
36  import java.util.List;
37  import java.util.Set;
38  import java.util.regex.Pattern;
39  import java.util.zip.ZipEntry;
40  import java.util.zip.ZipException;
41  import java.util.zip.ZipFile;
42  import java.util.zip.ZipOutputStream;
43  
44  
45  /**
46   * Adds classes from AAR and APK dependencies to the project compile classpath.
47   * 
48   * @author William Ferguson
49   * @author Benoit Billington
50   * @author Manfred Moser
51   */
52  @Component( role = AbstractMavenLifecycleParticipant.class, hint = "default" )
53  public final class ClasspathModifierLifecycleParticipant extends AbstractMavenLifecycleParticipant
54  {
55      /** 
56       * Mojo configuration parameter to determine if jar files found inside an apklib are 
57       * pulled onto the classpath and into the resulting apk, defaults to false
58       * @see INCLUDE_FROM_APKLIB_DEFAULT
59       */
60      private static final String INCLUDE_FROM_APKLIB_PARAM = "includeLibsJarsFromApklib";
61      /** 
62       * Mojo configuration parameter to determine if jar files found inside an aar are 
63       * pulled onto the classpath and into the resulting apk, defaults to false
64       * @see INCLUDE_FROM_AAR_DEFAULT
65       */
66      private static final String INCLUDE_FROM_AAR_PARAM = "includeLibsJarsFromAar";
67      /**
68       * Mojo configuration parameter to determine if we should warn about dependency conflicts with the provided
69       * dependencies.
70       * 
71       * @see DISABLE_CONFLICTING_DEPENDENCIES_WARNING_DEFAULT
72       */
73      private static final String DISABLE_CONFLICTING_DEPENDENCIES_WARNING_PARAM
74          = "disableConflictingDependenciesWarning";
75      private static final boolean INCLUDE_FROM_APKLIB_DEFAULT = false;
76      private static final boolean INCLUDE_FROM_AAR_DEFAULT = true;
77      private static final boolean DISABLE_CONFLICTING_DEPENDENCIES_WARNING_DEFAULT = false;
78  
79      /**
80       * Mojo configuration parameter that defines where AAR files should be unpacked.
81       * Default is /target/unpacked-libs
82       */
83      private static final String UNPACKED_LIBS_FOLDER_PARAM = "unpackedLibsFolder";
84  
85      @Requirement
86      private ArtifactResolver artifactResolver;
87  
88      @Requirement( hint = "default" )
89      private DependencyGraphBuilder dependencyGraphBuilder;
90  
91      @Requirement
92      private Logger log;
93      
94      private boolean addedJarFromLibs = false;
95      
96      @Override
97      public void afterProjectsRead( MavenSession session ) throws MavenExecutionException
98      {
99          log.debug( "" );
100         log.debug( "ClasspathModifierLifecycleParticipant#afterProjectsRead - start" );
101         log.debug( "" );
102 
103         log.debug( "CurrentProject=" + session.getCurrentProject() );
104         final List<MavenProject> projects = session.getProjects();
105         final DependencyResolver dependencyResolver = new DependencyResolver( log, dependencyGraphBuilder );
106         final ArtifactResolverHelper artifactResolverHelper = new ArtifactResolverHelper( artifactResolver, log );
107 
108         for ( MavenProject project : projects )
109         {
110             log.debug( "" );
111             log.debug( "project=" + project.getArtifact() );
112 
113             if ( ! AndroidExtension.isAndroidPackaging( project.getPackaging() ) )
114             {
115                 continue; // do not modify classpath if not an android project.
116             }
117 
118             final String unpackedLibsFolder
119                 = getMojoConfigurationParameter( project, UNPACKED_LIBS_FOLDER_PARAM, null );
120             final UnpackedLibHelper helper = new UnpackedLibHelper( artifactResolverHelper, project, log,
121                     unpackedLibsFolder == null ? null : new File( unpackedLibsFolder )
122             );
123 
124             final Set<Artifact> artifacts;
125 
126             // If there is an extension ClassRealm loaded for this project then use that
127             // as the ContextClassLoader so that Wagon extensions can be used to resolves dependencies.
128             final ClassLoader projectClassLoader = ( project.getClassRealm() != null )
129                     ? project.getClassRealm()
130                     : Thread.currentThread().getContextClassLoader();
131 
132             final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
133             try
134             {
135                 Thread.currentThread().setContextClassLoader( projectClassLoader );
136                 artifacts = dependencyResolver.getProjectDependenciesFor( project, session );
137             }
138             catch ( DependencyGraphBuilderException e )
139             {
140                 // Nothing to do. The resolution failure will be displayed by the standard resolution mechanism.
141                 continue;
142             }
143             finally
144             {
145                 Thread.currentThread().setContextClassLoader( originalClassLoader );
146             }
147 
148             boolean includeFromAar = getMojoConfigurationParameter( project, INCLUDE_FROM_AAR_PARAM,
149                     INCLUDE_FROM_AAR_DEFAULT );
150             boolean includeFromApklib = getMojoConfigurationParameter( project, INCLUDE_FROM_APKLIB_PARAM,
151                     INCLUDE_FROM_APKLIB_DEFAULT );
152             boolean disableConflictingDependenciesWarning = getMojoConfigurationParameter( project,
153                     DISABLE_CONFLICTING_DEPENDENCIES_WARNING_PARAM, DISABLE_CONFLICTING_DEPENDENCIES_WARNING_DEFAULT );
154 
155             log.debug( "projects deps: : " + artifacts );
156             
157             if ( !disableConflictingDependenciesWarning )
158             {
159                 ProvidedDependencyChecker checker = new ProvidedDependencyChecker();
160                 checker.checkProvidedDependencies( artifacts, log );
161             }
162             
163             for ( Artifact artifact : artifacts )
164             {
165                 final String type = artifact.getType();
166                 if ( type.equals( AndroidExtension.AAR ) )
167                 {
168                     // An AAR lib contains a classes jar that needs to be added to the classpath.
169                     // Create a placeholder classes.jar and add it to the compile classpath.
170                     // It will replaced with the real classes.jar by GenerateSourcesMojo.
171                     addClassesToClasspath( helper, project, artifact );
172 
173                     // An AAR may also contain zero or more internal libs in the libs folder.
174                     // If 'includeLibsJarsFromAar' config param is true then include them too.
175                     if ( includeFromAar )
176                     {
177                         // Add jar files in 'libs' into classpath.
178                         addLibsJarsToClassPath( helper, project, artifact );
179                     }
180                 }
181                 else if ( type.equals( AndroidExtension.APK ) )
182                 {
183                     // The only time that an APK will likely be a dependency is when this an an APK test project.
184                     // So add a placeholder (we cannot resolve the actual dep pre build) to the compile classpath.
185                     // The placeholder will be replaced with the real APK jar later.
186                     addClassesToClasspath( helper, project, artifact );
187                 }
188                 else if ( type.equals( AndroidExtension.APKLIB ) )
189                 {
190                     if ( includeFromApklib ) 
191                     {
192                       // Add jar files in 'libs' into classpath.
193                       addLibsJarsToClassPath( helper, project, artifact );
194                     }
195                 }
196             }
197         }
198 
199         if ( addedJarFromLibs )
200         {
201             log.warn(
202                     "Transitive dependencies should really be provided by Maven dependency management.\n"
203         + "          We suggest you to ask the above providers to package their component properly.\n"
204         + "          Things may break at compile and/or runtime due to multiple copies of incompatible libraries." );
205         }
206         log.debug( "" );
207         log.debug( "ClasspathModifierLifecycleParticipant#afterProjectsRead - finish" );
208     }
209 
210     private String getMojoConfigurationParameter( MavenProject project, String name, String defaultValue )
211     {
212         String value = PomConfigurationHelper.getPluginConfigParameter( project,
213                 name, defaultValue );
214         log.debug( name + " set to " + value );
215         return value;
216     }
217     
218     private boolean getMojoConfigurationParameter( MavenProject project, String name, boolean defaultValue )
219     {
220         return Boolean.valueOf( getMojoConfigurationParameter( project, name, Boolean.toString( defaultValue ) ) );
221     }
222 
223     /**
224      * Add jar files in libs into the project classpath.
225      */
226     private void addLibsJarsToClassPath( UnpackedLibHelper helper, MavenProject project, Artifact artifact )
227         throws MavenExecutionException
228     {
229          try
230          {
231              final File unpackLibFolder = helper.getUnpackedLibFolder( artifact );
232              final File artifactFile = helper.getArtifactToFile( artifact );
233              ZipFile zipFile = new ZipFile( artifactFile );
234              Enumeration enumeration = zipFile.entries();
235              while ( enumeration.hasMoreElements() )
236              {
237                  ZipEntry entry = ( ZipEntry )  enumeration.nextElement();
238                  String entryName = entry.getName();
239 
240                  // Only jar files under 'libs' directory to be processed.
241                  if ( Pattern.matches( "^libs/.+\\.jar$", entryName ) )
242                  {
243                      final File libsJarFile = new File( unpackLibFolder, entryName );
244                      log.warn( "Adding jar from libs folder to classpath: " + libsJarFile );
245 
246                      // In order to satisfy the LifecycleDependencyResolver on execution up to a phase that
247                      // has a Mojo requiring dependency resolution I need to create a dummy classesJar here.
248                      if ( !libsJarFile.getParentFile().exists() )
249                      {
250                          libsJarFile.getParentFile().mkdirs();
251                      }
252                      libsJarFile.createNewFile();
253 
254                      // Add the jar to the classpath.
255                      final Dependency dependency =
256                             createSystemScopeDependency( artifact, libsJarFile, libsJarFile.getName() );
257 
258                      project.getModel().addDependency( dependency );
259                      addedJarFromLibs = true;
260                  }
261              }
262          }
263          catch ( MojoExecutionException e )
264          {
265              log.debug( "Error extract jars" );
266          }
267          catch ( ZipException e )
268          {
269              log.debug( "Error" );
270          }
271          catch ( IOException e )
272          {
273              log.debug( "Error" );
274          }
275     }
276 
277     /**
278      * Add the dependent library classes to the project classpath.
279      */
280     private void addClassesToClasspath( UnpackedLibHelper helper, MavenProject project, Artifact artifact )
281         throws MavenExecutionException
282     {
283         // Work out where the dep will be extracted and calculate the file path to the classes jar.
284         // This is location where the GenerateSourcesMojo will extract the classes.
285         final File classesJar = helper.getUnpackedClassesJar( artifact );
286         log.debug( "Adding to classpath : " + classesJar );
287 
288         // In order to satisfy the LifecycleDependencyResolver on execution up to a phase that
289         // has a Mojo requiring dependency resolution I need to create a dummy classesJar here.
290         if ( !classesJar.exists() )
291         {
292             classesJar.getParentFile().mkdirs();
293             try
294             {
295                 final ZipOutputStream zipOutputStream = new ZipOutputStream( new FileOutputStream( classesJar ) );
296                 zipOutputStream.putNextEntry( new ZipEntry( "dummy" ) );
297                 zipOutputStream.close();
298                 log.debug( "Created dummy " + classesJar.getName() + " exist=" + classesJar.exists() );
299             }
300             catch ( IOException e )
301             {
302                 throw new MavenExecutionException( "Could not add " + classesJar.getName() + " as dependency", e );
303             }
304         }
305 
306         
307         // Modify the classpath to use an extracted dex file.  This will overwrite
308         // any exisiting dependencies with the same information.
309         final Dependency dependency = createSystemScopeDependency( artifact, classesJar, null );
310         final Dependency providedJar = findProvidedDependencies( dependency, project );
311         if ( providedJar != null ) 
312         {
313             project.getModel().removeDependency( providedJar );
314         }
315         project.getModel().addDependency( dependency );
316     }
317 
318     private Dependency createSystemScopeDependency( Artifact artifact, File location, String suffix )
319     {
320         String artifactId = artifact.getArtifactId();
321         if ( suffix != null )
322         {
323             artifactId += "_" + suffix;
324         }
325         final Dependency dependency = new Dependency();
326         dependency.setGroupId( artifact.getGroupId() );
327         dependency.setArtifactId( artifactId );
328         dependency.setVersion( artifact.getBaseVersion() );
329         dependency.setScope( Artifact.SCOPE_SYSTEM );
330         dependency.setSystemPath( location.getAbsolutePath() );
331         return dependency;
332     }
333     
334     private Dependency findProvidedDependencies( Dependency dexDependency, MavenProject project ) 
335     {
336         for ( Dependency dependency : project.getDependencies() ) 
337         {
338             if ( dependency.getScope().equals( Artifact.SCOPE_PROVIDED ) ) 
339             {
340                 if ( dependency.getArtifactId().equals( dexDependency.getArtifactId() ) 
341                         && dependency.getGroupId().equals( dexDependency.getGroupId() ) 
342                         && dependency.getType().equals( dexDependency.getType() ) 
343                         && dependency.getVersion().equals( dexDependency.getVersion() ) ) 
344                 {
345                     return dependency;
346                 }
347             }
348         }
349         return null;
350         
351     }
352     
353 }
354