/*
 * @(#)FailOnReportStyle.java
 *
 * Copyright (C) 2004 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the 
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software. 
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 *  DEALINGS IN THE SOFTWARE.
 */

package net.sourceforge.groboutils.codecoverage.v2.ant;

import java.text.NumberFormat;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;



/**
 * Checks the coverage report for a subset of classes whose total
 * coverage percentage isn't up-to-snuff.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/04/15 05:48:25 $
 * @since     March 15, 2003
 */
public class FailOnReportStyle implements IReportStyle
{
    private Vector includeExcludeSet = new Vector();
    private String failureProperty = null;
    private double minPercent = 80.0;
    
    private int markCount = 0;
    private int coverCount = 0;
    
    private static final NumberFormat DOUBLE_FORMATTER =
        NumberFormat.getInstance();
    static {
        DOUBLE_FORMATTER.setMaximumFractionDigits( 2 );
    }
    
    
    public static class IncludeAndExclude
    {
        String value = null;
        boolean module = false;
        boolean corp = false;
        boolean isInclude = true;
        
        public void setModule( String s )
        {
            this.module = true;
            this.value = s;
        }
        
        public void setClass( String f )
        {
            this.corp = true;
            this.value = f;
        }
        
        
        protected void check()
                throws BuildException
        {
            if (this.module && this.corp)
            {
                throw new BuildException(
                    "Only one of 'module' and 'class' attributes can be "+
                    "specified in an "+this.getType()+" element." );
            }
            if (!this.module && !this.corp)
            {
                throw new BuildException(
                    "One of 'module' or 'class' attributes must be "+
                    "specified in an "+this.getType()+" element." );
            }
        }
        
        protected String getType()
        {
            return (this.isInclude ? "include" : "exclude");
        }
    }
    
    
    static class ClassFilter
    {
        IFilterToken parts[];
        
        public ClassFilter( String filter )
                throws BuildException
        {
            if (filter == null || filter.length() <= 0 ||
                filter.charAt(0) == '.' || filter.indexOf( ".." ) >= 0 ||
                filter.indexOf( "//" ) >= 0 || filter.indexOf( "/." ) >= 0 ||
                filter.indexOf( "./" ) >= 0 || 
                filter.charAt(filter.length()-1) == '.' ||
                filter.charAt(filter.length()-1) == '/')
            {
                throw new BuildException( "Invalid class filter '"+filter+"'" );
            }
            if (filter.charAt(0) == '/')
            {
                if (filter.length() <= 1)
                {
                    throw new BuildException( "Invalid class filter '"+filter+"'" );
                }
                filter = filter.substring( 1 );
            }
            
            StringTokenizer st = new StringTokenizer( filter, "./", false );
            Vector v = new Vector();
            while (st.hasMoreTokens())
            {
                String s = st.nextToken();
                if (s.length() <= 0)
                {
                    throw new BuildException(
                        "Invalid class filter: no values between delimiters."
                        );
                }
                int len = s.length();
                if (s.charAt(0) == '*')
                {
                    if (len == 1)
                    {
                        v.addElement( ANY_TOKEN );
                    }
                    else
                    //if (s.equals( "**" ))
                    if (len == 2 && s.charAt(1) == '*')
                    {
                        v.addElement( NULL_TOKEN );
                    }
                    else
                    {
                        s = s.substring( 1 );
                        if (s.indexOf( '*' ) >= 0)
                        {
                            throw new BuildException(
                                "Invalid class filter: only 1 wildcard may "+
                                "be present between delimiters." );
                        }
                        v.addElement( new BeforeFilterToken( s ) );
                    }
                }
                else
                if (s.charAt( len - 1) == '*')
                {
                    s = s.substring( 0, len - 1 );
                    if (s.indexOf( '*' ) >= 0)
                    {
                        throw new BuildException(
                            "Invalid class filter: only 1 wildcard may "+
                            "be present between delimiters." );
                    }
                    v.addElement( new EndFilterToken( s ) );
                }
                else
                {
                    if (s.indexOf( '*' ) > 0)
                    {
                        throw new BuildException(
                            "Invalid class filter: a wildcard may "+
                            "only be present before and after a text part." );
                    }
                    v.addElement( new ExactFilterToken( s ) );
                }
            }
            if (v.size() <= 0)
            {
                throw new BuildException(
                    "Invalid class filter: no delimited elements" );
            }
            parts = new IFilterToken[ v.size() ];
            v.copyInto( parts );
        }
        
        
        public boolean match( String c )
        {
            if (c == null || c.length() <= 0)
            {
                throw new IllegalArgumentException( "no null args." );
            }
            
            StringTokenizer st = new StringTokenizer( c, ".", false );
            int filterPos = 0;
            while (st.hasMoreTokens())
            {
                if (filterPos >= this.parts.length)
                {
                    return false;
                }
                String s = st.nextToken();
                
                // special handling for the '**' filter
                if (this.parts[filterPos] == NULL_TOKEN)
                {
                    // if the rest of the filters are '**', early out
                    if (filterPos == this.parts.length - 1)
                    {
                        return true;
                    }
                    
                    // this kind of pattern matching isn't greedy.
                    if (this.parts[filterPos+1].match( s ))
                    {
                        // we can advance to the filter after the
                        // one just matched
                        filterPos += 2;
                    }
                    // else the next filterPos doesn't match this token,
                    // so keep this token for the next match
                }
                else
                {
                    // check if the next filter doesn't match, so we can
                    // early out.
                    if (!this.parts[filterPos++].match( s ))
                    {
                        return false;
                    }
                }
            }
            
            // no more tokens, but are there more filters?  if so, then
            // the token didn't match everything
            return (filterPos >= this.parts.length);
        }
    }
    
