/*
 * 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.maven.plugins.shade;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
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.zip.ZipException;

import org.apache.maven.plugins.shade.relocation.Relocator;
import org.apache.maven.plugins.shade.resource.ResourceTransformer;
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.Remapper;
import org.objectweb.asm.commons.RemappingClassAdapter;

/**
 * @author Jason van Zyl
 * @plexus.component
 */
public class DefaultShader
    extends AbstractLogEnabled
    implements Shader
{
    public void shade( Set jars, File uberJar, List relocators, List resourceTransformers )
        throws IOException
    {
        Set resources = new HashSet();

        RelocatorRemapper remapper = new RelocatorRemapper( relocators );

        JarOutputStream jos = new JarOutputStream( new FileOutputStream( uberJar ) );

        for ( Iterator i = jars.iterator(); i.hasNext(); )
        {
            File jar = (File) i.next();

            JarFile jarFile = new JarFile( jar );

            for ( Enumeration j = jarFile.entries(); j.hasMoreElements(); )
            {
                JarEntry entry = (JarEntry) j.nextElement();

                String name = entry.getName();
                String mappedName = remapper.map( name );

                InputStream is = jarFile.getInputStream( entry );
                if ( !entry.isDirectory() )
                {
                    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, jar, name, is );
                    }
                    else
                    {
                        if ( !resourceTransformed( resourceTransformers, mappedName, is ) )
                        {
                            // 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();
        }

        for ( Iterator i = resourceTransformers.iterator(); i.hasNext(); )
        {
            ResourceTransformer transformer = (ResourceTransformer) i.next();

            if ( transformer.hasTransformedResource() )
            {
                transformer.modifyOutputStream( jos );
            }
        }

        IOUtil.close( jos );
    }

    private void addDirectory( Set 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 );
            }
        }
        jos.putNextEntry( new JarEntry( name ) );
        resources.add( name );
    }

    private void addRemappedClass( RelocatorRemapper remapper, JarOutputStream jos, File jar, String name, InputStream is )
        throws IOException
    {
        if ( !remapper.hasRelocators() ) 
        {
            jos.putNextEntry( new JarEntry( name ) );

            IOUtil.copy( is, jos );
            return;
        }
        
        ClassReader cr = new ClassReader( is );

        ClassWriter cw = new ClassWriter( cr, 0 );

        ClassVisitor cv = new RemappingClassAdapter( cw, remapper );

        cr.accept( cv, ClassReader.EXPAND_FRAMES );

        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().warn( "We have a duplicate " + mappedName + " in " + jar );
        }
    }

    private boolean resourceTransformed( List resourceTransformers, String name, InputStream is )
        throws IOException
    {
        boolean resourceTransformed = false;

        for ( Iterator k = resourceTransformers.iterator(); k.hasNext(); )
        {
            ResourceTransformer transformer = (ResourceTransformer) k.next();

            if ( transformer.canTransformResource( name ) )
            {
                transformer.processResource( is );

                resourceTransformed = true;

                break;
            }
        }
        return resourceTransformed;
    }

    private void addResource( Set 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
    {
        List relocators;

        public RelocatorRemapper( List relocators )
        {
            this.relocators = relocators;
        }
        
        public boolean hasRelocators() 
        {
            return !relocators.isEmpty();
        }

        public Object mapValue( Object object )
        {
            return object;
        }

        public String map( String name )
        {
            for ( Iterator i = relocators.iterator(); i.hasNext(); )
            {
                Relocator r = (Relocator) i.next();

                if ( r.canRelocate( name ) )
                {
                    return r.relocate( name );
                }
            }

            return name;
        }
    }
}

@@ -4820,32 +4820,77 @@
   %7D%0D%0A        %7D%0D%0A
+        //directory entries must end in %22/%22%0D%0A
         jos.putN
@@ -4913,24 +4913,30 @@
 rEntry( name
+ + %22/%22
  ) );%0D%0A     

Fix problems with directories in the generated jars