//==============================================================================
// FilenamePattern.java
//==============================================================================

package tribble.util;

import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;

import java.lang.Character;
import java.lang.Cloneable;
import java.lang.Exception;
import java.lang.IllegalArgumentException;
import java.lang.NullPointerException;
import java.lang.String;
import java.lang.System;


/*******************************************************************************
* Filename pattern matcher.
*
* <p>
* A <i>filename pattern</i> is a kind of <i>regular expression</i> that is used
* for matching filenames.
*
* <p>
* The {@link #matches(String) matches()} method is used to match a pattern
* against a filename.  A static {@link #matches(String, String) matches()}
* method is also provided for convenience.
*
* <p>
* Methods {@link #accept(File) accept(File)} and
* {@link #accept(File, String) accept(File, String)} are also provided, which
* implement the <tt>java.io.FileFilter</tt> and <tt>java.io.FilenameFilter</tt>
* interfaces, respectively.
*
* <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
* <p>
* <hr width="50%">
* <a name="-syntax"></a>
* <p>
* <b> Filename Pattern Syntax </b>
*
* <p>
* A filename pattern is composed of <i>regular</i> characters and
* <i>special pattern matching</i> characters, which are:
*
* <dl>
*  <dd>
*   <dl>
*    <p>
*    <dt> <tt> ? </tt>
*    <dd> Matches any single character.
*
*    <p>
*    <dt> <tt> * </tt>
*    <dd> Matches zero or more characters.
*
*    <p>
*    <dt> <tt> [<i>abc</i>] </tt>
*    <dd> Matches a single character from character set
*     <tt>{<i>a,b,c</i>}</tt>.
*
*    <p>
*    <dt> <tt> [<i>a</i>-<i>b</i>] </tt>
*    <dd> Matches a single character from the character range
*     <tt>{<i>a...b</i>}</tt>.  Note that character <tt><i>a</i></tt> must be
*     lexicographically less than or equal to character <tt><i>b</i></tt>.
*
*    <p>
*    <dt> <tt> [^<i>a</i>] </tt>
*    <dd> Matches a single character that is not from character set or range
*     <tt>{<i>a</i>}</tt>.  Note that the <tt>^</tt> character must occur
*     immediately to the right of the opening bracket.
*
*    <p>
*    <dt> <tt> / </tt>
*    <dd> Matches one or more directory path separator characters.
*     This is sensitive to the underlying operating system, so that this will
*     match the native directory path separator character for underlying system;
*     for example, it will match <tt>'/'</tt> on Unix systems and match both
*     <tt>'/'</tt> and <tt>'\'</tt> on MS-Windows systems.  This makes it
*     possible to use the same filename pattern on any operating system.
*
*    <p>
*    <dt> <tt> !<i>pat</i> </tt>
*    <dd> Negates the sense of the rest of pattern <i>pat</i>.
*      For example, pattern <tt>"a*"</tt> matches filename <tt>"abc"</tt>, but
*      pattern <tt>"!a*"</tt> does not.
*
*    <p>
*    <dt> <tt> \<i>c</i> </tt>
*    <dd> Removes (escapes) any special meaning of character <i>c</i>.
*
*   </dl>
*  </dd>
* </dl>
*
* <p>
* The special pattern matching characters listed above are the default settings
* for a given filename pattern matcher object, but these can be changed by
* calling method {@link #setSpecialChar setSpecialChar()}.
*
* <p>
* By default, a filename pattern matcher performs case-sensitive matching
* depending on the underlying operating system (e.g., matching is case-sensitive
* for Unix, but case-insensitive for MS/Windows).  This behavior can be changed
* by calling method {@link #setIgnoreCase setIgnoreCase()}.
*
* <p>
* An empty string (<tt>""</tt>) is not a valid filename pattern.
*
* <p>
* No filename pattern matches an empty filename (<tt>""</tt>).
*
* <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
* <p>
* <hr width="50%">
* <a name="-examples"></a>
* <p>
* <b> Examples </b>
*
* <p>
* <dl>
*  <dd>
*   <dl>
*    <dt> <tt> abc </tt>
*    <dd> Matches <tt>"abc"</tt>.
*     If case-insensitive matching is enabled, it also matches
*     <tt>"Abc"</tt>, <tt>"ABC"</tt>, <tt>"aBc"</tt>, etc.
*
*    <dt> <tt> a?c </tt>
*    <dd> Matches <tt>"abc"</tt>, <tt>"a2c"</tt>, <tt>"a.c"</tt>, etc.
*
*    <dt> <tt> a* </tt>
*    <dd> Matches <tt>"a"</tt>, <tt>"abc"</tt>, <tt>"a3.p.txt"</tt>, etc.
*
*    <dt> <tt> a.* </tt>
*    <dd> Matches <tt>"a."</tt>, <tt>"a.txt"</tt>, <tt>"a.old.java"</tt>, etc.
*
*    <dt> <tt> a*x </tt>
*    <dd> Matches <tt>"ax"</tt>, <tt>"ab37x"</tt>, <tt>"a.txt.x"</tt>, etc.
*
*    <dt> <tt> *a* </tt>
*    <dd> Matches <tt>"a"</tt>, <tt>"ax"</tt>, <tt>"claw"</tt>, <tt>"bra"</tt>,
*     etc.
*
*    <dt> <tt> a.[ch] </tt>
*    <dd> Matches <tt>"a.c"</tt> and <tt>"a.h"</tt>.
*
*    <dt> <tt> a.[ch]?? </tt>
*    <dd> Matches <tt>"a.cpp"</tt>, <tt>"a.hxx"</tt>, <tt>"a.hlp"</tt>, etc.
*
*    <dt> <tt> a.[d-fm] </tt>
*    <dd> Matches <tt>"a.d"</tt>, <tt>"a.e"</tt>, <tt>"a.f"</tt>, and
*     <tt>"a.m"</tt>.
*
*    <dt> <tt> a.[^a-cg-z0-9] </tt>
*    <dd> Matches <tt>"a.d"</tt>, <tt>"a.e"</tt>, <tt>"a.f"</tt>, etc.
*
*    <dt> <tt> [mb][ey]* </tt>
*    <dd> Matches <tt>"me"</tt>, <tt>"mybook"</tt>, <tt>"be.o"</tt>,
*     <tt>"bean.cnt"</tt>, <tt>"byebye"</tt>, etc.
*
*    <dt> <tt> *.!c </tt>
*    <dd> Matches <tt>"foo.h"</tt>, <tt>"a.java"</tt>, etc., but
*     not <tt>"foo.c"</tt>, <tt>"a.c"</tt>, etc.
*
*    <dt> <tt> a*!*x </tt>
*    <dd> Matches <tt>"aye"</tt>, <tt>"axe7"</tt>, <tt>"a.txt"</tt>, etc., but
*     not <tt>"bake"</tt>, <tt>"ax"</tt>, <tt>"abo.x"</tt>, etc.
*
*    <dt> <tt> src/*.? </tt>
*    <dd> Matches <tt>"src/foo.c"</tt>, <tt>"src/x.h"</tt>, <tt>"src//b.o"</tt>,
*     etc.
*
*    <dt> <tt> &#42;/*.txt </tt>
*    <dd> Matches <tt>"my/foo.txt"</tt>, <tt>"/x.txt"</tt>, <tt>"a//b.txt"</tt>,
*     etc.
*
*    <dt> <tt> /&#42;/*.txt </tt>
*    <dd> Matches <tt>"/my/foo.txt"</tt>, <tt>"//a//b.txt"</tt>, etc.
*
*    <dt> <tt> !* </tt>
*    <dd> Degenerate case that does not match any filename.
*
*   </dl>
*  </dd>
* </dl>
*
* <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
* <p>
* <hr width="50%">
* <a name="-ref"></a>
* <p>
* <b> References </b>
*
* <p>
* This pattern matching implementation is based on the filename pattern matching
* of Unix (POSIX).
*
* <p>
* This pattern matching implementation is <i>not</i> based on the regular
* expression matching capabilities provided by the Java standard library
* (in JRE 1.4+, see <tt>java.lang.String.matches()</tt> for details).
*
* <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
* <p>
* <hr width="50%">
* <a name="-future"></a>
* <p>
* <b> Future Enhancements </b>
*
* <p>
* A future revision of this code may support a special pattern of <tt>"**"</tt>,
* which matches zero or more directory path prefixes.
*
* <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
* <dl>
* <dt> <b>Source code:</b> </dt>
*  <dd>
*   <a href="../../../src/java/tribble/util/FilenamePattern.java"
*    >http://david.tribble.com/src/java/tribble/util/FilenamePattern.java</a>
*  </dd>
* <dt> <b>Documentation:</b> </dt>
*  <dd>
*   <a href="FilenamePattern.html"
*    >http://david.tribble.com/docs/tribble/util/FilenamePattern.html</a>
*  </dd>
* </dl>
*
* <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
* @version	$Revision: 1.9 $ $Date: 2008/10/10 21:53:21 $
* @since	2003-02-09
* @author	David R. Tribble (david&#64;tribble.com).
*	<br>
*	Copyright ©2003-2008 by David R. Tribble, all rights reserved.<br>
*	Permission is granted to any person or entity except those designated by
*	by the United States Department of State as a terrorist, or terrorist
*	government or agency, to use and distribute this source code provided
*	that the original copyright notice remains present and unaltered.
*/

