org.getobjects.eocontrol.EOQualifier.java Source code

Java tutorial

Introduction

Here is the source code for org.getobjects.eocontrol.EOQualifier.java

Source

/*
  Copyright (C) 2006-2008 Helge Hess
    
  This file is part of Go.
    
  Go is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.
    
  Go is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.
    
  You should have received a copy of the GNU Lesser General Public
  License along with Go; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

package org.getobjects.eocontrol;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.getobjects.foundation.NSKeyValueHolder;
import org.getobjects.foundation.UObject;

/**
 * EOQualifier
 * <p>
 * Subclasses represent 'query' expressions like those found in SQL
 * WHERE statements.
 * <p>
 * Commonly used subclasses:
 * <ul>
 *   <li>EOAndQualifier / EOOrQualifier
 *   <li>EOKeyValueQualifier
 *   <li>EOKeyComparisonQualifier
 * </ul>
 * 
 * <p>
 * Qualifiers are commonly rewritten as SQL qualifiers for evaluation in
 * database servers. But they can also be applied in memory on Collection
 * objects. To do that, qualifiers support the EOQualifierEvaluation
 * interface.
 */
public class EOQualifier extends EOExpression {
    @SuppressWarnings("hiding")
    protected static final Log log = LogFactory.getLog("EOQualifier");

    /* parsing */

    /**
     * Parses the EOQualifier given in _fmt. If the format contains % patterns
     * like %@, %i, the values are filled in from the varargs list.
     */
    public static EOQualifier qualifierWithQualifierFormat(final String _fmt, final Object... _args) {
        return parseV(_fmt, _args);
    }

    public static EOQualifier parse(final String _fmt, final Object... _args) {
        return parseV(_fmt, _args);
    }

    public static EOQualifier parseV(final String _fmt, final Object[] _args) {
        EOQualifierParser parser = new EOQualifierParser(_fmt.toCharArray(), _args);
        final EOQualifier q = parser.parseQualifier();
        // TODO: check error
        parser.reset();
        return q;
    }

    /* factory */

    /**
     * This method returns a set of EOKeyValueQualifiers combined with an
     * EOAndQualifier. The keys/values for the EOKeyValueQualifier are taken
     * from the Map.
     * <p>
     * Example:<pre>
     *   { lastname = 'Duck'; firstname = 'Donald'; city = 'Hausen' }</pre>
     * Results in:<pre>
     *   lastname = 'Duck' AND firstname = 'Donald' AND city = 'Hausen'</pre>
     *   
     * @return an EOQualifier for the given Map, or null if the Map was empty 
     */
    public static EOQualifier qualifierToMatchAllValues(final Map _values) {
        if (_values == null)
            return null;
        int size = _values.size();
        if (size == 0)
            return null;

        final EOQualifier[] qs = new EOQualifier[size];
        for (Object key : _values.keySet()) {
            size--;
            qs[size] = new EOKeyValueQualifier((String) key, _values.get(key));
        }
        if (size == 1)
            return qs[0];
        return new EOAndQualifier(qs);
    }

    /**
     * This method returns a set of EOKeyValueQualifiers combined with an
     * EOOrQualifier. The keys/values for the EOKeyValueQualifier are taken
     * from the Map.
     * <p>
     * Example:<pre>
     *   { lastname = 'Duck'; firstname = 'Duck'; city = 'Duck' }</pre>
     * Results in:<pre>
     *   lastname = 'Duck' OR firstname = 'Duck' OR city = 'Duck'</pre>
     *   
     * @return an EOQualifier for the given Map, or null if the Map was empty 
     */
    public static EOQualifier qualifierToMatchAnyValue(final Map _values) {
        if (_values == null)
            return null;
        int size = _values.size();
        if (size == 0)
            return null;

        final EOQualifier[] qs = new EOQualifier[size];
        for (Object key : _values.keySet()) {
            size--;
            qs[size] = new EOKeyValueQualifier((String) key, _values.get(key));
        }
        if (size == 1)
            return qs[0];
        return new EOOrQualifier(qs);
    }

