View Javadoc
1   package org.apache.maven.plugins.shade.filter;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.artifact.Artifact;
23  import org.apache.maven.artifact.DependencyResolutionRequiredException;
24  import org.apache.maven.plugin.logging.Log;
25  import org.apache.maven.project.MavenProject;
26  import org.vafer.jdependency.Clazz;
27  import org.vafer.jdependency.Clazzpath;
28  import org.vafer.jdependency.ClazzpathUnit;
29  
30  import static java.nio.charset.StandardCharsets.UTF_8;
31  
32  import java.io.BufferedReader;
33  import java.io.File;
34  import java.io.FileInputStream;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.io.InputStreamReader;
38  import java.util.Collections;
39  import java.util.Enumeration;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Set;
43  import java.util.jar.JarEntry;
44  import java.util.jar.JarFile;
45  import java.util.zip.ZipException;
46  
47  /**
48   * A filter that prevents the inclusion of classes not required in the final jar.
49   */
50  public class MinijarFilter
51      implements Filter
52  {
53  
54      private Log log;
55  
56      private Set<Clazz> removable;
57  
58      private int classesKept;
59  
60      private int classesRemoved;
61  
62      //[MSHADE-209] This is introduced only for testing purposes which shows
63      // there is something wrong with the design of this class. (SoC?)
64      // unfortunately i don't have a better idea at the moment.
65      MinijarFilter( int classesKept, int classesRemoved, Log log )
66      {
67          this.classesKept = classesKept;
68          this.classesRemoved = classesRemoved;
69          this.log = log;
70      }
71  
72      /**
73       * @param project {@link MavenProject}
74       * @param log {@link Log}
75       * @throws IOException in case of error.
76       */
77      public MinijarFilter( MavenProject project, Log log )
78          throws IOException
79      {
80          this( project, log, Collections.<SimpleFilter>emptyList() );
81      }
82  
83      /**
84       * @param project {@link MavenProject}
85       * @param log {@link Log}
86       * @param simpleFilters {@link SimpleFilter}
87       * @throws IOException in case of errors.
88       * @since 1.6
89       */
90      public MinijarFilter( MavenProject project, Log log, List<SimpleFilter> simpleFilters )
91          throws IOException
92      {
93        this.log = log;
94  
95        File artifactFile = project.getArtifact().getFile();
96  
97          if ( artifactFile != null )
98          {
99            Clazzpath cp = new Clazzpath();
100 
101           ClazzpathUnit artifactUnit = cp.addClazzpathUnit( new FileInputStream( artifactFile ), project.toString() );
102 
103             for ( Artifact dependency : project.getArtifacts() )
104             {
105                 addDependencyToClasspath( cp, dependency );
106             }
107 
108             removable = cp.getClazzes();
109             if ( removable.remove( new Clazz( "module-info" ) ) )
110             {
111                 log.warn( "Removing module-info from " + artifactFile.getName() );
112             }
113             removePackages( artifactUnit );
114             removable.removeAll( artifactUnit.getClazzes() );
115             removable.removeAll( artifactUnit.getTransitiveDependencies() );
116             removeSpecificallyIncludedClasses( project,
117                 simpleFilters == null ? Collections.<SimpleFilter>emptyList() : simpleFilters );
118             removeServices( project, cp );
119         }
120     }
121 
122     private void removeServices( final MavenProject project, final Clazzpath cp )
123     {
124         boolean repeatScan;
125         do
126         {
127             repeatScan = false;
128             final Set<Clazz> neededClasses = cp.getClazzes();
129             neededClasses.removeAll( removable );
130             try
131             {
132                 // getRuntimeClasspathElements returns a list of
133                 //  - the build output directory
134                 //  - all the paths to the dependencies' jars
135                 // We thereby need to ignore the build directory because we don't want
136                 // to remove anything from it, as it's the starting point of the
137                 // minification process.
138                 for ( final String fileName : project.getRuntimeClasspathElements() )
139                 {
140                     // Ignore the build directory from this project
141                     if ( fileName.equals( project.getBuild().getOutputDirectory() ) )
142                     {
143                         continue;
144                     }
145                     if ( removeServicesFromJar( cp, neededClasses, fileName ) )
146                     {
147                         repeatScan = true;
148                     }
149                 }
150             }
151             catch ( final DependencyResolutionRequiredException e )
152             {
153                 log.warn( e.getMessage() );
154             }
155         }
156         while ( repeatScan );
157     }
158 
159     private boolean removeServicesFromJar( Clazzpath cp, Set<Clazz> neededClasses, String fileName )
160     {
161         boolean repeatScan = false;
162         try ( final JarFile jar = new JarFile( fileName ) )
163         {
164             for ( final Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements(); )
165             {
166                 final JarEntry jarEntry = entries.nextElement();
167                 if ( jarEntry.isDirectory() || !jarEntry.getName().startsWith( "META-INF/services/" ) )
168                 {
169                     continue;
170                 }
171 
172                 final String serviceClassName = jarEntry.getName().substring( "META-INF/services/".length() );
173                 final boolean isNeededClass = neededClasses.contains( cp.getClazz( serviceClassName ) );
174                 if ( !isNeededClass )
175                 {
176                     continue;
177                 }
178 
179                 try ( final BufferedReader configFileReader = new BufferedReader(
180                         new InputStreamReader( jar.getInputStream( jarEntry ), UTF_8 ) ) )
181                 {
182                     // check whether the found classes use services in turn
183                     repeatScan = scanServiceProviderConfigFile( cp, configFileReader );
184                 }
185                 catch ( final IOException e )
186                 {
187                     log.warn( e.getMessage() );
188                 }
189             }
190         }
191         catch ( final IOException e )
192         {
193             log.warn( "Not a JAR file candidate. Ignoring classpath element '" + fileName + "' (" + e + ")." );
194         }
195         return repeatScan;
196     }
197 
198     private boolean scanServiceProviderConfigFile( Clazzpath cp, BufferedReader configFileReader ) throws IOException
199     {
200         boolean serviceClassFound = false;
201         for ( String line = configFileReader.readLine(); line != null; line = configFileReader.readLine() )
202         {
203             final String className = line.split( "#", 2 )[0].trim();
204             if ( className.isEmpty() )
205             {
206                 continue;
207             }
208 
209             final Clazz clazz = cp.getClazz( className );
210             if ( clazz == null || !removable.contains( clazz ) )
211             {
212                 continue;
213             }
214 
215             log.debug( className + " was not removed because it is a service" );
216             removeClass( clazz );
217             serviceClassFound = true;
218         }
219         return serviceClassFound;
220     }
221 
222     private void removeClass( final Clazz clazz )
223     {
224         removable.remove( clazz );
225         removable.removeAll( clazz.getTransitiveDependencies() );
226     }
227 
228     private ClazzpathUnit addDependencyToClasspath( Clazzpath cp, Artifact dependency )
229         throws IOException
230     {
231         ClazzpathUnit clazzpathUnit = null;
232         try ( InputStream is = new FileInputStream( dependency.getFile() ) )
233         {
234             clazzpathUnit = cp.addClazzpathUnit( is, dependency.toString() );
235         }
236         catch ( ZipException e )
237         {
238             log.warn( dependency.getFile()
239                 + " could not be unpacked/read for minimization; dependency is probably malformed." );
240             IOException ioe = new IOException( "Dependency " + dependency.toString() + " in file "
241                 + dependency.getFile() + " could not be unpacked. File is probably corrupt", e );
242             throw ioe;
243         }
244         catch ( ArrayIndexOutOfBoundsException | IllegalArgumentException e )
245         {
246             // trap ArrayIndexOutOfBoundsExceptions caused by malformed dependency classes (MSHADE-107)
247             log.warn( dependency.toString()
248                 + " could not be analyzed for minimization; dependency is probably malformed." );
249         }
250 
251         return clazzpathUnit;
252     }
253 
254     private void removePackages( ClazzpathUnit artifactUnit )
255     {
256         Set<String> packageNames = new HashSet<>();
257         removePackages( artifactUnit.getClazzes(), packageNames );
258         removePackages( artifactUnit.getTransitiveDependencies(), packageNames );
259     }
260 
261     private void removePackages( Set<Clazz> clazzes, Set<String> packageNames )
262     {
263         for ( Clazz clazz : clazzes )
264         {
265             String name = clazz.getName();
266             while ( name.contains( "." ) )
267             {
268                 name = name.substring( 0, name.lastIndexOf( '.' ) );
269                 if ( packageNames.add( name ) )
270                 {
271                     removable.remove( new Clazz( name + ".package-info" ) );
272                 }
273             }
274         }
275     }
276 
277     private void removeSpecificallyIncludedClasses( MavenProject project, List<SimpleFilter> simpleFilters )
278         throws IOException
279     {
280         // remove classes specifically included in filters
281         Clazzpath checkCp = new Clazzpath();
282         for ( Artifact dependency : project.getArtifacts() )
283         {
284             File jar = dependency.getFile();
285 
286             for ( SimpleFilter simpleFilter : simpleFilters )
287             {
288                 if ( simpleFilter.canFilter( jar ) )
289                 {
290                     ClazzpathUnit depClazzpathUnit = addDependencyToClasspath( checkCp, dependency );
291                     if ( depClazzpathUnit != null )
292                     {
293                         Set<Clazz> clazzes = depClazzpathUnit.getClazzes();
294                         for ( final Clazz clazz : new HashSet<>( removable ) )
295                         {
296                             if ( clazzes.contains( clazz ) //
297                                 && simpleFilter.isSpecificallyIncluded( clazz.getName().replace( '.', '/' ) ) )
298                             {
299                                 log.debug( clazz.getName() + " not removed because it was specifically included" );
300                                 removeClass( clazz );
301                             }
302                         }
303                     }
304                 }
305             }
306         }
307     }
308 
309     @Override
310     public boolean canFilter( File jar )
311     {
312         return true;
313     }
314 
315     @Override
316     public boolean isFiltered( String classFile )
317     {
318         String className = classFile.replace( '/', '.' ).replaceFirst( "\\.class$", "" );
319         Clazz clazz = new Clazz( className );
320 
321         if ( removable != null && removable.contains( clazz ) )
322         {
323             log.debug( "Removing " + className );
324             classesRemoved += 1;
325             return true;
326         }
327 
328         classesKept += 1;
329         return false;
330     }
331 
332     @Override
333     public void finished()
334     {
335         int classesTotal = classesRemoved + classesKept;
336         if ( classesTotal != 0 )
337         {
338             log.info( "Minimized " + classesTotal + " -> " + classesKept + " (" + 100 * classesKept / classesTotal
339                 + "%)" );
340         }
341         else
342         {
343             log.info( "Minimized " + classesTotal + " -> " + classesKept );
344         }
345     }
346 }