public class FilenamePattern
    implements java.lang.Cloneable, java.io.FileFilter, java.io.FilenameFilter
{
    static final String		REV =
        "@(#)tribble/util/FilenamePattern.java $Revision: 1.9 $ $Date: 2008/10/10 21:53:21 $\n";


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Constants

    //----------------------------------
    // Default special filename pattern matching characters

    /** Default pattern character: Escape any special meaning. */
    public static final char		PAT_ESCAPE =	'\\';

    /** Default pattern character: Any single character. */
    public static final char		PAT_ANY =	'?';

    /** Default pattern character: Zero or more characters. */
    public static final char		PAT_CLOSURE =	'*';

    /** Default pattern character: Character set open. */
    public static final char		PAT_SET_OPEN =	'[';

    /** Default pattern character: Character set close. */
    public static final char		PAT_SET_CLOSE =	']';

    /** Default pattern character: Character set range. */
    public static final char		PAT_SET_THRU =	'-';

    /** Default pattern character: Character set exclusion. */
    public static final char		PAT_SET_EXCL =	'^';

    /** Default pattern character: Directory path separator. */
    public static final char		PAT_DIR_SEP =	'/';

    /** Default pattern character: Negation. */
    public static final char		PAT_NEGATE =	'!';


    //----------------------------------
    // Other constants

    /** System-dependent directory path separator character (1). */
    protected static final char		DIR_SEP1;

    /** System-dependent directory path separator character (2). */
    protected static final char		DIR_SEP2;

    /** System-dependent filename case-sensitivity flag. */
    protected static final boolean	IGNORE_CASE;


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Static class initializers

    /***************************************************************************
    * Static class initializer.
    *
    * <p>
    * This block is responsible for initializing the {@link #DIR_SEP1},
    * {@link #DIR_SEP2}, and {@link #IGNORE_CASE} constants.
    *
    * <p>
    * Note that this code is written to handle Unix and MS/Windows systems.
    * It may need to be modified to handle other operating systems.
    *
    * @since	1.1, 2003-02-09
    */

    static
    {
        char	sep;

        // Initialize DIR_SEP1 and DIR_SEP2
        sep = File.separatorChar;
        DIR_SEP1 = sep;

        if (sep == '\\')
        {
            DIR_SEP2 = '/';
            IGNORE_CASE = true;
        }
        else if (sep == '/')
        {
            DIR_SEP2 = sep;
            IGNORE_CASE = false;
        }
        else
        {
            DIR_SEP2 = sep;
            IGNORE_CASE = true;
        }
    }


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Static methods

    /***************************************************************************
    * Determine if a filename matches a filename pattern.
    *
    * <p>
    * <b> Note </b>
    *
    * <p>
    * The filename is matched against a filename pattern matcher that uses the
    * default settings for its special pattern matching characters.
    *
    * @param	pat
    * A filename pattern string.
    *
    * @param	fname
    * A filename to compare against <tt>pat</tt>.
    * Note that empty filenames (<tt>""</tt>) are not matched by any filename
    * pattern.
    *
    * @return
    * True if <tt>fname</tt> matches <tt>pat</tt>, otherwise false.
    *
    * @throws	IllegalArgumentException (unchecked)
    * Thrown if <tt>pat</tt> is malformed.
    *
    * @throws	NullPointerException (unchecked)
    * Thrown if <tt>pat</tt> or <tt>fname</tt> is null.
    *
    * @since	1.1, 2003-02-09
    */

    public static boolean matches(String pat, String fname)
        //throws IllegalArgumentException, NullPointerException
    {
        // Match a filename against a filename pattern with defaults
        return (new FilenamePattern(pat)).matches(fname);
    }


    /***************************************************************************
    * Determine if a character falls within a given character range, ignoring
    * case.
    *
    * <p>
    * Note that this method might exhibit slightly different behavior than the
    * <tt>String.compareToIgnoreCase()</tt> method for certain regional
    * characters.
    *
    * @param	ch
    * A character to compare.
    *
    * @param	lo
    * The lower character value in the range to compare.
    *
    * @param	hi
    * The upper character value in the range to compare.
    *
    * @return
    * True if character <tt>ch</tt> falls within the range <tt>[lo,hi]</tt>,
    * ignoring case, otherwise false.
    *
    * @see	#matchSubpattern matchSubpattern()
    *
    * @since	1.2, 2003-07-04
    */

    protected static boolean compareCharIgnoreCase(char ch, char lo, char hi)
    {
        char	ch2;
        char	lo2;
        char	hi2;

        // Compare lowercase equivalents
        ch2 = Character.toLowerCase(ch);
        lo2 = Character.toLowerCase(lo);
        hi2 = lo2;
        if (lo != hi)
            hi2 = Character.toLowerCase(hi);
        if (ch2 >= lo2  &&  ch2 <= hi2)
            return (true);

        // Compare uppercase equivalents
        ch2 = Character.toUpperCase(ch);
        lo2 = Character.toUpperCase(lo);
        hi2 = lo2;
        if (lo != hi)
            hi2 = Character.toUpperCase(hi);
        return (ch2 >= lo2  &&  ch2 <= hi2);
    }


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Variables

    /**
    * Filename pattern.
    * This is stored as a character array instead of a <tt>String</tt> object to
    * achieve a slight improvement in speed.
    */
    protected char[]		m_pat;

    /** Ignore case when matching filenames. */
    protected boolean		m_ignoreCase =	IGNORE_CASE;

    /** This pattern contains directory path separator characters. */
    protected boolean		m_hasDirSep;


    //----------------------------------
    // Special pattern matching characters

    /** Special pattern character: Escape any special meaning. */
    protected char		m_escape =	PAT_ESCAPE;

    /** Special pattern character: Match any single character. */
    protected char		m_any =		PAT_ANY;

    /** Special pattern character: Match zero or more characters. */
    protected char		m_closure =	PAT_CLOSURE;

    /** Special pattern character: Character set open. */
    protected char		m_setOpen =	PAT_SET_OPEN;

    /** Special pattern character: Character set close. */
    protected char		m_setClose =	PAT_SET_CLOSE;

    /** Special pattern character: Character set range. */
    protected char		m_setThru =	PAT_SET_THRU;

    /** Special pattern character: Character set exclusion. */
    protected char		m_setExcl =	PAT_SET_EXCL;

    /** Special pattern character: Directory path separator. */
    protected char		m_dirSep =	PAT_DIR_SEP;

    /** Special pattern character: Negation. */
    protected char		m_negate =	PAT_NEGATE;


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Constructors

    /***************************************************************************
    * Default constructor.
    *
    * @see	#setPattern setPattern()
    *
    * @since	1.1, 2003-02-09
    */

    public FilenamePattern()
    {
        // Do nothing
    }


    /***************************************************************************
    * Constructor.
    *
    * @param	pat
    * A filename pattern.
    *
    * @throws	IllegalArgumentException (unchecked)
    * Thrown if <tt>pat</tt> is malformed.
    *
    * @throws	NullPointerException (unchecked)
    * Thrown if <tt>pat</tt> is null.
    *
    * @see	#setPattern setPattern()
    *
    * @since	1.1, 2003-02-09
    */

    public FilenamePattern(String pat)
        //throws IllegalArgumentException, NullPointerException
    {
        // Establish the filename pattern
        setPattern(pat);
    }


    /***************************************************************************
    * Constructor.
    *
    * @param	pat
    * A filename pattern.
    *
    * @param	ignCase
    * If true, case is ignored when matching alphabetic characters in filenames
    * (e.g, the names <tt>"abc"</tt>, <tt>"Abc"</tt>, and <tt>"ABC"</tt> all
    * compare equal); if false, filename comparisons are case-sensitive and
    * alphabetic characters must match exactly.
    *
    * @throws	IllegalArgumentException (unchecked)
    * Thrown if <tt>pat</tt> is malformed.
    *
    * @throws	NullPointerException (unchecked)
    * Thrown if <tt>pat</tt> is null.
    *
    * @see	#setPattern setPattern()
    * @see	#setIgnoreCase setIgnoreCase()
    *
    * @since	1.6, 2004-10-19
    */

    public FilenamePattern(String pat, boolean ignCase)
        //throws IllegalArgumentException, NullPointerException
    {
        // Establish the filename pattern
        setIgnoreCase(ignCase);
        setPattern(pat);
    }


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Methods

    /***************************************************************************
    * Retrieve the pattern for the filename pattern matcher.
    *
    * @return
    * The filename pattern for this pattern matcher.
    * Note that this will be null if the pattern has not been established yet.
    *
    * @see	#setPattern setPattern()
    *
    * @since	1.7, 2004-10-21
    */

    public String getPattern()
    {
        // Get the pattern
        if (m_pat == null)
            return (null);
        return (new String(m_pat));
    }


    /***************************************************************************
    * Establish the pattern for the filename pattern matcher.
    *
    * <p>
    * This method is provided so that this filename pattern matcher can be
    * reused with different patterns.
    *
    * @param	pat
    * A filename pattern.
    * This is verified to be a well-formed pattern, using the current special
    * character settings of the filename pattern.
    *
    * @throws	IllegalArgumentException (unchecked)
    * Thrown if <tt>pat</tt> is malformed.
    *
    * @throws	NullPointerException (unchecked)
    * Thrown if <tt>pat</tt> is null.
    *
    * @see	#getPattern getPattern()
    *
    * @since	1.1, 2003-02-09
    */

    public void setPattern(String pat)
        //throws IllegalArgumentException, NullPointerException
    {
        // Validate the filename pattern
        if (pat == null)
            throw new NullPointerException("Null pattern");

        if (!isValidPattern(pat))
            throw new IllegalArgumentException("Malformed pattern: \""
                + pat + "\"");

        // Set the filename pattern
        m_pat = pat.toCharArray();

        // Check for directory path separator characters in the pattern
        m_hasDirSep = false;
        if (m_dirSep != '\0')
        {
            int		len;

            len = m_pat.length;
            for (int i = 0;  i < len;  i++)
            {
                char	ch;

                if (m_pat[i] == '\0')
                    ; // NULs are allowed
                else if (m_pat[i] == m_escape)
                    i++;
                else if (m_pat[i] == m_dirSep)
                {
                    m_hasDirSep = true;
                    break;
                }
            }
        }
    }


    /***************************************************************************
    * Modify a special pattern matching character for the filename pattern
    * matcher.
    *
    * <p>
    * This method should be called prior to calling
    * {@link #setPattern setPattern()}.
    *
    * @param	type
    * The type of the special pattern matching character to be modified, which
    * is one of the {@link #PAT_ESCAPE PAT_<i>XXX</i>} constants.
    *
    * @param	ch
    * The character to use as the special pattern matching character.
    * If this character is <tt>'\0'</tt> (<tt>NUL</tt>), the pattern matcher
    * will not recognize any character as the special pattern matching character
    * of type <tt>type</tt>, effectively disabling that kind of pattern
    * matching.
    *
    * @return
    * The previous value for the special pattern matching character.
    *
    * @throws	IllegalArgumentException (unchecked)
    * Thrown if <tt>type</tt> is not a valid special pattern character type.
    *
    * @since	1.1, 2003-02-09
    */

    public char setSpecialChar(int type, char ch)
        //throws IllegalArgumentException
    {
        char	prev;

        // Set the appropriate special pattern matching character
        switch (type)
        {
        case PAT_ESCAPE:
            prev = m_escape;
            m_escape = ch;
            return (prev);

        case PAT_ANY:
            prev = m_any;
            m_any = ch;
            return (prev);

        case PAT_CLOSURE:
            prev = m_closure;
            m_closure = ch;
            return (prev);

        case PAT_SET_OPEN:
            prev = m_setOpen;
            m_setOpen = ch;
            return (prev);

        case PAT_SET_CLOSE:
            prev = m_setClose;
            m_setClose = ch;
            return (prev);

        case PAT_SET_THRU:
            prev = m_setThru;
            m_setThru = ch;
            return (prev);

        case PAT_SET_EXCL:
            prev = m_setExcl;
            m_setExcl = ch;
            return (prev);

        case PAT_DIR_SEP:
            prev = m_dirSep;
            m_dirSep = ch;
            return (prev);

        case PAT_NEGATE:
            prev = m_negate;
            m_negate = ch;
            return (prev);

        default:
            // Invalid special pattern character type
            throw new IllegalArgumentException("Invalid special pattern "
                + "character type: '" + ((char) type) + "'");
        }
    }


    /***************************************************************************
    * Establish whether or not the filename pattern matcher ignores case.
    * (By default, the case-sensitivity of a pattern matcher is dependent upon
    * the underlying operating system.)
    *
    * @param	flag
    * If true, upper and lower case letters are considered to be identical when
    * matching filenames (e.g., filenames <tt>"abc"</tt>, <tt>"Abc"</tt>, and
    * <tt>"ABC"</tt> will all be considered to be the same filename).
    * If false, the pattern matcher is case-sensitive, and equivalent upper and
    * lower case letters are considered to be different.
    *
    * @return
    * The previous setting for this pattern matcher.
    *
    * @since	1.1, 2003-02-09
    */

    public boolean setIgnoreCase(boolean flag)
    {
        boolean		prev;

        // Establish the control setting
        prev = m_ignoreCase;
        m_ignoreCase = flag;
        return (prev);
    }


    /***************************************************************************
    * Determine if a filename matches the filename pattern.
    *
    * @param	fname
    * A filename to match against the filename pattern.
    * Note that empty filenames (<tt>""</tt>) are not matched by any filename
    * pattern.
    *
    * @return
    * True if the pattern matches <tt>fname</tt>, otherwise false.
    *
    * @throws	NullPointerException (unchecked)
    * Thrown if <tt>fname</tt> is null, or if this pattern matcher does not have
    * a filename pattern established.
    *
    * @since	1.1, 2003-02-09
    */

    public boolean matches(String fname)
        //throws IllegalArgumentException, NullPointerException
    {
        // Check args
        if (m_pat == null)
            throw new NullPointerException("Null pattern");

        if (fname == null)
            throw new NullPointerException("Null filename");

        if (fname.length() == 0)
            return (false);

        // Attempt to match the filename against the pattern
        return (matchSubpattern(0, fname, 0) >= 0);
    }


    /***************************************************************************
    * Determine if a filename matches the filename pattern.
    *
    * <p>
    * If this filename pattern matcher contains directory separator characters,
    * it attempts to match the full pathname formed by concatenating the
    * directory name <tt>dir</tt> with the filename <tt>fname</tt>.  Otherwise
    * it attempts to match only the filename, ignoring the directory name.
    *
    * @param	dir
    * The pathname of the directory containing <tt>fname</tt>.
    * (This pattern matcher implementation ignores this argument.)
    *
    * @param	fname
    * A filename to match against the pattern.
    *
    * @return
    * True if the filename matches the pattern, otherwise false.
    *
    * @since	1.1, 2003-02-09
    */

    public boolean accept(File dir, String fname)
        //implements java.io.FilenameFilter
    {
        // Attempt to match the filename
        if (m_hasDirSep)
            return (matches(dir.getPath() + DIR_SEP1 + fname));
        else
            return (matches(fname));
    }


    /***************************************************************************
    * Determine if a filename matches the filename pattern.
    *
    * <p>
    * If this filename pattern matcher contains directory separator characters,
    * it attempts to match the entire pathname of the given filename.  Otherwise
    * it attempts to match only the filename portion, ignoring the directory
    * prefix.
    *
    * @param	fname
    * A filename to match against the pattern.
    *
    * @return
    * True if the filename matches the pattern, otherwise false.
    *
    * @since	1.1, 2003-02-09
    */

    public boolean accept(File fname)
        //implements java.io.FileFilter
    {
        // Attempt to match the filename
        if (m_hasDirSep)
            return (matches(fname.getPath()));
        else
            return (matches(fname.getName()));
    }


    /***************************************************************************
    * Compare this object to another.
    *
    * <p>
    * <b> Note </b>
    * <p>
    * Two filename pattern matchers are considered the same (equal) if they have
    * identical internal patterns and identical control settings.
    *
    * @param	obj
    * Another object to compare this one to.
    * If <tt>obj</tt> is not a {@link FilenamePattern} or one of its subtypes,
    * it cannot be equal to this object.
    *
    * @return
    * True if this object is identical to <tt>obj</tt>, otherwise false.
    *
    * @since	1.7, 2004-10-21
    */

    public boolean equals(Object obj)
        //overrides java.lang.Object
    {
        FilenamePattern		that;

        // Compare this object to 'that'
        if (!(obj instanceof FilenamePattern))
            return (false);
        that = (FilenamePattern) obj;

        // Equal pattern matchers have the same internal pattern and settings
        if (m_pat != null)
        {
            int		len;

            len = this.m_pat.length;
            if (len != that.m_pat.length)
                return (false);

            for (int i = 0;  i < len;  i++)
                if (this.m_pat[i] != that.m_pat[i])
                    return (false);
        }

        if (this.m_ignoreCase != that.m_ignoreCase)
            return (false);
        if (this.m_hasDirSep != that.m_hasDirSep)
            return (false);
        if (this.m_escape != that.m_escape)
            return (false);
        if (this.m_any != that.m_any)
            return (false);
        if (this.m_closure != that.m_closure)
            return (false);
        if (this.m_setOpen != that.m_setOpen)
            return (false);
        if (this.m_setClose != that.m_setClose)
            return (false);
        if (this.m_setThru != that.m_setThru)
            return (false);
        if (this.m_setExcl != that.m_setExcl)
            return (false);
        if (this.m_dirSep != that.m_dirSep)
            return (false);
        if (this.m_negate != that.m_negate)
            return (false);

        // The pattern matcher objects are identical
        return (true);
    }


    /***************************************************************************
    * Calculate a hash code for this object.
    *
    * <p>
    * <b> Note </b>
    * <p>
    * Two filename pattern matchers are considered the same (equal) if they have
    * identical internal patterns and identical control settings.
    *
    * @return
    * A hash code for this object.
    *
    * @since	1.7, 2004-10-21
    */

    public int hashCode()
        //overrides java.lang.Object
    {
        int	h = 0x0000;

        // Compute the hash code
        if (m_pat != null)
        {
            for (int i = m_pat.length-1;  i >= 0;  i--)
                h = ((h << 3) | (h >>> 32-3)) ^ m_pat[i];
        }

        h = ((h << 1) | (h >>> 32-1)) ^ (m_ignoreCase ? 0x0001 : 0x0000);
        h = ((h << 1) | (h >>> 32-1)) ^ (m_hasDirSep ? 0x0001 : 0x0000);
        h = ((h << 1) | (h >>> 32-1)) ^ m_escape;
        h = ((h << 1) | (h >>> 32-1)) ^ m_any;
        h = ((h << 1) | (h >>> 32-1)) ^ m_closure;
        h = ((h << 1) | (h >>> 32-1)) ^ m_setOpen;
        h = ((h << 1) | (h >>> 32-1)) ^ m_setClose;
        h = ((h << 1) | (h >>> 32-1)) ^ m_setThru;
        h = ((h << 1) | (h >>> 32-1)) ^ m_setExcl;
        h = ((h << 1) | (h >>> 32-1)) ^ m_dirSep;
        h = ((h << 1) | (h >>> 32-1)) ^ m_negate;

        return (h);
    }


    /***************************************************************************
    * Clone this filename pattern matcher.
    *
    * <p>
    * <b> Note </b>
    * <p>
    * The internal pattern is shared by both this pattern matcher and the newly
    * created clone object.  If the internal pattern is to be modified by a
    * method in a subclass of this class, this method must be overridden so that
    * the clone gets its own copy of the internal pattern which it can then
    * freely modify.  (Calling method {@link #setPattern setPattern()} endows a
    * pattern matcher with an entirely new internal pattern, of course).
    *
    * @return
    * A filename pattern matcher with settings identical to this one.
    *
    * @since	1.7, 2004-10-21
    */

    public Object clone()
        //implements java.lang.Cloneable
        //overrides java.lang.Object
    {
        FilenamePattern		copy;

        // Create a clone of this pattern matcher
        copy = new FilenamePattern();

        // Note: 'm_pat[]' is shared among cloned objects
        copy.m_pat =        this.m_pat;

        copy.m_ignoreCase = this.m_ignoreCase;
        copy.m_hasDirSep =  this.m_hasDirSep;
        copy.m_escape =     this.m_escape;
        copy.m_any =        this.m_any;
        copy.m_closure =    this.m_closure;
        copy.m_setOpen =    this.m_setOpen;
        copy.m_setClose =   this.m_setClose;
        copy.m_setThru =    this.m_setThru;
        copy.m_setExcl =    this.m_setExcl;
        copy.m_dirSep =     this.m_dirSep;
        copy.m_negate =     this.m_negate;

        return (copy);
    }


    /***************************************************************************
    * Determine if a filename pattern is well-formed.
    * The current special character settings of the filename pattern are used.
    *
    * @param	pat
    * A filename pattern to verify.
    *
    * @return
    * True if <tt>pat</tt> is well-formed, otherwise false.
    *
    * @see	#setSpecialChar setSpecialChar()
    *
    * @since	1.1, 2003-02-09
    */

    protected boolean isValidPattern(String pat)
    {
        int	len;
        int	setOpen;
        boolean	setRange;

        // Validate the pattern
        len = pat.length();
        if (len == 0)
            return (false);

        setOpen =  0;
        setRange = false;

        for (int i = 0;  i < len;  i++)
        {
            char	pCh;

            // Examine a single pattern character
            pCh = pat.charAt(i);

            if (pCh == '\0')
            {
                // Special case, which must be first if any of the 'm_xxx'
                //  special pattern matching characters are '\0' (disabled)
            }
            else if (pCh == m_negate  &&  setOpen == 0)
            {
                // Ensure there is a negated subpattern present
                if (i+1 >= len)
                    return (false);
            }
            else if (pCh == m_escape)
            {
                // Ensure there is an escaped character present
                i++;
                if (i >= len)
                    return (false);
                setRange = false;
            }
            else if (pCh == m_setOpen  &&  setOpen == 0)
            {
                // Start of a character set
                setOpen++;
            }
            else if (pCh == m_setExcl  &&  setOpen > 0)
            {
                // Character set exclusion
            }
            else if (pCh == m_setThru  &&  setOpen > 0)
            {
                // Character set range
                setRange = true;
            }
            else if (pCh == m_setClose  &&  setRange)
            {
                // Incomplete character set range
                return (false);
            }
            else if (pCh == m_setClose  &&  setOpen > 0)
            {
                // End of a character set
                if (setOpen < 2)
                    return (false);
                setOpen = 0;
            }
            else if (setOpen > 0)
            {
                // Normal character, or the end of a character set range
                setOpen++;
                setRange = false;
            }
        }

        // Check for a well-formed pattern
        if (setOpen > 0  ||  setRange)
        {
            // Incomplete character set or character range
            return (false);
        }

        // The filename pattern is well-formed
        return (true);
    }


    /***************************************************************************
    * Determine if a filename substring matches a pattern substring.
    *
    * <p>
    * <b> Note </b>
    *
    * <p>
    * The pattern string ({@link #m_pat}) should be well-formed, otherwise the
    * pattern matching algorithm will become confused.
    *
    * @param	patOff
    * The index of the first (leftmost) character within the pattern to match.
    *
    * @param	fname
    * A filename string to compare against the pattern.
    * Note that empty filenames (<tt>""</tt>) are not matched by any filename
    * pattern.
    *
    * @param	fnameOff
    * The index of the first (leftmost) character within string <tt>fname</tt>
    * to match.
    *
    * @return
    * The index of one character past the last (rightmost) character within
    * filename string <tt>fname</tt> that matches the pattern, or a negative
    * value if the filename substring does not match the pattern substring.
    *
    * @see	#matches(String) matches()
    * @see	#compareCharIgnoreCase compareCharIgnoreCase()
    *
    * @since	1.1, 2003-02-09
    */

    protected int matchSubpattern(int patOff, String fname, int fnameOff)
    {
        int	patLen;
        int	fnameLen;
        int	pi;
        int	fi;

        // Attempt to match the string, one pattern character at a time
        patLen = m_pat.length;
        fnameLen = fname.length();

        for (pi = patOff, fi = fnameOff;  pi < patLen;  pi++)
        {
            char	pCh;
            int		fCh;

            // Examine the next character of the filename
            fCh = -1;
            if (fi < fnameLen)
                fCh = fname.charAt(fi);

            // Examine the next character of the filename pattern
            pCh = m_pat[pi];
            if (pCh == '\0')
            {
                // Special case, which must be first if any of the 'm_xxx'
                //  special pattern matching characters are '\0' (disabled)
                if (fCh != '\0')
                    return (-fi-1);

                // Submatch succeeded
                fi++;
            }
            else if (pCh == m_closure)
            {
                int	j;

                if (m_dirSep != '\0')
                {
                    // Match only up to the next directory path separator
                    for (j = fi;  j < fnameLen;  j++)
                    {
                        char	ch;

                        ch = fname.charAt(j);
                        if (ch == DIR_SEP1  ||  ch == DIR_SEP2)
                            break;
                    }
                }
                else
                    j = fnameLen;

                if (j == fnameOff)
                    return (-fi-1);

                // Match zero or more filename characters
                for ( ;  j >= fi;  j--)
                {
                    // Assume a match of substring 'fname[fi...j-1]',
                    //  attempt to match the rest of the pattern
                    if (matchSubpattern(pi+1, fname, j) >= 0)
                    {
                        // Successful submatch
                        return (j);
                    }
                }

                // Submatch failed
                return (-fi-1);
            }
            else if (pCh == m_any)
            {
                // Match any single character
                if (fCh < 0)
                    return (-fi-1);

                // Submatch succeeded
                fi++;
            }
            else if (pCh == m_escape)
            {
                // Escape the next pattern character
                if (pi+1 < patLen)
                    pCh = m_pat[++pi];

                if (pCh != fCh)
                {
                    if (m_ignoreCase)
                    {
                        if (!compareCharIgnoreCase((char) fCh, pCh, pCh))
                            return (-fi-1);
                    }
                    else
                        return (-fi-1);
                }

                // Submatch succeeded
                fi++;
            }
            else if (pCh == m_setOpen)
            {
                boolean		sense;
                boolean		found;

                // Match a single character from a character set
                if (fCh < 0)
                    return (-fi-1);

                // Check for character set exclusion
                pi++;
                if (pi < patLen)
                    pCh = m_pat[pi];
                sense = true;
                if (pCh == m_setExcl)
                {
                    sense = false;
                    pi++;
                }

                // Attempt to match a single character to the character set
                found = !sense;
                while (pi < patLen)
                {
                    char	loCh;

                    // Get the next character from the character set
                    pCh = m_pat[pi];
                    if (pCh == m_setClose)
                        break;
                    loCh = pCh;
                    pi++;

                    if (pCh == m_escape)
                    {
                        // Escape the special meaning of the set character
                        if (pi >= patLen)
                            return (-fi-1);
                        pCh = m_pat[pi++];
                        loCh = pCh;
                    }
                    else if (pi < patLen  &&  m_pat[pi] == m_setThru)
                    {
                        // Set specifies a range of characters
                        pi++;
                        if (pi >= patLen)
                            return (-fi-1);
                        pCh = m_pat[pi++];

                        if (pCh == m_escape)
                        {
                            // Escape the special meaning of the set character
                            if (pi >= patLen)
                                return (-fi-1);
                            pCh = m_pat[pi++];
                        }
                        else if (pCh == m_setClose)
                            return (-fi-1);
                    }

                    // Compare the filename character against the
                    //  character range set [loCh,pCh]
                    if (m_ignoreCase)
                    {
                        if (compareCharIgnoreCase((char) fCh, loCh, pCh))
                            found = sense;
                    }
                    else if (loCh <= fCh  &&  fCh <= pCh)
                        found = sense;
                }

                // Check for a match
                if (!found)
                    return (-fi-1);

                // Submatch succeeded
                fi++;
            }
            else if (pCh == m_dirSep)
            {
                // Match a single directory path separator character
                if (fCh < 0)
                    return (-fi-1);
                if (fCh != DIR_SEP1  &&  fCh != DIR_SEP2)
                    return (-fi-1);

                // Submatch succeeded
                fi++;
                if (pi+1 < patLen  &&  m_pat[pi+1] != m_dirSep  ||
                    pi+1 == patLen)
                {
                    // Match multiple consecutive path separators
                    while (fi < fnameLen)
                    {
                        fCh = fname.charAt(fi);
                        if (fCh != DIR_SEP1  &&  fCh != DIR_SEP2)
                            break;

                        fi++;
                    }
                }

                if (pi+1 < patLen)
                {
                    // Recursively match the rest of the pattern
                    if (matchSubpattern(pi+1, fname, fi) >= fi)
                        return (fi);

                    // Recursive match failed
                    return (-fi-1);
                }
            }
            else if (pCh == m_negate)
            {
                int	j;

                // Negate the sense of the rest of the subpattern match
                j = matchSubpattern(pi+1, fname, fi);
                if (j >= 0)
                {
                    // Successful submatch, so this match failed
                    return (-fi-1);
                }

                // Submatch failed, so this match succeeded
                return (-j-1);
            }
            else
            {
                // Match a single non-special character exactly
                if (fCh < 0)
                    return (-fi-1);

                if (pCh != fCh)
                {
                    if (m_ignoreCase)
                    {
                        if (!compareCharIgnoreCase((char) fCh, pCh, pCh))
                            return (-fi-1);
                    }
                    else
                        return (-fi-1);
                }

                // Submatch succeeded
                fi++;
            }
        }

        // Match the end of the string
        if (fi == fnameLen)
            return (fi);

        // Pattern match failed
        return (-fi-1);
    }


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Inner classes

    /***************************************************************************
    * Test driver class for class {@link FilenamePattern}.
    *
    * <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
    * <dl>
    * <dt> <b>Source code:</b> </dt>
    *  <dd>
    *   <a href="../../../src/java/tribble/util/FilenamePattern.java"
    *    >http://david.tribble.com/src/java/tribble/util/FilenamePattern.java</a>
    *  </dd>
    * <dt> <b>Documentation:</b> </dt>
    *  <dd>
    *   <a href="FilenamePattern.html"
    *    >http://david.tribble.com/docs/tribble/util/FilenamePattern.html</a>
    *  </dd>
    * </dl>
    *
    * <!-- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -->
    * @version	$Revision: 1.9 $ $Date: 2008/10/10 21:53:21 $
    * @since	{@link FilenamePattern} 1.9, 2008-10-10
    * @author	David R. Tribble (david&#64;tribble.com).
    *	<br>
    *	Copyright ©2003-2008 by David R. Tribble, all rights reserved.<br>
    *	Permission is granted to any person or entity except those designated by
    *	by the United States Department of State as a terrorist, or terrorist
    *	government or agency, to use and distribute this source code provided
    *	that the original copyright notice remains present and unaltered.
    */

    static abstract class Test
    {
        /***********************************************************************
        * Test driver for this class.
        *
        * <p>
        * <b> Usage </b>
        * <br>
        * <tt>
        * java tribble.util.FilenamePattern$Test <i>pattern filename result</i>
        * </tt>
        *
        * Where <tt><i>pattern</i></tt> is a filename pattern,
        * <tt><i>filename</i></tt> is a filename,
        * and <tt><i>result</i></tt> is the expected result, which is one of:
        * <dl>
        *  <dd> <tt>t</tt> - True </dd>
        *  <dd> <tt>f</tt> - False </dd>
        *  <dd> <tt>m</tt> - Malformed pattern
        *   (<tt>java.lang.IllegalArgumentException</tt>) </dd>
        * </dl>
        *
        * @param	args
        * Command line arguments.
        *
        * @since	1.9, 2008-10-10 (2003-02-09)
        */

        public static void main(String[] args)
        {
            String	exp =	"?";

            // Check args
            if (args.length < 3)
            {
                System.out.println("usage:  java " + Test.class.getName()
                    + " pattern filename t|f|m");
                System.exit(255);
            }

            try
            {
                FilenamePattern	m;
                String		pat;
                String		fname;
                boolean		res;

                pat =   args[0];
                fname = args[1];
                exp =   args[2];

                // Test case
                m = new FilenamePattern();
                System.out.println("pattern:  \"" + pat + "\"");
                System.out.println("filename: \"" + fname + "\"");
                m.setPattern(pat);
                res = m.matches(fname);
                System.out.print("-> " + (res ? "true, " : "false, "));
                System.out.println((res && exp.equals("t") ? "pass" :
                    !res && exp.equals("f") ? "pass" :
                    "FAIL ***"));
            }
            catch (Exception ex)
            {
                System.out.println("Exception: " + ex.getClass().getName());
                System.out.println(ex.getMessage());

                if (ex instanceof IllegalArgumentException)
                    System.out.println("-> "
                        + (exp.equals("m") ? "pass" : "FAIL ***"));
                else if (!(ex instanceof IllegalArgumentException))
                    ex.printStackTrace(System.out);
            }
        }
    }
}

// End FilenamePattern.java