    /**
     * This method returns an EOQualifier which matches a single 'key' against
     * a set of values.
     * <p>
     * Example:<pre>
     *   qualifierToMatchAnyValue('status', [ 1, 2, 3 ]);</pre>
     * Results in:<pre>
     *   status = 1 OR status = 2 OR status = 3</pre>
     * <p>
     * Note: the database adaptor might optimize this into a single IN qualifier.
     * 
     * @param _key - name of property/column (eg 'city')
     * @param _vs  - values to check against
     * @return an EOQualifier to match the values
     */
    public static EOQualifier qualifierToMatchAnyValue(String _key, Object[] _vs) {
        if (_vs == null || _vs.length == 0)
            return null;

        if (_vs.length == 1)
            return new EOKeyValueQualifier(_key, _vs[0]);

        final EOQualifier[] qs = new EOQualifier[_vs.length];
        for (int i = 0; i < _vs.length; i++)
            qs[i] = new EOKeyValueQualifier(_key, _vs[i]);
        return new EOOrQualifier(qs);
    }

    /**
     * Returns a qualifier which has its bindings resolved against the given
     * '_vals' object. The object is usually a Map or NSKeyValueHolder, but can be
     * any other object accessible using KVC.
     * <p>
     * Note that qualifiers w/o bindings just return self.
     * <p>
     * The syntax for bindings in string qualifiers is $binding (e.g.
     * lastname = $lastname). At runtime EOQualifierVariable objects represent
     * such bindings.
     * 
     * @param _vals        - the object containing the bindings
     * @param _requiresAll - whether all bindings are required
     * @return an EOQualifier with the bindings resolved
     */
    public EOQualifier qualifierWithBindings(Object _vals, boolean _requiresAll) {
        return this;
    }

    /**
     * Returns a qualifier which has its bindings resolved against the given
     * key/value pairs.
     * <p>
     * Note that qualifiers w/o bindings just return self.
     * <p>
     * Example:<pre>
     *   q = q.qualifierWithBindings("now", new Date());</pre>
     * 
     * @param _keysAndValues - the bindings as key/value pairs
     * @return an EOQualifier with the bindings resolved
     */
    public EOQualifier qualifierWithBindings(Object... _keysAndValues) {
        return this.qualifierWithBindings(new NSKeyValueHolder(_keysAndValues), true /* requires all */);
    }

    public EOExpression expressionWithBindings(final Object _vals, final boolean _requiresAll) {
        return this.qualifierWithBindings(_vals, _requiresAll);
    }

    /* utility */

    /**
     * Filters a collection by applying the qualifier on each item. Only items
     * matching the qualifier will be included in the resulting List.
     * 
     * @param _in - the Collection to be filtered
     * @return a List of objects matching the qualifier 
     */
    public List filterCollection(final Collection _in) {
        if (_in == null)
            return null;

        final EOQualifierEvaluation eval = (EOQualifierEvaluation) this;
        final ArrayList<Object> result = new ArrayList<Object>(_in.size());
        for (Object item : _in) {
            if (eval.evaluateWithObject(item))
                result.add(item);
        }

        result.trimToSize();
        return result;
    }

    /* project WOnder style helpers */

    /**
     * Disjoins (ORs) the given qualifier with the recipient. Example:<pre>
     *   q = EOQualifier.parse("lastname = 'Duck');
     *   q = q.or(EOQualifier.parse("firstname = 'Donald'");</pre>
     * 
     * @param _q - EOQualifier to disjoin
     * @return EOQualifier representing the disjoin, usually an EOOrQualifier
     */
    public EOQualifier or(final EOQualifier _q) {
        return _q != null ? EOOrQualifier.disjoin(this, _q) : this;
    }

    /**
     * Disjoins (ORs) the given qualifier with the recipient. Example:<pre>
     *   q = EOQualifier.parse("lastname = 'Duck');
     *   q = q.or("firstname = %@", "Donald");</pre>
     * 
     * @param _q    - EOQualifier format to disjoin
     * @param _args - optional varargs for patterns in used by the format
     * @return EOQualifier representing the disjoin, usually an EOOrQualifier
     */
    public EOQualifier or(final String _q, final Object... _args) {
        return _q != null ? this.or(EOQualifier.parseV(_q, _args)) : this;
    }

    /**
     * Conjoins (ANDs) the given qualifier with the recipient. Example:<pre>
     *   q = EOQualifier.parse("lastname = 'Duck');
     *   q = q.and(EOQualifier.parse("firstname = 'Donald'");</pre>
     * 
     * @param _q - EOQualifier to disjoin
     * @return EOQualifier representing the conjoin, usually an EOAndQualifier
     */
    public EOQualifier and(final EOQualifier _q) {
        return _q != null ? EOAndQualifier.conjoin(this, _q) : this;
    }

