package org.apache.maven.plugins.shade; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import com.google.common.base.Joiner; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipException; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.shade.filter.Filter; import org.apache.maven.plugins.shade.relocation.Relocator; import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer; import org.apache.maven.plugins.shade.resource.ResourceTransformer; import org.codehaus.plexus.component.annotations.Component; import org.codehaus.plexus.logging.AbstractLogEnabled; import org.codehaus.plexus.util.IOUtil; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.commons.ClassRemapper; import org.objectweb.asm.commons.Remapper; /** * @author Jason van Zyl */ @Component(role = Shader.class, hint = "default") public class DefaultShader extends AbstractLogEnabled implements Shader { public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionException { Set<String> resources = new HashSet<String>(); ResourceTransformer manifestTransformer = null; List<ResourceTransformer> transformers = new ArrayList<ResourceTransformer>(shadeRequest.getResourceTransformers()); for (Iterator<ResourceTransformer> it = transformers.iterator(); it.hasNext();) { ResourceTransformer transformer = it.next(); if (transformer instanceof ManifestResourceTransformer) { manifestTransformer = transformer; it.remove(); } } RelocatorRemapper remapper = new RelocatorRemapper(shadeRequest.getRelocators()); // noinspection ResultOfMethodCallIgnored shadeRequest.getUberJar().getParentFile().mkdirs(); FileOutputStream fileOutputStream = new FileOutputStream(shadeRequest.getUberJar()); JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(fileOutputStream)); if (manifestTransformer != null) { for (File jar : shadeRequest.getJars()) { JarFile jarFile = newJarFile(jar); for (Enumeration<JarEntry> en = jarFile.entries(); en.hasMoreElements();) { JarEntry entry = en.nextElement(); String resource = entry.getName(); if (manifestTransformer.canTransformResource(resource)) { resources.add(resource); manifestTransformer.processResource(resource, jarFile.getInputStream(entry), shadeRequest.getRelocators()); break; } } } if (manifestTransformer.hasTransformedResource()) { manifestTransformer.modifyOutputStream(jos); } } // CHECKSTYLE_OFF: MagicNumber Multimap<String, File> duplicates = HashMultimap.create(10000, 3); // CHECKSTYLE_ON: MagicNumber for (File jar : shadeRequest.getJars()) { getLogger().debug("Processing JAR " + jar); List<Filter> jarFilters = getFilters(jar, shadeRequest.getFilters()); JarFile jarFile = newJarFile(jar); for (Enumeration<JarEntry> j = jarFile.entries(); j.hasMoreElements();) { JarEntry entry = j.nextElement(); String name = entry.getName(); if ("META-INF/INDEX.LIST".equals(name)) { // we cannot allow the jar indexes to be copied over or the // jar is useless. Ideally, we could create a new one // later continue; } if (!entry.isDirectory() && !isFiltered(jarFilters, name)) { InputStream is = jarFile.getInputStream(entry); String mappedName = remapper.map(name); int idx = mappedName.lastIndexOf('/'); if (idx != -1) { // make sure dirs are created String dir = mappedName.substring(0, idx); if (!resources.contains(dir)) { addDirectory(resources, jos, dir); } } if (name.endsWith(".class")) { duplicates.put(name, jar); addRemappedClass(remapper, jos, jar, name, is); } else if (shadeRequest.isShadeSourcesContent() && name.endsWith(".java")) { // Avoid duplicates if (resources.contains(mappedName)) { continue; } addJavaSource(resources, jos, mappedName, is, shadeRequest.getRelocators()); } else { if (!resourceTransformed(transformers, mappedName, is, shadeRequest.getRelocators())) { // Avoid duplicates that aren't accounted for by the resource transformers if (resources.contains(mappedName)) { continue; } addResource(resources, jos, mappedName, is); } } IOUtil.close(is); } } jarFile.close(); } // CHECKSTYLE_OFF: MagicNumber Multimap<Collection<File>, String> overlapping = HashMultimap.create(20, 15); // CHECKSTYLE_ON: MagicNumber for (String clazz : duplicates.keySet()) { Collection<File> jarz = duplicates.get(clazz); if (jarz.size() > 1) { overlapping.put(jarz, clazz); } } // Log a summary of duplicates for (Collection<File> jarz : overlapping.keySet()) { List<String> jarzS = new LinkedList<String>(); for (File jjar : jarz) { jarzS.add(jjar.getName()); } List<String> classes = new LinkedList<String>(); for (String clazz : overlapping.get(jarz)) { classes.add(clazz.replace(".class", "").replace("/", ".")); } getLogger().warn(Joiner.on(", ").join(jarzS) + " define " + classes.size() + " overlapping classes: "); int max = 10; for (int i = 0; i < Math.min(max, classes.size()); i++) { getLogger().warn(" - " + classes.get(i)); } if (classes.size() > max) { getLogger().warn(" - " + (classes.size() - max) + " more..."); } } if (overlapping.keySet().size() > 0) { getLogger().warn("maven-shade-plugin has detected that some class files are"); getLogger().warn("present in two or more JARs. When this happens, only one"); getLogger().warn("single version of the class is copied to the uber jar."); getLogger().warn("Usually this is not harmful and you can skip these warnings,"); getLogger().warn("otherwise try to manually exclude artifacts based on"); getLogger().warn("mvn dependency:tree -Ddetail=true and the above output."); getLogger().warn("See http://docs.codehaus.org/display/MAVENUSER/Shade+Plugin"); } for (ResourceTransformer transformer : transformers) { if (transformer.hasTransformedResource()) { transformer.modifyOutputStream(jos); } } IOUtil.close(jos); for (Filter filter : shadeRequest.getFilters()) { filter.finished(); } } private JarFile newJarFile(File jar) throws IOException { try { return new JarFile(jar); } catch (ZipException zex) { // JarFile is not very verbose and doesn't tell the user which file it was // so we will create a new Exception instead throw new ZipException("error in opening zip file " + jar); } } private List<Filter> getFilters(File jar, List<Filter> filters) { List<Filter> list = new ArrayList<Filter>(); for (Filter filter : filters) { if (filter.canFilter(jar)) { list.add(filter); } } return list; } private void addDirectory(Set<String> resources, JarOutputStream jos, String name) throws IOException { if (name.lastIndexOf('/') > 0) { String parent = name.substring(0, name.lastIndexOf('/')); if (!resources.contains(parent)) { addDirectory(resources, jos, parent); } } // directory entries must end in "/" JarEntry entry = new JarEntry(name + "/"); jos.putNextEntry(entry); resources.add(name); } private void addRemappedClass(RelocatorRemapper remapper, JarOutputStream jos, File jar, String name, InputStream is) throws IOException, MojoExecutionException { if (!remapper.hasRelocators()) { try { jos.putNextEntry(new JarEntry(name)); IOUtil.copy(is, jos); } catch (ZipException e) { getLogger().debug("We have a duplicate " + name + " in " + jar); } return; } ClassReader cr = new ClassReader(is); // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool. // Copying the original constant pool should be avoided because it would keep references // to the original class names. This is not a problem at runtime (because these entries in the // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin // that use the constant pool to determine the dependencies of a class. ClassWriter cw = new ClassWriter(0); final String pkg = name.substring(0, name.lastIndexOf('/') + 1); ClassVisitor cv = new ClassRemapper(cw, remapper) { @Override public void visitSource(String source, String debug) { if (source == null) { super.visitSource(source, debug); } else { String fqSource = pkg + source; String mappedSource = remapper.map(fqSource); String filename = mappedSource.substring(mappedSource.lastIndexOf('/') + 1); super.visitSource(filename, debug); } } }; try { cr.accept(cv, ClassReader.EXPAND_FRAMES); } catch (Throwable ise) { throw new MojoExecutionException("Error in ASM processing class " + name, ise); } byte[] renamedClass = cw.toByteArray(); // Need to take the .class off for remapping evaluation String mappedName = remapper.map(name.substring(0, name.indexOf('.'))); try { // Now we put it back on so the class file is written out with the right extension. jos.putNextEntry(new JarEntry(mappedName + ".class")); IOUtil.copy(renamedClass, jos); } catch (ZipException e) { getLogger().debug("We have a duplicate " + mappedName + " in " + jar); } } private boolean isFiltered(List<Filter> filters, String name) { for (Filter filter : filters) { if (filter.isFiltered(name)) { return true; } } return false; } private boolean resourceTransformed(List<ResourceTransformer> resourceTransformers, String name, InputStream is, List<Relocator> relocators) throws IOException { for (ResourceTransformer transformer : resourceTransformers) { if (transformer.canTransformResource(name)) { getLogger().debug("Transforming " + name + " using " + transformer.getClass().getName()); transformer.processResource(name, is, relocators); return true; } } return false; } private void addJavaSource(Set<String> resources, JarOutputStream jos, String name, InputStream is, List<Relocator> relocators) throws IOException { jos.putNextEntry(new JarEntry(name)); String sourceContent = IOUtil.toString(new InputStreamReader(is, "UTF-8")); for (Relocator relocator : relocators) { sourceContent = relocator.applyToSourceContent(sourceContent); } OutputStreamWriter writer = new OutputStreamWriter(jos, "UTF-8"); IOUtil.copy(sourceContent, writer); writer.flush(); resources.add(name); } private void addResource(Set<String> resources, JarOutputStream jos, String name, InputStream is) throws IOException { jos.putNextEntry(new JarEntry(name)); IOUtil.copy(is, jos); resources.add(name); } class RelocatorRemapper extends Remapper { private final Pattern classPattern = Pattern.compile("(\\[*)?L(.+);"); List<Relocator> relocators; public RelocatorRemapper(List<Relocator> relocators) { this.relocators = relocators; } public boolean hasRelocators() { return !relocators.isEmpty(); } @Override public Object mapValue(Object object) { if (object instanceof String) { String name = (String) object; String value = name; String prefix = ""; String suffix = ""; Matcher m = classPattern.matcher(name); if (m.matches()) { prefix = m.group(1) + "L"; suffix = ";"; name = m.group(2); } for (Relocator r : relocators) { if (r.canRelocateClass(name)) { value = prefix + r.relocateClass(name) + suffix; break; } else if (r.canRelocatePath(name)) { value = prefix + r.relocatePath(name) + suffix; break; } } return value; } return super.mapValue(object); } @Override public String map(String name) { String value = name; String prefix = ""; String suffix = ""; Matcher m = classPattern.matcher(name); if (m.matches()) { prefix = m.group(1) + "L"; suffix = ";"; name = m.group(2); } for (Relocator r : relocators) { if (r.canRelocatePath(name)) { value = prefix + r.relocatePath(name) + suffix; break; } } return value; } } }