/*
 * Copyright (C) 2009 Jayway AB
 * Copyright (C) 2007-2008 JVending Masa
 *
 * Licensed 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 com.simpligility.maven.plugins.android.phase01generatesources;

import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
import com.simpligility.maven.plugins.android.common.AaptCommandBuilder.AaptPackageCommandBuilder;
import com.simpligility.maven.plugins.android.common.DependencyResolver;
import com.simpligility.maven.plugins.android.common.FileRetriever;
import com.simpligility.maven.plugins.android.configuration.BuildConfigConstant;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.repository.RepositorySystem;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.zip.ZipUnArchiver;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.logging.console.ConsoleLogger;
import org.w3c.dom.Document;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKSOURCES;

/**
 * Generates R.java based on resources specified by the resources configuration parameter.
 * Generates java files based on aidl files.
 *
 * @author hugo.josefson@jayway.com
 * @author Manfred Moser - manfred@simpligility.com
 * @author William Ferguson - william.ferguson@xandar.com.au
 * @author Malachi de AElfweald malachid@gmail.com
 */
@Mojo(
        name = "generate-sources",
        defaultPhase = LifecyclePhase.GENERATE_SOURCES,
        requiresDependencyResolution = ResolutionScope.COMPILE
)
public class GenerateSourcesMojo extends AbstractAndroidMojo
{

    /**
     *
Whether to produce a warning if there is an aar dependency that has an apklib artifact in its dependency
     * tree. The combination of aar library including or depending on an apklib has been deprecated and may not be
     * supported by future plugin versions. Traversing the dependency graph is done for all project dependencies
     * present in build classpath.

     *
     *
It is recommended to keep this set to true to catch possible issues as soon as possible.

     */
    @Parameter( defaultValue = "true" )
    protected boolean warnOnApklibDependencies;

    /**
     * Whether to fail the build if one of the dependencies and/or the project duplicate a layout file.
     *

     * Such a scenario generally means that the build will fail with a compilation error due to missing resource files.
     * This is because any Ids contained in the duplicate layout files can never be generated by aapt, as appt
     * only picks up the first resource file it finds in its path.
     */
    @Parameter( defaultValue = "true" )
    private boolean failOnConflictingLayouts;

    /**
     * Whether to fail the build if one of the dependencies and/or the project have similar package in the
     * AndroidManifest.
     *

     * Such scenario generally means that the build will fail with a compilation error due to
     * missing resources in the R file generated.
     */
    @Parameter( defaultValue = "true" )
    private boolean failOnDuplicatePackages;

    /**
     * Override default generated folder containing aidl classes
     */
    @Parameter(
            property = "android.genDirectoryAidl",
            defaultValue = "${project.build.directory}/generated-sources/aidl"
    )
    protected File genDirectoryAidl;

    /**
     * The directory containing the aidl files.
     */
    @Parameter( property = "android.aidlSourceDirectory", defaultValue = "${project.basedir}/src/main/aidl" )
    protected File aidlSourceDirectory;

    /**
     *
Parameter designed to generate custom BuildConfig constants
     */
    @Parameter( property = "android.buildConfigConstants" )
    protected BuildConfigConstant[] buildConfigConstants;

    /**
     */
    @Component
    private RepositorySystem repositorySystem;

    /**
     * Pre AMP-4 AndroidManifest file.
     */
    @Parameter( readonly = true, defaultValue = "${project.basedir}/AndroidManifest.xml" )
    private File androidManifestFilePre4;

    /**
     * Pre AMP-4 android resources folder.
     */
    @Parameter( readonly = true, defaultValue = "${project.basedir}/res" )
    private File resourceDirectoryPre4;

    /**
     * Pre AMP-4 android assets directory.
     */
    @Parameter( readonly = true, defaultValue = "${project.basedir}/assets" )
    private File assetsDirectoryPre4;

    /**
     * Pre AMP-4 native libraries folder.
     */
    @Parameter( readonly = true, defaultValue = "${project.basedir}/libs" )
    private File nativeLibrariesDirectoryPre4;

    /**
     * If any non-standard files/folders exist and have NOT been explicitly configured then fail the build.
     */
    @Parameter( defaultValue = "true" )
    private boolean failOnNonStandardStructure;

    /**
     * Which dependency scopes should not be included when unpacking dependencies
     */
    protected static final List<String> EXCLUDED_DEPENDENCY_SCOPES_FOR_EXTRACTION = Arrays.asList(
            Artifact.SCOPE_SYSTEM, Artifact.SCOPE_IMPORT
    );

    /**
     * Generates the sources.
     *
     * @throws MojoExecutionException if it fails.
     * @throws MojoFailureException   if it fails.
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        // If the current POM isn't an Android-related POM, then don't do
        // anything. This helps work with multi-module projects.
        if ( !isCurrentProjectAndroid() )
        {
            return;
        }

        validateStandardLocations();

        try
        {
            targetDirectory.mkdirs();
            copyManifest();

            // Check for a deprecated AAR > APKLIB artifact combination
            if ( warnOnApklibDependencies )
            {
                checkForApklibDependencies();
            }

            // TODO Do we really want to continue supporting APKSOURCES? How long has it been deprecated
            extractSourceDependencies();

            // Extract the apklib and aar dependencies into unpacked-libs so that they can be referenced in the build.
            extractLibraryDependencies();

            // Copy project assets to combinedAssets so that aapt has a single assets folder to load.
            copyFolder( assetsDirectory, combinedAssets );

            final String[] relativeAidlFileNames1 = findRelativeAidlFileNames( aidlSourceDirectory );
            final String[] relativeAidlFileNames2 = findRelativeAidlFileNames( extractedDependenciesJavaSources );
            final Map<String, String[]> relativeApklibAidlFileNames = new HashMap<String, String[]>();

            if ( !isInstrumentationTest() )
            {
                // Only add transitive APKLIB deps if we are building an APK and not an instrumentation test apk.
                for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB ) )
                {
                    final File libSourceFolder = getUnpackedApkLibSourceFolder( artifact );
                    final String[] apklibAidlFiles = findRelativeAidlFileNames( libSourceFolder );
                    relativeApklibAidlFileNames.put( artifact.getId(), apklibAidlFiles );
                }
            }

            checkPackagesForDuplicates();
            checkForConflictingLayouts();
            generateR();
            generateBuildConfig();

            // When compiling AIDL for this project,
            // make sure we compile AIDL for dependencies as well.
            // This is so project A, which depends on project B, can
            // use AIDL info from project B in its own AIDL
            final Map<File, String[]> files = new HashMap<File, String[]>();
            files.put( aidlSourceDirectory, relativeAidlFileNames1 );
            files.put( extractedDependenciesJavaSources, relativeAidlFileNames2 );

            if ( !isInstrumentationTest() )
            {
                // Only add transitive APKLIB deps if we are building an APK and not an instrumentation test apk.
                for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB ) )
                {
                    final File unpackedLibSourceFolder = getUnpackedApkLibSourceFolder( artifact );
                    files.put( unpackedLibSourceFolder, relativeApklibAidlFileNames.get( artifact.getId() ) );
                }
            }
            generateAidlFiles( files );
        }
        catch ( MojoExecutionException e )
        {
            getLog().error( "Error when generating sources.", e );
            throw e;
        }
    }

    /**
     * Performs some level of validation on the configured files and folders in light of the change
     * to AndroidStudio/Gradle style folder structures instead of the original Ant/Eclipse structures.
     *

     * Essentially we will be noisy if we see an artifact that looks like before
     * Android Maven Plugin 4 and is not explicitly configured.
     */
    private void validateStandardLocations() throws MojoExecutionException
    {
        boolean hasNonStandardStructure = false;
        if ( androidManifestFilePre4.exists() && !androidManifestFilePre4.equals( androidManifestFile ) )
        {
            getLog().warn( "Non-standard location of AndroidManifest.xml file found, but not configured:\n "
                    + androidManifestFilePre4 + "\nMove to the standard location src/main/AndroidManifest.xml\n"
                    + "Or configure androidManifestFile. \n" );
            hasNonStandardStructure = true;
        }
        if ( resourceDirectoryPre4.exists() && !resourceDirectoryPre4.equals( resourceDirectory ) )
        {
            getLog().warn( "Non-standard location of Android res folder found, but not configured:\n "
                    + resourceDirectoryPre4 + "\nMove to the standard location src/main/res/\n"
                    + "Or configure resourceDirectory. \n" );
            hasNonStandardStructure = true;
        }
        if ( assetsDirectoryPre4.exists() && !assetsDirectoryPre4.equals( assetsDirectory ) )
        {
            getLog().warn( "Non-standard location assets folder found, but not configured:\n "
                    + assetsDirectoryPre4 + "\nMove to the standard location src/main/assets/\n"
                    + "Or configure assetsDirectory. \n" );
            hasNonStandardStructure = true;
        }
        if ( nativeLibrariesDirectoryPre4.exists() && !nativeLibrariesDirectoryPre4.equals( nativeLibrariesDirectory ) )
        {
            getLog().warn( "Non-standard location native libs folder found, but not configured:\n "
                    + nativeLibrariesDirectoryPre4 + "\nMove to the standard location src/main/libs/\n"
                    + "Or configure nativeLibrariesDirectory. \n" );
            hasNonStandardStructure = true;
        }

        if ( hasNonStandardStructure && failOnNonStandardStructure )
        {
            throw new MojoExecutionException(
                    "\n\nFound files or folders in non-standard locations in the project!\n"
                            + "....This might be a side-effect of a migration to Android Maven Plugin 4+.\n"
                            + "....Please observe the warnings for specific files and folders above.\n"
                            + "....Ideally you should restructure your project.\n"
                            + "....Alternatively add explicit configuration overrides for files or folders.\n"
                            + "....Finally you could set failOnNonStandardStructure to false, potentially "
                            + "resulting in other failures.\n\n\n"
            );
        }
    }

