View Javadoc

1   package org.apache.maven.plugins.shade;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.MojoExecutionException;
23  import org.apache.maven.plugins.shade.filter.Filter;
24  import org.apache.maven.plugins.shade.relocation.Relocator;
25  import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer;
26  import org.apache.maven.plugins.shade.resource.ResourceTransformer;
27  import org.codehaus.plexus.component.annotations.Component;
28  import org.codehaus.plexus.logging.AbstractLogEnabled;
29  import org.codehaus.plexus.util.IOUtil;
30  import org.objectweb.asm.ClassReader;
31  import org.objectweb.asm.ClassVisitor;
32  import org.objectweb.asm.ClassWriter;
33  import org.objectweb.asm.commons.Remapper;
34  import org.objectweb.asm.commons.RemappingClassAdapter;
35  
36  import java.io.File;
37  import java.io.FileOutputStream;
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.util.ArrayList;
41  import java.util.Enumeration;
42  import java.util.HashSet;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Set;
46  import java.util.jar.JarEntry;
47  import java.util.jar.JarFile;
48  import java.util.jar.JarOutputStream;
49  import java.util.regex.Matcher;
50  import java.util.regex.Pattern;
51  import java.util.zip.ZipException;
52  
53  /**
54   * @author Jason van Zyl
55   */
56  @Component( role = Shader.class, hint = "default" )
57  public class DefaultShader
58      extends AbstractLogEnabled
59      implements Shader
60  {
61  
62      public void shade( Set<File> jars, File uberJar, List<Filter> filters, List<Relocator> relocators,
63                         List<ResourceTransformer> resourceTransformers )
64          throws IOException, MojoExecutionException
65      {
66          Set resources = new HashSet();
67  
68          ResourceTransformer manifestTransformer = null;
69          List transformers = new ArrayList( resourceTransformers );
70          for ( Iterator<ResourceTransformer> it = transformers.iterator(); it.hasNext(); )
71          {
72              ResourceTransformer transformer = it.next();
73              if ( transformer instanceof ManifestResourceTransformer )
74              {
75                  manifestTransformer = transformer;
76                  it.remove();
77              }
78          }
79  
80          RelocatorRemapper remapper = new RelocatorRemapper( relocators );
81  
82          uberJar.getParentFile().mkdirs();
83          JarOutputStream jos = new JarOutputStream( new FileOutputStream( uberJar ) );
84  
85          if ( manifestTransformer != null )
86          {
87              for ( File jar : jars )
88              {
89                  JarFile jarFile = newJarFile( jar );
90                  for ( Enumeration en = jarFile.entries(); en.hasMoreElements(); )
91                  {
92                      JarEntry entry = (JarEntry) en.nextElement();
93                      String resource = entry.getName();
94                      if ( manifestTransformer.canTransformResource( resource ) )
95                      {
96                          resources.add( resource );
97                          manifestTransformer.processResource( resource, jarFile.getInputStream( entry ), relocators );
98                          break;
99                      }
100                 }
101             }
102             if ( manifestTransformer.hasTransformedResource() )
103             {
104                 manifestTransformer.modifyOutputStream( jos );
105             }
106         }
107 
108         for ( File jar : jars )
109         {
110 
111             getLogger().debug( "Processing JAR " + jar );
112 
113             List jarFilters = getFilters( jar, filters );
114 
115             JarFile jarFile = newJarFile( jar );
116 
117             for ( Enumeration<JarEntry> j = jarFile.entries(); j.hasMoreElements(); )
118             {
119                 JarEntry entry = j.nextElement();
120 
121                 String name = entry.getName();
122 
123                 if ( "META-INF/INDEX.LIST".equals( name ) )
124                 {
125                     // we cannot allow the jar indexes to be copied over or the
126                     // jar is useless. Ideally, we could create a new one
127                     // later
128                     continue;
129                 }
130 
131                 if ( !entry.isDirectory() && !isFiltered( jarFilters, name ) )
132                 {
133                     InputStream is = jarFile.getInputStream( entry );
134 
135                     String mappedName = remapper.map( name );
136 
137                     int idx = mappedName.lastIndexOf( '/' );
138                     if ( idx != -1 )
139                     {
140                         // make sure dirs are created
141                         String dir = mappedName.substring( 0, idx );
142                         if ( !resources.contains( dir ) )
143                         {
144                             addDirectory( resources, jos, dir );
145                         }
146                     }
147 
148                     if ( name.endsWith( ".class" ) )
149                     {
150                         addRemappedClass( remapper, jos, jar, name, is );
151                     }
152                     else
153                     {
154                         if ( !resourceTransformed( transformers, mappedName, is, relocators ) )
155                         {
156                             // Avoid duplicates that aren't accounted for by the resource transformers
157                             if ( resources.contains( mappedName ) )
158                             {
159                                 continue;
160                             }
161 
162                             addResource( resources, jos, mappedName, is );
163                         }
164                     }
165 
166                     IOUtil.close( is );
167                 }
168             }
169 
170             jarFile.close();
171         }
172 
173         for ( Iterator i = transformers.iterator(); i.hasNext(); )
174         {
175             ResourceTransformer transformer = (ResourceTransformer) i.next();
176 
177             if ( transformer.hasTransformedResource() )
178             {
179                 transformer.modifyOutputStream( jos );
180             }
181         }
182 
183         IOUtil.close( jos );
184 
185         for ( Filter filter : filters )
186         {
187             filter.finished();
188         }
189     }
190 
191     private JarFile newJarFile( File jar )
192         throws IOException
193     {
194         try
195         {
196             return new JarFile( jar );
197         }
198         catch ( ZipException zex )
199         {
200             // JarFile is not very verbose and doesn't tell the user which file it was
201             // so we will create a new Exception instead
202             throw new ZipException( "error in opening zip file " + jar );
203         }
204     }
205 
206     private List<Filter> getFilters( File jar, List<Filter> filters )
207     {
208         List<Filter> list = new ArrayList<Filter>();
209 
210         for ( Filter filter : filters )
211         {
212             if ( filter.canFilter( jar ) )
213             {
214                 list.add( filter );
215             }
216 
217         }
218 
219         return list;
220     }
221 
222     private void addDirectory( Set resources, JarOutputStream jos, String name )
223         throws IOException
224     {
225         if ( name.lastIndexOf( '/' ) > 0 )
226         {
227             String parent = name.substring( 0, name.lastIndexOf( '/' ) );
228             if ( !resources.contains( parent ) )
229             {
230                 addDirectory( resources, jos, parent );
231             }
232         }
233 
234         // directory entries must end in "/"
235         JarEntry entry = new JarEntry( name + "/" );
236         jos.putNextEntry( entry );
237 
238         resources.add( name );
239     }
240 
241     private void addRemappedClass( RelocatorRemapper remapper, JarOutputStream jos, File jar, String name,
242                                    InputStream is )
243         throws IOException, MojoExecutionException
244     {
245         if ( !remapper.hasRelocators() )
246         {
247             try
248             {
249                 jos.putNextEntry( new JarEntry( name ) );
250                 IOUtil.copy( is, jos );
251             }
252             catch ( ZipException e )
253             {
254                 getLogger().warn( "We have a duplicate " + name + " in " + jar );
255             }
256 
257             return;
258         }
259 
260         ClassReader cr = new ClassReader( is );
261 
262         // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
263         // Copying the original constant pool should be avoided because it would keep references
264         // to the original class names. This is not a problem at runtime (because these entries in the
265         // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin
266         // that use the constant pool to determine the dependencies of a class.
267         ClassWriter cw = new ClassWriter( 0 );
268 
269         ClassVisitor cv = new RemappingClassAdapter( cw, remapper );
270 
271         try
272         {
273             cr.accept( cv, ClassReader.EXPAND_FRAMES );
274         }
275         catch ( Throwable ise )
276         {
277             throw new MojoExecutionException( "Error in ASM processing class " + name, ise );
278         }
279 
280         byte[] renamedClass = cw.toByteArray();
281 
282         // Need to take the .class off for remapping evaluation
283         String mappedName = remapper.map( name.substring( 0, name.indexOf( '.' ) ) );
284 
285         try
286         {
287             // Now we put it back on so the class file is written out with the right extension.
288             jos.putNextEntry( new JarEntry( mappedName + ".class" ) );
289 
290             IOUtil.copy( renamedClass, jos );
291         }
292         catch ( ZipException e )
293         {
294             getLogger().warn( "We have a duplicate " + mappedName + " in " + jar );
295         }
296     }
297 
298     private boolean isFiltered( List<Filter> filters, String name )
299     {
300         for ( Filter filter : filters )
301         {
302             if ( filter.isFiltered( name ) )
303             {
304                 return true;
305             }
306         }
307 
308         return false;
309     }
310 
311     private boolean resourceTransformed( List<ResourceTransformer> resourceTransformers, String name, InputStream is,
312                                          List<Relocator> relocators )
313         throws IOException
314     {
315         boolean resourceTransformed = false;
316 
317         for ( ResourceTransformer transformer : resourceTransformers )
318         {
319             if ( transformer.canTransformResource( name ) )
320             {
321                 getLogger().debug( "Transforming " + name + " using " + transformer.getClass().getName() );
322 
323                 transformer.processResource( name, is, relocators );
324 
325                 resourceTransformed = true;
326 
327                 break;
328             }
329         }
330         return resourceTransformed;
331     }
332 
333     private void addResource( Set resources, JarOutputStream jos, String name, InputStream is )
334         throws IOException
335     {
336         jos.putNextEntry( new JarEntry( name ) );
337 
338         IOUtil.copy( is, jos );
339 
340         resources.add( name );
341     }
342 
343     class RelocatorRemapper
344         extends Remapper
345     {
346 
347         private final Pattern classPattern = Pattern.compile( "(\\[*)?L(.+);" );
348 
349         List<Relocator> relocators;
350 
351         public RelocatorRemapper( List<Relocator> relocators )
352         {
353             this.relocators = relocators;
354         }
355 
356         public boolean hasRelocators()
357         {
358             return !relocators.isEmpty();
359         }
360 
361         public Object mapValue( Object object )
362         {
363             if ( object instanceof String )
364             {
365                 String name = (String) object;
366                 String value = name;
367 
368                 String prefix = "";
369                 String suffix = "";
370 
371                 Matcher m = classPattern.matcher( name );
372                 if ( m.matches() )
373                 {
374                     prefix = m.group( 1 ) + "L";
375                     suffix = ";";
376                     name = m.group( 2 );
377                 }
378 
379                 for ( Relocator r : relocators )
380                 {
381                     if ( r.canRelocateClass( name ) )
382                     {
383                         value = prefix + r.relocateClass( name ) + suffix;
384                         break;
385                     }
386                     else if ( r.canRelocatePath( name ) )
387                     {
388                         value = prefix + r.relocatePath( name ) + suffix;
389                         break;
390                     }
391                 }
392 
393                 return value;
394             }
395 
396             return super.mapValue( object );
397         }
398 
399         public String map( String name )
400         {
401             String value = name;
402 
403             String prefix = "";
404             String suffix = "";
405 
406             Matcher m = classPattern.matcher( name );
407             if ( m.matches() )
408             {
409                 prefix = m.group( 1 ) + "L";
410                 suffix = ";";
411                 name = m.group( 2 );
412             }
413 
414             for ( Relocator r : relocators )
415             {
416                 if ( r.canRelocatePath( name ) )
417                 {
418                     value = prefix + r.relocatePath( name ) + suffix;
419                     break;
420                 }
421             }
422 
423             return value;
424         }
425 
426     }
427 
428 }