/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Ant", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */
package org.apache.tools.ant.taskdefs.optional.javancss;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.Path;

/**
 * Ant task to produce a report on some basic code metrics.
 *
 * <p>This task wraps the JavaNCSS library for determining code metrics. The
 * library determines several code metrics such as object counts, non-commented
 * source statements (NCSS), cyclomatic complexity numbers (CCN), and javadoc
 * statements. These counts are subtotaled per function, class, and package.
 *
 * <p>JavaNCSS was developed by Christoph Clemens Lee and is available
 * <a href="http://www.kclee.com/clemens/java/javancss/">here</a>.
 *
 * Steve Jernigan has developed another Ant task called JavaNCSS2Ant,
 * <a href="https://sourceforge.net/projects/javancss2ant/">available on
 * SourceForge</a> which enables Ant to check user-specified
 * thresholds for each metric supported by the tool. Our original intent was
 * to have just one Ant task which performed both report generation and
 * threshold checking however this was not possible as it would require this
 * task to have access to the JavaNCSS libraries at compile time.
 *
 * @author Phillip Wells
 */
public class JavaNcssTask extends MatchingTask {
    /**
     * The command to be executed to run to tool.
     */
    private CommandlineJava commandline = new CommandlineJava();
    /**
     * Whether the build should halt if there is an error or a threshold is
     * exceeded.
     */
    private boolean abortOnFail = true;
    /**
     * The directory containing the source files to be scanned by the tool.
     */
    private File srcdir;
    /**
     * The classpath to be used.
     */
    private Path classpath;
    /**
     * The location of the output file.
     */
    private File outputfile;
    /**
     * The format of the output file. Allowable values are 'plain' or 'xml'.
     */
    private String format = "plain";
    /**
     * Indicates the failure of the JavaNCSS process.
     */
    private static final int FAILURE = 1;
    /**
     * Whether package metrics should be generated.
     */
    private boolean packageMetrics = true;
    /**
     * Whether class metrics should be generated.
     */
    private boolean classMetrics = true;
    /**
     * Whether function metrics should be generated.
     */
    private boolean functionMetrics = true;
    /**
     * Whether to only count total non-commenting source statements and nothing
     * else.
     */
    private boolean ncssOnly = false;

    /**
     * Creates a new instance of the task.
     */
    public JavaNcssTask() {
        commandline.setClassname("javancss.Main");
    }

    /**
     * Whether to only count total non-commenting source statements and
     * nothing else.
     * @param ncssOnly true if only NCSS should be counted; false otherwise.
     */
    public void setNcssOnly(boolean ncssOnly) {
        this.ncssOnly = ncssOnly;
    }

    /**
     * Sets the format of the output file.
     * @param format the format of the output file. Allowable values are 'plain'
     * or 'xml'.
     */
    public void setFormat(String format) {
        this.format = format;
    }

    /**
     * Whether package metrics should be generated.
     * @param packageMetrics true if they should; false otherwise.
     */
    public void setPackageMetrics(boolean packageMetrics) {
        this.packageMetrics = packageMetrics;
    }

    /**
     * Whether class/interface metrics should be generated.
     * @param classMetrics true if they should; false otherwise.
     */
    public void setClassMetrics(boolean classMetrics) {
        this.classMetrics = classMetrics;
    }

    /**
     * Whether function metrics should be generated.
     * @param functionMetrics true if they should; false otherwise.
     */
    public void setFunctionMetrics(boolean functionMetrics) {
        this.functionMetrics = functionMetrics;
    }

    /**
     * Sets the directory to be scanned by the tool. This should be the
     * directory containing the source files whose metrics are to be
     * analysed.
     * @param srcdir the directory to be scanned by the tool.
     */
    public void setSrcdir(File srcdir) {
        this.srcdir = srcdir;
    }

    /**
     * Sets the location of the output file.
     * @param outputfile the location of the output file.
     */
    public void setOutputfile(File outputfile) {
        this.outputfile = outputfile;
    }

    /**
     * Set the classpath to be used.
     * @param classpath the classpath to be used.
     */
    public void setClasspath(Path classpath) {
        if (this.classpath == null) {
            this.classpath = classpath;
        } else {
            this.classpath.append(classpath);
        }
    }

    /**
     * Creates a new JVM argument. Ignored if no JVM is forked.
     * @return a new JVM argument so that any argument can be passed to the JVM.
     */
    private Commandline.Argument createJvmarg() {
        return commandline.createVmArgument();
    }

