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