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