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.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
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
60
61
62 MinijarFilter(int classesKept, int classesRemoved, Log log) {
63 this.classesKept = classesKept;
64 this.classesRemoved = classesRemoved;
65 this.log = log;
66 }
67
68
69
70
71
72
73 public MinijarFilter(MavenProject project, Log log) throws IOException {
74 this(project, log, Collections.<SimpleFilter>emptyList(), Collections.<String>emptySet());
75 }
76
77
78
79
80
81
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
89
90
91
92
93
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
156
157
158
159
160
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
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
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
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
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 }