    // uses an anti-pattern: use a "null" token to be used for the "**"
    // filter.
    static interface IFilterToken
    {
        public boolean match( String part );
    }
    
    private static class BeforeFilterToken implements IFilterToken
    {
        private String m;
        public BeforeFilterToken( String s )
        {
            this.m = s;
        }
        public boolean match( String part )
        {
            return part.endsWith( this.m );
        }
    }
    
    private static class EndFilterToken implements IFilterToken
    {
        private String m;
        public EndFilterToken( String s )
        {
            this.m = s;
        }
        public boolean match( String part )
        {
            return part.startsWith( this.m );
        }
    }
    
    private static class ExactFilterToken implements IFilterToken
    {
        private String m;
        public ExactFilterToken( String s )
        {
            this.m = s;
        }
        public boolean match( String part )
        {
            return part.equals( this.m );
        }
    }
    
    private static class AnyFilterToken implements IFilterToken
    {
        public boolean match( String part )
        {
            return true;
        }
    }
    private static final IFilterToken ANY_TOKEN = new AnyFilterToken();
    private static final IFilterToken NULL_TOKEN = new AnyFilterToken();
    
    
    
    public void setPercentage( double v )
    {
        this.minPercent = v;
    }
    
    
    public void setProperty( String p )
    {
        this.failureProperty = p;
    }
    
    
    public void addInclude( IncludeAndExclude iae )
    {
        if (iae != null)
        {
            iae.isInclude = true;
            this.includeExcludeSet.addElement( iae );
        }
    }
    
    
    public void addExclude( IncludeAndExclude iae )
    {
        if (iae != null)
        {
            iae.isInclude = false;
            this.includeExcludeSet.addElement( iae );
        }
    }
    
    
    
    
    
