/*
* 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.fusesource.mvnplugins.uberize.transformer;
import org.fusesource.mvnplugins.uberize.relocation.Relocator;
import org.fusesource.mvnplugins.uberize.relocation.SimpleRelocator;
import org.fusesource.mvnplugins.uberize.relocation.PackageRelocation;
import org.fusesource.mvnplugins.uberize.Transformer;
import org.fusesource.mvnplugins.uberize.UberEntry;
import org.fusesource.mvnplugins.uberize.DefaultUberizer;
import org.fusesource.mvnplugins.uberize.Uberizer;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.FileUtils;
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 java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;
import java.util.Map.Entry;
/**
* Uses byte code manipulation to relocate java classes to a new package. It can optionally
* update resource files so that class names referenced in the files are updated with
* the new package names.
*
* @author Jason van Zyl
* @author <a href="http://hiramchirino.com">Hiram Chirino</a>
*/
public class ClassShader implements Transformer {
public PackageRelocation[] relocations;
public Resources resources;
private List<Relocator> getRelocators()
{
List<Relocator> relocators = new ArrayList<Relocator>();
if ( relocations == null )
{
return relocators;
}
for ( int i = 0; i < relocations.length; i++ )
{
PackageRelocation r = relocations[i];
relocators.add( new SimpleRelocator( r.getPattern(), r.getShadedPattern(), r.getExcludes() ) );
}
return relocators;
}
public void process(Uberizer uberizer, File workDir, TreeMap<String, UberEntry> nodes) throws IOException {
final List<Relocator> relocators = getRelocators();
// Perhaps there is no work for us to do.
if( relocators.isEmpty() ) {
return;
}
HashMap<String, String> relocatedClasses = uberizer.getClassRelocations();
RelocatorRemapper remapper = new RelocatorRemapper(relocators);
for (UberEntry node : new ArrayList<UberEntry>(nodes.values())) {
if( node.getSources().isEmpty() ) {
continue;
}
String path = node.getPath();
if ( path.endsWith( ".class" ) )
{
// Need to take the .class off for remapping evaluation
final String classPath = path.substring(0, path.indexOf('.'));
String remappedPath = remapper.map(classPath) + ".class";
byte[] modifiedClass;
File file = uberizer.pickOneSource(nodes, node);
InputStream is = new FileInputStream( file );
try {
ClassReader cr = new ClassReader( is );
ClassWriter cw = new ClassWriter( cr, 0 );
ClassVisitor cv = new RemappingClassAdapter( cw, remapper );
cr.accept( cv, ClassReader.EXPAND_FRAMES );
modifiedClass = cw.toByteArray();
} finally {
IOUtil.close( is );
}
String className = classPath.replace('/','.');
String mappedClassName = mapClassName(relocators, className);
if( mappedClassName != className ) {
relocatedClasses.put(className, mappedClassName);
}
// Write the file out
is = new ByteArrayInputStream(modifiedClass);
File classFile = DefaultUberizer.writeFile(workDir, remappedPath, is);
// Modify the node tree.
nodes.remove(path);
UberEntry update = new UberEntry(remappedPath, node).addSource(classFile);
nodes.put(update.getPath(), update);
}
}
// Should we update resources with the class name changes?
if( resources!=null && !relocatedClasses.isEmpty()) {
// regex for a non class name character
final String ncnc = "(\\A|\\z|[^\\p{L}\\p{Nd}\\.\\$\\_])";
for (UberEntry node : new ArrayList<UberEntry>(nodes.values())) {
String path = node.getPath();
if ( resources.matches(path) && !path.endsWith(".class")) {
File file = uberizer.pickOneSource(nodes, node);
String content = FileUtils.fileRead(file);
for (Entry<String, String> entry : relocatedClasses.entrySet()) {
// This regex is little complicated cause we don't want to partially match a class.
// For example, replacing the 'test.Foo' in the 'com.myco.test.Foo' would be
// a bad thing.
String regex = ncnc + Pattern.quote(entry.getKey()) + ncnc;
// Need to escape the $ litterals in the value...
String value = entry.getValue().replaceAll("\\$", "\\\\\\$");
content = content.replaceAll(regex, "$1"+ value +"$2");
}
File udpateFile = DefaultUberizer.prepareFile(workDir, node.getPath());
FileUtils.fileWrite(udpateFile.getPath(), content);
// Modify the node tree.
UberEntry update = new UberEntry(node).addSource(udpateFile);
nodes.put(node.getPath(), update);
}
}
}
}
public String mapClassName(List<Relocator> relocators, String name)
{
String value = name;
for ( Iterator i = relocators.iterator(); i.hasNext(); )
{
Relocator r = (Relocator) i.next();
if ( r.canRelocateClass( name ) )
{
value = r.relocateClass( name );
break;
}
}
return value;
}
class RelocatorRemapper extends Remapper
{
List<Relocator> relocators;
public RelocatorRemapper( List<Relocator> relocators )
{
this.relocators = relocators;
}
public boolean hasRelocators()
{
return !relocators.isEmpty();
}
public Object mapValue( Object object )
{
if ( object instanceof String )
{
String name = (String) object;
String value = name;
for ( Iterator i = relocators.iterator(); i.hasNext(); )
{
Relocator r = (Relocator) i.next();
if ( r.canRelocateClass( name ) )
{
value = r.relocateClass( name );
break;
}
else if ( r.canRelocatePath( name ) )
{
value = r.relocatePath( name );
break;
}
if ( name.length() > 0 && name.charAt( 0 ) == '[' )
{
int count = 0;
while ( name.length() > 0 && name.charAt(0) == '[' )
{
name = name.substring( 1 );
++count;
}
if ( name.length() > 0
&& name.charAt( 0 ) == 'L'
&& name.charAt( name.length() - 1 ) == ';' )
{
name = name.substring( 1, name.length() - 1 );
if ( r.canRelocatePath( name ) )
{
value = 'L' + r.relocatePath( name ) + ';';
while ( count > 0 )
{
value = '[' + value;
--count;
}
break;
}
if ( r.canRelocateClass( name ) )
{
value = 'L' + r.relocateClass( name ) + ';';
while (count > 0)
{
value = '[' + value;
--count;
}
break;
}
}
}
}
return value;
}
return super.mapValue( object );
}
public String map( String name )
{
String value = name;
for ( Iterator i = relocators.iterator(); i.hasNext(); )
{
Relocator r = (Relocator) i.next();
if ( r.canRelocatePath( name ) )
{
value = r.relocatePath( name );
break;
}
}
return value;
}
}
}