    /**
     * Conjoins (ANDs) the given qualifier with the recipient. Example:<pre>
     *   q = EOQualifier.parse("lastname = 'Duck');
     *   q = q.and("firstname = %@", "Donald");</pre>
     * 
     * @param _q    - EOQualifier format to conjoin
     * @param _args - optional varargs for patterns in used by the format
     * @return EOQualifier representing the conjoin, usually an EAndQualifier
     */
    public EOQualifier and(final String _q, final Object... _args) {
        return _q != null ? this.and(EOQualifier.parseV(_q, _args)) : this;
    }

    /**
     * Negates the qualifier. Example:<pre>
     *   ds.fetchByQualifier(isLockedQualifier.not())</pre>
     * 
     * @return the negated qualifier, usually an EONotQualifier
     */
    public EOQualifier not() {
        return new EONotQualifier(this);
    }

    /* comparison interface */

    public static interface Comparison {

        public boolean isEqualTo(Object _other);

        public boolean isNotEqualTo(Object _other);

        public boolean isGreaterThan(Object _other);

        public boolean isGreaterThanOrEqualTo(Object _other);

        public boolean isLessThan(Object _other);

        public boolean isLessThanOrEqualTo(Object _other);

        public boolean doesContain(Object _other);

        public boolean doesLike(Object _other);

        public boolean doesCaseInsensitiveLike(Object _other);
    }

    /* comparison operations */

    public enum ComparisonOperation {
        UNKNOWN, EQUAL_TO, NOT_EQUAL_TO, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, LIKE, CASE_INSENSITIVE_LIKE
    }

    public static ComparisonOperation operationForString(final String _s) {
        if (_s == null)
            return ComparisonOperation.UNKNOWN;

        final int len = _s.length();

        if (len == 1) {
            final char c0 = _s.charAt(0);
            if (c0 == '=')
                return ComparisonOperation.EQUAL_TO;
            if (c0 == '>')
                return ComparisonOperation.GREATER_THAN;
            if (c0 == '<')
                return ComparisonOperation.LESS_THAN;
            return ComparisonOperation.UNKNOWN;
        }

        if (len == 2) {
            char c0 = _s.charAt(0);
            char c1 = _s.charAt(1);

            if (c0 == '!' && c1 == '=')
                return ComparisonOperation.NOT_EQUAL_TO;

            if ((c0 == '>' && c1 == '=') || (c0 == '=' && c1 == '>'))
                return ComparisonOperation.GREATER_THAN_OR_EQUAL;

            if ((c0 == '<' && c1 == '=') || (c0 == '=' && c1 == '<'))
                return ComparisonOperation.LESS_THAN_OR_EQUAL;

            if (c0 == '=' && c1 == '=')
                return ComparisonOperation.EQUAL_TO;

            if (c0 == 'I' && c1 == 'N')
                return ComparisonOperation.CONTAINS;

            return ComparisonOperation.UNKNOWN;
        }

        if (len == 4 && "like".compareToIgnoreCase(_s) == 0)
            return ComparisonOperation.LIKE;
        if (len == 5 && "ilike".compareToIgnoreCase(_s) == 0)
            return ComparisonOperation.CASE_INSENSITIVE_LIKE;
        if (len == 20 && "caseInsensitiveLike:".compareToIgnoreCase(_s) == 0)
            return ComparisonOperation.CASE_INSENSITIVE_LIKE;
        if (len == 19 && "caseInsensitiveLike".compareToIgnoreCase(_s) == 0)
            return ComparisonOperation.CASE_INSENSITIVE_LIKE;

        return ComparisonOperation.UNKNOWN;
    }

    public static String stringForOperation(final ComparisonOperation _op) {
        switch (_op) {
        case EQUAL_TO:
            return "=";
        case NOT_EQUAL_TO:
            return "!=";
        case GREATER_THAN:
            return ">";
        case GREATER_THAN_OR_EQUAL:
            return ">=";
        case LESS_THAN:
            return "<";
        case LESS_THAN_OR_EQUAL:
            return "<=";
        case CONTAINS:
            return "IN";
        case LIKE:
            return "LIKE";
        case CASE_INSENSITIVE_LIKE:
            return "caseInsensitiveLike:";
        default:
            return null;
        }
    }

    /* comparison support */