    /**
     * Called when the task is finished generating all the reports.  This
     * may be useful for styles that join all the reports together.
     */
    public void reportComplete( Project project, Vector errorList )
            throws BuildException
    {
        double actual = getActualPercentage();
        project.log(
            "Testing for minimal coverage of "+
            DOUBLE_FORMATTER.format( this.minPercent )+
            ", and found an actual coverage of "+
            DOUBLE_FORMATTER.format( actual ), Project.MSG_VERBOSE );
        if (actual < this.minPercent)
        {
            // post an error
            if (this.failureProperty != null)
            {
                project.setNewProperty( this.failureProperty,
                    Double.toString( actual ) );
            }
            errorList.addElement(
                "Did not have sufficient coverage: requires "+
                DOUBLE_FORMATTER.format( this.minPercent ) +
                ", but found " + DOUBLE_FORMATTER.format( actual ) + "." );
        }
    }
    
    
    public void generateReport( Project project, Document doc,
            String moduleName )
            throws BuildException
    {
        Vector inClass = new Vector();
        Vector exClass = new Vector();
        Vector inMod = new Vector();
        Vector exMod = new Vector();
        setupFilters( inClass, exClass, inMod, exMod );
        
        if (!"all".equalsIgnoreCase( moduleName ) &&
            !matchModule( moduleName, inMod, exMod ))
        {
            return;
        }
        
        
        NodeList ccList = doc.getElementsByTagName( "classcoverage" );
        for (int ccIndex = 0; ccIndex < ccList.getLength(); ++ccIndex)
        {
            Element ccEl = (Element)ccList.item( ccIndex );
            
            NodeList coverList = ccEl.getElementsByTagName( "cover" );
            for (int coverIndex = 0; coverIndex < coverList.getLength();
                ++coverIndex)
            {
                Element coverEl = (Element)coverList.item( coverIndex );
                NodeList modList = coverEl.getElementsByTagName(
                    "modulecover" );
                if (modList != null && modList.getLength() > 0)
                {
                    for (int modIndex = 0; modIndex < modList.getLength();
                        ++modIndex)
                    {
                        Element modEl = (Element)modList.item( modIndex );
                        if (matchModule( modEl.getAttribute( "measure" ),
                            inMod, exMod ))
                        {
                            cover( project, modEl );
                        }
                    }
                }
                else
                {
                    cover( project, coverEl );
                }
            }
        }
        /*
  <classcoverage classname="x.main" classsignature="x.main-2208445997" package="x" sourcefile="main.java">
    <cover covered="25" display-covered="25" display-percentcovered="86.21" display-total="29" display-weightedpercent="375.01" percentcovered="86.20689655172414" total="29" weightedpercent="375.00766949585847">
      <modulecover covered="19" display-covered="19" display-percentcovered="86.36" display-total="22" display-weightedpercent="375.66" measure="BytecodeCount" percentcovered="86.36363636363636" total="22" weightedpercent="375.66213314244806"></modulecover>
      <modulecover covered="6" display-covered="6" display-percentcovered="85.71" display-total="7" display-weightedpercent="374.35" measure="LineCount" percentcovered="85.71428571428571" total="7" weightedpercent="374.3532058492689"></modulecover>
    </cover>
        */
        
    }
    
    
    protected void cover( Project project, Element el )
    {
        String coveredS = el.getAttribute( "covered" );
        String markedS = el.getAttribute( "total" );
        try
        {
            int cov = Integer.parseInt( coveredS );
            int mar = Integer.parseInt( markedS );
            
            // if any of the values were invalid, then this shouldn't be reached
            if (cov > 0 && mar > 0)
            {
                this.coverCount += cov;
                this.markCount += mar;
            }
        }
        catch (NumberFormatException e)
        {
            project.log( "Invalid covered attribute: expected a number.",
                Project.MSG_VERBOSE );
        }
    }
    
    
    protected double getActualPercentage()
    {
        if (this.markCount == 0)
        {
            return 100.0;
        }
        // else
        return ((double)this.coverCount * 100.0) / (double)this.markCount;
    }
    
    
    protected boolean matchModule( String moduleName,
            Vector inMod, Vector exMod )
    {
        // it must pass all inMod and fail all exMod
        if (!matchModuleInSet( moduleName, inMod ))
        {
            return false;
        }
        if (matchModuleInSet( moduleName, exMod ))
        {
            return false;
        }
        return true;
    }
    
    
    protected boolean matchModuleInSet( String moduleName, Vector set )
    {
        Enumeration e = set.elements();
        while (e.hasMoreElements())
        {
            String m = (String)e.nextElement();
            if ("*".equals(m) || moduleName.equalsIgnoreCase( m ))
            {
                return true;
            }
        }
        return false;
    }
    
    
    protected boolean matchClass( String className,
            Vector inClass, Vector exClass )
    {
        if (!matchClassInSet( className, inClass ))
        {
            return false;
        }
        if (matchClassInSet( className, exClass ))
        {
            return false;
        }
        return true;
    }
    
    
    protected boolean matchClassInSet( String className, Vector set )
    {
        Enumeration e = set.elements();
        while (e.hasMoreElements())
        {
            ClassFilter cf = (ClassFilter)e.nextElement();
            if (cf.match( className ))
            {
                return true;
            }
        }
        return false;
    }
    
    
    protected void setupFilters( Vector includeClass, Vector excludeClass,
            Vector includeModule, Vector excludeModule )
            throws BuildException
    {
        Enumeration e = this.includeExcludeSet.elements();
        while (e.hasMoreElements())
        {
            IncludeAndExclude iae = (IncludeAndExclude)e.nextElement();
            iae.check();
            if (iae.module)
            {
                if (iae.isInclude)
                {
                    includeModule.addElement( iae.value );
                }
                else
                {
                    excludeModule.addElement( iae.value );
                }
            }
            else
            // if (iae.corp)
            {
                if (iae.isInclude)
                {
                    includeClass.addElement( new ClassFilter( iae.value ) );
                }
                else
                {
                    excludeClass.addElement( new ClassFilter( iae.value ) );
                }
            }
        }
        
        // by default, if no includes are specified, then there's an implicit
        // include **
        if (includeModule.size() <= 0)
        {
            includeModule.addElement( "*" );
        }
        if (includeClass.size() <= 0)
        {
            includeClass.addElement( new ClassFilter( "**" ) );
        }
    }
}