    /**
     * Copy the AndroidManifest.xml from androidManifestFile to destinationManifestFile
     *
     * @throws MojoExecutionException
     */
    protected void copyManifest() throws MojoExecutionException
    {
        getLog().debug( "copyManifest: " + androidManifestFile + " -> " + destinationManifestFile );
        if ( androidManifestFile == null )
        {
            getLog().debug( "Manifest copying disabled. Using default manifest only" );
            return;
        }

        try
        {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse( androidManifestFile );
            Source source = new DOMSource( doc );

            TransformerFactory xfactory = TransformerFactory.newInstance();
            Transformer xformer = xfactory.newTransformer();
            xformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );

            FileWriter writer = null;
            try
            {
                destinationManifestFile.getParentFile().mkdirs();

                writer = new FileWriter( destinationManifestFile, false );
                if ( doc.getXmlEncoding() != null && doc.getXmlVersion() != null )
                {
                    String xmldecl = String.format( "<?xml version=\"%s\" encoding=\"%s\"?>%n",
                            doc.getXmlVersion(), doc.getXmlEncoding() );

                    writer.write( xmldecl );
                }
                Result result = new StreamResult( writer );
                xformer.transform( source, result );
                getLog().info( "Manifest copied from " + androidManifestFile + " to " + destinationManifestFile );
            }
            finally
            {
                IOUtils.closeQuietly( writer );
            }
        }
        catch ( Exception e )
        {
            getLog().error( "Error during copyManifest" );
            throw new MojoExecutionException( "Error during copyManifest", e );
        }
    }

    /**
     * Extract the source dependencies.
     *
     * @throws MojoExecutionException if it fails.
     */
    protected void extractSourceDependencies() throws MojoExecutionException
    {
        for ( Artifact artifact : getDirectDependencyArtifacts() )
        {
            String type = artifact.getType();
            if ( type.equals( APKSOURCES ) )
            {
                getLog().debug( "Detected apksources dependency " + artifact + " with file " + artifact.getFile()
                        + ". Will resolve and extract..." );

                final File apksourcesFile = resolveArtifactToFile( artifact );
                getLog().debug( "Extracting " + apksourcesFile + "..." );
                extractApksources( apksourcesFile );
            }
        }

        if ( extractedDependenciesJavaResources.exists() )
        {
            projectHelper.addResource( project, extractedDependenciesJavaResources.getAbsolutePath(), null, null );
            project.addCompileSourceRoot( extractedDependenciesJavaSources.getAbsolutePath() );
        }
    }

    /**
     * @deprecated Support APKSOURCES artifacts has been deprecated. Use APKLIB instead.
     */
    @Deprecated
    private void extractApksources( File apksourcesFile ) throws MojoExecutionException
    {
        if ( apksourcesFile.isDirectory() )
        {
            getLog().warn( "The apksources artifact points to '" + apksourcesFile
                    + "' which is a directory; skipping unpacking it." );
            return;
        }
        final UnArchiver unArchiver = new ZipUnArchiver( apksourcesFile )
        {
            @Override
            protected Logger getLogger()
            {
                return new ConsoleLogger( Logger.LEVEL_DEBUG, "dependencies-unarchiver" );
            }
        };
        extractedDependenciesDirectory.mkdirs();
        unArchiver.setDestDirectory( extractedDependenciesDirectory );
        try
        {
            unArchiver.extract();
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "ArchiverException while extracting " + apksourcesFile.getAbsolutePath()
                    + ". Message: " + e.getLocalizedMessage(), e );
        }
    }

    private void extractLibraryDependencies() throws MojoExecutionException
    {
        final Collection<Artifact> artifacts = getTransitiveDependencyArtifacts(
                EXCLUDED_DEPENDENCY_SCOPES_FOR_EXTRACTION );

        getLog().info( "Extracting libs" );

        // The only library dependencies we want included in an instrumentation test are APK and AARs.
        // Any APKLIB classes have already been compiled into the APK.
        final boolean instrumentationTest = isInstrumentationTest();

        for ( Artifact artifact : artifacts )
        {
            final String type = artifact.getType();
            if ( type.equals( APKLIB ) && !instrumentationTest )
            {
                getLog().info( "Extracting apklib " + artifact.getArtifactId() + "..." );
                extractApklib( artifact );
            }
            else if ( type.equals( AAR ) )
            {
                getLog().info( "Extracting aar " + artifact.getArtifactId() + "..." );
                extractAarLib( artifact );
            }
            else if ( type.equals( APK ) )
            {
                getLog().info( "Extracting apk " + artifact.getArtifactId() + "..." );
                extractApkClassesJar( artifact );
            }
            else
            {
                getLog().debug( "Not extracting " + artifact.getArtifactId() + "..." );
            }
        }
    }

    /**
     * Extracts ApkLib and adds the assets and apklib sources and resources to the build.
     */
    private void extractApklib( Artifact apklibArtifact ) throws MojoExecutionException
    {
        getUnpackedLibHelper().extractApklib( apklibArtifact );

        // Copy the assets to the the combinedAssets folder.
        // Add the apklib source and resource to the compile.
        // NB apklib sources are added to compileSourceRoot because we may need to compile against them.
        //    This means the apklib classes will be compiled into target/classes and packaged with this build.
        copyFolder( getUnpackedLibAssetsFolder( apklibArtifact ), combinedAssets );

        final File apklibSourceFolder = getUnpackedApkLibSourceFolder( apklibArtifact );
        final List<String> resourceExclusions = Arrays.asList( "**/*.java", "**/*.aidl" );
        projectHelper.addResource( project, apklibSourceFolder.getAbsolutePath(), null, resourceExclusions );
        project.addCompileSourceRoot( apklibSourceFolder.getAbsolutePath() );
    }

    /**
     * Extracts AarLib and if this is an APK build then adds the assets and resources to the build.
     */
    private void extractAarLib( Artifact aarArtifact ) throws MojoExecutionException
    {
        getUnpackedLibHelper().extractAarLib( aarArtifact );

        // Copy the assets to the the combinedAssets folder, but only if an APK build.
        // Ie we only want to package assets that we own.
        // Assets should only live within their owners or the final APK.
        if ( isAPKBuild() )
        {
            copyFolder( getUnpackedLibAssetsFolder( aarArtifact ), combinedAssets );
        }

        // Aar lib resources should only be included if we are building an apk.
        // So we need to extract them into a folder that we then add to the resource classpath.
        if ( isAPKBuild() )
        {
            // (If we are including SYSTEM_SCOPE artifacts when considering resources for APK packaging)
            // then adding the AAR resources to the project would have them added twice.
            getLog().debug( "Not adding AAR resources to resource classpath : " + aarArtifact );
        }
    }

    /**
     * Copies a dependent APK jar over the top of the placeholder created for it in AarMavenLifeCycleParticipant.
     *

     * This is necessary because we need the classes of the APK added to the compile classpath.
     * NB APK dependencies are uncommon as they should really only be used in a project that tests an apk.
     *
     * @param artifact APK dependency for this project whose classes will be copied over.
     */
    private void extractApkClassesJar( Artifact artifact ) throws MojoExecutionException
    {
        final File apkClassesJar = getUnpackedLibHelper().getJarFileForApk( artifact );
        final File unpackedClassesJar = getUnpackedLibHelper().getUnpackedClassesJar( artifact );
        try
        {
            FileUtils.copyFile( apkClassesJar, unpackedClassesJar );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException(
                    "Could not copy APK classes jar " + apkClassesJar + " to " + unpackedClassesJar, e );
        }
        getLog().debug( "Copied APK classes jar into compile classpath : " + unpackedClassesJar );

    }

    /**
     * Traverses the list of project dependencies looking for "AAR depends on APKLIB" artifact combination
     * that has been deprecated. For each occurrence of an AAR artifact with APKLIB direct or transitive dependency,
     * produces a warning message to inform the user. Future plugin versions may default to skipping or not handling
     * unsupported artifacts during build lifecycle.
     *
     * @throws MojoExecutionException
     */
    private void checkForApklibDependencies() throws MojoExecutionException
    {
        final boolean isAarBuild = project.getPackaging().equals( AAR );

        final DependencyResolver dependencyResolver = getDependencyResolver();

        final Set<Artifact> allArtifacts = project.getArtifacts();
        Set<Artifact> dependencyArtifacts = getArtifactResolverHelper().getFilteredArtifacts( allArtifacts );

        boolean foundApklib = false;

        for ( Artifact artifact : dependencyArtifacts )
        {
            final String type = artifact.getType();
            if ( type.equals( APKLIB ) && isAarBuild )
            {
                getLog().warn( "Detected APKLIB transitive dependency: " + artifact.getId() );
                foundApklib = true;
            }
            else if ( type.equals( AAR ) )
            {
                final Set<Artifact> dependencies = dependencyResolver
                        .getLibraryDependenciesFor( session, repositorySystem, artifact );
                for ( Artifact dependency : dependencies )
                {
                    if ( dependency.getType().equals( APKLIB ) )
                    {
                        getLog().warn( "Detected " + artifact.getId() + " that depends on APKLIB: "
                                + dependency.getId() );
                        foundApklib = true;
                    }
                }
            }
        }

        if ( foundApklib )
        {
            getLog().warn( "AAR libraries should not depend or include APKLIB artifacts.\n"
                    + "APKLIBs have been deprecated and the combination of the two may yield unexpected results.\n"
                    + "Check the problematic AAR libraries for newer versions that use AAR packaging." );
        }
    }

    /**
     * Checks packages in AndroidManifest.xml file of project and all dependent libraries for duplicates.
     *
Generate warning if duplicates presents.
     *
(in case of packages similarity R.java and BuildConfig files will be overridden)
     *
     * @throws MojoExecutionException
     */
    private void checkPackagesForDuplicates() throws MojoExecutionException
    {
        Set<Artifact> dependencyArtifacts = getTransitiveDependencyArtifacts( AAR, APKLIB );

        if ( dependencyArtifacts.isEmpty() )
        {
            //if no AAR or APKLIB dependencies presents than only one package presents
            return;
        }

        Map<String, Set<Artifact>> packageCompareMap = getPackageCompareMap( dependencyArtifacts );

        List<String> duplicatesMessageList = new ArrayList<String>();
        for ( Map.Entry<String, Set<Artifact>> entry : packageCompareMap.entrySet() )
        {
            Set<Artifact> artifacts = entry.getValue();
            if ( artifacts != null && artifacts.size() > 1 )
            {
                StringBuilder messageBuilder = new StringBuilder();
                for ( Artifact item : artifacts )
                {
                    messageBuilder
                            .append( messageBuilder.length() > 0 ? ", " : "    [" )
                            .append( item.getArtifactId() );
                }
                messageBuilder
                        .append( "] have similar package='" )
                        .append( entry.getKey() )
                        .append( "'" );
                duplicatesMessageList.add( messageBuilder.toString() );
            }
        }
        if ( !duplicatesMessageList.isEmpty() )
        {
            List<String> messageList = new ArrayList<String>();
            messageList.add( "" );
            messageList.add( "Duplicate packages detected in AndroidManifest.xml files" );
            messageList.add( "" );
            messageList.add( "Such scenario generally means that the build will fail with a compilation error due to"
                    + " missing resources in R file." );
            messageList.add( "You should consider renaming some of the duplicate packages listed below"
                    + " to avoid the conflict." );
            messageList.add( "" );
            messageList.add( "Conflicting artifacts:" );
            messageList.addAll( duplicatesMessageList );
            messageList.add( "" );

            if ( failOnDuplicatePackages )
            {
                StringBuilder builder = new StringBuilder();
                for ( String line : messageList )
                {
                    builder.append( line );
                    builder.append( "\n" );
                }
                builder.append( "\n" );
                builder.append( "You can downgrade the failure to a warning " );
                builder.append( "by setting the 'failOnDuplicatePackages' plugin property to false." );
                throw new MojoExecutionException( builder.toString() );
            }
            for ( String messageLine : messageList )
            {
                getLog().warn( messageLine );
            }
        }
    }

    /**
     * Looks for dependencies that have a layout file with the same name as either one of the project layout files
     * or one of the other dependencies. If such a duplicate occurs then it will fail the build or generate a warning
     * depending upon the value of the failOnConflictingLayouts build parameter.
     *
     * @throws MojoExecutionException
     */
    private void checkForConflictingLayouts() throws MojoExecutionException
    {
        final ConflictingLayoutDetector detector = new ConflictingLayoutDetector();

        // Add layout files for this project
        final FileRetriever retriever = new FileRetriever( "layout*/*.xml" );
        detector.addLayoutFiles( getAndroidManifestPackageName(), retriever.getFileNames( resourceDirectory ) );

        // Add layout files for all dependencies.
        for ( final Artifact dependency : getTransitiveDependencyArtifacts( AAR, APKLIB ) )
        {
            final String packageName = extractPackageNameFromAndroidArtifact( dependency );
            final String[] layoutFiles = retriever.getFileNames( getUnpackedLibResourceFolder( dependency ) );
            detector.addLayoutFiles( packageName, layoutFiles );
        }

        final Collection<ConflictingLayout> conflictingLayouts = detector.getConflictingLayouts();
        getLog().debug( "checkConflictingLayouts - conflicts : " + conflictingLayouts );
        if ( !conflictingLayouts.isEmpty() )
        {
            final List<String> sb = new ArrayList<String>();
            sb.add( "" );
            sb.add( "" );
            sb.add( "Duplicate layout files have been detected across more than one Android package." );
            sb.add( "" );
            sb.add( "Such a scenario generally means that the build will fail with a compilation error due to" );
            sb.add( "missing resource files. You should consider renaming some of the layout files listed below" );
            sb.add( "to avoid the conflict." );
            sb.add( "" );
            sb.add( "However, if you believe you know better, then you can downgrade the failure to a warning" );
            sb.add( "by setting the failOnConflictingLayouts plugin property to false." );
            sb.add( "But you really don't want to do that." );
            sb.add( "" );
            sb.add( "Conflicting Layouts:" );
            for ( final ConflictingLayout layout : conflictingLayouts )
            {
                sb.add( "    " + layout.getLayoutFileName() + "  packages=" + layout.getPackageNames().toString() );
            }
            sb.add( "" );

            if ( failOnConflictingLayouts )
            {
                final StringBuilder builder = new StringBuilder();
                for ( final String line : sb )
                {
                    builder.append( line );
                    builder.append( "\n" );
                }
                throw new MojoExecutionException( builder.toString() );
            }

            for ( final String line : sb )
            {
                getLog().warn( line );
            }
        }
    }

    /**
     * Provides map with all provided dependencies or project itself grouped by package name
     *
     * @param dependencyArtifacts artifacts that should be grouped by package name
     * @return map of with package names(String) and sets of artifacts (Set<Artifact>)
     * that have similar package names
     * @throws MojoExecutionException
     */
    Map<String, Set<Artifact>> getPackageCompareMap( Set<Artifact> dependencyArtifacts ) throws MojoExecutionException
    {
        if ( dependencyArtifacts == null )
        {
            throw new IllegalArgumentException( "dependencies must be initialized" );
        }

        Map<String, Set<Artifact>> packageCompareMap = new HashMap<String, Set<Artifact>>();

        Set<Artifact> artifactSet = new HashSet<Artifact>();
        artifactSet.add( project.getArtifact() );
        packageCompareMap.put( getAndroidManifestPackageName(), artifactSet );

        for ( Artifact artifact : dependencyArtifacts )
        {
            String libPackage = extractPackageNameFromAndroidArtifact( artifact );

            Set<Artifact> artifacts = packageCompareMap.get( libPackage );
            if ( artifacts == null )
            {
                artifacts = new HashSet<Artifact>();
                packageCompareMap.put( libPackage, artifacts );
            }
            artifacts.add( artifact );
        }
        return packageCompareMap;
    }

    private void generateR() throws MojoExecutionException
    {
        getLog().info( "Generating R file for " + project.getArtifact() );

        genDirectory.mkdirs();

        final AaptPackageCommandBuilder commandBuilder = AaptCommandBuilder
                .packageResources( getLog() )
                .makePackageDirectories()
                .setResourceConstantsFolder( genDirectory )
                .forceOverwriteExistingFiles()
                .disablePngCrunching()
                .generateRIntoPackage( customPackage )
                .setPathToAndroidManifest( destinationManifestFile )
                .addResourceDirectoriesIfExists( getResourceOverlayDirectories() )
                .addResourceDirectoryIfExists( resourceDirectory )
                // Need to include any AAR or APKLIB dependencies when generating R because if any local
                // resources directly reference dependent resources then R generation will crash.
                .addResourceDirectoriesIfExists( getLibraryResourceFolders() )
                .autoAddOverlay()
                .addRawAssetsDirectoryIfExists( combinedAssets )
                .addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() )
                .addConfigurations( configurations )
                .setVerbose( aaptVerbose )
                // We need to generate R.txt for all projects as it needs to be consumed when generating R class.
                // It also needs to be consumed when packaging aar.
                .generateRTextFile( targetDirectory )
                // If a proguard file is defined then output Proguard options to it.
                .setProguardOptionsOutputFile( proguardFile )
                .makeResourcesNonConstant( AAR.equals( project.getArtifact().getType() ) )
                .addExtraArguments( aaptExtraArgs );

        getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
        try
        {
            final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
            executor.setLogger( getLog() );
            executor.setCaptureStdOut( true );
            final List<String> commands = commandBuilder.build();
            executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
        }
        catch ( ExecutionException e )
        {
            throw new MojoExecutionException( "", e );
        }

        final ClassLoader compileClassLoader = getCompileClassLoader();
        final ResourceClassGenerator resGenerator = new ResourceClassGenerator(
                this,
                targetDirectory,
                genDirectory,
                compileClassLoader
        );
        generateCorrectRJavaForApklibDependencies( resGenerator );
        generateCorrectRJavaForAarDependencies( resGenerator );

        getLog().info( "Adding R gen folder to compile classpath: " + genDirectory );
        project.addCompileSourceRoot( genDirectory.getAbsolutePath() );
    }

    /**
     * @return ClassLoader containing the compile paths.
     */
    private ClassLoader getCompileClassLoader()
    {
        try
        {
            final List<String> runtimeClasspathElements = project.getCompileClasspathElements();
            final ClassLoaderFactory factory = new ClassLoaderFactory( runtimeClasspathElements );
            return factory.create();
        }
        catch ( DependencyResolutionRequiredException e )
        {
            throw new IllegalStateException( "Mojo should have resolved dependencies", e );
        }
    }

    /**
     * Generate correct R.java for apklibs dependencies of a current project
     *
     * @throws MojoExecutionException
     */
    private void generateCorrectRJavaForApklibDependencies( ResourceClassGenerator resourceGenerator )
            throws MojoExecutionException
    {
        getLog().debug( "" );
        getLog().debug( "#generateCorrectRJavaFoApklibDeps" );

        // Generate R.java for apklibs
        // Compatibility with Apklib which isn't present in AndroidBuilder
        getLog().debug( "Generating Rs for apklib deps of project " + project.getArtifact() );
        final Set<Artifact> apklibDependencies = getTransitiveDependencyArtifacts( APKLIB );
        for ( final Artifact artifact : apklibDependencies )
        {
            getLog().debug( "Generating apklib R.java for " + artifact.getArtifactId() + "..." );
            generateRForApkLibDependency( artifact );
        }

        // Generate corrected R.java for APKLIB dependencies, but only if this is an APK build.
        if ( !apklibDependencies.isEmpty() && APK.equals( project.getArtifact().getType() ) )
        {
            // Generate R.java for each APKLIB based on R.txt
            getLog().debug( "" );
            getLog().debug( "Rewriting R files for APKLIB dependencies : " + apklibDependencies );
            resourceGenerator.generateLibraryRs( apklibDependencies );
        }
    }

    /**
     * Generate correct R.java for aar dependencies of a current project
     *
     * @throws MojoExecutionException if it could not generate the R java for one of the libraries.
     */
    private void generateCorrectRJavaForAarDependencies( ResourceClassGenerator resourceGenerator )
            throws MojoExecutionException
    {
        // Generate corrected R.java for AAR dependencies.
        final Set<Artifact> aarLibraries = getTransitiveDependencyArtifacts( AAR );
        if ( !aarLibraries.isEmpty() )
        {
            // Generate R.java for each AAR based on R.txt
            getLog().debug( "Generating R file for AAR dependencies" );
            resourceGenerator.generateLibraryRs( aarLibraries );
        }
    }

    private List<File> getLibraryResourceFolders()
    {
        final List<File> resourceFolders = new ArrayList<File>();
        for ( Artifact artifact : getTransitiveDependencyArtifacts( AAR, APKLIB ) )
        {
            getLog().debug( "Considering dep artifact : " + artifact );
            final File resourceFolder = getUnpackedLibResourceFolder( artifact );
            if ( resourceFolder.exists() )
            {
                getLog().debug( "Adding apklib or aar resource folder : " + resourceFolder );
                resourceFolders.add( resourceFolder );
            }
        }
        return resourceFolders;
    }

    /**
     * Executes aapt to generate the R class for the given apklib.
     *
     * @param apklibArtifact apklib for which to generate the R class.
     * @throws MojoExecutionException if it fails.
     */
    private void generateRForApkLibDependency( Artifact apklibArtifact ) throws MojoExecutionException
    {
        final File unpackDir = getUnpackedLibFolder( apklibArtifact );
        getLog().debug( "Generating incomplete R file for apklib: " + apklibArtifact.getGroupId()
                + ":" + apklibArtifact.getArtifactId() );
        final File apklibManifest = new File( unpackDir, "AndroidManifest.xml" );
        final File apklibResDir = new File( unpackDir, "res" );

        List<File> dependenciesResDirectories = new ArrayList<File>();
        final Set<Artifact> apklibDeps = getDependencyResolver()
                .getLibraryDependenciesFor( this.session, this.repositorySystem, apklibArtifact );
        getLog().debug( "apklib=" + apklibArtifact + "  dependencies=" + apklibDeps );
        for ( Artifact dependency : apklibDeps )
        {
            // Add in the resources that are dependencies of the apklib.
            final String extension = dependency.getType();
            final File dependencyResDir = getUnpackedLibResourceFolder( dependency );
            if ( ( extension.equals( APKLIB ) || extension.equals( AAR ) ) && dependencyResDir.exists() )
            {
                dependenciesResDirectories.add( dependencyResDir );
            }
        }

        // Create combinedAssets for this apklib dependency - can't have multiple -A args
        final File apklibCombAssets = new File( getUnpackedLibFolder( apklibArtifact ), "combined-assets" );
        for ( Artifact dependency : apklibDeps )
        {
            // Accumulate assets for dependencies of the apklib (if they exist).
            final String extension = dependency.getType();
            final File dependencyAssetsDir = getUnpackedLibAssetsFolder( dependency );
            if ( ( extension.equals( APKLIB ) || extension.equals( AAR ) ) )
            {
                copyFolder( dependencyAssetsDir, apklibCombAssets );
            }
        }
        // Overlay the apklib dependency assets (if they exist)
        final File apkLibAssetsDir = getUnpackedLibAssetsFolder( apklibArtifact );
        copyFolder( apkLibAssetsDir, apklibCombAssets );

        final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        executor.setLogger( getLog() );

        final AaptCommandBuilder commandBuilder = AaptCommandBuilder
                .packageResources( getLog() )
                .makeResourcesNonConstant()
                .makePackageDirectories()
                .setResourceConstantsFolder( genDirectory )
                .generateRIntoPackage( extractPackageNameFromAndroidManifest( apklibManifest ) )
                .setPathToAndroidManifest( apklibManifest )
                .addResourceDirectoryIfExists( apklibResDir )
                .addResourceDirectoriesIfExists( dependenciesResDirectories )
                .autoAddOverlay()
                .addRawAssetsDirectoryIfExists( apklibCombAssets )
                .addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() )
                .addConfigurations( configurations )
                .setVerbose( aaptVerbose )
                .addExtraArguments( aaptExtraArgs )
                // We need to generate R.txt for all projects as it needs to be consumed when generating R class.
                // It also needs to be consumed when packaging aar.
                .generateRTextFile( unpackDir );

        getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
        try
        {
            executor.setCaptureStdOut( true );
            final List<String> commands = commandBuilder.build();
            executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
        }
        catch ( ExecutionException e )
        {
            throw new MojoExecutionException( "", e );
        }
    }

    private void generateBuildConfig() throws MojoExecutionException
    {
        getLog().debug( "Generating BuildConfig file" );

        // Create the BuildConfig for our package.
        String packageName = extractPackageNameFromAndroidManifest( destinationManifestFile );
        if ( StringUtils.isNotBlank( customPackage ) )
        {
            packageName = customPackage;
        }
        generateBuildConfigForPackage( packageName );

        // Skip BuildConfig generation for dependencies if this is an AAR project
        if ( project.getPackaging().equals( AAR ) )
        {
            return;
        }

        // Generate the BuildConfig for any APKLIB and some AAR dependencies.
        // Need to generate for AAR, because some old AARs like ActionBarSherlock do not have BuildConfig (or R)
        for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
        {
            if ( skipBuildConfigGeneration( artifact ) )
            {
                getLog().info( "Skip BuildConfig.java generation for "
                        + artifact.getGroupId() + " " + artifact.getArtifactId() );
                continue;
            }

            final String depPackageName = extractPackageNameFromAndroidArtifact( artifact );

            generateBuildConfigForPackage( depPackageName );
        }
    }

    private boolean skipBuildConfigGeneration( Artifact artifact ) throws MojoExecutionException
    {
        if ( artifact.getType().equals( AAR ) )
        {
            String depPackageName = extractPackageNameFromAndroidArtifact( artifact );

            if ( isBuildConfigPresent( artifact, depPackageName ) )
            {
                return true;
            }

            Set<Artifact> transitiveDep = getArtifactResolverHelper()
                    .getFilteredArtifacts( project.getArtifacts(), AAR );

            for ( Artifact transitiveArtifact : transitiveDep )
            {
                if ( isBuildConfigPresent( transitiveArtifact, depPackageName ) )
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Check if given artifact includes a matching BuildConfig class
     *
     * @throws MojoExecutionException
     */
    private boolean isBuildConfigPresent( Artifact artifact ) throws MojoExecutionException
    {
        String depPackageName = extractPackageNameFromAndroidArtifact( artifact );

        return isBuildConfigPresent( artifact, depPackageName );
    }

    /**
     * Check whether the artifact includes a BuildConfig located in a given package.
     *
     * @param artifact    an AAR artifact to look for BuildConfig in
     * @param packageName BuildConfig package name
     * @throws MojoExecutionException
     */
    private boolean isBuildConfigPresent( Artifact artifact, String packageName ) throws MojoExecutionException
    {
        try
        {
            JarFile jar = new JarFile( getUnpackedAarClassesJar( artifact ) );
            JarEntry entry = jar.getJarEntry( packageName.replace( '.', '/' ) + "/BuildConfig.class" );

            return ( entry != null );
        }
        catch ( IOException e )
        {
            getLog().error( "Error generating BuildConfig ", e );
            throw new MojoExecutionException( "Error generating BuildConfig", e );
        }
    }

    private void generateBuildConfigForPackage( String packageName ) throws MojoExecutionException
    {
        getLog().debug( "Creating BuildConfig for " + packageName );

        File outputFolder = new File( genDirectory, packageName.replace( ".", File.separator ) );
        outputFolder.mkdirs();

        StringBuilder buildConfig = new StringBuilder();
        buildConfig.append( "package " ).append( packageName ).append( ";\n\n" );
        buildConfig.append( "public final class BuildConfig {\n" );
        buildConfig.append( "  public static final boolean DEBUG = " ).append( !release ).append( ";\n" );
        for ( BuildConfigConstant constant : buildConfigConstants )
        {
            String value = constant.getValue();
            if ( "String".equals( constant.getType() ) )
            {
                value = "\"" + value + "\"";
            }

            buildConfig.append( "  public static final " )
                    .append( constant.getType() )
                    .append( " " )
                    .append( constant.getName() )
                    .append( " = " )
                    .append( value )
                    .append( ";\n" );
        }
        buildConfig.append( "}\n" );

        File outputFile = new File( outputFolder, "BuildConfig.java" );
        try
        {
            FileUtils.writeStringToFile( outputFile, buildConfig.toString() );
        }
        catch ( IOException e )
        {
            getLog().error( "Error generating BuildConfig ", e );
            throw new MojoExecutionException( "Error generating BuildConfig", e );
        }
    }

    /**
     * Given a map of source directories to list of AIDL (relative) filenames within each,
     * runs the AIDL compiler for each, such that all source directories are available to
     * the AIDL compiler.
     *
     * @param files Map of source directory File instances to the relative paths to all AIDL files within
     * @throws MojoExecutionException If the AIDL compiler fails
     */
    private void generateAidlFiles( Map<File /*sourceDirectory*/, String[] /*relativeAidlFileNames*/> files )
            throws MojoExecutionException
    {
        List<String> protoCommands = new ArrayList<String>();
        protoCommands.add( "-p" + getAndroidSdk().getPathForFrameworkAidl() );

        genDirectoryAidl.mkdirs();
        getLog().info( "Adding AIDL gen folder to compile classpath: " + genDirectoryAidl );
        project.addCompileSourceRoot( genDirectoryAidl.getPath() );
        Set<File> sourceDirs = files.keySet();
        for ( File sourceDir : sourceDirs )
        {
            protoCommands.add( "-I" + sourceDir );
        }
        for ( File sourceDir : sourceDirs )
        {
            for ( String relativeAidlFileName : files.get( sourceDir ) )
            {
                File targetDirectory = new File( genDirectoryAidl, new File( relativeAidlFileName ).getParent() );
                targetDirectory.mkdirs();

                final String shortAidlFileName = new File( relativeAidlFileName ).getName();
                final String shortJavaFileName = shortAidlFileName.substring( 0, shortAidlFileName.lastIndexOf( "." ) )
                        + ".java";
                final File aidlFileInSourceDirectory = new File( sourceDir, relativeAidlFileName );

                List<String> commands = new ArrayList<String>( protoCommands );
                commands.add( aidlFileInSourceDirectory.getAbsolutePath() );
                commands.add( new File( targetDirectory, shortJavaFileName ).getAbsolutePath() );
                try
                {
                    CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
                    executor.setLogger( this.getLog() );
                    executor.setCaptureStdOut( true );
                    executor.executeCommand( getAndroidSdk().getAidlPath(), commands, project.getBasedir(),
                            false );
                }
                catch ( ExecutionException e )
                {
                    throw new MojoExecutionException( "", e );
                }
            }
        }
    }

    private String[] findRelativeAidlFileNames( File sourceDirectory )
    {
        final FileRetriever retriever = new FileRetriever( "**/*.aidl" );
        final String[] relativeAidlFileNames = retriever.getFileNames( sourceDirectory );
        if ( relativeAidlFileNames.length == 0 )
        {
            getLog().debug( "ANDROID-904-002: No aidl files found" );
        }
        else
        {
            getLog().info( "ANDROID-904-002: Found aidl files: Count = " + relativeAidlFileNames.length );
        }
        return relativeAidlFileNames;
    }

    /**
     * @return true if the pom type is APK, APKLIB, or APKSOURCES
     */
    private boolean isCurrentProjectAndroid()
    {
        Set<String> androidArtifacts = new HashSet<String>()
        {
            {
                addAll( Arrays.asList( APK, APKLIB, APKSOURCES, AAR ) );
            }
        };
        return androidArtifacts.contains( project.getArtifact().getType() );
    }

}


/*
 * Copyright (C) 2009 Jayway AB
 * Copyright (C) 2007-2008 JVending Masa
 *
 * Licensed 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 com.simpligility.maven.plugins.android.phase08preparepackage;

import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.IncludeExcludeSet;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.common.Const;
import com.simpligility.maven.plugins.android.common.ZipExtractor;
import com.simpligility.maven.plugins.android.configuration.Dex;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;

/**
 * Converts compiled Java classes to the Android dex format.
 *
 * @author hugo.josefson@jayway.com
 */
@Mojo(
    name = "dex",
    defaultPhase = LifecyclePhase.PREPARE_PACKAGE,
    requiresDependencyResolution = ResolutionScope.COMPILE
)
public class DexMojo extends AbstractAndroidMojo
{

    /**
     * Configuration for the dex command execution. It can be configured in the plugin configuration like so
     *
     *

     * <dex>
     *   <dexMechanism>d8|dex</dexMechanism>
     *   <jvmArguments>
     *     <jvmArgument>-Xms256m</jvmArgument>
     *     <jvmArgument>-Xmx512m</jvmArgument>
     *   </jvmArguments>
     *   <coreLibrary>true|false</coreLibrary>
     *   <noLocals>true|false</noLocals>
     *   <forceJumbo>true|false</forceJumbo>
     *   <optimize>true|false</optimize>
     *   <preDex>true|false</preDex>
     *   <preDexLibLocation>path to predexed libraries, defaults to target/dexedLibs</preDexLibLocation>
     *   <incremental>true|false</incremental>
     *   <multiDex>true|false</multiDex>
     *   <generateMainDexList>true|false</generateMainDexList>
     *   <mainDexList>path to class list file</mainDexList>
     *   <minimalMainDex>true|false</minimalMainDex>
     * </dex>
     *

     *
     * or via properties dex.* or command line parameters android.dex.*
     */
    @Parameter
    private Dex dex;

    /**
     * Extra JVM Arguments. Using these you can e.g. increase memory for the jvm running the build.
     */
    @Parameter( property = "android.dex.jvmArguments", defaultValue = "-Xmx1024M" )
    private String[] dexJvmArguments;

    /**
     * Decides whether to pass the --core-library flag to dx.
     */
    @Parameter( property = "android.dex.coreLibrary", defaultValue = "false" )
    private boolean dexCoreLibrary;

    /**
     * Decides whether to pass the --no-locals flag to dx.
     */
    @Parameter( property = "android.dex.noLocals", defaultValue = "false" )
    private boolean dexNoLocals;

    /**
     * Decides whether to pass the --no-optimize flag to dx.
     */
    @Parameter( property = "android.dex.optimize", defaultValue = "true" )
    private boolean dexOptimize;

    /**
     * Decides whether to predex the jars.
     */
    @Parameter( property = "android.dex.predex", defaultValue = "false" )
    private boolean dexPreDex;

    /**
     * Decides whether to use force jumbo mode.
     */
    @Parameter( property = "android.dex.forcejumbo", defaultValue = "false" )
    private boolean dexForceJumbo;

    /**
     * Path to predexed libraries.
     */
    @Parameter(
        property = "android.dex.dexPreDexLibLocation",
        defaultValue = "${project.build.directory}${file.separator}dexedLibs"
    )
    private String dexPreDexLibLocation;

    /**
     * Decides whether to pass the --incremental flag to dx.
     */
    @Parameter( property = "android.dex.incremental", defaultValue = "false" )
    private boolean dexIncremental;

    /**
     * The name of the obfuscated JAR.
     */
    @Parameter( property = "android.proguard.obfuscatedJar" )
    private File obfuscatedJar;

    /**
     * Decides whether to pass the --multi-dex flag to dx.
     */
    @Parameter( property = "android.dex.multidex", defaultValue = "false" )
    private boolean dexMultiDex;

    /**
     * Full path to class list to multi dex
     */
    @Parameter( property = "android.dex.maindexlist" )
    private String dexMainDexList;

    /**
     * Decides whether to pass the --minimal-main-dex flag to dx.
     */
    @Parameter( property = "android.dex.minimalmaindex", defaultValue = "false" )
    private boolean dexMinimalMainDex;

    /**
     * Decides whether to generate main dex list.
     * Supported from build tools version 22.0.0+
     *
     * Note: if set to true, dexMinimalMainDex is set to true, and dexMainDexList
     * is set to generated main dex list.
     */
    @Parameter( property = "android.dex.generatemaindexlist", defaultValue = "false" )
    private boolean dexGenerateMainDexList;

    /**
     * Additional command line parameters passed to dx.
     */
    @Parameter( property = "android.dex.dexarguments" )
    private String dexArguments;

    /**
     * Skips transitive dependencies. May be useful if the target classes directory is populated with the
     * {@code maven-dependency-plugin} and already contains all dependency classes.
     */
    @Parameter( property = "skipDependencies", defaultValue = "false" )
    private boolean skipDependencies;

    /**
     * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the
     * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty
     * strings are ignored. Example:
     *

     *     <artifactTypeSet>
     *         <includes>
     *             <include>aar</include>
     *         <includes>
     *         <excludes>
     *             <exclude>jar</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     *

     */
    @Parameter( property = "artifactTypeSet" )
    private IncludeExcludeSet artifactTypeSet;

    /**
     * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The
     * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can
     * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId},
     * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using
     * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact
     * range. Empty strings are ignored. Example:
     *

     *     <artifactTypeSet>
     *         <includes>
     *             <include>foo-group:foo-artifact:1.0-SNAPSHOT</include>
     *             <include>bar-group:bar-artifact:1.0-SNAPSHOT</include>
     *             <include>baz-group:*</include>
     *         <includes>
     *         <excludes>
     *             <exclude>qux-group:qux-artifact:*</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     *

     */
    @Parameter( property = "artifactSet" )
    private IncludeExcludeSet artifactSet;

    private String[] parsedJvmArguments;
    private boolean parsedCoreLibrary;
    private boolean parsedNoLocals;
    private boolean parsedOptimize;
    private boolean parsedPreDex;
    private boolean parsedForceJumbo;
    private String parsedPreDexLibLocation;
    private boolean parsedIncremental;
    private boolean parsedMultiDex;
    private String parsedMainDexList;
    private boolean parsedMinimalMainDex;
    private boolean parsedGenerateMainDexList;
    private String parsedDexArguments;
    private DexMechanism parsedDexMechanism;

    /**
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        parseConfiguration();

        getLog().debug( "DexMechanism set to " + parsedDexMechanism );
        if ( parsedDexMechanism != DexMechanism.Dex )
        {
            getLog().info( "Not executing DexMojo because DexMechanism set to " + parsedDexMechanism );
            return;
        }

        if ( getJack().isEnabled() )
        {
            //Dexxing is handled by Jack
            return;
        }

        CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        executor.setLogger( getLog() );

        File outputFile;
        if ( parsedMultiDex )
        {
            outputFile = targetDirectory;
            if ( parsedGenerateMainDexList )
            {
                getAndroidSdk().assertThatBuildToolsVersionIsAtLeast(
                    "22.0.0", "generate main dex list" );
                File generatedMainDexClassesList = generateMainDexClassesList( executor );
                parsedMainDexList = generatedMainDexClassesList.getAbsolutePath();
                parsedMinimalMainDex = true;
            }
        }
        else
        {
            outputFile = new File( targetDirectory, "classes.dex" );
        }
        if ( generateApk )
        {
            runDex( executor, outputFile );
        }

        if ( attachJar )
        {
            File jarFile = new File( targetDirectory + File.separator
                + finalName + ".jar" );
            projectHelper.attachArtifact( project, "jar", project.getArtifact().getClassifier(), jarFile );
        }

        if ( attachSources )
        {
            // Also attach an .apksources, containing sources from this project.
            final File apksources = createApkSourcesFile();
            projectHelper.attachArtifact( project, "apksources", apksources );
        }
    }

    /**
     * Gets the input files for dex. This is a combination of directories and jar files.
     *
     * @return
     */
    private Set< File > getDexInputFiles() throws MojoExecutionException
    {
        Set< File > inputs = new HashSet< File >();

        if ( obfuscatedJar != null && obfuscatedJar.exists() )
        {
            // proguard has been run, use this jar
            getLog().debug( "Adding dex input (obfuscatedJar) : " + obfuscatedJar );
            inputs.add( obfuscatedJar );
        }
        else
        {
            getLog().debug( "Using non-obfuscated input" );
            // no proguard, use original config
            inputs.add( projectOutputDirectory );
            getLog().debug( "Adding dex input : " + project.getBuild().getOutputDirectory() );
            for ( Artifact artifact : filterArtifacts( getTransitiveDependencyArtifacts(), skipDependencies,
                artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
                artifactSet.getExcludes() ) )
            {
                if ( artifact.getType().equals( Const.ArtifactType.NATIVE_SYMBOL_OBJECT )
                    || artifact.getType().equals( Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE ) )
                {
                    // Ignore native dependencies - no need for dexer to see those
                }
                else if ( artifact.getType().equals( APKLIB ) )
                {
                    // Any jars in the libs folder should now be
                    // automatically included because they will be a transitive dependency.
                }
                else if ( artifact.getType().equals( AAR ) )
                {
                    // The Aar classes.jar should now be automatically included
                    // because it will be a transitive dependency. As should any jars in the libs folder.
                }
                else if ( artifact.getType().equals( APK ) )
                {
                    // We need to dex the APK classes including the APK R.
                    // But we don't want to add a second instance of the embedded Rs for any of the APK's dependencies
                    // as they will already have been generated to target/classes. The R values from the APK will be
                    // the correct ones, so best solution is to extract the APK classes (including all Rs) to
                    // target/classes overwriting any generated Rs and let dex pick up the values from there.
                    getLog().debug( "Extracting APK classes to target/classes : " + artifact.getArtifactId() );
                    final File apkClassesJar = getUnpackedLibHelper().getJarFileForApk( artifact );
                    getLog().debug( "Extracting APK : " + apkClassesJar + " to " + targetDirectory );
                    final ZipExtractor extractor = new ZipExtractor( getLog() );
                    extractor.extract( apkClassesJar, targetDirectory, ".class" );
                }
                else
                {
                    getLog().debug( "Adding dex input : " + artifact.getFile() );
                    inputs.add( artifact.getFile().getAbsoluteFile() );
                }
            }
        }

        return inputs;
    }

    private void parseConfiguration()
    {
        // config in pom found
        if ( dex != null )
        {
            // the if statements make sure that properties/command line
            // parameter overrides configuration
            // and that the dafaults apply in all cases;
            if ( dex.getJvmArguments() == null )
            {
                parsedJvmArguments = dexJvmArguments;
            }
            else
            {
                parsedJvmArguments = dex.getJvmArguments();
            }
            if ( dex.isCoreLibrary() == null )
            {
                parsedCoreLibrary = dexCoreLibrary;
            }
            else
            {
                parsedCoreLibrary = dex.isCoreLibrary();
            }
            if ( dex.isNoLocals() == null )
            {
                parsedNoLocals = dexNoLocals;
            }
            else
            {
                parsedNoLocals = dex.isNoLocals();
            }
            if ( dex.isOptimize() == null )
            {
                parsedOptimize = dexOptimize;
            }
            else
            {
                parsedOptimize = dex.isOptimize();
            }
            if ( dex.isPreDex() == null )
            {
                parsedPreDex = dexPreDex;
            }
            else
            {
                parsedPreDex = dex.isPreDex();
            }
            if ( dex.getPreDexLibLocation() == null )
            {
                parsedPreDexLibLocation = dexPreDexLibLocation;
            }
            else
            {
                parsedPreDexLibLocation = dex.getPreDexLibLocation();
            }
            if ( dex.isIncremental() == null )
            {
                parsedIncremental = dexIncremental;
            }
            else
            {
                parsedIncremental = dex.isIncremental();
            }
            if ( dex.isForceJumbo() == null )
            {
                parsedForceJumbo = dexForceJumbo;
            }
            else
            {
                parsedForceJumbo = dex.isForceJumbo();
            }
            if ( dex.isMultiDex() == null )
            {
                parsedMultiDex = dexMultiDex;
            }
            else
            {
                parsedMultiDex = dex.isMultiDex();
            }
            if ( dex.getMainDexList() == null )
            {
                parsedMainDexList = dexMainDexList;
            }
            else
            {
                parsedMainDexList = dex.getMainDexList();
            }
            if ( dex.isMinimalMainDex() == null )
            {
                parsedMinimalMainDex = dexMinimalMainDex;
            }
            else
            {
                parsedMinimalMainDex = dex.isMinimalMainDex();
            }
            if ( dex.isGenerateMainDexList() == null )
            {
                parsedGenerateMainDexList = dexGenerateMainDexList;
            }
            else
            {
                parsedGenerateMainDexList = dex.isGenerateMainDexList();
            }
            if ( dex.getDexArguments() == null )
            {
                parsedDexArguments = dexArguments;
            }
            else
            {
                parsedDexArguments = dex.getDexArguments();
            }
            parsedDexMechanism = dex.getDexMechanism();

        }
        else
        {
            parsedJvmArguments = dexJvmArguments;
            parsedCoreLibrary = dexCoreLibrary;
            parsedNoLocals = dexNoLocals;
            parsedOptimize = dexOptimize;
            parsedPreDex = dexPreDex;
            parsedPreDexLibLocation = dexPreDexLibLocation;
            parsedIncremental = dexIncremental;
            parsedForceJumbo = dexForceJumbo;
            parsedMultiDex = dexMultiDex;
            parsedMainDexList = dexMainDexList;
            parsedMinimalMainDex = dexMinimalMainDex;
            parsedGenerateMainDexList = dexGenerateMainDexList;
            parsedDexArguments = dexArguments;
            parsedDexMechanism = DexMechanism.Dex;
        }
    }

    private Set< File > preDex( CommandExecutor executor, Set< File > inputFiles ) throws MojoExecutionException
    {
        Set< File > filtered = new HashSet< File >();
        getLog().info( "Pre dex-ing libraries for faster dex-ing of the final application." );

        for ( File inputFile : inputFiles )
        {
            if ( inputFile.getName().matches( ".*\\.jar$" ) )
            {
                List< String > commands = dexDefaultCommands();

                File predexJar = predexJarPath( inputFile );
                commands.add( "--output=" + predexJar.getAbsolutePath() );
                commands.add( inputFile.getAbsolutePath() );
                filtered.add( predexJar );

                if ( !predexJar.isFile() || predexJar.lastModified() < inputFile.lastModified() )
                {
                    getLog().info( "Pre-dex ing jar: " + inputFile.getAbsolutePath() );
                    executeJava( commands, executor );
                }
            }
            else
            {
                filtered.add( inputFile );
            }
        }

        return filtered;
    }

    private File predexJarPath( File inputFile )
    {
        final File predexLibsDirectory = new File( parsedPreDexLibLocation.trim() );
        predexLibsDirectory.mkdirs();
        return new File( predexLibsDirectory, inputFile.getName() );
    }

    private List< String > dexDefaultCommands() throws MojoExecutionException
    {
        List< String > commands = jarDefaultCommands();
        commands.add( getAndroidSdk().getDxJarPath() );
        commands.add( "--dex" );
        return commands;
    }

    private List<String> jarDefaultCommands()
    {
        List< String > commands = javaDefaultCommands();
        commands.add( "-jar" );
        return commands;
    }

    private List<String> javaDefaultCommands()
    {
        List< String > commands = new ArrayList< String > ();
        if ( parsedJvmArguments != null )
        {
            for ( String jvmArgument : parsedJvmArguments )
            {
                // preserve backward compatibility allowing argument with or
                // without dash (e.g. Xmx512m as well as
                // -Xmx512m should work) (see
                // http://code.google.com/p/maven-android-plugin/issues/detail?id=153)
                if ( !jvmArgument.startsWith( "-" ) )
                {
                    jvmArgument = "-" + jvmArgument;
                }
                getLog().debug( "Adding jvm argument " + jvmArgument );
                commands.add( jvmArgument );
            }
        }
        return commands;
    }

    private void runDex( CommandExecutor executor, File outputFile )
        throws MojoExecutionException
    {
        final List< String > commands = dexDefaultCommands();
        final Set< File > inputFiles = getDexInputFiles();
        Set< File > filteredFiles = inputFiles;
        if ( parsedPreDex )
        {
            filteredFiles = preDex( executor, inputFiles );
        }
        if ( !parsedOptimize )
        {
            commands.add( "--no-optimize" );
        }
        if ( parsedCoreLibrary )
        {
            commands.add( "--core-library" );
        }
        if ( parsedIncremental )
        {
            commands.add( "--incremental" );
        }
        if ( parsedNoLocals )
        {
            commands.add( "--no-locals" );
        }
        if ( parsedForceJumbo )
        {
            commands.add( "--force-jumbo" );
        }
        if ( parsedMultiDex )
        {
            commands.add( "--multi-dex" );
            if ( parsedMainDexList != null )
            {
                commands.add( "--main-dex-list=" + parsedMainDexList );
            }
            if ( parsedMinimalMainDex )
            {
                commands.add( "--minimal-main-dex" );
            }
        }
        if ( parsedDexArguments != null )
        {
            commands.add( parsedDexArguments );
        }
        commands.add( "--output=" + outputFile.getAbsolutePath() );
        for ( File inputFile : filteredFiles )
        {
            commands.add( inputFile.getAbsolutePath() );
        }

        getLog().info( "Convert classes to Dex : " + outputFile );
        executeJava( commands, executor );
    }

    private String executeJava( final List<String> commands, CommandExecutor executor ) throws MojoExecutionException
    {
        final String javaExecutable = getJavaExecutable().getAbsolutePath();
        getLog().debug( javaExecutable + " " + commands.toString() );
        try
        {
            executor.setCaptureStdOut( true );
            executor.executeCommand( javaExecutable, commands, project.getBasedir(), false );
            return executor.getStandardOut();
        }
        catch ( ExecutionException e )
        {
            throw new MojoExecutionException( "", e );
        }
    }

    /**
     * Figure out the full path to the current java executable.
     *
     * @return the full path to the current java executable.
     */
    private static File getJavaExecutable()
    {
        final String javaHome = System.getProperty( "java.home" );
        final String slash = File.separator;
        return new File( javaHome + slash + "bin" + slash + "java" );
    }

    private File generateMainDexClassesJar( CommandExecutor executor ) throws MojoExecutionException
    {
        List< String> commands = jarDefaultCommands();
        commands.add( getAndroidSdk().getProguardJarPath() );
        commands.add( "-dontnote" );
        commands.add( "-dontwarn" );
        commands.add( "-forceprocessing" );
        commands.add( "-dontoptimize" );
        commands.add( "-dontpreverify" );
        commands.add( "-dontobfuscate" );

        Set< File> inputFiles = getDexInputFiles();
        for ( File inputFile : inputFiles )
        {
            commands.add( "-injars" );
            commands.add( inputFile.getAbsolutePath() + "(!META-INF/**)" );
        }

        commands.add( "-libraryjars" );
        commands.add( getAndroidSdk().getShrinkedAndroidJarPath() );

        commands.add( "-include" );
        commands.add( getAndroidSdk().getMainDexClassesRulesPath() );

        commands.add( "-outjars" );
        File mainDexClassesJar = new File( targetDirectory, "mainDexClasses.jar" );
        commands.add( mainDexClassesJar.getAbsolutePath() );

        getLog().info( "Generating main dex classes jar : " + mainDexClassesJar );
        executeJava( commands, executor );

        return mainDexClassesJar;
    }

    private File generateMainDexClassesList( CommandExecutor executor ) throws MojoExecutionException
    {
        File mainDexClassesJar = generateMainDexClassesJar( executor );
        List< String> commands = javaDefaultCommands();

        commands.add( "-Djava.ext.dirs=" + getAndroidSdk().getBuildToolsLibDirectoryPath() );
        commands.add( "com.android.multidex.MainDexListBuilder" );
        commands.add( mainDexClassesJar.getAbsolutePath() );

        Set< File> inputFiles = getDexInputFiles();
        StringBuilder sb = new StringBuilder();
        sb.append( StringUtils.join( inputFiles, File.pathSeparatorChar ) );
        commands.add( sb.toString() );

        File mainDexClassesList = new File( targetDirectory, "mainDexClasses.txt" );

        getLog().info( "Generating main dex classes list : " + mainDexClassesList );

        String output = executeJava( commands, executor );
        try
        {
            FileUtils.writeStringToFile( mainDexClassesList, output );
        }
        catch ( IOException ex )
        {
            throw new MojoExecutionException( "Failed to write command output with main dex classes list to "
                + mainDexClassesList, ex );
        }
        return mainDexClassesList;
    }

    /**
     * @return
     * @throws MojoExecutionException
     */
    protected File createApkSourcesFile() throws MojoExecutionException
    {
        final File apksources = new File( targetDirectory, finalName
            + ".apksources" );
        FileUtils.deleteQuietly( apksources );

        try
        {
            JarArchiver jarArchiver = new JarArchiver();
            jarArchiver.setDestFile( apksources );

            addDirectory( jarArchiver, assetsDirectory, "assets" );
            addDirectory( jarArchiver, resourceDirectory, "res" );
            addDirectory( jarArchiver, sourceDirectory, "src/main/java" );
            addJavaResources( jarArchiver, resources );

            jarArchiver.createArchive();
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "ArchiverException while creating .apksource file.", e );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "IOException while creating .apksource file.", e );
        }

        return apksources;
    }

    /**
     * Makes sure the string ends with "/"
     *
     * @param prefix
     *            any string, or null.
     * @return the prefix with a "/" at the end, never null.
     */
    protected String endWithSlash( String prefix )
    {
        prefix = StringUtils.defaultIfEmpty( prefix, "/" );
        if ( !prefix.endsWith( "/" ) )
        {
            prefix = prefix + "/";
        }
        return prefix;
    }

    /**
     * Adds a directory to a {@link JarArchiver} with a directory prefix.
     *
     * @param jarArchiver
     * @param directory
     *            The directory to add.
     * @param prefix
     *            An optional prefix for where in the Jar file the directory's contents should go.
     */
    protected void addDirectory( JarArchiver jarArchiver, File directory, String prefix )
    {
        if ( directory != null && directory.exists() )
        {
            final DefaultFileSet fileSet = new DefaultFileSet();
            fileSet.setPrefix( endWithSlash( prefix ) );
            fileSet.setDirectory( directory );
            jarArchiver.addFileSet( fileSet );
        }
    }

    /**
     * @param jarArchiver
     * @param javaResources
     */
    protected void addJavaResources( JarArchiver jarArchiver, List< Resource > javaResources )
    {
        for ( Resource javaResource : javaResources )
        {
            addJavaResource( jarArchiver, javaResource );
        }
    }

    /**
     * Adds a Java Resources directory (typically "src/main/resources") to a {@link JarArchiver}.
     *
     * @param jarArchiver
     * @param javaResource
     *            The Java resource to add.
     */
    protected void addJavaResource( JarArchiver jarArchiver, Resource javaResource )
    {
        if ( javaResource != null )
        {
            final File javaResourceDirectory = new File( javaResource.getDirectory() );
            if ( javaResourceDirectory.exists() )
            {
                final DefaultFileSet javaResourceFileSet = new DefaultFileSet();
                javaResourceFileSet.setDirectory( javaResourceDirectory );
                javaResourceFileSet.setPrefix( endWithSlash( "src/main/resources" ) );
                jarArchiver.addFileSet( javaResourceFileSet );
            }
        }
    }
}