    protected static Map<Class, ComparisonSupport> classToSupport;
    protected static final ComparisonSupport defaultSupport;

    /* Note: be careful not to retain classes which might be unloaded by the
     *       servlet container.
     */
    public static void setSupportForClass(ComparisonSupport _sup, Class _cls) {
        classToSupport.put(_cls, _sup);
    }

    public static ComparisonSupport supportForClass(Class _cls) {
        if (_cls == null)
            return defaultSupport;

        ComparisonSupport sup = classToSupport.get(_cls);
        if (sup != null)
            return sup;

        while (_cls != null) {
            if ((sup = classToSupport.get(_cls)) != null) {
                /* Note: We do not cache because otherwise the user code can't change
                 *       the support for a given parent class.
                 *       TBD: improve this situation.
                 */
                return sup;
            }
            _cls = _cls.getSuperclass();
        }
        return defaultSupport;
    }

    // TODO: check for "Comparison" support?
    public static class ComparisonSupport {

        public boolean compareOperation(ComparisonOperation _op, Object _lhs, Object _rhs) {
            switch (_op) {
            case EQUAL_TO:
                return this.isEqualTo(_lhs, _rhs);
            case NOT_EQUAL_TO:
                return this.isNotEqualTo(_lhs, _rhs);
            case GREATER_THAN:
                return this.isGreaterThan(_lhs, _rhs);
            case GREATER_THAN_OR_EQUAL:
                return this.isGreaterThanOrEqualTo(_lhs, _rhs);
            case LESS_THAN:
                return this.isLessThan(_lhs, _rhs);
            case LESS_THAN_OR_EQUAL:
                return this.isLessThanOrEqualTo(_lhs, _rhs);
            case CONTAINS:
                return this.doesContain(_lhs, _rhs);
            case LIKE:
                return this.doesLike(_lhs, _rhs);
            case CASE_INSENSITIVE_LIKE:
                return this.doesCaseInsensitiveLike(_lhs, _rhs);
            default:
                return false;
            }
        }

        public boolean isEqualTo(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return true;
            if (_lhs == null || _rhs == null)
                return false;
            return _lhs.equals(_rhs);
        }

        public boolean isNotEqualTo(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return false;
            if (_lhs == null || _rhs == null)
                return true;
            return !this.isEqualTo(_lhs, _rhs);
        }

        @SuppressWarnings("unchecked")
        public boolean isGreaterThan(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return false;
            if (this.isEqualTo(_lhs, _rhs))
                return false;

            /* Note: at least Integer.compareTo() doesn't accept null */
            if (_rhs == null)
                return true;
            if (_lhs == null)
                return false;

            if (_lhs instanceof Comparable)
                return ((Comparable) _lhs).compareTo(_rhs) > 0;
            if (_rhs instanceof Comparable)
                return ((Comparable) _rhs).compareTo(_lhs) < 0;

            return false;
        }

        public boolean isGreaterThanOrEqualTo(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return true;
            if (this.isEqualTo(_lhs, _rhs))
                return true;
            return this.isGreaterThan(_lhs, _rhs);
        }

        public boolean isLessThan(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return false;
            if (this.isEqualTo(_lhs, _rhs))
                return false;

            return !this.isGreaterThan(_lhs, _rhs);
        }

        public boolean isLessThanOrEqualTo(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return true;
            if (this.isEqualTo(_lhs, _rhs))
                return true;
            return this.isLessThan(_lhs, _rhs);
        }

        public boolean doesContain(final Object _item, final Object _col) {
            if (_col == null || _item == null)
                return false;

            if (_col instanceof Collection)
                return ((Collection) _col).contains(_item);

            return false;
        }

        public boolean doesLike(final Object _object, final Object _pattern) {
            if (_object == null || _pattern == null)
                return false;

            String spat = _pattern.toString();
            if (spat.equals("*")) /* match everything */
                return true;

            // TODO: we should support much more, we only support prefix/suffix/infix

            final boolean startsWithStar = spat.charAt(0) == '*';
            final boolean endsWithStar = spat.charAt(spat.length() - 1) == '*';

            String os = _object.toString();
            if (startsWithStar && endsWithStar)
                spat = spat.substring(1, spat.length() - 1);
            else if (startsWithStar)
                spat = spat.substring(1);
            else if (endsWithStar)
                spat = spat.substring(0, spat.length() - 1);
            else
                ;

            if (spat.indexOf('*') != -1)
                log.warn("LIKE pattern contains unprocessed patterns: " + _pattern);

            if (startsWithStar && endsWithStar)
                return os.indexOf(spat) != -1;

            if (startsWithStar)
                return os.endsWith(spat);

            if (endsWithStar)
                return os.startsWith(spat);

            return os.equals(spat);
        }