    /**
     * Sets whether the build should halt if there is an error or a threshold is
     * exceeded.
     * @param abortOnFail true if it should; false otherwise.
     */
    public void setAbortOnFail(boolean abortOnFail) {
        this.abortOnFail = abortOnFail;
    }

    /**
     * Executes this task.
     * @throws BuildException if an error occurs.
     */
    public void execute() throws BuildException {
        if (srcdir == null) {
            throw new BuildException("srcdir attribute must be set!");
        }
        if (!srcdir.exists()) {
            throw new BuildException("srcdir does not exist!");
        }
        if (!srcdir.isDirectory()) {
            throw new BuildException("srcdir is not a directory!");
        }

        int exitValue = generateReport();
        if (exitValue == FAILURE) {
            if (abortOnFail) {
                throw new BuildException("JavaNcss failed", location);
            } else {
                log("JavaNcss failed", Project.MSG_ERR);
            }
        }
    }

    /**
     * Generates the report.
     * @return the return code of the forked process.
     * @throws BuildException if an error occurs whilst generating the report.
     */
    private int generateReport() {
        setJvmArguments();
        setCommandLineArguments();
        Execute execute = new Execute(new LogStreamHandler(this,
                                                           Project.MSG_INFO,
                                                           Project.MSG_WARN));
        execute.setCommandline(commandline.getCommandline());
        if (outputfile != null) {
            log("Report to be stored in " + outputfile.getPath());
        }
        log("Executing: " + commandline.toString(), Project.MSG_VERBOSE);
        try {
            return execute.execute();
        } catch (IOException e) {
            throw new BuildException("Process fork failed.", e, location);
        }
    }

    /**
     * Builds a list of all files to be analysed. We need to do this when
     * testing thresholds as the Javancss object does not have a constructor
     * that lets us make use of the -recursive option
     */
    private Vector findFilesToAnalyse() {
        DirectoryScanner ds = super.getDirectoryScanner(srcdir);
        String files[] = ds.getIncludedFiles();
        if (files.length == 0) {
            log("No files in specified directory " + srcdir, 3);
        }
        return copyFiles(files);
    }

    /**
     * Converts the specified array of filenames into a vector of paths.
     * @param filesArray an array of filenames.
     * @return a vector of paths. The path is constructed by prepending this
     * task's source directory to each filename.
     */
    private Vector copyFiles(String[] filesArray) {
        Vector returnVector = new Vector(filesArray.length);
        for (int i = 0; i < filesArray.length; i++) {
            returnVector.addElement(srcdir + File.separator + filesArray[i]);
        }
        return returnVector;
    }

    /**
     * Maybe creates a nested classpath element.
     */
    public Path createClasspath() {
        if (classpath == null) {
            classpath = new Path(project);
        }
        return classpath.createPath();
    }

    /**
     * Sets the arguments to the JVM.
     */
    private void setJvmArguments() {
        createClasspath();

        if (classpath.toString().length() > 0) {
            createJvmarg().setValue("-classpath");
            createJvmarg().setValue(classpath.toString());
        }
    }

    /**
     * Sets the command line arguments to be sent to JavaNCSS.
     */
    private void setCommandLineArguments() {
        // Set metrics to be generated
        if (ncssOnly) {
            commandline.createArgument().setValue("-ncss");
        } else {
            if (packageMetrics) {
                commandline.createArgument().setValue("-package");
            }
            if (classMetrics) {
                commandline.createArgument().setValue("-object");
            }
            if (functionMetrics) {
                commandline.createArgument().setValue("-function");
            }
        }

        // Set format of report
        if (format.equals("xml")) {
            commandline.createArgument().setValue("-xml");
        }

        // Set location of report
        if (outputfile != null) {
            commandline.createArgument().setValue("-out");
            commandline.createArgument().setValue(outputfile.getPath());
        }

        // Set source code to be processed
        commandline.createArgument().setValue("@" + createSourceListFile().getPath());
    }

    /**
     * Creates a temporary file containing a list of all source files to be
     * analysed.
     * @return a file containing a list of all specified source files.
     */
    private File createSourceListFile() {
        Vector fileList = findFilesToAnalyse();
        File srcListFile;
        try {
            srcListFile = File.createTempFile("srcList", null);
            srcListFile.deleteOnExit();
            FileOutputStream fos = new FileOutputStream(srcListFile);
            PrintWriter pw = new PrintWriter(fos);
            for (int i = 0; i < fileList.size(); i++) {
                log(fileList.elementAt(i).toString(), 3);
                pw.println(fileList.elementAt(i).toString());
            }
            pw.close();
            fos.close();
        } catch (IOException e) {
            throw new BuildException(e, location);
        }
        return srcListFile;
    }
}