package com.simpligility.maven.plugins.android.standalonemojos;

import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.config.ConfigHandler;
import com.simpligility.maven.plugins.android.config.ConfigPojo;
import com.simpligility.maven.plugins.android.config.PullParameter;
import com.simpligility.maven.plugins.android.configuration.Zipalign;

import org.apache.commons.io.FilenameUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.FileUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;

/**
 * ZipalignMojo can run the zipalign command against the apk. Implements parsing parameters from pom or command line
 * arguments and sets useful defaults as well.
 *
 * @author Manfred Moser - manfred@simpligility.com
 */
@Mojo( name = "zipalign" )
public class ZipalignMojo extends AbstractAndroidMojo
{

    /**
     * The configuration for the zipalign goal. As soon as a zipalign goal is invoked the command will be executed
     * unless the skip parameter is set. By default the input file is the apk produced by the build in target. The
     * outputApk will use the postfix -aligned.apk. The following shows a default full configuration of the zipalign
     * goal as an example for changes as plugin configuration.
     *

     * <zipalign>
     *     <skip>false</skip>
     *     <verbose>true</verbose>
     *     <inputApk>${project.build.directory}/${project.finalName}.apk</inputApk>
     *     <outputApk>${project.build.directory}/${project.finalName}-aligned.apk</outputApk>
     * </zipalign>
     *

     *
     * Values can also be configured as properties on the command line as android.zipalign.*
     * or in pom or settings file as properties like zipalign.*.
     */
    @Parameter
    @ConfigPojo
    private Zipalign zipalign;

