1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
61
62
63 MinijarFilter(int classesKept, int classesRemoved, Log log) {
64 this.classesKept = classesKept;
65 this.classesRemoved = classesRemoved;
66 this.log = log;
67 }
68
69
70
71
72
73
74 public MinijarFilter(MavenProject project, Log log) throws IOException {
75 this(project, log, Collections.<SimpleFilter>emptyList(), Collections.<String>emptySet());
76 }
77
78
79
80
81
82
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
90
91
92
93
94
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
158
159
160
161
162
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
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
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
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
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 }