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