    /**
     * Skip the zipalign goal execution. Defaults to "true".
     * @see com.simpligility.maven.plugins.android.configuration.Zipalign#skip
     */
    @Parameter( property = "android.zipalign.skip" )
    private Boolean zipalignSkip;

    @PullParameter( defaultValue = "true" )
    private Boolean parsedSkip;

    /**
     * Activate verbose output for the zipalign goal execution. Defaults to "false".
     * @see com.simpligility.maven.plugins.android.configuration.Zipalign#verbose
     */
    @Parameter( property = "android.zipalign.verbose" )
    private Boolean zipalignVerbose;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedVerbose;

    /**
     * The apk file to be zipaligned. Per default the file is taken from build directory (target normally) using the
     * build final name as file name and apk as extension.
     *
     * @see com.simpligility.maven.plugins.android.configuration.Zipalign#inputApk
     */
    @Parameter( property = "android.zipalign.inputApk" )
    private String zipalignInputApk;

    @PullParameter ( defaultValueGetterMethod = "getInputApkPath" )
    private String parsedInputApk;

    /**
     * The apk file produced by the zipalign goal. Per default the file is placed into the build directory (target
     * normally) using the build final name appended with "-aligned" as file name and apk as extension.
     *
     * @see com.simpligility.maven.plugins.android.configuration.Zipalign#outputApk
     */
    @Parameter( property = "android.zipalign.outputApk" )
    private String zipalignOutputApk;

