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