/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2000-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * @(#)Selector.java	1.11 06/29/07
 */ 

package com.sun.messaging.jmq.util.selector;

import java.util.*;

import com.sun.messaging.jmq.util.lists.WeakValueHashMap;


/**
 * A class that implements JMS selectors. See section 3.8 of the JMS 1.1 spec.
 *
 */
public class Selector {

    public static boolean DEBUG = false;
    public static boolean VERBOSE_DEBUG = false;

    // Tokens that can appear in a selector string. Note that some
    // tokens are considered "compound". I.e. they are built from 
    // one or more primitive tokens.
    static final int INVALID              = 500;    // Illegal token
    static final int STARTING             = 0;

    // Operators 
    static final int OR                   = 1;    // OR, or
    static final int AND                  = 2;    // AND, and
    static final int NOT                  = 3;    // NOT, not

    static final int NOT_EQUALS           = 4;    // <>
    static final int LTE                  = 5;    // <=
    static final int LT                   = 6;    // <
    static final int GTE                  = 7;    // >=
    static final int GT                   = 8;    // >
    static final int EQUALS               = 9;    // =

    static final int UNARY_PLUS           = 10;   // +
    static final int UNARY_MINUS          = 11;   // -
    static final int MULTIPLY             = 12;   // *
    static final int DIVIDE               = 13;   // /
    static final int PLUS                 = 14;   // +
    static final int MINUS                = 15;   // -

    static final int BETWEEN              = 16;   // BETWEEN, between
    static final int NOT_BETWEEN          = 17;   // Compound: "NOT BETWEEN"
    static final int IN                   = 18;   // IN
    static final int NOT_IN               = 19;   // Compound: "NOT IN"
    static final int LIKE                 = 20;   // LIKE
    static final int ESCAPE               = 21;   // ESCAPE
    static final int NOT_LIKE             = 22;   // Compound: "NOT LIKE"
    static final int IS_NULL              = 23;   // Compound: "IS NULL"
    static final int IS_NOT_NULL          = 24;   // Compound: "IS NOT NULL"
    static final int IS                   = 25;   // IS
    static final int IS_NOT               = 26;   // Compound "IS NOT"
    static final int LEFT_PAREN           = 27;   // (
    static final int RIGHT_PAREN          = 28;   // )

    static final int COMMA                = 29;   // , 
    // Operands
    static final int IDENTIFIER           = 101;   // Java identifier
    static final int STRING               = 102;   // '...'
    static final int DOUBLE               = 103;   // -57.92
    static final int LONG                 = 104;   // +347
    static final int TRUE                 = 105;   // TRUE, true
    static final int FALSE                = 106;   // FALSE, false
    static final int JMS_FIELD            = 107;   // JMS*
    static final int RANGE                = 108;   // Compound: "15 AND 19"
    static final int LIST                 = 109;   // Compound: "('US', 'UK', 'Peru')"
    static final int WHITESPACE           = 110;   // Whitespace
    static final int NULL                 = 111;   // NULL token
    static final int UNKNOWN              = 112;   // Unknown result
    static final int RE                   = 113;   // LIKE regular expression

    // Markers
    static final int AND_MARKER           = 200;
    static final int OR_MARKER            = 201;

    /* 
     * The JMS specification specifically states that type conversions
     * between Strings and numeric values should not be performed.
     * See JMS 1.1 section 3.8.1.1. For example if you set a string property:
     *    msg.setStringProperty("count", "2")
     * then the following should evaluate to false because a string cannot be
     * used in an arithmetic expression:
     *    "count = 2"
     * The above expression should be "count = '2'"
     * The older selector implementation supported this type conversion
     * for some expressions. This introduces the possibility that some
     * applications may be relying on this bug, and will break now that
     * it is fixed. This boolean let's use switch to the old behavior.
     *
     * If convertTypes is true, then the selector evaluator will convert
     * string values to numeric values. Currently for only = and <>
     */
    private static boolean convertTypes = false;

    /*
     * True to short circuit boolean evaluation. For example
     * if you have "e1 AND e2", you do not need to evaluate e2 if e1 is
     * false. This boolean is just a safetyvalve in case there is a flaw
     * in the shortCircuit algorithm
     */
    private static boolean shortCircuit = true;

    private boolean usesProperties = false;
    private boolean usesFields     = false;

    private static HashMap keywords = null;
    
    private static HashSet headers = null;

    // Original selector string
    private String      selector = null;

    // Compiled selector string. An array of SelectorTokens in RPN
    private Object[]      compiledSelector = null;

    // Stack used for evaluation
    private Stack stack = new Stack();

    // The selector cache is used to cache selectors. This way we can
    // return the same Selector instance for identical selector strings.
    // The selectors are cached in a WeakValueHashMap. This means once
    // the Selector is no longer referenced it is garbage collected and
    // removed from the HashMap.
    private static WeakValueHashMap selectorCache = null;

    static {
        keywords = new HashMap();

        keywords.put("NOT", new Integer(NOT));
        keywords.put("AND", new Integer(AND));
        keywords.put("OR", new Integer(OR));
        keywords.put("BETWEEN", new Integer(BETWEEN));
        keywords.put("LIKE", new Integer(LIKE));
        keywords.put("IN", new Integer(IN));
        keywords.put("IS", new Integer(IS));
        keywords.put("ESCAPE", new Integer(ESCAPE));
        keywords.put("NULL", new Integer(NULL));
        keywords.put("TRUE", new Integer(TRUE));
        keywords.put("FALSE", new Integer(FALSE));
        
        headers = new HashSet(6);
        
        headers.add("JMSDeliveryMode");
        headers.add("JMSPriority");
        headers.add("JMSMessageID");
        headers.add("JMSTimestamp");
        headers.add("JMSCorrelationID");
        headers.add("JMSType");

        selectorCache = new WeakValueHashMap("SelectorCache");
    }

    public static void setConvertTypes(boolean b) {
        convertTypes = b;
    }

    public static boolean getConvertTypes() {
        return convertTypes;
    }

    public static void setShortCircuit(boolean b) {
        shortCircuit = b;
    }

    public static boolean getShortCircuit() {
        return shortCircuit;
    }

    /**
     * Compiles a selector string into a Selector object.
     * This also checks to ensure that the passed selector string
     * is a valid expression.
     *
     * @param   selector    Selector string as specified in JMS 1.1 
     */
    public static Selector compile(String selector) 
        throws SelectorFormatException{
     
        if (selector == null || selector.length() == 0) {
            return null;
        }

        Selector o = null;
        synchronized (selectorCache) {
            // First check if selector is already in cache.
            o = (Selector)selectorCache.get(selector);

            if (o == null) {
                // Selector not in cache. Create a new one and stick it in the
                // cache.
                o = new Selector(selector);
                o.compile();
                selectorCache.put(selector, o);
            } 
        }

        return o;
    }

    /**
     * Create a Selector.
     *
     * @param   selector    Selector string as specified in JMS 1.1 
     */
    private Selector(String selector) {
        this.selector = selector;
    }

    /**
     * Compile the Selector
     * <p>
     * Compiles the selector into its binary form. This must
     * be called before match(). A call to compile also performs
     * an evaluation to make sure the selector is a valid expression.
     */
    public synchronized void compile() throws SelectorFormatException {

        /*
         * This isn't the most efficient implementation possible,
         * but compilation doesn't need to be as fast as evaluation.
         * Note that we do some extra work (mainly by tracking more
         * token values than we need to) to make debugging easier.
         * Also, we do this in multiple passes simply because it is easier
         * to understand, and optimizing compile speed is not a priority.
         *
         * A compiled selector consists of a token stream. This steam
         * is held in a LinkedList initially, but is an Object[] in its
         * final compiled form. Each token is ecapsulated by a SelectorToken
         * object. This object contains the token constant (OR, STRING, etc)
         * as well as an optional associated value (like the actual
         * string value for a STRING token).
         *
         * By the time the compilation is done we want everything boiled
         * down to operators and operands.
         *
         * The final compiled form is in Reverse Polish Notation (RPN).
         */

        /* First pass: tokenize into primatives. 
         * Add a trailing space to the selector cleanly terminate parsing
         */
        LinkedList l = tokenize(selector + " ");

        if (VERBOSE_DEBUG) {
            dumpTokens(l);
        }

        /* Second pass: aggregate primatives into compound tokens (if any)
         * For example this converts the 3 primative tokens: IS NOT NULL
         * to the single token IS_NOT_NULL
         */
        l = aggregate(l);
        if (VERBOSE_DEBUG) {
            dumpTokens(l);
            System.out.println();
        }

        /* Third pass: prepare
         * This pass prepares some of the more funky operations
         * (LIKE, BETWEEN, IN, etc) for evaluation. For example it
         * takes all the strings in the IN list and puts them in 
         * a hash table and making that a single operand.
         */
        l = prepare(l);
        if (VERBOSE_DEBUG) {
            dumpTokens(l);
            System.out.println();
        }

        /* Fourth pass: Perform any additional validation
         */
        validate(l);

        /* Fith pass: convert to RPN. This removes parens and 
         * prepares the stream for simple stack based execution.
         * The output from this is an Object[] of SelectorTokens
         */
        compiledSelector = convertToRPN(l);

        if (DEBUG) {
            System.out.println(toDebugString());
        }

        // At this point compiledSelector has a token stream that is all
        // ready for evaluation! We perform one evaluation to catch any
        // errors that may occur at runtime. We do this with empty
        // property hashtables

        this.match(new HashMap(0), new HashMap(0));
    }

    /**
     * Parse selector string into token primatives. This uses
     * a state machine to track state. Each state has a number.
     */
    private LinkedList tokenize(String selector)
                                    throws SelectorFormatException {
        LinkedList buf = new LinkedList();
        int len = selector.length();
        int state = 0;

        // A buffer to hold the token string.
        StringBuffer tokenBuf = new StringBuffer(80);

        int token = STARTING;
        int lastToken = STARTING;
        int radix = 10;
        int i = 0;

        for (i = 0; i < len; i++) {
            char c = selector.charAt(i);
            Object value = null;

            switch (state) {

            case 0:
                tokenBuf.delete(0, tokenBuf.length());
                switch (c) {
                case ',':
                    token = Selector.COMMA;
                    tokenBuf.append(c);
                    value = tokenBuf.toString();
                    break;
                case '=':
                    if (lastToken == Selector.EQUALS) {
                        // We do an explicit check for == since this may be
                        // a common error.
                        throw new SelectorFormatException(
                            "Invalid operator ==, use =",
                            selector, i);
                    }
                    token = Selector.EQUALS;
                    tokenBuf.append(c);
                    value = tokenBuf.toString();
                    break;
                case '/':
                    token = Selector.DIVIDE;
                    tokenBuf.append(c);
                    value = tokenBuf.toString();
                    break;
                case '*':
                    token = Selector.MULTIPLY;
                    tokenBuf.append(c);
                    value = tokenBuf.toString();
                    break;
                case '(':
                    token = Selector.LEFT_PAREN;
                    tokenBuf.append(c);
                    value = tokenBuf.toString();
                    break;
                case ')':
                    token = Selector.RIGHT_PAREN;
                    tokenBuf.append(c);
                    value = tokenBuf.toString();
                    break;
                case '-':
                    // If last token was an operator then this is unary
                    if (lastToken == STARTING ||
                        (isOperator(lastToken) && lastToken != RIGHT_PAREN ) ) {
                        token = Selector.UNARY_MINUS;
                        tokenBuf.append(c);
                        value = tokenBuf.toString();
                    } else {
                        token = Selector.MINUS;
                        tokenBuf.append(c);
                        value = tokenBuf.toString();
                    }
                    break;
                case '+':
                    // If last token was an operator then this is unary
                    if (lastToken == STARTING || 
                        (isOperator(lastToken) && lastToken != RIGHT_PAREN ) ) {
                        token = Selector.UNARY_PLUS;
                        tokenBuf.append(c);
                        value = tokenBuf.toString();
                    } else {
                        token = Selector.PLUS;
                        tokenBuf.append(c);
                        value = tokenBuf.toString();
                    }
                    break;
                case '>':
                    // GT or GTE.
                    tokenBuf.append(c);
                    state = 1;
                    break;
                case '<':
                    // LT, LTE, or NOT_EQUALS
                    tokenBuf.append(c);
                    state = 2;
                    break;
                case '\'':
                    // Start of a string literal
                    state = 9;
                    break;
                case '.':
                    // Start of a float
                    tokenBuf.append(c);
                    state = 6;
                    break;
                case '0':
                    // Start of octal or hex numeric constant
                    tokenBuf.append(c);
                    state = 3;
                    break;
                default:
                    if (Character.isJavaIdentifierStart(c)) {
                        // Start of an identifier
                        tokenBuf.append(c);
                        state = 11;
                    } else if (Character.isDigit(c)) {
                        // Start of a number
                        tokenBuf.append(c);
                        state = 5;
                    } else if (Character.isWhitespace(c)) {
                        // Whitespace. Ignore.
                        token = Selector.WHITESPACE;
                    } else {
                        // Invalid character
                        throw new SelectorFormatException(
                            "Invalid character " + c,
                            selector, i);
                    }
                }
                break;

            // Saw a >
            case 1:
                switch (c) {
                case '=':
                    tokenBuf.append(c);
                    token = Selector.GTE;
                    value = tokenBuf.toString();
                    state = 0;
                    break;
                default:
                    token = Selector.GT;
                    value = tokenBuf.toString();
                    state = 0;
                    i--; // pushback delimiter
                    break;
                }
                break;


            // Saw a <
            case 2:
                switch (c) {
                case '=':
                    tokenBuf.append(c);
                    token = Selector.LTE;
                    value = tokenBuf.toString();
                    state = 0;
                    break;
                case '>':
                    tokenBuf.append(c);
                    token = Selector.NOT_EQUALS;
                    value = tokenBuf.toString();
                    state = 0;
                    break;
                default:
                    token = Selector.LT;
                    value = tokenBuf.toString();
                    state = 0;
                    i--; // pushback delimiter
                    break;
                }
                break;

            // Either an octal or hex numeric constant
            case 3:
                // We go to state 5 whether it's a hex or an octal constant.
                // This means we may get something like 049h which is invalid.
                // But when we go to construct the java.lang number object
                // we'll catch this.
                if (c == 'x' || c == 'X') {
                    // Hex. Don't remember X, just that we're in base 16
                    radix = 16;
                    state = 5;
                } else if (Character.isDigit(c)) {
                    // Octal
                    radix = 8;
                    tokenBuf.append(c);
                    state = 5;
                } else {
                    // Hit a delimeter. Back up and make state 5 handle this 0
                    i--;
                    state = 5;
                }
                break;

            // Working on a number!
            case 5:
                if ( (radix == 16 && isHexDigit(c)) ||
                     Character.isDigit(c) ) {
                    tokenBuf.append(c);
                } else if (c == '.') {
                    // It's a float. Go get decimal portion
                    tokenBuf.append(c);
                    state = 6;
                } else if (c == 'E' || c == 'e') {
                    // It's a float. Go get exponential
                    tokenBuf.append(c);
                    state = 7;
                } else {
                    // Hit delimeter. It's just an integer
                    token = Selector.LONG;

                     // Handle this here, cause if the value is MIN_LONG
                     // we can't create the absoute value of it!
                     if (lastToken == UNARY_MINUS) {
                         tokenBuf.insert(0, '-');
                         // Remove UNARY_MINUS from token stream
                         buf.removeLast();
                     }

                    try {
                        value = Long.valueOf(tokenBuf.toString(), radix);
                        radix = 10;
                    } catch (NumberFormatException e) {
                        throw new SelectorFormatException(
                            "Invalid numeric constant: " + e.getMessage(),
                            selector, i);
                    }
                    state = 0;

                    if (c == 'l' || c == 'L') {
                        // If it is a trailing L then we skip it.
                        // We always use longs
                    } else {
                        i--; // pushback delimiter
                    }
                }
                break;

            // Working on decimal portion of a float
            case 6:
                if (Character.isDigit(c)) {
                    tokenBuf.append(c);
                } else if (c == 'E' || c == 'e') {
                    // Go get exponential
                    tokenBuf.append(c);
                    state = 7;
                } else {
                    // Hit delimeter.
                    token = Selector.DOUBLE;
                    try {
                        value = Double.valueOf(tokenBuf.toString());
                    } catch (NumberFormatException e) {
                        throw new SelectorFormatException(
                            "Invalid numeric constant: " + e.getMessage(),
                            selector, i);
                    }
                    state = 0;
                    if (c == 'd' || c == 'D' || c == 'f' || c == 'F') {
                        // Trailing qualifier. Just skip it. Everything is a D
                    } else {
                        i--; // pushback delimiter
                    }
                }
                break;

            // Starting to work on exponential portion of a float
            case 7:
                if (Character.isDigit(c)) {
                    tokenBuf.append(c);
                    state = 8;
                } else if (c == '-') {
                    tokenBuf.append(c);
                    state = 8;
                } else {
                    // Hit delimeter. Nothing after the E
                    token = Selector.DOUBLE;
                    try {
                        value = Double.valueOf(tokenBuf.toString());
                    } catch (NumberFormatException e) {
                        throw new SelectorFormatException(
                            "Invalid numeric constant: " + e.getMessage(),
                            selector, i);
                    }
                    state = 0;
                    if (c == 'd' || c == 'D' || c == 'f' || c == 'F') {
                        // Trailing qualifier. Just skip it. Everything is a D
                    } else {
                        i--; // pushback delimiter
                    }
                }
                break;

            // Finishing work on exponential portion of a float
            case 8:
                if (Character.isDigit(c)) {
                    tokenBuf.append(c);
                } else {
                    // Hit delimeter. 
                    token = Selector.DOUBLE;
                    try {
                        value = Double.valueOf(tokenBuf.toString());
                    } catch (NumberFormatException e) {
                        throw new SelectorFormatException(
                            "Invalid numeric constant: " + e.getMessage(),
                            selector, i);
                    }
                    state = 0;
                    if (c == 'd' || c == 'D' || c == 'f' || c == 'F') {
                        // Trailing qualifier. Just skip it. Everything is a D
                    } else {
                        i--; // pushback delimiter
                    }
                }
                break;

            // Working on a string literal
            case 9:
                if (c == '\'') {
                    state = 10;
                } else {
                    tokenBuf.append(c);
                }
                break;

            // Is this the end of a string? Or an escaped single quote
            case 10:
                if (c == '\'') {
                    // Escaped single quote. Put it in token and continue
                    state = 9;
                    tokenBuf.append(c);
                } else {
                    // Hit delimeter. 
                    token = Selector.STRING;
                    value = tokenBuf.toString();
                    state = 0;
                    i--; // pushback delimiter
                }
                break;

            // Working on an identifier
            case 11:
                if (Character.isJavaIdentifierPart(c)) {
                    tokenBuf.append(c);
                } else {
                    value = tokenBuf.toString();
                    // OK, we either have an identifier, or a keyword.
                    // this method handles figuring that out.
                    token = identifierToKeyWord((String)value);
                    state = 0;
                    i--; // pushback delimiter
                }
                break;
            default:
                // This should never happen.
                throw new SelectorFormatException(
                    "Selector tokenizer in bad state: " + state +
                        " tokenBuf=" + tokenBuf + " char=" + c,
                        selector, i);
            }

            // We detect if the Selector uses message properties
            // (as opposed to just JMS fields).
            if (token == Selector.IDENTIFIER) {
                usesProperties = true;
            } else if (token == Selector.JMS_FIELD) {
                usesFields = true;
            }

            if (state == 0 && token == Selector.INVALID) {
                // This should never happen.
                throw new SelectorFormatException(
                    "Unknown token: " + token +
                        " tokenBuf=" + tokenBuf,
                        selector, i);
            }

            if (state == 0 && token != Selector.WHITESPACE) {
                buf.add(SelectorToken.getInstance(token, value));
                lastToken = token;
                radix = 10;
            }
        }

        if (state == 9) {
            // Missing closing quote
            throw new SelectorFormatException(
                    "Missing closing quote", selector, i);
        } else if (state != 0) {
            throw new SelectorFormatException(
                    "Invalid Expression", selector, i);
        }

        return buf;
    }

    // Check if s is a keyword, JMS field, or generic identifier
    private int identifierToKeyWord(String s) {
        Integer n = (Integer)keywords.get(s.toUpperCase());
        if (n != null) {
            return n.intValue();
        } else if (s.startsWith("JMS")) {
            if(headers.contains(s))
                return JMS_FIELD;
            else
                return IDENTIFIER;
        } else {
            return IDENTIFIER;
        }
    }

    private boolean isHexDigit(char c) {
        return (Character.isDigit(c) || 
                c == 'a' || c == 'A' ||
                c == 'b' || c == 'B' ||
                c == 'c' || c == 'C' ||
                c == 'd' || c == 'D' ||
                c == 'e' || c == 'E' ||
                c == 'f' || c == 'F');
    }

    /**
     * Aggregate primatives into compound tokens (if any).
     * This performs the following conversions:
     *  NOT BETWEEN => NOT_BETWEEN
     *  NOT IN      => NOT_IN
     *  NOT LIKE    => NOT_LIKE
     *  IS NULL     => IS_NULL
     *  IS NOT NULL => IS_NOT_NULL
     */
    private LinkedList aggregate(LinkedList in)
        throws SelectorFormatException {
        LinkedList out = new LinkedList();

        SelectorToken token0;
        SelectorToken token1;
        SelectorToken token2;
        int len = in.size();

        for (int i = 0; i < len; i++) {
            token0 = (SelectorToken)in.get(i);
            token1 = null;
            token2 = null;

            if (i + 1 < len) {
                token1 = (SelectorToken)in.get(i + 1);
            }

            if (i + 2 < len) {
                token2 = (SelectorToken)in.get(i + 2);
            }

            switch (token0.getToken()) {
            case Selector.NOT:
                if (token1 == null) {
                    // NOT
                    out.add(token0);
                } else if (token1.getToken() == Selector.BETWEEN) {
                    // NOT BETWEEN
                    out.add(SelectorToken.getInstance(Selector.NOT_BETWEEN,
                                (String)token0.getValue() + " " + 
                                (String)token1.getValue() ));
                    // Skip BETWEEN
                    i++;
                } else if (token1.getToken() == Selector.IN) {
                    // NOT IN
                    out.add(SelectorToken.getInstance(Selector.NOT_IN,
                                (String)token0.getValue() + " " + 
                                (String)token1.getValue() ));
                    // Skip IN
                    i++;
                } else if (token1.getToken() == Selector.LIKE) {
                    // NOT LIKE
                    out.add(SelectorToken.getInstance(Selector.NOT_LIKE,
                                (String)token0.getValue() + " " + 
                                (String)token1.getValue() ));
                    // Skip LIKE
                    i++;
                } else {
                    // NOT
                    out.add(token0);
                }
                break;

            case Selector.IS:
                if (token1 == null) {
                    // just IS
                    out.add(token0);
                } else if (token1.getToken() == Selector.NULL) {
                    // IS NULL
                    out.add(SelectorToken.getInstance(Selector.IS_NULL,
                                (String)token0.getValue() + " " + 
                                (String)token1.getValue()));
                    // Skip NULL
                    i++;
                } else if (token1.getToken() == Selector.NOT) {
                    // IS NOT
                    if (token2 == null) {
                        // just IS NOT
                        out.add(SelectorToken.getInstance(Selector.IS_NOT,
                                (String)token0.getValue() + " " + 
                                (String)token1.getValue()));
                        // Skip NOT
                        i++;
                    } else if (token2.getToken() == Selector.NULL) {
                        // IS NOT NULL
                        out.add(SelectorToken.getInstance(Selector.IS_NOT_NULL,
                                (String)token0.getValue() + " " + 
                                (String)token1.getValue() + " " +
                                (String)token2.getValue()));
                        // Skip NOT NULL
                        i++;
                        i++;
                    } else {
                        // just IS NOT
                        out.add(SelectorToken.getInstance(Selector.IS_NOT,
                                (String)token0.getValue() + " " + 
                                (String)token1.getValue()));
                        // Skip NOT
                        i++;
                    }
                } else {
                    // Just IS
                    out.add(token0);
                }
                break;
            default:
                // Simple token
                out.add(token0);
                break;
            }
        }

        return out;
    }

    /**
     * Prepare list for conversion to RPN.
     * This step prepares some of the more funky operations into
     * a format that can be more easily evaluated using a simple RPN
     * expression evaluator.
     * It performs the following:
     *
     * Replaces the AND in the BETWEEN and NOT_BETWEEN constructs with
     * a comma. The comma ensures we correctly convert the arithmetic
     * expressions in the BETWEEN ranges to RPN. This is especially true
     * when you take into account unary minus (ie BETWEEN - 1 and 5).
     * Then BETWEEN is just treated as an operator that requires 3 operands
     * and the COMMA is ignmored.
     *
     * Converts the list construct in the IN and NOT_IN operations into 
     * a single token (operand) that has a HashMap for it's value.
     *
     * Detects the ESCAPE keyword and converts the LIKE regular expression
     * string into a simple object that continas the string and the
     * escape character, so when we go to evaluate it we can do the
     * right thing based on the RE package we use.
     */
    private LinkedList prepare(LinkedList in)
        throws SelectorFormatException {
        LinkedList out = new LinkedList();

        SelectorToken token0;
        SelectorToken token1;
        SelectorToken token2;
        int len = in.size();

        for (int i = 0; i < len; i++) {
            token0 = (SelectorToken)in.get(i);

            switch (token0.getToken()) {

            case Selector.BETWEEN:
            case Selector.NOT_BETWEEN:
                out.add(token0);
                i++;
                // OKAY we saw a BETWEEN. Scan forward until we hit an AND
                // and convert it to a COMMA
                while (i < len) {
                    token0 = (SelectorToken)in.get(i);
                    if (token0.getToken() == Selector.AND) {
                        out.add(SelectorToken.getInstance(Selector.COMMA, ","));
                        break;
                    }
                    out.add(token0);
                    i++;
                }
                break;

            case Selector.IN:
            case Selector.NOT_IN:
                out.add(token0);
                i++;
                token0 = (SelectorToken)in.get(i);
                if (token0.getToken() != Selector.LEFT_PAREN) {
                    throw new SelectorFormatException(
                            "Missing ( in IN statement", selector);
                }
                // Skip open paren
                i++;
                // OK convert list of strings into a HashSet
                HashSet set = new HashSet();
                while (i < len) {
                    token0 = (SelectorToken)in.get(i);

                    if (token0.getToken() == Selector.RIGHT_PAREN) {
                        // skip close paren and terminate
                        break;
                    }

                    if (token0.getToken() == Selector.COMMA) {
                        // skip commas
                        i++;
                        continue;
                    }

                    if (token0.getToken() != Selector.STRING) {
                        throw new SelectorFormatException(
                            "IN requires string literal: " +
                            token0.getValue(), selector);
                    }

                    // Put string in HashMap
                    set.add(token0.getValue());
                    i++;
                }

                // Put list token with HashSet as value. This now becomes
                // the right operand for IN and NOT_IN
                out.add(SelectorToken.getInstance(Selector.LIST, set));
                break;


            case Selector.LIKE:
            case Selector.NOT_LIKE:
                out.add(token0);
                i++;
                // String literal should be next token
                token0 = (SelectorToken)in.get(i);
                if (token0.getToken() != Selector.STRING) {
                    throw new SelectorFormatException(
                            "LIKE requires string literal: " +
                            token0.getValue(), selector);
                }

                // String literal is the regular expression
                String re = (String)token0.getValue();
                String escape = null;
                i++;
                if (i < len) {
                    token0 = (SelectorToken)in.get(i);
                    if (token0.getToken() == Selector.ESCAPE) {
                        // Get escape string
                        i++;
                        token0 = (SelectorToken)in.get(i);
                        if (token0.getToken() != Selector.STRING) {
                            throw new SelectorFormatException(
                          "ESCAPE requires string literal: " +
                            token0.getValue(), selector);
                        } else {
                            escape = (String)token0.getValue();
                        }
                    } else {
                        i--; // push back token since it wasn't ESCAPE
                    }
                }
                out.add(SelectorToken.getInstance(Selector.RE,
                        new RegularExpression(re, escape)));
                break;

            default:
                // Simple token
                out.add(token0);
                break;
            }
        }

        return out;
    }

    /**
     * Validate expression
     * This does a simple, final syntax check before conversion to RPN.
     * It detects invalid expressions such as "= red 'color'"
     */
    private void validate(LinkedList in)
        throws SelectorFormatException {

        SelectorToken token;
        int len = in.size();
        int prevToken = STARTING;

        for (int i = 0; i < len; i++) {
            token = (SelectorToken)in.get(i);

            // If the current token is an operand, then the previous
            // token must be an operator (or STARTING)
            if (!isOperator(token)) {
                if (prevToken != STARTING &&
                   !isOperator(prevToken)) {
                    throw new SelectorFormatException(
                        "Missing operator", selector);
                }
            }

            prevToken = token.getToken();
        }

        return;
    }

    /**
     * Convert the token stream into Reverse Polish Notation (aka RPN
     * or postfix notation). This helps detect syntax errors and
     * prepares the expression for evaluation. Here is the procedure
     * for converting infix to postfix:
     *
     * Scan infix expression from left to right.
     * A. When an operand is encountered move it immediately to the
     *    RPN expression.
     * B. When an operator is encountered:
     *    1. First pop operators from the stack and place them into the
     *       RPN expression until either the stack is empty or the precedence
     *       level of the top operator in the stack is LESS than the
     *       precedence of the operator encountered in the scan.
     *    2. Then push the operator encountered onto the stack
     * C. When a left paren is encountered push it onto the stack
     *    (it creates a "sub-stack").
     * D. When unstacking operators stop when a left paren comes to the top
     *    of the stack.
     * E. When a right paren is encountered when scanning the expression unstack
     *    operators until a matching left paren is found in the stack.
     *    Pop left paren and disgard. Disgard right paren. 
     * F. When the entire expression has been scanned pop any remaining
     *    operators from the stack and place into the RPN expression.
     *
     * The following is done to support evaluation short circuit:
     *
     * After you have pushed an AND (OR) operator onto the stack, insert the
     * AND_MARKER (OR_MARKER) into the RPN expression.
     */
    private Object[] convertToRPN(LinkedList in)
        throws SelectorFormatException {
        Stack stack = new Stack();

        // For this final pass we convert to a fixed size array to
        // make final evaluation faster. We make the array larger to
        // handle markers if we have any.
        Object[] out = new Object[(int)(in.size() * 1.5)];
        int i = 0;

        Iterator iter = in.iterator();

        while (iter.hasNext()) {
            SelectorToken token = (SelectorToken)iter.next();

            if (!isOperator(token)) {
                // Operand. Move directly to RPN
                out[i++] = token;
                continue;
            }

            if (token.getToken() == LEFT_PAREN) {
                // Push ( immediately on stack
                stack.push(token);
                continue;
            }

            SelectorToken t = null;
            if (token.getToken() == RIGHT_PAREN) {
                // Pop operators until we encounter a left paren
                do {
                    if (stack.empty()) {
                        throw new SelectorFormatException(
                            "Missing (", selector);
                    }

                    t = (SelectorToken)stack.pop();
                    if (t.getToken() != LEFT_PAREN) {
                        out[i++] = t;
                    }
                } while (t.getToken() != LEFT_PAREN);
                continue;
            }

            // Operator is not a paren. Copy operators off of stack
            // until we hit one with a lower priority than the one 
            // from the scanned expression.
            while (!stack.empty()) {
                t = (SelectorToken)stack.peek();
                if (t.getToken() == LEFT_PAREN) {
                    break;
                }
                if (getPrecedence(t) < getPrecedence(token)) {
                    // Stop if precedence of top operator is less than
                    // operator from expression scan.
                    break;
                }
                // Copy higher precedence operators to RPN expression
                out[i++] = (SelectorToken)stack.pop();
            }

            // Push operator from scanned expression onto stack
            stack.push(token);

            if (shortCircuit) {
                // Markers are used to short circuit expression evaluation
                // If we just pushed an AND or OR onto the stack, put the
                // corresponding marker into the expression
                if (token.getToken() == Selector.AND) {
                    out[i++] = SelectorToken.getInstance(Selector.AND_MARKER);
                } else if (token.getToken() == Selector.OR) {
                    out[i++] = SelectorToken.getInstance(Selector.OR_MARKER);
                }
            }
        }

        // Expression has been scanned. Pop all remaining operators
        // off of stack and put in expression.
        while (!stack.empty()) {
            out[i] = (SelectorToken)stack.pop();
            if ( ((SelectorToken)out[i]).getToken() == LEFT_PAREN ) {
                throw new SelectorFormatException(
                            "Missing )", selector);
            }
            i++;
        }

        return out;
    }

    /**
     * Evaluate the selector using the passed properties and message fields.
     * compile() must have been called before calling match(). 
     *
     * @param  properties   HashMap containing message properties. These
     *                      should be String/Object pairs. 
     *                      If usesProperties() returns 'false' then message
     *                      properties are not needed to evaluate the expression
     *                      and this parameter may be null.
     * @param   fields      HashMap containg JMS Message fields. These
     *                      should be String/Object pairs.
     *                      If usesFields() returns 'false' then JMS fields
     *                      are not needed to evaluate the expression
     *                      and this parameter may be null.
     *
     * @return  true if expression evaluates to true, else false.
     *
     * @throws  SelectorFormatException if the selector syntax is invalid
     */
    public synchronized boolean match(Map properties, Map fields)
        throws SelectorFormatException {

        /*
         * This method is synchronized primarily because of the runtime
         * stack. If the stack was local then we wouldn't need to synchronize,
         * but we'd be creating a new stack on every match() call.
         */

        /*
         * You evaluate an RPN using a stack. It goes like this:
         * A. Scan RPN expression from left to right
         * B. When you encounter an operand push it onto the stack.
         * C. When you encounter an operator pop off as many operands
         *    as you need (in our case 1, 2 or 3), and apply operator
         *    to the operands. 
         * D. Push result onto the stack
         * E. When scan is complete the final result is on top of the stack
         *
         * The following is performed when supporting evaluation short circuit:
         *
         * If an AND_MARKER is encountered during scanning:
         *   If the top of the evaluation stack is FALSE then scan the
         *     RPN expression until the AND operator is encountered. Skip the
         *     AND operator. If during this scan additional AND_MARKERS are
         *     encountered then continue scanning expression until you have
         *     skipped as man AND operators as AND_MARKERS encountered.
         *     Then continue evaluation.
         *   Else skip the AND_MARKER and continue evaluation.
         *
         * If an OR_MARKER is encountered during scanning:
         *   If the top of the evaluation stack is TRUE then scan the
         *     RPN expression until the OR operator is encountered. Skip the
         *     OR operator. If during this scan additional OR_MARKERS are
         *     encountered then continue scanning expression until you have
         *     skipped as man OR operators as OR_MARKERS encountered.
         *     Then continue evaluation.
         *   Else skip the OR_MARKER and continue evaluation.
         */

        stack.clear();
        SelectorToken   token, operand1, operand2, operand3;

        int markers = 0;

        try {

        for (int i = 0; i < compiledSelector.length; i++) {
            token = (SelectorToken)compiledSelector[i];

            if (token == null) {
                // RPN may be shorter than the original since we 
                // remove parens.
                break;
            }

            if (shortCircuit) {

            // Short circuit boolean expressions
            if (token.getToken() == Selector.AND_MARKER) {
                // We hit an AND_MARKER.
                int t = ((SelectorToken)stack.peek()).getToken();
                if (t == Selector.FALSE) {
                    // Top of eval stack is FALSE. Short circuit by scanning
                    // until we hit an AND operator. If we see other AND_MARKERS
                    // we must skip as many operators as markers.
                    markers = 1;
                    while (markers > 0) {
                        token = (SelectorToken)compiledSelector[++i];
                        if (token.getToken() == Selector.AND_MARKER) {
                            markers++;
                        } else if (token.getToken() == Selector.AND) {
                            markers--;
                        } else {
                        }
                    }
                    // Completed short circuit. Continue evaluation
                    continue;
                } else {
                    // Not a short circuit. Skip marker and continue
                    continue;
                }
            } else if (token.getToken() == Selector.OR_MARKER) {
                // We hit an OR_MARKER.
                int t = ((SelectorToken)stack.peek()).getToken();
                if (t == Selector.TRUE) {
                    // Top of eval stack is TRUE. Short circuit by scanning
                    // until we hit an OR operator. If we see other OR_MARKERS
                    // we must skip as many operators as markers.
                    markers = 1;
                    while (markers > 0) {
                        token = (SelectorToken)compiledSelector[++i];
                        if (token.getToken() == Selector.OR_MARKER) {
                            markers++;
                        } else if (token.getToken() == Selector.OR) {
                            markers--;
                        } else {
                        }
                    }
                    // Completed short circuit. Continue evaluation
                    continue;
                } else {
                    // Not a short circuit. Skip marker and continue
                    continue;
                }
            }

            } // if shortCircuit


            // Push operands onto stack
            if (!isOperator(token)) {
                if (token.getToken() == IDENTIFIER) {
                    // Expand identifier
                    Object value;
                    if (properties == null) {
                        value = null;
                    } else {
                        value = properties.get((String)token.getValue());
                    }
                    if (value == null) {
                        stack.push(SelectorToken.getInstance(UNKNOWN, null));
                    } else {
                        stack.push(propertyToToken(value));
                    }
                } else if (token.getToken() == JMS_FIELD) {
                    // Expand identifier
                    Object value;
                    if (fields == null) {
                        value = null;
                    } else {
                        value = fields.get((String)token.getValue());
                    }
                    if (value == null) {
                        stack.push(SelectorToken.getInstance(UNKNOWN, null));
                    } else {
                        stack.push(propertyToToken(value));
                    }
                } else {
                    // A literal operand
                    stack.push(token);
                }
                continue;
            }

            if (token.getToken() == COMMA) {
                // Comma's are no-ops. They were there to ensure conversion
                // from infix to RPN went correctly for BETWEEN ranges.
                continue;
            }

            // Handle operator. We know we'll need at least one operand
            // so get it now.
            operand1 = (SelectorToken)stack.pop();

            // Process operator
            switch (token.getToken()) {

                // For OR, AND, and NOT we have to handle UNKNOWN.
                // See Section 3.8.1.2 of the JMS 1.1 spec
                case OR:
                    operand2 = (SelectorToken)stack.pop();
                    if (operand1.getToken() == TRUE ||
                        operand2.getToken() == TRUE) {
                        stack.push(SelectorToken.getInstance(TRUE));
                    } else if (operand1.getToken() == FALSE &&
                               operand2.getToken() == FALSE) {
                        stack.push(SelectorToken.getInstance(FALSE));
                    } else {
                        stack.push(SelectorToken.getInstance(UNKNOWN));
                    }
                    break;
                case AND:
                    operand2 = (SelectorToken)stack.pop();
                    if (operand1.getToken() == TRUE &&
                        operand2.getToken() == TRUE) {
                        stack.push(SelectorToken.getInstance(TRUE));
                    } else  if (operand1.getToken() == FALSE ||
                                operand2.getToken() == FALSE) {
                        stack.push(SelectorToken.getInstance(FALSE));
                    } else {
                        stack.push(SelectorToken.getInstance(UNKNOWN));
                    }
                    break;
                case NOT:
                    if (operand1.getToken() == TRUE) {
                        stack.push(SelectorToken.getInstance(FALSE));
                    } else if (operand1.getToken() == FALSE) {
                        stack.push(SelectorToken.getInstance(TRUE));
                    } else {
                        stack.push(SelectorToken.getInstance(UNKNOWN));
                    }
                    break;
                case EQUALS:
                    operand2 = (SelectorToken)stack.pop();

                    if (isNumeric(operand1) || isNumeric(operand2)) {
                        stack.push(doNumericOperation(
                                    token, operand2, operand1));
                    } else if (operand1.equals(operand2)) {
                        stack.push(SelectorToken.getInstance(TRUE));
                    } else {
                        stack.push(SelectorToken.getInstance(FALSE));
                    }
                    break;
                case NOT_EQUALS:
                    operand2 = (SelectorToken)stack.pop();

                    if (isNumeric(operand1) || isNumeric(operand2)) {
                        stack.push(doNumericOperation(
                                    token, operand2, operand1));
                    } else if (operand1.equals(operand2)) {
                        stack.push(SelectorToken.getInstance(FALSE));
                    } else {
                        stack.push(SelectorToken.getInstance(TRUE));
                    }
                    break;

                case LT:
                case LTE:
                case GT:
                case GTE:
                    operand2 = (SelectorToken)stack.pop();

                    // operand2 is first. It is actually the first
                    // operation. They are reversed on the stack
                    stack.push(doNumericOperation(
                                    token, operand2, operand1));
                    break;

                case PLUS:
                case MINUS:
                case MULTIPLY:
                case DIVIDE:
                    operand2 = (SelectorToken)stack.pop();
                    stack.push(doNumericOperation(
                                    token, operand2, operand1));
                    break;

                case UNARY_MINUS:
                    stack.push(doNumericOperation(token, operand1, null));
                    break;

                case UNARY_PLUS:
                    stack.push(doNumericOperation(token, operand1, null));
                    break;

                case BETWEEN:
                case NOT_BETWEEN:
                    // Operand 1 is the second range value
                    SelectorToken max = operand1;

                    // Operand 2 is the first range value 
                    SelectorToken min = (SelectorToken)stack.pop();

                    // Operand 3 is the operand on the left side of BETWEEN
                    SelectorToken operand = (SelectorToken)stack.pop();

                    boolean between = false;

                    // The operands may be floats or longs. We use
                    // doNumericOperation to handle this for us
                    if (doNumericOperation(SelectorToken.getInstance(GTE),
                            operand, min).getToken() == TRUE &&
                        doNumericOperation(SelectorToken.getInstance(LTE),
                            operand, max).getToken() == TRUE) {
                        between = true;
                    }

                    if (token.getToken() == BETWEEN) {
                        if (between) {
                            stack.push(SelectorToken.getInstance(TRUE));
                        } else {
                            stack.push(SelectorToken.getInstance(FALSE));
                        }
                    } else {
                        if (between) {
                            stack.push(SelectorToken.getInstance(FALSE));
                        } else {
                            stack.push(SelectorToken.getInstance(TRUE));
                        }
                    }
                    break;

                case IN:
                case NOT_IN:

                    // operand2 is the identifier
                    operand2 = (SelectorToken)stack.pop();

                    if (! (operand2.getValue() instanceof String)) {
                        throw new SelectorFormatException(
                            "IN requires string operand: " +
                                operand2.getValue(), selector);
                    }

                    // operand1 is the string list 
                    HashSet set = (HashSet)operand1.getValue();

                    if (operand2.getToken() == UNKNOWN) {
                        // If operand is unknow, result is unknown.
                        stack.push(SelectorToken.getInstance(FALSE));
                    } else if (set.contains((String)operand2.getValue())) {
                        if (token.getToken() == IN) {
                            stack.push(SelectorToken.getInstance(TRUE));
                        } else {
                            stack.push(SelectorToken.getInstance(FALSE));
                        }
                    } else {
                        if (token.getToken() == IN) {
                            stack.push(SelectorToken.getInstance(FALSE));
                        } else {
                            stack.push(SelectorToken.getInstance(TRUE));
                        }
                    }
                    break;

                case LIKE:
                case NOT_LIKE:
                    // operand2 is the identifier
                    operand2 = (SelectorToken)stack.pop();

                    if (! (operand2.getValue() instanceof String)) {
                        throw new SelectorFormatException(
                            "LIKE requires string operand: " +
                                operand2.getValue(), selector);
                    }

                    // operand1 is the RE
                    RegularExpression re =
                            (RegularExpression)operand1.getValue();

                    if (operand2.getToken() == UNKNOWN) {
                        // If operand is unknow, result is unknown.
                        stack.push(SelectorToken.getInstance(FALSE));
                    } else if (re.match((String)operand2.getValue())) {
                        if (token.getToken() == LIKE) {
                            stack.push(SelectorToken.getInstance(TRUE));
                        } else {
                            stack.push(SelectorToken.getInstance(FALSE));
                        }
                    } else {
                        if (token.getToken() == LIKE) {
                            stack.push(SelectorToken.getInstance(FALSE));
                        } else {
                            stack.push(SelectorToken.getInstance(TRUE));
                        }
                    }
                    break;

                case IS_NULL:
                    if (operand1.getToken() == UNKNOWN) {
                        stack.push(SelectorToken.getInstance(TRUE));
                    } else {
                        stack.push(SelectorToken.getInstance(FALSE));
                    }
                    break;
                case IS_NOT_NULL:
                    if (operand1.getToken() != UNKNOWN) {
                        stack.push(SelectorToken.getInstance(TRUE));
                    } else {
                        stack.push(SelectorToken.getInstance(FALSE));
                    }
                    break;
                default:
                        throw new SelectorFormatException(
                            "Unknown operator: " + token, selector);
            }
        }


        // All done!
        // The top of the stack better hold a boolean!
        token = (SelectorToken)stack.pop();

        } catch (java.util.EmptyStackException e) {
            throw new SelectorFormatException("Missing operand", selector);
        } catch (java.lang.ArithmeticException e) {
            throw new SelectorFormatException(e.toString(), selector);
        }

        if (!stack.empty()) {
           throw new SelectorFormatException(
              "Missing operator", selector);
        } else if (token.getToken() == TRUE) {
            return true;
        } else if (token.getToken() == FALSE) {
            return false;
        } else if (token.getToken() == UNKNOWN) {
            return false;
        } else {
           throw new SelectorFormatException(
              "Non-boolean expression", selector);
        }
    }

    private SelectorToken propertyToToken(Object value) {
        if (value instanceof String) {
            return SelectorToken.getInstance(STRING, value);
        } else if (value instanceof Boolean) {
            boolean b = ((Boolean)value).booleanValue();
            if (b) {
                return SelectorToken.getInstance(TRUE);
            } else {
                return SelectorToken.getInstance(FALSE);
            }
        } else if (value instanceof Double) {
            return SelectorToken.getInstance(DOUBLE, value);
        } else if (value instanceof Float) {
            double d = ((Float)value).floatValue();
            return SelectorToken.getInstance(DOUBLE, new Double(d));
        } else if (value instanceof Long) {
            return SelectorToken.getInstance(LONG, value);
        } else if (value instanceof Integer) {
            long l = ((Integer)value).intValue();
            return SelectorToken.getInstance(LONG, new Long(l));
        } else if (value instanceof Short) {
            long l = ((Short)value).shortValue();
            return SelectorToken.getInstance(LONG, new Long(l));
        } else if (value instanceof Byte) {
            long l = ((Byte)value).byteValue();
            return SelectorToken.getInstance(LONG, new Long(l));
        }

        return null;
    }

    private SelectorToken convertStringToNumber(String s)
        throws SelectorFormatException {

        try {
            Long l = Long.valueOf(s);
            return SelectorToken.getInstance(LONG, l);
        } catch (NumberFormatException e) {
            try {
                // Hmmm...maybe it's a double
                Double d = Double.valueOf(s);
                return SelectorToken.getInstance(DOUBLE, d);
            } catch (NumberFormatException e2) {
                throw new SelectorFormatException(
                      "Cannot convert string to number '" + s + "'", selector);
            }
        }
    }

    /**
     * Perform a numeric operation. God this is lame. There must
     * be a better way, but it's late and I'm not thinking well.
     *
     * The operands are either Long or Double.
     */
    private SelectorToken doNumericOperation(
         SelectorToken t, SelectorToken op1, SelectorToken op2)
         throws SelectorFormatException {

         boolean b = false;
         boolean is1L = false;
         boolean is2L = false;
         long    val1L = 0, val2L = 0;
         double  val1D = 0, val2D = 0;


         if ((!isNumeric(op1) && op1.getToken() != UNKNOWN)) {
            if (convertTypes && op1.getToken() == STRING) {
                op1 = convertStringToNumber((String)op1.getValue());
            } else {
                throw new SelectorFormatException(
                            "Non-numeric argument '" + op1.getValue() +
                                "'", selector);
            }
         }

         if (op2 != null && (!isNumeric(op2) && op2.getToken() != UNKNOWN)) {
            if (convertTypes && op2.getToken() == STRING) {
                op2 = convertStringToNumber((String)op2.getValue());
            } else {
                throw new SelectorFormatException(
                            "Non-numeric argument '" + op2.getValue() +
                                "'", selector);
            }
         }
           

        if (op1.getToken() == UNKNOWN ||
            (op2 != null && op2.getToken() == UNKNOWN)) {
            // Operation with a UNKNOWN argument is always UNKNOWN
            return SelectorToken.getInstance(UNKNOWN);
        }


        if (op1.getValue() instanceof Long) {
            is1L = true;
            val1L = ((Long)op1.getValue()).longValue();
            val1D = ((Long)op1.getValue()).doubleValue();
        } else {
            is1L = false;
            val1L = ((Double)op1.getValue()).longValue();
            val1D = ((Double)op1.getValue()).doubleValue();
        }

        if (op2 != null) {
            if (op2.getValue() instanceof Long) {
                is2L = true;
                val2L = ((Long)op2.getValue()).longValue();
                val2D = ((Long)op2.getValue()).doubleValue();
            } else {
                is2L = false;
                val2L = ((Double)op2.getValue()).longValue();
                val2D = ((Double)op2.getValue()).doubleValue();
            }
        }

        switch (t.getToken()) {
        case EQUALS:
        case NOT_EQUALS:
            if (is1L && is2L) {
                b = val1L == val2L;
            } else if (is1L) {
                b = val1L == val2D;
            } else if (is2L) {
                b = val1D == val2L;
            } else {
                b = val1D == val2D;
            }
            if (t.getToken() == EQUALS) {
              return SelectorToken.getInstance(b ? TRUE: FALSE);
            } else {
              return SelectorToken.getInstance(b ? FALSE: TRUE);
            }

        case LT:
            if (is1L && is2L) {
                b = val1L < val2L;
            } else if (is1L) {
                b = val1L < val2D;
            } else if (is2L) {
                b = val1D < val2L;
            } else {
                b = val1D < val2D;
            }
            return SelectorToken.getInstance(b ? TRUE: FALSE);
        case LTE:
            if (is1L && is2L) {
                b = val1L <= val2L;
            } else if (is1L) {
                b = val1L <= val2D;
            } else if (is2L) {
                b = val1D <= val2L;
            } else {
                b = val1D <= val2D;
            }
            return SelectorToken.getInstance(b ? TRUE: FALSE);
        case GT:
            if (is1L && is2L) {
                b = val1L > val2L;
            } else if (is1L) {
                b = val1L > val2D;
            } else if (is2L) {
                b = val1D > val2L;
            } else {
                b = val1D > val2D;
            }
            return SelectorToken.getInstance(b ? TRUE: FALSE);
        case GTE:
            if (is1L && is2L) {
                b = val1L >= val2L;
            } else if (is1L) {
                b = val1L >= val2D;
            } else if (is2L) {
                b = val1D >= val2L;
            } else {
                b = val1D >= val2D;
            }
            return SelectorToken.getInstance(b ? TRUE: FALSE);
        case PLUS:
            if (is1L && is2L) {
                long v = val1L + val2L;
                return SelectorToken.getInstance(LONG, new Long(v));
            } else {
                double d = val1D + val2D;
                return SelectorToken.getInstance(DOUBLE, new Double(d));
            }
        case UNARY_PLUS:
            // Unary plus is a no-op
            return(op1);

        case MINUS:
            if (is1L && is2L) {
                long v = val1L - val2L;
                return SelectorToken.getInstance(LONG, new Long(v));
            } else {
                double d = val1D - val2D;
                return SelectorToken.getInstance(DOUBLE, new Double(d));
            }
        case UNARY_MINUS:
            if (is1L) {
                long v = - val1L;
                return SelectorToken.getInstance(LONG, new Long(v));
            } else {
                double d = - val1D;
                return SelectorToken.getInstance(DOUBLE, new Double(d));
            }
        case MULTIPLY:
            if (is1L && is2L) {
                long v = val1L * val2L;
                return SelectorToken.getInstance(LONG, new Long(v));
            } else {
                double d = val1D * val2D;
                return SelectorToken.getInstance(DOUBLE, new Double(d));
            }
        case DIVIDE:
            if (is1L && is2L) {
                long v = val1L / val2L;
                return SelectorToken.getInstance(LONG, new Long(v));
            } else {
                double d = val1D / val2D;
                return SelectorToken.getInstance(DOUBLE, new Double(d));
            }
        default:
            throw new SelectorFormatException(
                "Unknown numeric operation: " + t, selector);
        }

    }

    private static boolean isNumeric(SelectorToken t) {
        int tok = t.getToken();
        return ((tok == DOUBLE) || (tok == LONG));
    }

    private static boolean isOperator(SelectorToken t) {
        return (t.getToken() < 100);
    }

    private static boolean isOperator(int t) {
        return (t < 100);
    }

    // Return a number representing the precedene of an operator:
    // 
    private static int getPrecedence(SelectorToken t) {

        switch (t.getToken()) {


        case OR:
            return 10;

        case AND:
            return 11;

        case NOT:
            return 12;

        case EQUALS:
        case NOT_EQUALS:
            return 20;

        case LT:
        case LTE:
        case GT:
        case GTE:
            return 21;

        case IN:
        case NOT_IN:
        case LIKE:
        case NOT_LIKE:
        case IS_NULL:
        case IS_NOT_NULL:
        case BETWEEN:
        case NOT_BETWEEN:
            return 30;

        case PLUS:
        case MINUS:
            return 40;

        case MULTIPLY:
        case DIVIDE:
            return 41;

        case COMMA: 
            return 42;

        case UNARY_PLUS:
        case UNARY_MINUS:
            return 43;


        case LEFT_PAREN:
        case RIGHT_PAREN:
            return 50;


        default:
            return 1;
        }
    }

    /**
     * Two Selector instances are equal if they are the same instance
     * or if the selector strings match.
     */
    public boolean equals(Object o) {

        // Since we cache Selectors it should be the case that Selectors
        // with the same selector string will be the same instance.
        if (this == o) return true;

        if (!(o instanceof Selector)) {
            return false;
        }

        Selector obj = (Selector)o;
        return (this.selector.equals(obj.selector));
    }

    public int hashCode() {
        /* Return the hashCode for the Selector string */
        return selector.hashCode();
    }

    public String toString() {
        return selector;
    }

    /**
     * Check if the Selector uses properties.
     *
     * @return  true if the selector expression uses properties
     *          false if the selector does not use properties (ie
     *          it only uses JMS header fields).
     */
    public boolean usesProperties() {
        return usesProperties;
    }

    /**
     * Check if the Selector uses JMS Header Fiedls.
     *
     * @return  true if the selector expression uses JMS fields.
     *          false if the selector does not use JMS fields (ie
     *          it only uses JMS properties).
     */
    public boolean usesFields() {
        return usesFields;
    }

    private static void dumpTokens(LinkedList l) {

        Iterator iter = l.iterator();

        while (iter.hasNext()) {
            SelectorToken token = (SelectorToken)iter.next();
            System.out.print(token.toString());
        }
        System.out.println();
    }

    public String toDebugString() {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < compiledSelector.length; i++) {
            if (compiledSelector[i] != null) {
                sb.append(compiledSelector[i].toString());
            }
        }
        return sb.toString() + " cachesize=" + selectorCache.size() ;
    }


/**
 * Main for testing Selector class.
 *
 * usage: java Selector [-d] [-D] [-l] [selector string]
 * -d   Turn on debug
 * -D   Turn on verbose debug
 * -l   Loop and generate simple performance info. Only valid if a
 *      [selector string] is provide.
 * [selector string] evaluate specified string. If no string is provided
 *                   then run a simple unit test.
 */
public static void main(String args[]) {

    HashMap props = new HashMap();
    HashMap fields = new HashMap();
    boolean convert = false;
    int loop = 0;


    /* Dummy up some message properties and fields */
    props.put("color", "red");
    props.put("description", "Galvanized hot dipped wing nuts");
    props.put("size", new Integer(1024));
    props.put("msgnum", new Integer(5));
    props.put("msgnumStr", "5");
    props.put("price", new Float(1.50));
    props.put("quantity", new Long(500));
    props.put("minlong", new Long(Long.MIN_VALUE));
    props.put("maxlong", new Long(Long.MAX_VALUE));

    props.put("trueProp", new Boolean(true));
    props.put("falseProp", new Boolean(false));
    props.put("byteProp", new Byte((byte)4));
    props.put("shortProp", new Short((short)4));
    props.put("intProp", new Integer(4));
    props.put("negIntProp", new Integer(-4));
    props.put("floatProp", new Float(4.0));
    props.put("stringProp", "4");
    props.put("nullProp", null);

    /* Throw in some more complex ones */
    props.put("Event", "*Service Change*Restart*");
    props.put("Region", "*EA*SO*WE*BC*");
    props.put("Airspace", "*ASSS*ARCC*BVNF*");
    props.put("LIDFAC", "*ZDC/ARTCC*EKN/RCAG*");
    props.put("SVCPDC", "*ECOM/CA*YTR/RCG*");
    props.put("CLASS", "*1*2*3*4*5*");
    props.put("USI", "*USISAMPLEUSI000*");
    
    props.put("JMSXUserID", "testUser");
    fields.put("JMSDeliveryMode", "PERSISTENT");
    fields.put("JMSPriority", new Integer(7));
    fields.put("JMSTimestamp", new Long(System.currentTimeMillis()));
    fields.put("JMSCorrelationID", "123456789");
    fields.put("JMSType", "order");
    fields.put("JMSMessageID", "messageid_" + System.currentTimeMillis());

    System.out.println("\nProperties=" + props + "\n");
    System.out.println("\nFields=" + fields + "\n");

    for (int i = 0; i < args.length; i++) {
        if (args[i].equals("-d")) {
            DEBUG = true;
            continue;
        }

        if (args[i].equals("-D")) {
            DEBUG = true;
            VERBOSE_DEBUG = true;
            continue;
        }

        if (args[i].equals("-l")) {
            loop = 500000;
            continue;
        }

        if (args[i].equals("-n")) {
            Selector.setShortCircuit(false);
            continue;
        }

        if (args[i].equals("-c")) {
            convert = true;
            Selector.setConvertTypes(convert);
            continue;
        }

        System.out.println("\nshortCircuit=" + shortCircuit );
        Selector selector = null;
        try {
            selector = Selector.compile(args[i]);
        } catch (SelectorFormatException e) {
            System.out.println("Compile Error:\n" + e);
            System.exit(1);
        }

        try {
            System.out.println(selector.match(props, fields));
        } catch (SelectorFormatException e) {
            System.out.println("Runtime Error:\n" + e);
            System.exit(1);
        }

        if (loop > 0) {
            long start = System.currentTimeMillis();
            for (int n = 0; n < loop; n++) {
                try {
                    selector.match(props, fields);
                } catch (SelectorFormatException e) {
                    System.out.println("Runtime Error:\n" + e);
                    System.exit(1);
                }
            }
            long stop = System.currentTimeMillis();
            System.out.println("Evaluated " + loop + " matches in " +
                ((stop - start) / 1000.0) + " secs");
            System.out.println((loop / ((stop - start)/1000.0)) +
                " matches/sec");
        }
        System.exit(0);
    }

    // Simple selector tests
    String[][] tests = {
        /* Selector                                         Result */
        {"color = 'red'",                                   "true"},
        {"color = 'blue'",                                  "false"},
        {"color <> 'red'",                                  "false"},
        {"color <> 'blue'",                                 "true"},
        {"color in ('red', 'white', 'blue')",               "true"},
        {"color in ('orange', 'white', 'blue')",            "false"},
        {"color not in ('orange', 'white', 'blue')",        "true"},
        {"description like '%hot%'",                        "true"},
        {"description not like '%hot%'",                    "false"},
        {"color like 'r_d'",                                "true"},
        {"color like 'r_d' or color like 'bl_e'", 	    "true"},
        {"color like 'r_d' and (color like 'b%' or color like '%d')", "true"},
        {"quantity between 400 and 1000.0",                 "true"},
        {"price between 1.10 and 2",                        "true"},
        {"price not between 5 and 10e2",                    "true"},
        {"price not between 5 and 10e2 and price between 1 and 2",    "true"},
        {"price not between 5 and 10e2 or price between 1 and 2",     "true"},
        {"nullProp is null and price is not null",          "true"},
        {"nullProp is null or  price is not null",          "true"},
        {"price is not null",                               "true"},
        {"price > 0.75",                                    "true"},
        {"price < 9.75",                                    "true"},
        {"price >= 1.50",                                   "true"},
        {"price <= 1.50",                                   "true"},
        {"price > 9.75",                                    "false"},
        {"price >= 9.75",                                   "false"},
        {"msgnum > 1.75",                                   "true"},
        {"size > msgnum",                                   "true"},
        {"size > price",                                    "true"},
        {"size > price + msgnum",                           "true"},
        {"size > price * msgnum",                           "true"},
        {"quantity * price > 3.00",                         "true"},
        {"JMSXUserID = 'testUser'",                         "true"},
        {"JMSMessageID like '%~_%' escape '~'",             "true"},
        {"JMSTimestamp > 4",                                "true"},
        {"JMSCorrelationID like '1_34__%9'",                "true"},
        {"JMSType <> 'quote'",                              "true"},
        {"JMSPriority > 5",                                 "true"},
        {"JMSPriority < JMSTimestamp",                      "true"},
        {"byteProp = 4",                                    "true"},
        {"byteProp <> 5.0",                                 "true"},
        {"shortProp <> 5.0",                                "true"},
        {"intProp <> 5.0",                                  "true"},
        {"byteProp = shortProp",                            "true"},
        {"byteProp = floatProp",                            "true"},
        {"floatProp = 4.0 ",                                "true"},
        {"floatProp * 2 > byteProp",                        "true"},
        {"stringProp = '4'",                                "true"},
        {"stringProp =  4" ,                                "error"},
        {"stringProp <> '5'",                               "true"},
        {"stringProp <>  5",                                "error"},
        {"byteProp <> 4",                                   "false"},
        {"byteProp = 5.0",                                  "false"},
        {"shortProp = 5.0",                                 "false"},
        {"intProp = 5.0",                                   "false"},

        {"1 + 4 * 5 = 21",                                  "true"},
        {"1+4*5=21",                                        "true"},
        {"1 + -4 * 5 = -19",                                "true"},
        {"(1 + 4) * +5 = 25",                               "true"},
        {"(1 + 4) * -5 = -25",                              "true"},
        {"(1 + 4) * 5 = (3 + 2) * 5",                       "true"},
        {"1 + (4 * 5) = 21",                                "true"},
        {"1 +  4 * 5  = 21",                                "true"},
        {"(1 +  4) * 5 = 25",                               "true"},
        {"(1 +  4) - 5 = 0 ",                               "true"},
        {"2.0 * 4E2 + 5 = 805.0",                           "true"},
        {"2.0 * 4E2 + 5 = 805",                             "true"},
        {"2.0 = 2.0",                                       "true"},
        {"1.0+2.0*3.0-4.0/4.0 = 6",                         "true"},

        {"price > 0.75 OR color <> 'blue'",                 "true"},
        {"(price > 0.75 OR color <> 'blue') AND color <> 'green'",    "true"},
        {"     2 * quantity between msgnum AND msgnum * size",        "true"},
        {"NOT (2 * quantity between msgnum AND msgnum * size)",       "false"},

        {"-price < 0 AND +negIntProp < 0",                  "true"},
        {"negIntProp+4 = 0 AND intProp-1 = 3",              "true"},
        {"- intProp + intProp = 0",                         "true"},
        {"intProp between -1 and 5",                        "true"},
        {"intProp between 1 and 5 AND intProp between -1 and 5",      "true"},

        {"minlong=" + Long.MIN_VALUE + " AND maxlong=" + Long.MAX_VALUE,
                                                            "true"},

        // An expression with a NULL property is always NULL
        {"unknownProp NOT IN ('foo','jms','test')",         "false"},
        {"nullProp NOT IN ('foo','jms','test')",            "false"},
        {"unknownProp NOT LIKE '1_3'",                      "false"},
        {"nullProp    NOT LIKE '1_3'",                      "false"},

        {"0x1d = 29",                                       "true"},
        {"0x1D = 29",                                       "true"},
        {"035 = 29",                                        "true"},
        {"29L = 29",                                        "true"},
        {"29l = 29",                                        "true"},
        {"18. = 1.8e1",                                     "true"},
        {"18. = .18e2",                                     "true"},
        {"18.0f = .18e2",                                   "true"},
        {"18.0F = .18e2",                                   "true"},
        {"18.0d = .18e2",                                   "true"},
        {"18.0D = .18e2",                                   "true"},
        {".7e4 = 7000.0",                                   "true"},
        {" is null nullProp",                               "true"},
        {"NOT is null nullProp",                            "false"},
        {" is null unknownProp",                            "true"},
        {" is not null nullProp",                           "false"},
        {"NOT is not null nullProp",                        "true"},
        {" is not null unknownProp",                        "false"},

        {"TRUE",                                            "true"},
        {"NOT TRUE",                                        "false"},
        {"(NOT (NOT (NOT (NOT TRUE))))",                    "true"},
        {"FALSE",                                           "false"},
        {"NOT FALSE",                                       "true"},
        {"trueProp",                                        "true"},
        {"NOT trueProp",                                    "false"},
        {"trueProp = TRUE",                                 "true"},
        {"trueProp = FALSE",                                "false"},
        {"trueProp <> FALSE",                               "true"},
        {"falseProp",                                       "false"},
        {"falseProp = TRUE",                                "false"},
        {"falseProp = FALSE",                               "true"},
        {"falseProp <> TRUE",                               "true"},
        {"NOT falseProp",                                   "true"},

        {"description LIKE '%nuts%' AND color in ('black', 'white') OR color = 'blue'", "false"},
        {"description LIKE '%nuts%' OR color in ('black', 'white') OR color = 'blue'", "true"},

        {"Event LIKE '%*Service Change*%' OR Event LIKE '%*Restart*%' AND Region LIKE '%*EA*%' AND Airspace LIKE '%*ARCC*%'",               "true"},

        // Make sure we don't have any blatant errors in short circuit code
        {"color = 'red' OR color <> 'blue' AND color <> 'green'",   "true"},
        {"color = 'white' OR color <> 'blue' AND color <> 'green'", "true"},
        {"color = 'white' OR color <> 'red' AND color <> 'green'",  "false"},
        {"color = 'red' OR color <> 'blue' AND color <> 'red'",     "true"},
        {"(color = 'red' OR color <> 'blue') AND color <> 'red'",   "false"},
        {"(color = 'red' OR color <> 'blue') AND NOT color <> 'red'",  "true"},

        {"true OR true OR true OR true",                    "true"},
        {"(true OR true) OR (true OR true)",                "true"},
        {"true OR false OR true OR false",                  "true"},
        {"false OR false OR false OR true",                 "true"},
        {"true OR false OR false OR false",                 "true"},
        {"false OR false OR false OR false",                "false"},
        {"false OR false OR false OR true",                 "true"},

        {"true AND true AND true AND true",                 "true"},
        {"(true AND false) AND (true AND false)",           "false"},
        {"false AND false AND false AND true",              "false"},
        {"true AND false AND false AND false",              "false"},
        {"false AND true AND true AND true",                "false"},
        {"true AND true AND true AND false",                "false"},

        {"true AND false OR true AND true",                 "true"},
        {"true OR false AND true OR false",                 "true"},
        {"(true OR false) AND (true OR false)",             "true"},
        {"NOT ((true OR false) AND (true OR false))",       "false"},

        {"color in ('red', 'white', 'blue'(",               "error"},
        {"size not between 'red'  and 'green'",             "error"},
        {"+ + + +",                                         "error"},
        {"1 2 3 4",                                         "error"},
        {"= = = =",                                         "error"},
        {"((1 + 2) * 4 = 3",                                "error"},
        {"red red red",                                     "error"},
        {"4 >> 1",                                          "error"},
        {"color = 'red",                                    "error"},
        {"color == 'red'",                                  "error"},

        {"intProp BETWEEN 'foo' and 'test'",                "error"},
        {"intProp > 'foo'",                                 "error"},
        {"color    > 'foo'",                                "error"},
        {"unknownProp > 'foo'",                             "error"},
        {"unknownProp < 'foo'",                             "error"},
        {"unknownProp =< 'foo'",                            "error"},
        {"unknownProp >= 'foo'",                            "error"},
        {"intProp >= 'foo'",                                "error"},
        {"intProp < 'foo'",                                 "error"},
        {"intProp <= 'foo'",                                "error"},
        {"intProp between 'foo' and 'bar'",                 "error"},
        {"color    between 1 and 7",                        "error"},
        {"'color'    between 1 and 7",                      "error"},
        {"7 in ('red', 'blue')",                            "error"},
        {"intProp in ('red', 'blue')",                      "error"},
        {"7 not in ('red', 'blue')",                        "error"},
        {"intProp not in ('red', 'blue')",                  "error"},
        {"NULL = 0",                                        "error"},
        {"=color 'red'",                                    "error"},
        {"size like '7'",                                   "error"},
        {"size not like '7'",                               "error"},
        {"4 = 'red'",                                       "error"},
        {"4 <> 'red'",                                      "error"},
        {"'red' <> 4",                                      "error"},
        {"'red' =  4",                                      "error"},
        {"intProp = 'red'",                                 "error"},
        {"intProp <> 'red'",                                "error"},
        {"'red' = intProp",                                 "error"},
        {"'red' <> intProp",                                "error"},
        {"msgnumStr = 5",                                   "error"},
        {"msgnum    = '5'",                                 "error"},
        {"300 + 150 / 0 = 300",                             "error"},
    };


    int failCnt = 0;

    for (int n = 0; n < tests.length; n++) {

        Selector selector = null;
        String expected = tests[n][1];
        String actual = null;
        String result;
        HashMap _props = null;
        HashMap _fields = null;

        try {
            selector = Selector.compile(tests[n][0]);
            // Test optimization
            if (selector.usesProperties()) {
                _props = props;
            } else {
                _props = null;
            }
            if (selector.usesFields()) {
                _fields = fields;
            } else {
                _fields = null;
            }
            if (selector.match(_props, _fields)) {
                actual = "true";
            } else {
                actual = "false";
            }
        } catch (SelectorFormatException e) {
            actual = "error";
            if (!actual.equals(expected)) {
                System.out.println(e);
            }
        }

        if (actual.equals(expected)) {
            result = "      PASS";
        } else {
            result = "***** FAIL";
            failCnt++;
        }

        System.out.println(result + " " + tests[n][0] +
            " : expected=" + expected + " actual=" + actual);
    }

    System.out.println (tests.length + " tests: " + (tests.length - failCnt) +
        " passed " + failCnt + " failed ");

    if (failCnt > 0) {
        System.exit(1);
    } else {
        System.exit(0);
    }

}

}
