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                 for ( final String fileName : project.getRuntimeClasspathElements() )
133                 {
134                     try ( final JarFile jar = new JarFile( fileName ) )
135                     {
136                         for ( final Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements(); )
137                         {
138                             final JarEntry jarEntry = entries.nextElement();
139                             if ( jarEntry.isDirectory() || !jarEntry.getName().startsWith( "META-INF/services/" ) )
140                             {
141                                 continue;
142                             }
143 
144                             final String serviceClassName =
145                               jarEntry.getName().substring( "META-INF/services/".length() );
146                             final boolean isNeededClass = neededClasses.contains( cp.getClazz( serviceClassName ) );
147                             if ( !isNeededClass )
148                             {
149                                 continue;
150                             }
151 
152                             try ( final BufferedReader bufferedReader =
153                             new BufferedReader( new InputStreamReader( jar.getInputStream( jarEntry ), UTF_8 ) ) )
154                             {
155                                 for ( String line = bufferedReader.readLine(); line != null;
156                                     line = bufferedReader.readLine() )
157                                 {
158                                     final String className = line.split( "#", 2 )[0].trim();
159                                     if ( className.isEmpty() )
160                                     {
161                                         continue;
162                                     }
163 
164                                     final Clazz clazz = cp.getClazz( className );
165                                     if ( clazz == null || !removable.contains( clazz ) )
166                                     {
167                                         continue;
168                                     }
169 
170                                     log.debug( className + " was not removed because it is a service" );
171                                     removeClass( clazz );
172                                     repeatScan = true; // check whether the found classes use services in turn
173                                 }
174                             }
175                             catch ( final IOException e )
176                             {
177                                 log.warn( e.getMessage() );
178                             }
179                         }
180                     }
181                     catch ( final IOException e )
182                     {
183                         log.warn( e.getMessage() );
184                     }
185                 }
186             }
187             catch ( final DependencyResolutionRequiredException e )
188             {
189                 log.warn( e.getMessage() );
190             }
191         }
192         while ( repeatScan );
193     }
194 
195     private void removeClass( final Clazz clazz )
196     {
197         removable.remove( clazz );
198         removable.removeAll( clazz.getTransitiveDependencies() );
199     }
200 
201     private ClazzpathUnit addDependencyToClasspath( Clazzpath cp, Artifact dependency )
202         throws IOException
203     {
204         ClazzpathUnit clazzpathUnit = null;
205         try ( InputStream is = new FileInputStream( dependency.getFile() ) )
206         {
207             clazzpathUnit = cp.addClazzpathUnit( is, dependency.toString() );
208         }
209         catch ( ZipException e )
210         {
211             log.warn( dependency.getFile()
212                 + " could not be unpacked/read for minimization; dependency is probably malformed." );
213             IOException ioe = new IOException( "Dependency " + dependency.toString() + " in file "
214                 + dependency.getFile() + " could not be unpacked. File is probably corrupt", e );
215             throw ioe;
216         }
217         catch ( ArrayIndexOutOfBoundsException | IllegalArgumentException e )
218         {
219             // trap ArrayIndexOutOfBoundsExceptions caused by malformed dependency classes (MSHADE-107)
220             log.warn( dependency.toString()
221                 + " could not be analyzed for minimization; dependency is probably malformed." );
222         }
223 
224         return clazzpathUnit;
225     }
226 
227     private void removePackages( ClazzpathUnit artifactUnit )
228     {
229         Set<String> packageNames = new HashSet<>();
230         removePackages( artifactUnit.getClazzes(), packageNames );
231         removePackages( artifactUnit.getTransitiveDependencies(), packageNames );
232     }
233 
234     private void removePackages( Set<Clazz> clazzes, Set<String> packageNames )
235     {
236         for ( Clazz clazz : clazzes )
237         {
238             String name = clazz.getName();
239             while ( name.contains( "." ) )
240             {
241                 name = name.substring( 0, name.lastIndexOf( '.' ) );
242                 if ( packageNames.add( name ) )
243                 {
244                     removable.remove( new Clazz( name + ".package-info" ) );
245                 }
246             }
247         }
248     }
249 
250     private void removeSpecificallyIncludedClasses( MavenProject project, List<SimpleFilter> simpleFilters )
251         throws IOException
252     {
253         // remove classes specifically included in filters
254         Clazzpath checkCp = new Clazzpath();
255         for ( Artifact dependency : project.getArtifacts() )
256         {
257             File jar = dependency.getFile();
258 
259             for ( SimpleFilter simpleFilter : simpleFilters )
260             {
261                 if ( simpleFilter.canFilter( jar ) )
262                 {
263                     ClazzpathUnit depClazzpathUnit = addDependencyToClasspath( checkCp, dependency );
264                     if ( depClazzpathUnit != null )
265                     {
266                         Set<Clazz> clazzes = depClazzpathUnit.getClazzes();
267                         for ( final Clazz clazz : new HashSet<>( removable ) )
268                         {
269                             if ( clazzes.contains( clazz ) //
270                                 && simpleFilter.isSpecificallyIncluded( clazz.getName().replace( '.', '/' ) ) )
271                             {
272                                 log.debug( clazz.getName() + " not removed because it was specifically included" );
273                                 removeClass( clazz );
274                             }
275                         }
276                     }
277                 }
278             }
279         }
280     }
281 
282     @Override
283     public boolean canFilter( File jar )
284     {
285         return true;
286     }
287 
288     @Override
289     public boolean isFiltered( String classFile )
290     {
291         String className = classFile.replace( '/', '.' ).replaceFirst( "\\.class$", "" );
292         Clazz clazz = new Clazz( className );
293 
294         if ( removable != null && removable.contains( clazz ) )
295         {
296             log.debug( "Removing " + className );
297             classesRemoved += 1;
298             return true;
299         }
300 
301         classesKept += 1;
302         return false;
303     }
304 
305     @Override
306     public void finished()
307     {
308         int classesTotal = classesRemoved + classesKept;
309         if ( classesTotal != 0 )
310         {
311             log.info( "Minimized " + classesTotal + " -> " + classesKept + " (" + 100 * classesKept / classesTotal
312                 + "%)" );
313         }
314         else
315         {
316             log.info( "Minimized " + classesTotal + " -> " + classesKept );
317         }
318     }
319 }