package org.apache.maven.plugin.jar;

/*
 * 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.
 */

import org.apache.commons.lang.SystemUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

/**
 * Signs a JAR using jarsigner.
 *
 * @author <a href="jerome@coffeebreaks.org">Jerome Lacoste</a>
 * @version $Id: JarSignMojo.java 802529 2009-08-09 12:05:35Z bentmann $
 * @goal sign
 * @phase package
 * @requiresProject
 * @todo refactor the common code with javadoc plugin
 * @requiresDependencyResolution runtime
 * @deprecated As of version 2.3, this goal is no longer supported in favor of the dedicated maven-jarsigner-plugin.
 */
public class JarSignMojo
    extends AbstractMojo
{
    /**
     * Set this to <code>true</code> to disable signing.
     * Useful to speed up build process in development environment.
     *
     * @parameter expression="${maven.jar.sign.skip}" default-value="false"
     */
    private boolean skip;

    /**
     * The working directory in which the jarsigner executable will be run.
     *
     * @parameter expression="${workingdir}" default-value="${basedir}"
     * @required
     */
    private File workingDirectory;

    /**
     * Directory containing the generated JAR.
     *
     * @parameter expression="${project.build.directory}"
     * @required
     * @readonly
     */
    private File basedir;

    /**
     * Name of the generated JAR (without classifier and extension).
     *
     * @parameter alias="jarname" expression="${project.build.finalName}"
     * @required
     */
    private String finalName;

    /**
     * Path of the jar to sign. When specified, the finalName is ignored.
     *
     * @parameter alias="jarpath" default-value="${project.build.directory}/${project.build.finalName}.${project.packaging}"
     */
    private File jarPath;

    /**
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     *
     * @parameter expression="${keystore}"
     */
    private String keystore;

    /**
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     *
     * @parameter expression="${storepass}"
     */
    private String storepass;

    /**
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     *
     * @parameter expression="${keypass}"
     */
    private String keypass;

    /**
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     *
     * @parameter expression="${sigfile}"
     * @todo make a File?
     */
    private String sigfile;

    /**
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     * <p/>
     * Not specifying this argument will sign the jar in-place (your original jar is going to be overwritten).
     *
     * @parameter expression="${signedjar}"
     */
    private File signedjar;

    /**
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     * The corresponding option in the command line is -storetype.
     *
     * @parameter expression="${type}"
     */
    private String type;

    /**
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     *
     * @parameter expression="${alias}"
     * @required
     */
    private String alias;

    /**
     * Automatically verify a jar after signing it.
     * <p/>
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     *
     * @parameter expression="${verify}" default-value="false"
     */
    private boolean verify;

    /**
     * Skip attaching the signed artifact. By default the signed artifact is attached.
     * This is not a Mojo parameter as we shouldn't need this when using this mojo.
     * Just needed when reusing the implementation. See MJAR-84 for discussions.
     */
    private boolean skipAttachSignedArtifact;

    /**
     * Enable verbose.
     * See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
     *
     * @parameter expression="${verbose}" default-value="false"
     */
    private boolean verbose;

    /**
     * @component
     */
    private MavenProjectHelper projectHelper;

    /**
     * The Maven project.
     *
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

    /**
     * Classifier to use for the generated artifact.
     * If not specified, the generated artifact becomes the primary artifact.
     *
     * @parameter expression="${classifier}"
     */
    private String classifier;

    public void execute()
        throws MojoExecutionException
    {
        if ( skip )
        {
            getLog().info( "Skipping JAR signing for file: " + getJarFile().getAbsolutePath() );
            return;
        }

        if ( project != null )
        {
            ArtifactHandler artifactHandler = project.getArtifact().getArtifactHandler();
            if ( artifactHandler != null && !"java".equals( artifactHandler.getLanguage() ) )
            {
                getLog().debug( "Not executing jar:sign as the project is not a Java module" );
                return;
            }
        }

        // we use this mojo to check if there's a need to sign.
        // If we sign and if we need to verify, we reuse it to check the signature
        JarSignVerifyMojo verifyMojo = createJarSignVerifyMojo();

        verifyMojo.setWorkingDir( workingDirectory );

        verifyMojo.setBasedir( basedir );

        File signedJarFile = signedjar != null ? signedjar : getJarFile();

        verifyMojo.setVerbose( verbose );

        verifyMojo.setJarPath( signedJarFile );

        if ( signedJarFile.exists() )
        {
            verifyMojo.setErrorWhenNotSigned( false );
            verifyMojo.execute();
        }

        if ( verifyMojo.isSigned() )
        {
            getLog().info( "JAR " + signedJarFile.getAbsoluteFile() + " is already signed. Skipping." );
            return;
        }

        signJar();

        if ( this.verify )
        {
            verifyMojo.setErrorWhenNotSigned( true );
            verifyMojo.execute();
        }
    }

    protected JarSignVerifyMojo createJarSignVerifyMojo()
    {
        return new JarSignVerifyMojo();
    }


    File getJarFile()
    {
        if ( jarPath != null )
        {
            return jarPath;
        }
        else
        {
            return AbstractJarMojo.getJarFile( basedir, finalName, null );
        }
    }

    void signJar()
        throws MojoExecutionException
    {
        List arguments = new ArrayList();

        Commandline commandLine = new Commandline();

        commandLine.setExecutable( getJarsignerPath() );

        addArgIf( arguments, verbose, "-verbose" );

        // I believe Commandline to add quotes where appropriate, although I haven't tested it enough.
        // FIXME addArgIfNotEmpty will break those parameters containing a space.
        // Look at webapp:gen-keystore for a way to fix that
        addArgIfNotEmpty( arguments, "-keystore", this.keystore );
        addArgIfNotEmpty( arguments, "-storepass", this.storepass );
        addArgIfNotEmpty( arguments, "-keypass", this.keypass );
        addArgIfNotEmpty( arguments, "-signedjar", this.signedjar );
        addArgIfNotEmpty( arguments, "-storetype", this.type );
        addArgIfNotEmpty( arguments, "-sigfile", this.sigfile );

        arguments.add( getJarFile() );

        addArgIf( arguments, alias != null, this.alias );

        for ( Iterator it = arguments.iterator(); it.hasNext(); )
        {
            commandLine.createArgument().setValue( it.next().toString() );
        }

        commandLine.setWorkingDirectory( workingDirectory.getAbsolutePath() );

        createParentDirIfNecessary( signedjar );

        if ( signedjar == null )
        {
            getLog().debug( "Signing JAR in-place (overwritting original JAR)." );
        }

        if ( getLog().isDebugEnabled() )
        {
            getLog().debug( "Executing: " + purgePassword( commandLine ) );
        }

        // jarsigner may ask for some input if the parameters are missing or incorrect.
        // This should take care of it and make it fail gracefully
        final InputStream inputStream = new InputStream()
        {
            public int read()
            {
                return -1;
            }
        };
        StreamConsumer outConsumer = new StreamConsumer()
        {
            public void consumeLine( String line )
            {
                getLog().info( line );
            }
        };
        final StringBuffer errBuffer = new StringBuffer();
        StreamConsumer errConsumer = new StreamConsumer()
        {
            public void consumeLine( String line )
            {
                errBuffer.append( line );
                getLog().warn( line );
            }
        };

        try
        {
            int result = executeCommandLine( commandLine, inputStream, outConsumer, errConsumer );

            if ( result != 0 )
            {
                throw new MojoExecutionException( "Result of " + purgePassword( commandLine ) +
                    " execution is: \'" + result + "\'." );
            }
        }
        catch ( CommandLineException e )
        {
            throw new MojoExecutionException( "command execution failed", e );
        }

        // signed in place, no need to attach
        if ( signedjar == null || skipAttachSignedArtifact )
        {
            return;
        }

        if ( classifier != null )
        {
            projectHelper.attachArtifact( project, "jar", classifier, signedjar );
        }
        else
        {
            project.getArtifact().setFile( signedjar );
        }
    }

    private String purgePassword( Commandline commandLine )
    {
        String out = commandLine.toString();
        if ( keypass != null && out.indexOf( keypass ) != -1 )
        {
            out = StringUtils.replace( out, keypass, "******" );
        }
        return out;
    }

    private void createParentDirIfNecessary( File file )
    {
        if ( file != null )
        {
            File fileDir = file.getParentFile();

            if ( fileDir != null )
            { // not a relative path
                boolean mkdirs = fileDir.mkdirs();
                getLog().debug( "mdkirs: " + mkdirs + " " + fileDir );
            }
        }
    }

    // taken from JavadocReport then slightly refactored
    // should probably share with other plugins that use $JAVA_HOME/bin tools

    /**
     * Get the path of jarsigner tool depending the OS.
     *
     * @return the path of the jarsigner tool
     */
    private String getJarsignerPath()
    {
        return getJDKCommandPath( "jarsigner", getLog() );
    }

    private static String getJDKCommandPath( String command, Log logger )
    {
        String path = getJDKCommandExe( command ).getAbsolutePath();
        logger.debug( command + " executable=[" + path + "]" );
        return path;
    }

    private static File getJDKCommandExe( String command )
    {
        String fullCommand = command + ( SystemUtils.IS_OS_WINDOWS ? ".exe" : "" );

        File exe;

        // For IBM's JDK 1.2
        if ( SystemUtils.IS_OS_AIX )
        {
            exe = new File( SystemUtils.getJavaHome() + "/../sh", fullCommand );
        }
        else if ( SystemUtils.IS_OS_MAC_OSX )
        {
            exe = new File( SystemUtils.getJavaHome() + "/bin", fullCommand );
        }
        else
        {
            exe = new File( SystemUtils.getJavaHome() + "/../bin", fullCommand );
        }

        return exe;
    }

    // Helper methods. Could/should be shared e.g. with JavadocReport

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * conditionally based on the given flag.
     *
     * @param arguments
     * @param b         the flag which controls if the argument is added or not.
     * @param value     the argument value to be added.
     */
    private void addArgIf( List arguments, boolean b, String value )
    {
        if ( b )
        {
            arguments.add( value );
        }
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * if the the value is not null or empty.
     * <p/>
     * Moreover, the value could be comma separated.
     *
     * @param arguments
     * @param key       the argument name.
     * @param value     the argument value to be added.
     * @see #addArgIfNotEmpty(java.util.List,String,Object,boolean)
     */
    private void addArgIfNotEmpty( List arguments, String key, Object value )
    {
        addArgIfNotEmpty( arguments, key, value, false );
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * if the the value is not null or empty.
     * <p/>
     * Moreover, the value could be comma separated.
     *
     * @param arguments
     * @param key       the argument name.
     * @param value     the argument value to be added.
     * @param repeatKey repeat or not the key in the command line
     */
    private void addArgIfNotEmpty( List arguments, String key, Object value, boolean repeatKey )
    {
        if ( value != null && !StringUtils.isEmpty( value.toString() ) )
        {
            arguments.add( key );

            StringTokenizer token = new StringTokenizer( value.toString(), "," );
            while ( token.hasMoreTokens() )
            {
                String current = token.nextToken().trim();

                if ( !StringUtils.isEmpty( current ) )
                {
                    arguments.add( current );

                    if ( token.hasMoreTokens() && repeatKey )
                    {
                        arguments.add( key );
                    }
                }
            }
        }
    }

    //
    // methods used for tests purposes - allow mocking and simulate automatic setters
    //

    protected int executeCommandLine( Commandline commandLine, InputStream inputStream, StreamConsumer stream1,
                                      StreamConsumer stream2 )
        throws CommandLineException
    {
        return CommandLineUtils.executeCommandLine( commandLine, inputStream, stream1, stream2 );
    }

    public void setWorkingDir( File workingDir )
    {
        this.workingDirectory = workingDir;
    }

    public void setBasedir( File basedir )
    {
        this.basedir = basedir;
    }

    public void setKeystore( String keystore )
    {
        this.keystore = keystore;
    }

    public void setKeypass( String keypass )
    {
        this.keypass = keypass;
    }

    public void setSignedJar( File signedjar )
    {
        this.signedjar = signedjar;
    }

    public void setAlias( String alias )
    {
        this.alias = alias;
    }

    // hiding for now - I don't think this is required to be seen
    /*
     public void setFinalName( String finalName )
     {
     this.finalName = finalName;
     }
     */

    public void setJarPath( File jarPath )
    {
        this.jarPath = jarPath;
    }

    public void setStorepass( String storepass )
    {
        this.storepass = storepass;
    }

    public void setSigFile( String sigfile )
    {
        this.sigfile = sigfile;
    }

    public void setType( String type )
    {
        this.type = type;
    }

    public void setVerbose( boolean verbose )
    {
        this.verbose = verbose;
    }

    public void setSkipAttachSignedArtifact( boolean skipAttachSignedArtifact )
    {
        this.skipAttachSignedArtifact = skipAttachSignedArtifact;
    }

    public void setProject( MavenProject project )
    {
        this.project = project;
    }

    public void setVerify( boolean verify )
    {
        this.verify = verify;
    }
}
