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