blob: c053012379135d4249ba641b0c23fe8c2fe11c99 [file] [log] [blame]
/*
* 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;
}
}
}