        public boolean doesCaseInsensitiveLike(Object _object, Object _pattern) {
            return false;
        }
    }

    public static class StringComparisonSupport extends ComparisonSupport {
        // TODO: implement me

        public boolean doesContain(final Object _item, final Object _col) {
            if (_col == null || _item == null)
                return false;

            if (_col instanceof Collection)
                return ((Collection) _col).contains(_item);

            return ((String) _col).indexOf((String) _item) != -1;
        }

        // TODO: implement doesLike

        public boolean doesCaseInsensitiveLike(Object _object, Object _pattern) {
            if (_object == null || _pattern == null)
                return false;
            return this.doesLike(((String) _object).toLowerCase(), ((String) _pattern).toLowerCase());
        }
    }

    public static class DateComparisonSupport extends ComparisonSupport {
        // TODO: write a test which ensures that Calendar coercion works

        public boolean isEqualTo(Object _lhs, Object _rhs) {
            if (_lhs == _rhs)
                return true;
            if (_lhs == null || _rhs == null)
                return false;

            if (_lhs instanceof Calendar)
                _lhs = ((Calendar) _lhs).getTime();
            if (_rhs instanceof Calendar)
                _rhs = ((Calendar) _rhs).getTime();

            return _lhs.equals(_rhs);
        }

        public boolean isGreaterThan(Object _lhs, Object _rhs) {
            if (_lhs == _rhs)
                return false;
            if (_rhs == null)
                return true;
            if (_lhs == null)
                return false;

            if (_lhs instanceof Calendar)
                _lhs = ((Calendar) _lhs).getTime();
            if (_rhs instanceof Calendar)
                _rhs = ((Calendar) _rhs).getTime();

            return ((Date) _lhs).compareTo((Date) _rhs) > 0;
        }

        public boolean isLessThan(Object _lhs, Object _rhs) {
            if (_lhs == _rhs)
                return false;
            if (_rhs == null)
                return false;
            if (_lhs == null)
                return true;

            if (_lhs instanceof Calendar)
                _lhs = ((Calendar) _lhs).getTime();
            if (_rhs instanceof Calendar)
                _rhs = ((Calendar) _rhs).getTime();

            return ((Date) _lhs).compareTo((Date) _rhs) < 0;
        }
    }

    public static class BooleanComparisonSupport extends ComparisonSupport {
        // TODO: complete me me

        public boolean isEqualTo(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return true;
            if (_lhs == null || _rhs == null)
                return false;

            if (_lhs instanceof Boolean && !(_rhs instanceof Boolean)) {
                /* special support for Boolean comparisons, required for SOPE compat */
                return ((Boolean) _lhs).booleanValue() == UObject.boolValue(_rhs);
            }

            return _lhs.equals(_rhs);
        }

        public boolean isNotEqualTo(final Object _lhs, final Object _rhs) {
            if (_lhs == _rhs)
                return false;
            if (_lhs == null || _rhs == null)
                return true;

            if (_lhs instanceof Boolean && !(_rhs instanceof Boolean)) {
                /* special support for Boolean comparisons, required for SOPE compat */
                return ((Boolean) _lhs).booleanValue() != UObject.boolValue(_rhs);
            }

            return !this.isEqualTo(_lhs, _rhs);
        }
    }

    public static class CollectionComparisonSupport extends ComparisonSupport {

        public boolean doesContain(final Object _item, final Object _col) {
            if (_col == null || _item == null)
                return false;
            return ((Collection) _col).contains(_item);
        }
    }

    /* static init */

    static {
        classToSupport = new ConcurrentHashMap<Class, ComparisonSupport>(4);
        defaultSupport = new ComparisonSupport();

        classToSupport.put(Date.class, new DateComparisonSupport());
        classToSupport.put(String.class, new StringComparisonSupport());
        classToSupport.put(Boolean.class, new BooleanComparisonSupport());

        // TBD: Check whether thats correct. It compares the Date values of
        //      Calendar objects, not the full Calendar object (eg timezone)
        classToSupport.put(Calendar.class, new DateComparisonSupport());
    }
}