    @PullParameter( defaultValueGetterMethod = "getOutputApkPath" )
    private String parsedOutputApk;

    /**
     * the apk file to be zipaligned.
     */
    private File apkFile;
    /**
     * the output apk file for the zipalign process.
     */
    private File alignedApkFile;

    /**
     *
Classifier to add to the artifact generated.

     */
    @Parameter ( property = "android.zipalign.classifier" )
    private String zipalignClassifier;

    @PullParameter( defaultValue = "aligned" )
    private String parsedClassifier;

    /**
     * Execute the mojo by parsing the confign and actually doing the zipalign.
     *
     * @throws MojoExecutionException
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {

        // If we're not on a supported packaging with just skip (Issue 87)
        // http://code.google.com/p/maven-android-plugin/issues/detail?id=87
        if ( ! SUPPORTED_PACKAGING_TYPES.contains( project.getPackaging() ) )
        {
            getLog().info( "Skipping zipalign on " + project.getPackaging() );
            return;
        }

        ConfigHandler configHandler = new ConfigHandler( this, this.session, this.execution );
        configHandler.parseConfiguration();

        parsedInputApk = FilenameUtils.separatorsToSystem( parsedInputApk );
        parsedOutputApk = FilenameUtils.separatorsToSystem( parsedOutputApk );

        getLog().debug( "skip:" + parsedSkip );
        getLog().debug( "verbose:" + parsedVerbose );
        getLog().debug( "inputApk:" + parsedInputApk );
        getLog().debug( "outputApk:" + parsedOutputApk );
        getLog().debug( "classifier:" + parsedClassifier );

        if ( parsedSkip )
        {
            getLog().info( "Skipping zipalign" );
        }
        else
        {
            boolean outputToSameFile = sameOutputAsInput();

            CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
            executor.setLogger( this.getLog() );

            String command = getAndroidSdk().getZipalignPath();

            List<String> parameters = new ArrayList<String>();
            if ( parsedVerbose )
            {
                parameters.add( "-v" );
            }
            parameters.add( "-f" ); // force overwriting existing output file
            parameters.add( "4" ); // byte alignment has to be 4!
            parameters.add( parsedInputApk );
            String outputApk = outputToSameFile ? getTemporaryOutputApkFilename() : parsedOutputApk;
            parameters.add( outputApk );

            try
            {
                getLog().info( "Running command: " + command );
                getLog().info( "with parameters: " + parameters );
                executor.setCaptureStdOut( true );
                executor.executeCommand( command, parameters );

                if ( FileUtils.fileExists( outputApk ) )
                {
                    if ( outputToSameFile )
                    {
                        // No needs to attach zipaligned apk to artifacts
                        try
                        {
                            FileUtils.rename( new File( outputApk ),  new File( parsedInputApk ) );
                        }
                        catch ( IOException e )
                        {
                            getLog().error( "Failed to replace original apk with aligned "
                                    + getFullPathWithName( outputApk ), e );
                        }
                    }
                    else
                    {
                        // Attach the resulting artifact (Issue 88)
                        // http://code.google.com/p/maven-android-plugin/issues/detail?id=88
                        projectHelper.attachArtifact( project, APK, parsedClassifier, new File( outputApk ) );
                        getLog().info( "Attach " + getFullPathWithName( outputApk )  + " as '"
                                + parsedClassifier + "' to the project" );
                    }
                }
                else
                {
                    getLog().error( "Cannot attach " + getFullPathWithName( outputApk ) + " to the project"
                            + " - The file does not exist" );
                }
            }
            catch ( ExecutionException e )
            {
                throw new MojoExecutionException( "", e );
            }
        }
    }

    private String getFullPathWithName( String filename )
    {
        return FilenameUtils.getFullPath( filename ) + FilenameUtils.getName( filename );
    }

    private boolean sameOutputAsInput()
    {
        return getFullPathWithName( parsedInputApk ).equals( getFullPathWithName( parsedOutputApk ) );
    }

    // zipalign doesn't allow output file to be same as input
    private String getTemporaryOutputApkFilename()
    {
        return parsedOutputApk.substring( 0, parsedOutputApk.lastIndexOf( '.' ) ) + "-aligned-temp.apk";
    }

    /**
     * Gets the apk file location from basedir/target/finalname.apk
     *
     * @return absolute path.
     */
    // used via PullParameter annotation - do not remove
    private String getInputApkPath()
    {
        if ( apkFile == null )
        {
            apkFile = new File( targetDirectory, finalName + "." + APK );
        }
        return apkFile.getAbsolutePath();
    }

    /**
     * Gets the apk file location from basedir/target/finalname-aligned.apk. "-aligned" is the inserted string for the
     * output file.
     *
     * @return absolute path.
     */
    // used via PullParameter annotation - do not remove
    private String getOutputApkPath()
    {
        if ( alignedApkFile == null )
        {
            alignedApkFile = new File( targetDirectory,
                    finalName + "-aligned." + APK );
        }
        return alignedApkFile.getAbsolutePath();
    }
}