View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.shade;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.BufferedOutputStream;
25  import java.io.ByteArrayInputStream;
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.InputStreamReader;
30  import java.io.OutputStreamWriter;
31  import java.io.PushbackInputStream;
32  import java.io.Writer;
33  import java.nio.ByteBuffer;
34  import java.nio.ByteOrder;
35  import java.nio.charset.StandardCharsets;
36  import java.nio.file.Files;
37  import java.util.ArrayList;
38  import java.util.Arrays;
39  import java.util.Calendar;
40  import java.util.Collection;
41  import java.util.Collections;
42  import java.util.Enumeration;
43  import java.util.HashMap;
44  import java.util.HashSet;
45  import java.util.Iterator;
46  import java.util.LinkedList;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Objects;
50  import java.util.Set;
51  import java.util.concurrent.Callable;
52  import java.util.jar.JarEntry;
53  import java.util.jar.JarFile;
54  import java.util.jar.JarOutputStream;
55  import java.util.regex.Matcher;
56  import java.util.regex.Pattern;
57  import java.util.zip.CRC32;
58  import java.util.zip.ZipEntry;
59  import java.util.zip.ZipException;
60  
61  import org.apache.maven.plugin.MojoExecutionException;
62  import org.apache.maven.plugins.shade.filter.Filter;
63  import org.apache.maven.plugins.shade.relocation.Relocator;
64  import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer;
65  import org.apache.maven.plugins.shade.resource.ReproducibleResourceTransformer;
66  import org.apache.maven.plugins.shade.resource.ResourceTransformer;
67  import org.codehaus.plexus.util.IOUtil;
68  import org.codehaus.plexus.util.io.CachingOutputStream;
69  import org.objectweb.asm.ClassReader;
70  import org.objectweb.asm.ClassVisitor;
71  import org.objectweb.asm.ClassWriter;
72  import org.objectweb.asm.commons.ClassRemapper;
73  import org.objectweb.asm.commons.Remapper;
74  import org.slf4j.Logger;
75  import org.slf4j.LoggerFactory;
76  
77  /**
78   * @author Jason van Zyl
79   */
80  @Singleton
81  @Named
82  public class DefaultShader implements Shader {
83      private static final int BUFFER_SIZE = 32 * 1024;
84  
85      private final Logger logger;
86  
87      public DefaultShader() {
88          this(LoggerFactory.getLogger(DefaultShader.class));
89      }
90  
91      public DefaultShader(final Logger logger) {
92          this.logger = Objects.requireNonNull(logger);
93      }
94  
95      // workaround for MSHADE-420
96      private long getTime(ZipEntry entry) {
97          if (entry.getLastModifiedTime() == null) {
98              return -1;
99          }
100         long mtime = entry.getLastModifiedTime().toMillis();
101         if (hasX5455ExtendedTimestamp(entry)) {
102             Calendar cal = Calendar.getInstance();
103             cal.setTimeInMillis(mtime);
104             mtime = mtime - (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET));
105         }
106         return mtime;
107     }
108 
109     /**
110      * Returns {@code true} if passed in {@link ZipEntry} has extra data that contains the extended timestamp (0x5455).
111      *
112      * @see <a href="https://commons.apache.org/proper/commons-compress/apidocs/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestamp.html">X5455_ExtendedTimestamp</a>
113      */
114     private static boolean hasX5455ExtendedTimestamp(ZipEntry zipEntry) {
115         if (zipEntry.getExtra() != null) {
116             ByteBuffer extraData = ByteBuffer.wrap(zipEntry.getExtra());
117             extraData.order(ByteOrder.LITTLE_ENDIAN);
118             while (extraData.hasRemaining()) {
119                 int id = extraData.getShort() & 0xffff;
120                 int length = extraData.getShort() & 0xffff;
121 
122                 if (id == 0x5455) {
123                     // Extended TS is present
124                     return true;
125                 } else {
126                     // skip to next
127                     extraData.position(extraData.position() + length);
128                 }
129             }
130         }
131         return false;
132     }
133 
134     public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionException {
135         Set<String> resources = new HashSet<>();
136 
137         ManifestResourceTransformer manifestTransformer = null;
138         List<ResourceTransformer> transformers = new ArrayList<>(shadeRequest.getResourceTransformers());
139         for (Iterator<ResourceTransformer> it = transformers.iterator(); it.hasNext(); ) {
140             ResourceTransformer transformer = it.next();
141             if (transformer instanceof ManifestResourceTransformer) {
142                 manifestTransformer = (ManifestResourceTransformer) transformer;
143                 it.remove();
144             }
145         }
146 
147         final DefaultPackageMapper packageMapper = new DefaultPackageMapper(shadeRequest.getRelocators());
148 
149         // noinspection ResultOfMethodCallIgnored
150         shadeRequest.getUberJar().getParentFile().mkdirs();
151 
152         try (JarOutputStream out =
153                 new JarOutputStream(new BufferedOutputStream(new CachingOutputStream(shadeRequest.getUberJar())))) {
154             goThroughAllJarEntriesForManifestTransformer(shadeRequest, resources, manifestTransformer, out);
155 
156             // CHECKSTYLE_OFF: MagicNumber
157             Map<String, HashSet<File>> duplicates = new HashMap<>();
158             // CHECKSTYLE_ON: MagicNumber
159 
160             shadeJars(shadeRequest, resources, transformers, out, duplicates, packageMapper);
161 
162             // CHECKSTYLE_OFF: MagicNumber
163             Map<Collection<File>, HashSet<String>> overlapping = new HashMap<>();
164             // CHECKSTYLE_ON: MagicNumber
165 
166             for (String clazz : duplicates.keySet()) {
167                 Collection<File> jarz = duplicates.get(clazz);
168                 if (jarz.size() > 1) {
169                     overlapping.computeIfAbsent(jarz, k -> new HashSet<>()).add(clazz);
170                 }
171             }
172 
173             // Log a summary of duplicates
174             logSummaryOfDuplicates(overlapping);
175 
176             if (!overlapping.keySet().isEmpty()) {
177                 showOverlappingWarning();
178             }
179 
180             for (ResourceTransformer transformer : transformers) {
181                 if (transformer.hasTransformedResource()) {
182                     transformer.modifyOutputStream(out);
183                 }
184             }
185         }
186 
187         for (Filter filter : shadeRequest.getFilters()) {
188             filter.finished();
189         }
190     }
191 
192     /**
193      * {@link InputStream} that can peek ahead at zip header bytes.
194      */
195     private static class ZipHeaderPeekInputStream extends PushbackInputStream {
196 
197         private static final byte[] ZIP_HEADER = new byte[] {0x50, 0x4b, 0x03, 0x04};
198 
199         private static final int HEADER_LEN = 4;
200 
201         protected ZipHeaderPeekInputStream(InputStream in) {
202             super(in, HEADER_LEN);
203         }
204 
205         public boolean hasZipHeader() throws IOException {
206             final byte[] header = new byte[HEADER_LEN];
207             int len = super.read(header, 0, HEADER_LEN);
208             if (len != -1) {
209                 super.unread(header, 0, len);
210             }
211             return Arrays.equals(header, ZIP_HEADER);
212         }
213     }
214 
215     /**
216      * Data holder for CRC and Size.
217      */
218     private static class CrcAndSize {
219 
220         private final CRC32 crc = new CRC32();
221 
222         private long size;
223 
224         CrcAndSize(InputStream inputStream) throws IOException {
225             load(inputStream);
226         }
227 
228         private void load(InputStream inputStream) throws IOException {
229             byte[] buffer = new byte[BUFFER_SIZE];
230             int bytesRead;
231             while ((bytesRead = inputStream.read(buffer)) != -1) {
232                 this.crc.update(buffer, 0, bytesRead);
233                 this.size += bytesRead;
234             }
235         }
236 
237         public void setupStoredEntry(JarEntry entry) {
238             entry.setSize(this.size);
239             entry.setCompressedSize(this.size);
240             entry.setCrc(this.crc.getValue());
241             entry.setMethod(ZipEntry.STORED);
242         }
243     }
244 
245     private void shadeJars(
246             ShadeRequest shadeRequest,
247             Set<String> resources,
248             List<ResourceTransformer> transformers,
249             JarOutputStream jos,
250             Map<String, HashSet<File>> duplicates,
251             DefaultPackageMapper packageMapper)
252             throws IOException {
253         for (File jar : shadeRequest.getJars()) {
254 
255             logger.debug("Processing JAR " + jar);
256 
257             List<Filter> jarFilters = getFilters(jar, shadeRequest.getFilters());
258             if (jar.isDirectory()) {
259                 shadeDir(
260                         shadeRequest,
261                         resources,
262                         transformers,
263                         packageMapper,
264                         jos,
265                         duplicates,
266                         jar,
267                         jar,
268                         "",
269                         jarFilters);
270             } else {
271                 shadeJar(shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, jarFilters);
272             }
273         }
274     }
275 
276     @SuppressWarnings("checkstyle:ParameterNumber")
277     private void shadeDir(
278             ShadeRequest shadeRequest,
279             Set<String> resources,
280             List<ResourceTransformer> transformers,
281             DefaultPackageMapper packageMapper,
282             JarOutputStream jos,
283             Map<String, HashSet<File>> duplicates,
284             File jar,
285             File current,
286             String prefix,
287             List<Filter> jarFilters)
288             throws IOException {
289         final File[] children = current.listFiles();
290         if (children == null) {
291             return;
292         }
293         for (final File file : children) {
294             final String name = prefix + file.getName();
295             if (file.isDirectory()) {
296                 try {
297                     shadeDir(
298                             shadeRequest,
299                             resources,
300                             transformers,
301                             packageMapper,
302                             jos,
303                             duplicates,
304                             jar,
305                             file,
306                             prefix + file.getName() + '/',
307                             jarFilters);
308                     continue;
309                 } catch (Exception e) {
310                     throw new IOException(String.format("Problem shading JAR %s entry %s: %s", current, name, e), e);
311                 }
312             }
313             if (isFiltered(jarFilters, name) || isExcludedEntry(name)) {
314                 continue;
315             }
316 
317             try {
318                 shadeJarEntry(
319                         shadeRequest,
320                         resources,
321                         transformers,
322                         packageMapper,
323                         jos,
324                         duplicates,
325                         jar,
326                         new Callable<InputStream>() {
327                             @Override
328                             public InputStream call() throws Exception {
329                                 return Files.newInputStream(file.toPath());
330                             }
331                         },
332                         name,
333                         file.lastModified(),
334                         -1 /*ignore*/);
335             } catch (Exception e) {
336                 throw new IOException(String.format("Problem shading JAR %s entry %s: %s", current, name, e), e);
337             }
338         }
339     }
340 
341     @SuppressWarnings("checkstyle:ParameterNumber")
342     private void shadeJar(
343             ShadeRequest shadeRequest,
344             Set<String> resources,
345             List<ResourceTransformer> transformers,
346             DefaultPackageMapper packageMapper,
347             JarOutputStream jos,
348             Map<String, HashSet<File>> duplicates,
349             File jar,
350             List<Filter> jarFilters)
351             throws IOException {
352         try (JarFile jarFile = newJarFile(jar)) {
353 
354             for (Enumeration<JarEntry> j = jarFile.entries(); j.hasMoreElements(); ) {
355                 final JarEntry entry = j.nextElement();
356 
357                 String name = entry.getName();
358 
359                 if (entry.isDirectory() || isFiltered(jarFilters, name) || isExcludedEntry(name)) {
360                     continue;
361                 }
362 
363                 try {
364                     shadeJarEntry(
365                             shadeRequest,
366                             resources,
367                             transformers,
368                             packageMapper,
369                             jos,
370                             duplicates,
371                             jar,
372                             new Callable<InputStream>() {
373                                 @Override
374                                 public InputStream call() throws Exception {
375                                     return jarFile.getInputStream(entry);
376                                 }
377                             },
378                             name,
379                             getTime(entry),
380                             entry.getMethod());
381                 } catch (Exception e) {
382                     throw new IOException(String.format("Problem shading JAR %s entry %s: %s", jar, name, e), e);
383                 }
384             }
385         }
386     }
387 
388     private boolean isExcludedEntry(final String name) {
389         if ("META-INF/INDEX.LIST".equals(name)) {
390             // we cannot allow the jar indexes to be copied over or the
391             // jar is useless. Ideally, we could create a new one
392             // later
393             return true;
394         }
395 
396         if ("module-info.class".equals(name)) {
397             logger.warn("Discovered module-info.class. " + "Shading will break its strong encapsulation.");
398             return true;
399         }
400         return false;
401     }
402 
403     @SuppressWarnings("checkstyle:ParameterNumber")
404     private void shadeJarEntry(
405             ShadeRequest shadeRequest,
406             Set<String> resources,
407             List<ResourceTransformer> transformers,
408             DefaultPackageMapper packageMapper,
409             JarOutputStream jos,
410             Map<String, HashSet<File>> duplicates,
411             File jar,
412             Callable<InputStream> inputProvider,
413             String name,
414             long time,
415             int method)
416             throws Exception {
417         try (InputStream in = inputProvider.call()) {
418             String mappedName = packageMapper.map(name, true, false);
419 
420             int idx = mappedName.lastIndexOf('/');
421             if (idx != -1) {
422                 // make sure dirs are created
423                 String dir = mappedName.substring(0, idx);
424                 if (!resources.contains(dir)) {
425                     addDirectory(resources, jos, dir, time);
426                 }
427             }
428 
429             duplicates.computeIfAbsent(name, k -> new HashSet<>()).add(jar);
430             if (name.endsWith(".class")) {
431                 addRemappedClass(jos, jar, name, time, in, packageMapper);
432             } else if (shadeRequest.isShadeSourcesContent() && name.endsWith(".java")) {
433                 // Avoid duplicates
434                 if (resources.contains(mappedName)) {
435                     return;
436                 }
437 
438                 addJavaSource(resources, jos, mappedName, time, in, shadeRequest.getRelocators());
439             } else {
440                 if (!resourceTransformed(transformers, mappedName, in, shadeRequest.getRelocators(), time)) {
441                     // Avoid duplicates that aren't accounted for by the resource transformers
442                     if (resources.contains(mappedName)) {
443                         logger.debug("We have a duplicate " + name + " in " + jar);
444                         return;
445                     }
446 
447                     addResource(resources, jos, mappedName, inputProvider, time, method);
448                 } else {
449                     duplicates.computeIfAbsent(name, k -> new HashSet<>()).remove(jar);
450                 }
451             }
452         }
453     }
454 
455     private void goThroughAllJarEntriesForManifestTransformer(
456             ShadeRequest shadeRequest,
457             Set<String> resources,
458             ManifestResourceTransformer manifestTransformer,
459             JarOutputStream jos)
460             throws IOException {
461         if (manifestTransformer != null) {
462             for (File jar : shadeRequest.getJars()) {
463                 try (JarFile jarFile = newJarFile(jar)) {
464                     for (Enumeration<JarEntry> en = jarFile.entries(); en.hasMoreElements(); ) {
465                         JarEntry entry = en.nextElement();
466                         String resource = entry.getName();
467                         if (manifestTransformer.canTransformResource(resource)) {
468                             resources.add(resource);
469                             try (InputStream inputStream = jarFile.getInputStream(entry)) {
470                                 manifestTransformer.processResource(
471                                         resource, inputStream, shadeRequest.getRelocators(), getTime(entry));
472                             }
473                             break;
474                         }
475                     }
476                 }
477             }
478             if (manifestTransformer.hasTransformedResource()) {
479                 manifestTransformer.modifyOutputStream(jos);
480             }
481         }
482     }
483 
484     private void showOverlappingWarning() {
485         logger.warn("maven-shade-plugin has detected that some files are");
486         logger.warn("present in two or more JARs. When this happens, only one");
487         logger.warn("single version of the file is copied to the uber jar.");
488         logger.warn("Usually this is not harmful and you can skip these warnings,");
489         logger.warn("otherwise try to manually exclude artifacts based on");
490         logger.warn("mvn dependency:tree -Ddetail=true and the above output.");
491         logger.warn("See https://maven.apache.org/plugins/maven-shade-plugin/");
492     }
493 
494     private void logSummaryOfDuplicates(Map<Collection<File>, HashSet<String>> overlapping) {
495         for (Collection<File> jarz : overlapping.keySet()) {
496             List<String> jarzS = new ArrayList<>();
497 
498             for (File jjar : jarz) {
499                 jarzS.add(jjar.getName());
500             }
501 
502             Collections.sort(jarzS); // deterministic messages to be able to compare outputs (useful on CI)
503 
504             List<String> classes = new LinkedList<>();
505             List<String> resources = new LinkedList<>();
506 
507             for (String name : overlapping.get(jarz)) {
508                 if (name.endsWith(".class")) {
509                     classes.add(name.replace(".class", "").replace("/", "."));
510                 } else {
511                     resources.add(name);
512                 }
513             }
514 
515             // CHECKSTYLE_OFF: LineLength
516             final Collection<String> overlaps = new ArrayList<>();
517             if (!classes.isEmpty()) {
518                 if (resources.size() == 1) {
519                     overlaps.add("class");
520                 } else {
521                     overlaps.add("classes");
522                 }
523             }
524             if (!resources.isEmpty()) {
525                 if (resources.size() == 1) {
526                     overlaps.add("resource");
527                 } else {
528                     overlaps.add("resources");
529                 }
530             }
531 
532             final List<String> all = new ArrayList<>(classes.size() + resources.size());
533             all.addAll(classes);
534             all.addAll(resources);
535 
536             logger.warn(String.join(", ", jarzS) + " define " + all.size() + " overlapping "
537                     + String.join(" and ", overlaps) + ": ");
538             // CHECKSTYLE_ON: LineLength
539 
540             Collections.sort(all);
541 
542             int max = 10;
543 
544             for (int i = 0; i < Math.min(max, all.size()); i++) {
545                 logger.warn("  - " + all.get(i));
546             }
547 
548             if (all.size() > max) {
549                 logger.warn("  - " + (all.size() - max) + " more...");
550             }
551         }
552     }
553 
554     private JarFile newJarFile(File jar) throws IOException {
555         try {
556             return new JarFile(jar);
557         } catch (ZipException zex) {
558             // JarFile is not very verbose and doesn't tell the user which file it was
559             // so we will create a new Exception instead
560             throw new ZipException("error in opening zip file " + jar);
561         }
562     }
563 
564     private List<Filter> getFilters(File jar, List<Filter> filters) {
565         List<Filter> list = new ArrayList<>();
566 
567         for (Filter filter : filters) {
568             if (filter.canFilter(jar)) {
569                 list.add(filter);
570             }
571         }
572 
573         return list;
574     }
575 
576     private void addDirectory(Set<String> resources, JarOutputStream jos, String name, long time) throws IOException {
577         if (name.lastIndexOf('/') > 0) {
578             String parent = name.substring(0, name.lastIndexOf('/'));
579             if (!resources.contains(parent)) {
580                 addDirectory(resources, jos, parent, time);
581             }
582         }
583 
584         // directory entries must end in "/"
585         JarEntry entry = new JarEntry(name + "/");
586         entry.setTime(time);
587         jos.putNextEntry(entry);
588 
589         resources.add(name);
590     }
591 
592     private void addRemappedClass(
593             JarOutputStream jos, File jar, String name, long time, InputStream is, DefaultPackageMapper packageMapper)
594             throws IOException, MojoExecutionException {
595         if (packageMapper.relocators.isEmpty()) {
596             try {
597                 JarEntry entry = new JarEntry(name);
598                 entry.setTime(time);
599                 jos.putNextEntry(entry);
600                 IOUtil.copy(is, jos);
601             } catch (ZipException e) {
602                 logger.debug("We have a duplicate " + name + " in " + jar);
603             }
604 
605             return;
606         }
607 
608         // Keep the original class, in case nothing was relocated by ShadeClassRemapper. This avoids binary
609         // differences between classes, simply because they were rewritten and only details like constant pool or
610         // stack map frames are slightly different.
611         byte[] originalClass = IOUtil.toByteArray(is);
612 
613         ClassReader cr = new ClassReader(new ByteArrayInputStream(originalClass));
614 
615         // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
616         // Copying the original constant pool should be avoided because it would keep references
617         // to the original class names. This is not a problem at runtime (because these entries in the
618         // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin
619         // that use the constant pool to determine the dependencies of a class.
620         ClassWriter cw = new ClassWriter(0);
621 
622         final String pkg = name.substring(0, name.lastIndexOf('/') + 1);
623         final ShadeClassRemapper cv = new ShadeClassRemapper(cw, pkg, packageMapper);
624 
625         try {
626             cr.accept(cv, ClassReader.EXPAND_FRAMES);
627         } catch (Throwable ise) {
628             throw new MojoExecutionException("Error in ASM processing class " + name, ise);
629         }
630 
631         // If nothing was relocated by ShadeClassRemapper, write the original class, otherwise the transformed one
632         final byte[] renamedClass;
633         if (cv.remapped) {
634             logger.debug("Rewrote class bytecode: " + name);
635             renamedClass = cw.toByteArray();
636         } else {
637             logger.debug("Keeping original class bytecode: " + name);
638             renamedClass = originalClass;
639         }
640 
641         // Need to take the .class off for remapping evaluation
642         String mappedName = packageMapper.map(name.substring(0, name.indexOf('.')), true, false);
643 
644         try {
645             // Now we put it back on so the class file is written out with the right extension.
646             JarEntry entry = new JarEntry(mappedName + ".class");
647             entry.setTime(time);
648             jos.putNextEntry(entry);
649 
650             jos.write(renamedClass);
651         } catch (ZipException e) {
652             logger.debug("We have a duplicate " + mappedName + " in " + jar);
653         }
654     }
655 
656     private boolean isFiltered(List<Filter> filters, String name) {
657         for (Filter filter : filters) {
658             if (filter.isFiltered(name)) {
659                 return true;
660             }
661         }
662 
663         return false;
664     }
665 
666     private boolean resourceTransformed(
667             List<ResourceTransformer> resourceTransformers,
668             String name,
669             InputStream is,
670             List<Relocator> relocators,
671             long time)
672             throws IOException {
673         boolean resourceTransformed = false;
674 
675         for (ResourceTransformer transformer : resourceTransformers) {
676             if (transformer.canTransformResource(name)) {
677                 logger.debug("Transforming " + name + " using "
678                         + transformer.getClass().getName());
679 
680                 if (transformer instanceof ReproducibleResourceTransformer) {
681                     ((ReproducibleResourceTransformer) transformer).processResource(name, is, relocators, time);
682                 } else {
683                     transformer.processResource(name, is, relocators);
684                 }
685 
686                 resourceTransformed = true;
687 
688                 break;
689             }
690         }
691         return resourceTransformed;
692     }
693 
694     private void addJavaSource(
695             Set<String> resources,
696             JarOutputStream jos,
697             String name,
698             long time,
699             InputStream is,
700             List<Relocator> relocators)
701             throws IOException {
702         JarEntry entry = new JarEntry(name);
703         entry.setTime(time);
704         jos.putNextEntry(entry);
705 
706         String sourceContent = IOUtil.toString(new InputStreamReader(is, StandardCharsets.UTF_8));
707 
708         for (Relocator relocator : relocators) {
709             sourceContent = relocator.applyToSourceContent(sourceContent);
710         }
711 
712         final Writer writer = new OutputStreamWriter(jos, StandardCharsets.UTF_8);
713         writer.write(sourceContent);
714         writer.flush();
715 
716         resources.add(name);
717     }
718 
719     private void addResource(
720             Set<String> resources, JarOutputStream jos, String name, Callable<InputStream> input, long time, int method)
721             throws Exception {
722         ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(input.call());
723         try {
724             final JarEntry entry = new JarEntry(name);
725 
726             // We should not change compressed level of uncompressed entries, otherwise JVM can't load these nested jars
727             if (inputStream.hasZipHeader() && method == ZipEntry.STORED) {
728                 new CrcAndSize(inputStream).setupStoredEntry(entry);
729                 inputStream.close();
730                 inputStream = new ZipHeaderPeekInputStream(input.call());
731             }
732 
733             entry.setTime(time);
734 
735             jos.putNextEntry(entry);
736 
737             IOUtil.copy(inputStream, jos);
738 
739             resources.add(name);
740         } finally {
741             inputStream.close();
742         }
743     }
744 
745     private interface PackageMapper {
746         /**
747          * Map an entity name according to the mapping rules known to this package mapper
748          *
749          * @param entityName entity name to be mapped
750          * @param mapPaths map "slashy" names like paths or internal Java class names, e.g. {@code com/acme/Foo}?
751          * @param mapPackages  map "dotty" names like qualified Java class or package names, e.g. {@code com.acme.Foo}?
752          * @return mapped entity name, e.g. {@code org/apache/acme/Foo} or {@code org.apache.acme.Foo}
753          */
754         String map(String entityName, boolean mapPaths, boolean mapPackages);
755     }
756 
757     /**
758      * A package mapper based on a list of {@link Relocator}s
759      */
760     private static class DefaultPackageMapper implements PackageMapper {
761         private static final Pattern CLASS_PATTERN = Pattern.compile("(\\[*)?L(.+);");
762 
763         private final List<Relocator> relocators;
764 
765         private DefaultPackageMapper(final List<Relocator> relocators) {
766             this.relocators = relocators;
767         }
768 
769         @Override
770         public String map(String entityName, boolean mapPaths, final boolean mapPackages) {
771             String value = entityName;
772 
773             String prefix = "";
774             String suffix = "";
775 
776             Matcher m = CLASS_PATTERN.matcher(entityName);
777             if (m.matches()) {
778                 prefix = m.group(1) + "L";
779                 suffix = ";";
780                 entityName = m.group(2);
781             }
782 
783             for (Relocator r : relocators) {
784                 if (mapPackages && r.canRelocateClass(entityName)) {
785                     value = prefix + r.relocateClass(entityName) + suffix;
786                     break;
787                 } else if (mapPaths && r.canRelocatePath(entityName)) {
788                     value = prefix + r.relocatePath(entityName) + suffix;
789                     break;
790                 }
791             }
792             return value;
793         }
794     }
795 
796     private static class LazyInitRemapper extends Remapper {
797         private PackageMapper relocators;
798 
799         @Override
800         public Object mapValue(Object object) {
801             return object instanceof String ? relocators.map((String) object, true, true) : super.mapValue(object);
802         }
803 
804         @Override
805         public String map(String name) {
806             // NOTE: Before the factoring out duplicate code from 'private String map(String, boolean)', this method did
807             // the same as 'mapValue', except for not trying to replace "dotty" package-like patterns (only "slashy"
808             // path-like ones). The refactoring retains this difference. But actually, all unit and integration tests
809             // still pass, if both variants are unified into one which always tries to replace both pattern types.
810             //
811             //  TODO: Analyse if this case is really necessary and has any special meaning or avoids any known problems.
812             //   If not, then simplify DefaultShader.PackageMapper.map to only have the String parameter and assume
813             //   both boolean ones to always be true.
814             return relocators.map(name, true, false);
815         }
816     }
817 
818     // TODO: we can avoid LazyInitRemapper N instantiations (and use a singleton)
819     //       reimplementing ClassRemapper there.
820     //       It looks a bad idea but actually enables us to respect our relocation API which has no
821     //       consistency with ASM one which can lead to multiple issues for short relocation patterns
822     //       plus overcome ClassRemapper limitations we can care about (see its javadoc for details).
823     //
824     // NOTE: very short term we can just reuse the same LazyInitRemapper and let the constructor set it.
825     //       since multithreading is not faster in this processing it would be more than sufficient if
826     //       caring of this 2 objects per class allocation (but keep in mind the visitor will allocate way more ;)).
827     //       Last point which makes it done this way as of now is that perf seems not impacted at all.
828     private static class ShadeClassRemapper extends ClassRemapper implements PackageMapper {
829         private final String pkg;
830         private final PackageMapper packageMapper;
831         private boolean remapped;
832 
833         ShadeClassRemapper(
834                 final ClassVisitor classVisitor, final String pkg, final DefaultPackageMapper packageMapper) {
835             super(classVisitor, new LazyInitRemapper() /* can't be init in the constructor with "this" */);
836             this.pkg = pkg;
837             this.packageMapper = packageMapper;
838 
839             // use this to enrich relocators impl with "remapped" logic
840             LazyInitRemapper.class.cast(remapper).relocators = this;
841         }
842 
843         @Override
844         public void visitSource(final String source, final String debug) {
845             if (source == null) {
846                 super.visitSource(null, debug);
847                 return;
848             }
849 
850             final String fqSource = pkg + source;
851             final String mappedSource = map(fqSource, true, false);
852             final String filename = mappedSource.substring(mappedSource.lastIndexOf('/') + 1);
853             super.visitSource(filename, debug);
854         }
855 
856         @Override
857         public String map(final String entityName, boolean mapPaths, final boolean mapPackages) {
858             final String mapped = packageMapper.map(entityName, true, mapPackages);
859             if (!remapped) {
860                 remapped = !mapped.equals(entityName);
861             }
862             return mapped;
863         }
864     }
865 }