| /* |
| * 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. |
| */ |
| |
| package org.apache.storm.hack; |
| |
| import org.apache.storm.hack.relocation.Relocator; |
| import org.apache.storm.hack.resource.ResourceTransformer; |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.ClassVisitor; |
| import org.objectweb.asm.ClassWriter; |
| import org.objectweb.asm.commons.Remapper; |
| import org.objectweb.asm.commons.RemappingClassAdapter; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.*; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarInputStream; |
| import java.util.jar.JarOutputStream; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.zip.ZipException; |
| |
| /** |
| * This is based off of |
| * |
| * https://github.com/apache/maven-plugins.git |
| * |
| * maven-shade-plugin-2.4.1 |
| */ |
| public class DefaultShader { |
| private static final Logger LOG = LoggerFactory.getLogger(DefaultShader.class); |
| |
| public void shadeJarStream(ShadeRequest shadeRequest, InputStream in, final OutputStream fileOutputStream) |
| throws IOException { |
| Set<String> resources = new HashSet<>(); |
| |
| List<ResourceTransformer> transformers = |
| new ArrayList<>( shadeRequest.getResourceTransformers() ); |
| LOG.debug("Transformers {}", transformers); |
| |
| RelocatorRemapper remapper = new RelocatorRemapper( shadeRequest.getRelocators() ); |
| LOG.debug("Remapper {}", remapper); |
| |
| JarOutputStream jos = new JarOutputStream( new BufferedOutputStream( fileOutputStream ) ); |
| |
| JarInputStream jarFile = new JarInputStream(in); |
| |
| for ( JarEntry entry = jarFile.getNextJarEntry(); entry != null; entry = jarFile.getNextJarEntry()) |
| { |
| String name = entry.getName(); |
| LOG.debug("Processing " + name); |
| remapper.setClassName(name); |
| if ( "META-INF/INDEX.LIST".equals( name ) ) |
| { |
| LOG.debug("Skipping INDEX.LIST..."); |
| // 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() ) |
| { |
| InputStream is = jarFile; |
| |
| String mappedName = remapper.map( name ); |
| LOG.debug(name + " -> " + mappedName); |
| |
| 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" ) ) |
| { |
| addRemappedClass( remapper, jos, name, is ); |
| } |
| else if ( 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 ); |
| } |
| } |
| } |
| } |
| |
| jarFile.close(); |
| |
| for ( ResourceTransformer transformer : transformers ) |
| { |
| if ( transformer.hasTransformedResource() ) |
| { |
| transformer.modifyOutputStream( jos ); |
| } |
| } |
| |
| jos.close(); |
| } |
| |
| 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 + "/"); |
| LOG.debug("Adding JAR directory " + entry); |
| jos.putNextEntry(entry); |
| |
| resources.add(name); |
| } |
| |
| private void addRemappedClass( RelocatorRemapper remapper, JarOutputStream jos, String name, |
| InputStream is ) |
| throws IOException |
| { |
| LOG.debug("Remapping class... "+name); |
| if ( !remapper.hasRelocators() ) |
| { |
| try |
| { |
| LOG.debug("Just copy class..."); |
| jos.putNextEntry( new JarEntry( name ) ); |
| IOUtil.copy( is, jos ); |
| } |
| catch ( ZipException e ) |
| { |
| LOG.info( "zip exception ", e); |
| } |
| |
| 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 RemappingClassAdapter( cw, remapper ) |
| { |
| @Override |
| public void visitSource( final String source, final String debug ) |
| { |
| LOG.debug("visitSource "+source); |
| if ( source == null ) |
| { |
| super.visitSource( source, debug ); |
| } |
| else |
| { |
| final String fqSource = pkg + source; |
| final String mappedSource = remapper.map( fqSource ); |
| final String filename = mappedSource.substring( mappedSource.lastIndexOf( '/' ) + 1 ); |
| LOG.debug("Remapped to "+filename); |
| super.visitSource( filename, debug ); |
| } |
| } |
| }; |
| |
| try |
| { |
| cr.accept( cv, ClassReader.EXPAND_FRAMES ); |
| } |
| catch ( Throwable ise ) |
| { |
| throw new IOException( "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( '.' ) ) ); |
| LOG.debug("Remapped class name to "+mappedName); |
| |
| try |
| { |
| // Now we put it back on so the class file is written out with the right extension. |
| jos.putNextEntry( new JarEntry( mappedName + ".class" ) ); |
| jos.write(renamedClass); |
| } |
| catch ( ZipException e ) |
| { |
| LOG.info( "zip exception ", e); |
| } |
| } |
| |
| private boolean resourceTransformed( List<ResourceTransformer> resourceTransformers, String name, InputStream is, |
| List<Relocator> relocators ) |
| throws IOException |
| { |
| boolean resourceTransformed = false; |
| |
| for ( ResourceTransformer transformer : resourceTransformers ) |
| { |
| if ( transformer.canTransformResource( name ) ) |
| { |
| LOG.debug( "Transforming " + name + " using " + transformer.getClass().getName() ); |
| |
| transformer.processResource( name, is, relocators ); |
| |
| resourceTransformed = true; |
| |
| break; |
| } |
| } |
| return resourceTransformed; |
| } |
| |
| 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" ); |
| writer.append(sourceContent); |
| 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(.+);" ); |
| |
| private final List<Relocator> relocators; |
| |
| private final HashSet<String> warned = new HashSet<>(); |
| |
| private String className = "UNKNOWN"; |
| |
| public RelocatorRemapper( List<Relocator> relocators) |
| { |
| this.relocators = relocators; |
| } |
| |
| public boolean hasRelocators() |
| { |
| return !relocators.isEmpty(); |
| } |
| |
| public void setClassName(String className) { |
| this.className = className; |
| } |
| |
| @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 orig = 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; |
| if (!warned.contains(orig)) { |
| LOG.warn("Relocating {} to {} in {}. {}", orig, value, className, r.getWarnMessage()); |
| warned.add(orig); |
| } |
| break; |
| } |
| } |
| |
| return value; |
| } |
| |
| } |
| |
| } |