/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2004, University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs;

import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Class to pre-screen class files, so that only a subset are analyzed. This
 * supports the -onlyAnalyze command line option.
 * 
 * Modified February 2006 in four ways: a) don't break windows platform by
 * hard-coding '/' as the directory separator b) store list of Matchers, not
 * Patterns, so we don't keep instantiating Matchers c) fix suffix bug, so
 * FooBar and Foo$Bar no longer match Bar d) addAllowedPackage() can now handle
 * unicode chars in filenames, though we still may not be handling every case
 * mentioned in section 7.2.1 of the JLS
 * 
 * @see FindBugs
 * @author David Hovemeyer
 */
public class ClassScreener implements IClassScreener {
    private static final boolean DEBUG = SystemProperties.getBoolean("findbugs.classscreener.debug");

    /**
     * regular expression fragment to match a directory separator. note: could
     * use File.separatorChar instead, but that could be argued to be not
     * general enough
     */
    private static final String SEP = "[/\\\\]"; // could include ':' for
                                                 // classic macOS

    private static final String START = "(?:^|" + SEP + ")"; // (?:) is a
                                                             // non-capturing
                                                             // group

    /**
     * regular expression fragment to match a char of a class or package name.
     * Actually, we just allow any char except a dot or a directory separator.
     */
    private static final String JAVA_IDENTIFIER_PART = "[^./\\\\]";

    private LinkedList<Matcher> patternList;

    /**
     * Constructor. By default, the ClassScreener will match <em>all</em> class
     * files. Once addAllowedClass() and addAllowedPackage() are called, the
     * ClassScreener will only match the explicitly specified classes and
     * packages.
     */
    public ClassScreener() {
        this.patternList = new LinkedList<Matcher>();
    }

    /**
     * replace the dots in a fully-qualified class/package name to a regular
     * expression fragment that will match file names.
     * 
     * @param dotsName
     *            such as "java.io" or "java.io.File"
     * @return regex fragment such as "java[/\\\\]io" (single backslash escaped
     *         twice)
     */
    private static String dotsToRegex(String dotsName) {
        /*
         * oops, next line requires JDK 1.5 return dotsName.replace("$",
         * "\\$").replace(".", SEP); could use String.replaceAll(regex, repl)
         * but that can be problematic--javadoc says "Note that backslashes (\)
         * and dollar signs ($) in the replacement string may cause the results
         * to be different than if it were being treated as a literal
         * replacement"
         */
        String tmp = dotsName.replace("$", "\\$");
        return tmp.replace(".", SEP);
        // note: The original code used the \Q and \E regex quoting constructs
        // to escape $.
    }

    /**
     * Add the name of a class to be matched by the screener.
     * 
     * @param className
     *            name of a class to be matched
     */
    public void addAllowedClass(String className) {
        String classRegex = START + dotsToRegex(className) + ".class$";
        if (DEBUG)
            System.out.println("Class regex: " + classRegex);
        patternList.add(Pattern.compile(classRegex).matcher(""));
    }

    /**
     * Add the name of a package to be matched by the screener. All class files
     * that appear to be in the package should be matched.
     * 
     * @param packageName
     *            name of the package to be matched
     */
    public void addAllowedPackage(String packageName) {
        if (packageName.endsWith(".")) {
            packageName = packageName.substring(0, packageName.length() - 1);
        }

        String packageRegex = START + dotsToRegex(packageName) + SEP + JAVA_IDENTIFIER_PART + "+.class$";
        if (DEBUG)
            System.out.println("Package regex: " + packageRegex);
        patternList.add(Pattern.compile(packageRegex).matcher(""));
    }

    /**
     * Add the name of a prefix to be matched by the screener. All class files
     * that appear to be in the package specified by the prefix, or a more
     * deeply nested package, should be matched.
     * 
     * @param prefix
     *            name of the prefix to be matched
     */
    public void addAllowedPrefix(String prefix) {
        if (prefix.endsWith(".")) {
            prefix = prefix.substring(0, prefix.length() - 1);
        }
        if (DEBUG)
            System.out.println("Allowed prefix: " + prefix);
        String packageRegex = START + dotsToRegex(prefix) + SEP;
        if (DEBUG)
            System.out.println("Prefix regex: " + packageRegex);
        patternList.add(Pattern.compile(packageRegex).matcher(""));
    }

    /*
     * (non-Javadoc)
     * 
     * @see edu.umd.cs.findbugs.IClassScreener#matches(java.lang.String)
     */
    public boolean matches(String fileName) {
        // Special case: if no classes or packages have been defined,
        // then the screener matches all class files.
        if (patternList.isEmpty())
            return true;

        if (DEBUG)
            System.out.println("Matching: " + fileName);

        // Scan through list of regexes
        for (Matcher matcher : patternList) {
            if (DEBUG)
                System.out.print("\tTrying [" + matcher.pattern());
            matcher.reset(fileName);
            if (matcher.find()) {
                if (DEBUG)
                    System.out.println("]: yes!");
                return true;
            }
            if (DEBUG)
                System.out.println("]: no");
        }
        return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see edu.umd.cs.findbugs.IClassScreener#vacuous()
     */
    public boolean vacuous() {
        return patternList.isEmpty();
    }
}

// vim:ts=4
