1 package org.apache.maven.plugins.shade.filter;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
63
64
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
74
75
76
77 public MinijarFilter( MavenProject project, Log log )
78 throws IOException
79 {
80 this( project, log, Collections.<SimpleFilter>emptyList() );
81 }
82
83
84
85
86
87
88
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
133
134
135
136
137
138 for ( final String fileName : project.getRuntimeClasspathElements() )
139 {
140
141 if ( fileName.equals( project.getBuild().getOutputDirectory() ) )
142 {
143 continue;
144 }
145 if ( removeServicesFromJar( cp, neededClasses, fileName ) )
146 {
147 repeatScan = true;
148 }
149 }
150 }
151 catch ( final DependencyResolutionRequiredException e )
152 {
153 log.warn( e.getMessage() );
154 }
155 }
156 while ( repeatScan );
157 }
158
159 private boolean removeServicesFromJar( Clazzpath cp, Set<Clazz> neededClasses, String fileName )
160 {
161 boolean repeatScan = false;
162 try ( final JarFile jar = new JarFile( fileName ) )
163 {
164 for ( final Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements(); )
165 {
166 final JarEntry jarEntry = entries.nextElement();
167 if ( jarEntry.isDirectory() || !jarEntry.getName().startsWith( "META-INF/services/" ) )
168 {
169 continue;
170 }
171
172 final String serviceClassName = jarEntry.getName().substring( "META-INF/services/".length() );
173 final boolean isNeededClass = neededClasses.contains( cp.getClazz( serviceClassName ) );
174 if ( !isNeededClass )
175 {
176 continue;
177 }
178
179 try ( final BufferedReader configFileReader = new BufferedReader(
180 new InputStreamReader( jar.getInputStream( jarEntry ), UTF_8 ) ) )
181 {
182
183 repeatScan = scanServiceProviderConfigFile( cp, configFileReader );
184 }
185 catch ( final IOException e )
186 {
187 log.warn( e.getMessage() );
188 }
189 }
190 }
191 catch ( final IOException e )
192 {
193 log.warn( "Not a JAR file candidate. Ignoring classpath element '" + fileName + "' (" + e + ")." );
194 }
195 return repeatScan;
196 }
197
198 private boolean scanServiceProviderConfigFile( Clazzpath cp, BufferedReader configFileReader ) throws IOException
199 {
200 boolean serviceClassFound = false;
201 for ( String line = configFileReader.readLine(); line != null; line = configFileReader.readLine() )
202 {
203 final String className = line.split( "#", 2 )[0].trim();
204 if ( className.isEmpty() )
205 {
206 continue;
207 }
208
209 final Clazz clazz = cp.getClazz( className );
210 if ( clazz == null || !removable.contains( clazz ) )
211 {
212 continue;
213 }
214
215 log.debug( className + " was not removed because it is a service" );
216 removeClass( clazz );
217 serviceClassFound = true;
218 }
219 return serviceClassFound;
220 }
221
222 private void removeClass( final Clazz clazz )
223 {
224 removable.remove( clazz );
225 removable.removeAll( clazz.getTransitiveDependencies() );
226 }
227
228 private ClazzpathUnit addDependencyToClasspath( Clazzpath cp, Artifact dependency )
229 throws IOException
230 {
231 ClazzpathUnit clazzpathUnit = null;
232 try ( InputStream is = new FileInputStream( dependency.getFile() ) )
233 {
234 clazzpathUnit = cp.addClazzpathUnit( is, dependency.toString() );
235 }
236 catch ( ZipException e )
237 {
238 log.warn( dependency.getFile()
239 + " could not be unpacked/read for minimization; dependency is probably malformed." );
240 IOException ioe = new IOException( "Dependency " + dependency.toString() + " in file "
241 + dependency.getFile() + " could not be unpacked. File is probably corrupt", e );
242 throw ioe;
243 }
244 catch ( ArrayIndexOutOfBoundsException | IllegalArgumentException e )
245 {
246
247 log.warn( dependency.toString()
248 + " could not be analyzed for minimization; dependency is probably malformed." );
249 }
250
251 return clazzpathUnit;
252 }
253
254 private void removePackages( ClazzpathUnit artifactUnit )
255 {
256 Set<String> packageNames = new HashSet<>();
257 removePackages( artifactUnit.getClazzes(), packageNames );
258 removePackages( artifactUnit.getTransitiveDependencies(), packageNames );
259 }
260
261 private void removePackages( Set<Clazz> clazzes, Set<String> packageNames )
262 {
263 for ( Clazz clazz : clazzes )
264 {
265 String name = clazz.getName();
266 while ( name.contains( "." ) )
267 {
268 name = name.substring( 0, name.lastIndexOf( '.' ) );
269 if ( packageNames.add( name ) )
270 {
271 removable.remove( new Clazz( name + ".package-info" ) );
272 }
273 }
274 }
275 }
276
277 private void removeSpecificallyIncludedClasses( MavenProject project, List<SimpleFilter> simpleFilters )
278 throws IOException
279 {
280
281 Clazzpath checkCp = new Clazzpath();
282 for ( Artifact dependency : project.getArtifacts() )
283 {
284 File jar = dependency.getFile();
285
286 for ( SimpleFilter simpleFilter : simpleFilters )
287 {
288 if ( simpleFilter.canFilter( jar ) )
289 {
290 ClazzpathUnit depClazzpathUnit = addDependencyToClasspath( checkCp, dependency );
291 if ( depClazzpathUnit != null )
292 {
293 Set<Clazz> clazzes = depClazzpathUnit.getClazzes();
294 for ( final Clazz clazz : new HashSet<>( removable ) )
295 {
296 if ( clazzes.contains( clazz )
297 && simpleFilter.isSpecificallyIncluded( clazz.getName().replace( '.', '/' ) ) )
298 {
299 log.debug( clazz.getName() + " not removed because it was specifically included" );
300 removeClass( clazz );
301 }
302 }
303 }
304 }
305 }
306 }
307 }
308
309 @Override
310 public boolean canFilter( File jar )
311 {
312 return true;
313 }
314
315 @Override
316 public boolean isFiltered( String classFile )
317 {
318 String className = classFile.replace( '/', '.' ).replaceFirst( "\\.class$", "" );
319 Clazz clazz = new Clazz( className );
320
321 if ( removable != null && removable.contains( clazz ) )
322 {
323 log.debug( "Removing " + className );
324 classesRemoved += 1;
325 return true;
326 }
327
328 classesKept += 1;
329 return false;
330 }
331
332 @Override
333 public void finished()
334 {
335 int classesTotal = classesRemoved + classesKept;
336 if ( classesTotal != 0 )
337 {
338 log.info( "Minimized " + classesTotal + " -> " + classesKept + " (" + 100 * classesKept / classesTotal
339 + "%)" );
340 }
341 else
342 {
343 log.info( "Minimized " + classesTotal + " -> " + classesKept );
344 }
345 }
346 }