Queries.java :  » Database-ORM » MMBase » org » mmbase » bridge » util » Java Open Source

Java Open Source » Database ORM » MMBase 
MMBase » org » mmbase » bridge » util » Queries.java
/*

This software is OSI Certified Open Source Software.
OSI Certified is a certification mark of the Open Source Initiative.

The license (Mozilla version 1.0) can be read at the MMBase site.
See http://www.MMBase.org/license

*/

package org.mmbase.bridge.util;
import java.util.*;

import org.mmbase.bridge.*;
import org.mmbase.bridge.implementation.BasicQuery;
import org.mmbase.module.core.ClusterBuilder;
import org.mmbase.module.core.MMBase;
import org.mmbase.storage.search.*;
import org.mmbase.storage.search.legacy.ConstraintParser;
import org.mmbase.util.*;
import org.mmbase.util.logging.*;

/**
 * This class contains various utility methods for manipulating and creating query objects.
 * Most essential methods are available on the Query object itself, but too specific or legacy-ish
 * methods are put here.
 *
 * @author Michiel Meeuwissen
 * @version $Id: Queries.java,v 1.96 2008/02/29 11:00:05 michiel Exp $
 * @see  org.mmbase.bridge.Query
 * @since MMBase-1.7
 */
abstract public class Queries {

    public static final int OPERATOR_BETWEEN = -1; // not a FieldCompareConstraint (numeric)
    public static final int OPERATOR_IN = 10000; // not a FieldCompareConstraint (non numeric)
    public static final int OPERATOR_NULL = 10001; // FieldIsNullConstraint

    private static final Logger log = Logging.getLoggerInstance(Queries.class);

    /**
     * Translates a string to a search direction constant. If the string is <code>null</code> then
     * 'BOTH' is returned.
     * @param search string representation of the searchdir constant
     * @return Searchdir constant as in {@link RelationStep}
     * @see ClusterBuilder#getSearchDir The same function, only with another return value if String is <code>null</code>
     */
    public static int getRelationStepDirection(String search) {
        if (search == null) {
            return RelationStep.DIRECTIONS_BOTH;
        }
        search = search.toUpperCase();
        if ("DESTINATION".equals(search)) {
            return RelationStep.DIRECTIONS_DESTINATION;
        } else if ("SOURCE".equals(search)) {
            return RelationStep.DIRECTIONS_SOURCE;
        } else if ("BOTH".equals(search)) {
            return RelationStep.DIRECTIONS_BOTH;
        } else if ("ALL".equals(search)) {
            return RelationStep.DIRECTIONS_ALL;
        } else if ("EITHER".equals(search)) {
            return RelationStep.DIRECTIONS_EITHER;
        } else {
            throw new BridgeException("'" + search + "' cannot be converted to a relation-step direction constant");
        }
    }

    /**
     * Creates a Query object using arguments for {@link Cloud#getList(String, String, String, String, String, String, String, boolean)}
     * (this function is of course implemented using this utility). This is useful to convert (legacy) code which uses
     * getList, but you want to use new Query features without rewriting the complete thing.
     *
     * It can also be simply handy to specify things as Strings.
     *
     * @param cloud
     * @param startNodes
     * @param nodePath
     * @param fields
     * @param constraints
     * @param orderby
     * @param directions
     * @param searchDir
     * @param distinct
     * @return New query object
     * @todo Should this method be part of Cloud itself?
     */
    public static Query createQuery(Cloud cloud, String startNodes, String nodePath, String fields, String constraints, String orderby, String directions, String searchDir, boolean distinct) {

        // the bridge test case say that you may also specifiy empty string (why?)
        if ("".equals(startNodes) || "-1".equals(startNodes)) {
            startNodes = null;
        }
        if ("".equals(fields)) {
            fields = null;
        }
        if ("".equals(constraints)) {
            constraints = null;
        }
        if ("".equals(searchDir)) {
            searchDir = null;
        }
        if ("".equals(orderby)) {
            orderby = null;
        }

        if ("".equals(directions)) {
            directions = null;
        }
        // check invalid search command
        Encode encoder = new Encode("ESCAPE_SINGLE_QUOTE");
        // if(startNodes != null) startNodes = encoder.encode(startNodes);
        // if(nodePath != null) nodePath = encoder.encode(nodePath);
        // if(fields != null) fields = encoder.encode(fields);
        if (orderby != null) {
            orderby = encoder.encode(orderby);
        }
        if (directions != null) {
            directions = encoder.encode(directions);
        }
        if (searchDir != null) {
            searchDir = encoder.encode(searchDir);
        }
        if (constraints != null) {
            constraints = ConstraintParser.convertClauseToDBS(constraints);
            if (! ConstraintParser.validConstraints(constraints)) {
                throw new BridgeException("invalid constraints:" + constraints);
            }
            if (! constraints.substring(0, 5).equalsIgnoreCase("WHERE")) {
                /// WHERE is used in org.mmbase.util.QueryConvertor
                constraints = "WHERE " + constraints;
            }
        }


        // create query object
        //TODO: remove this code... classes under org.mmbase.bridge.util must not use the core

        // getMultilevelSearchQuery must perhaps move to a utility container org.mmbase.storage.search.Queries or so.

        ClusterBuilder clusterBuilder = MMBase.getMMBase().getClusterBuilder();
        int search = -1;
        if (searchDir != null) {
            search = ClusterBuilder.getSearchDir(searchDir);
        }

        List<String> snodes   = StringSplitter.split(startNodes);
        List<String> tables   = StringSplitter.split(nodePath);
        List<String> f        = StringSplitter.split(fields);
        List<String> orderVec = StringSplitter.split(orderby);
        List<String> d        = StringSplitter.split(directions);
        try {
            // pitty that we can't use cloud.createQuery for this.
            // but all essential methods are on ClusterBuilder
            // XXX need casting here, something's wrong!!!
            Query query = new BasicQuery(cloud, clusterBuilder.getMultiLevelSearchQuery(snodes, f, distinct ? "YES" : "NO", tables, constraints, orderVec, d, search));
            return query;
        } catch (IllegalArgumentException iae) {
            throw new BridgeException(iae.getMessage() + ". (arguments: startNodes='" + startNodes + "', path='" + nodePath + "', fields='" + fields + "', constraints='" + constraints + "' orderby='" + orderby + "', directions='" + directions + "', searchdir='" + searchDir + "')" , iae);
        }
    }


    /**
     * Adds a 'legacy' constraint to the query, i.e. constraint(s) represented
     * by a string. Alreading existing constraints remain ('AND' is used).
     *
     * @param query query to add constraint to
     * @param constraints string representation of constraints
     * @return The new constraint, or null if nothing changed added.
     */
    public static Constraint addConstraints(Query query, String constraints) {
        if (constraints == null || constraints.equals("")) {
            return null;
        }

        // (Try to) parse constraints string to Constraint object.
        Constraint newConstraint = new ConstraintParser(query).toConstraint(constraints);
        addConstraint(query, newConstraint);
        return newConstraint;
    }

    /**
     * Adds a Constraint to the already present constraint (with AND).
     * @param query query to add the constraint to
     * @param newConstraint constraint to add
     * @return The new constraint.
     */
    public static Constraint addConstraint(Query query, Constraint newConstraint) {
        if (newConstraint == null) {
            return null;
        }

        Constraint constraint = query.getConstraint();

        if (constraint != null) {
            log.debug("compositing constraint");
            Constraint compConstraint = query.createConstraint(constraint, CompositeConstraint.LOGICAL_AND, newConstraint);
            query.setConstraint(compConstraint);
        } else {
            query.setConstraint(newConstraint);
        }
        return newConstraint;
    }

    /**
     * Creates a operator constant for use by createConstraint
     * @param s String representation of operator
     * @return FieldCompareConstraint operator constant
     * @see #createConstraint(Query, String, int, Object)
     * @see #createConstraint(Query, String, int, Object, Object, boolean)
     */
    public static int getOperator(String s) {
        String op = s.toUpperCase();
        // first: determine operator:
        if (op.equals("<") || op.equals("LESS") || op.equals("LT")) {
            return FieldCompareConstraint.LESS;
        } else if (op.equals("<=") || op.equals("LESS_EQUAL") || op.equals("LE")) {
            return FieldCompareConstraint.LESS_EQUAL;
        } else if (op.equals("=") || op.equals("EQUAL") || op.equals("") || op.equals("EQ")) {
            return FieldCompareConstraint.EQUAL;
        } else if (op.equals("!=") || op.equals("NOT_EQUAL") || op.equals("NE")) {
            return FieldCompareConstraint.NOT_EQUAL;
        } else if (op.equals(">") || op.equals("GREATER") || op.equals("GT")) {
            return FieldCompareConstraint.GREATER;
        } else if (op.equals(">=") || op.equals("GREATER_EQUAL") || op.equals("GE")) {
            return FieldCompareConstraint.GREATER_EQUAL;
        } else if (op.equals("LIKE")) {
            return FieldCompareConstraint.LIKE;
        } else if (op.equals("BETWEEN")) {
            return OPERATOR_BETWEEN;
        } else if (op.equals("IN")) {
            return OPERATOR_IN;
        } else if (op.equals("NULL")) {
            return OPERATOR_NULL;
            //} else if (op.equals("~") || op.equals("REGEXP")) {
            //  return FieldCompareConstraint.REGEXP;
        } else {
            throw new BridgeException("Unknown Field Compare Operator '" + op + "'");
        }
    }

    /**
     * Creates a part constant for use by createConstraint
     * @param s String representation of a datetime part
     * @return FieldValueDateConstraint part constant
     * @see #createConstraint(Query, String, int, Object, Object, boolean, int)
     */
    public static int getDateTimePart(String s) {
        String sPart = s.toUpperCase();
        if (sPart.equals("")) {
            return -1;
        } else if (sPart.equals("CENTURY")) {
            return FieldValueDateConstraint.CENTURY;
        } else if (sPart.equals("YEAR")) {
            return FieldValueDateConstraint.YEAR;
        } else if (sPart.equals("QUARTER")) {
            return FieldValueDateConstraint.QUARTER;
        } else if (sPart.equals("MONTH")) {
            return FieldValueDateConstraint.MONTH;
        } else if (sPart.equals("WEEK")) {
            return FieldValueDateConstraint.WEEK;
        } else if (sPart.equals("DAYOFYEAR")) {
            return FieldValueDateConstraint.DAY_OF_YEAR;
        } else if (sPart.equals("DAY") || sPart.equals("DAYOFMONTH")) {
            return FieldValueDateConstraint.DAY_OF_MONTH;
        } else if (sPart.equals("DAYOFWEEK")) {
            return FieldValueDateConstraint.DAY_OF_WEEK;
        } else if (sPart.equals("HOUR")) {
            return FieldValueDateConstraint.HOUR;
        } else if (sPart.equals("MINUTE")) {
            return FieldValueDateConstraint.MINUTE;
        } else if (sPart.equals("SECOND")) {
            return FieldValueDateConstraint.SECOND;
        } else if (sPart.equals("MILLISECOND")) {
            return FieldValueDateConstraint.MILLISECOND;
        } else {
            throw new BridgeException("Unknown datetime part '" + sPart + "'");
        }
    }

    /**
     * Used in implementation of createConstraint
     * @param stringValue string representation of a number
     * @return Number object
     * @throws BridgeException when failed to convert the string
     */
    protected static Number getNumberValue(String stringValue) throws BridgeException {
        if (stringValue == null) return null;
        try {
            return Integer.valueOf(stringValue);
        } catch (NumberFormatException e) {
            try {
                return Double.valueOf(stringValue);
            } catch (NumberFormatException e2) {
                if(stringValue.equalsIgnoreCase("true")) {
                    return Integer.valueOf(1);
                } else if(stringValue.equalsIgnoreCase("false")) {
                return Integer.valueOf(0);
                }
                throw new BridgeException("Operator requires number value ('" + stringValue + "' is not)");
            }
        }
    }

    /**
     * Used in implementation of createConstraint
     * @param fieldType Field Type constant (@link Field)
     * @param operator Compare operator
     * @param value value to convert
     * @return new Compare value
     */
    protected static Object getCompareValue(int fieldType, int operator, Object value) {
        return getCompareValue(fieldType, operator, value, -1, null);
    }

    /**
     * Used in implementation of createConstraint
     * @param fieldType Field Type constant (@link Field)
     * @param operator Compare operator
     * @param value value to convert
     * @param cloud The cloud may be used to pass locale sensitive properties which may be needed for comparisions (locales, timezones)
     * @return new Compare value
     * @since MMBase-1.8.2
     */
    protected static Object getCompareValue(int fieldType, int operator, Object value, int datePart, Cloud cloud) {
        if (operator == OPERATOR_IN) {
            SortedSet<Object> set;
            if (value instanceof SortedSet) {
                set = (SortedSet<Object>)value;
            } else if (value instanceof NodeList) {
                set = new TreeSet<Object>();
                for (Node node : ((NodeList)value)) {
                    set.add(getCompareValue(fieldType, FieldCompareConstraint.EQUAL, node.getNumber()));
                }
            } else if (value instanceof Collection) {
                set = new TreeSet<Object>();
                for (Object o : ((Collection)value)) {
                    set.add(getCompareValue(fieldType, FieldCompareConstraint.EQUAL, o));
                }
            } else {
                set = new TreeSet<Object>();
                if (!(value == null || value.equals(""))) {
                    set.add(getCompareValue(fieldType, FieldCompareConstraint.EQUAL, value));
                }
            }
            return set;
        }
        switch(fieldType) {
        case Field.TYPE_STRING:
            return value == null ? null : Casting.toString(value);
        case Field.TYPE_INTEGER:
        case Field.TYPE_FLOAT:
        case Field.TYPE_LONG:
        case Field.TYPE_DOUBLE:
        case Field.TYPE_NODE:
            if (value  instanceof Number) {
                return value;
            } else {
                return getNumberValue(value == null ? null : Casting.toString(value));
            }
        case Field.TYPE_DATETIME:
            //TimeZone     tz = cloud == null ? null : (TimeZone) cloud.getProperty("org.mmbase.timezone");
            if (datePart > -1) {
                return Casting.toInteger(value);
            } else {
                return Casting.toDate(value);
            }
        case Field.TYPE_BOOLEAN:
            return Casting.toBoolean(value) ? Boolean.TRUE : Boolean.FALSE;
        default:
            return value;
        }
    }

    /**
     * Defaulting version of {@link #createConstraint(Query, String, int, Object, Object, boolean, int)}.
     * Casesensitivity defaults to false, value2 to null (so 'BETWEEN' cannot be used), datePart set to -1 (so no date part comparison)
     * @param query      The query to create the constraint for
     * @param fieldName  The field to create the constraint on (as a string, so it can include the step), e.g. 'news.number'
     * @param operator   The operator to use. This constant can be produces from a string using {@link #getOperator(String)}.
     * @param value      The value to compare with, which must be of the right type. If field is number it might also be an alias.
     * @return The new constraint, or <code>null</code> it by chance the specified arguments did not lead to a new actual constraint (e.g. if value is an empty set)
     */
    public static Constraint createConstraint(Query query, String fieldName, int operator, Object value) {
        return createConstraint(query, fieldName, operator, value, null, false, -1);
    }

    /**
     * Defaulting version of {@link #createConstraint(Query, String, int, Object, Object, boolean, int)}.
     * DatePart set to -1 (so no date part comparison)
     * @param query      The query to create the constraint for
     * @param fieldName  The field to create the constraint on (as a string, so it can include the step), e.g. 'news.number'
     * @param operator   The operator to use. This constant can be produces from a string using {@link #getOperator(String)}.
     * @param value      The value to compare with, which must be of the right type. If field is number it might also be an alias.
     * @param value2     The other value (only relevant if operator is BETWEEN, the only terniary operator)
     * @param caseSensitive  Whether it should happen case sensitively (not relevant for number fields)
     * @return The new constraint, or <code>null</code> it by chance the specified arguments did not lead to a new actual constraint (e.g. if value is an empty set)
     */
    public static Constraint createConstraint(Query query, String fieldName, int operator, Object value,  Object value2, boolean caseSensitive) {
        return createConstraint(query, fieldName, operator, value, value2, caseSensitive, -1);
    }

    /**
     * Creates a constraint smartly, depending on the type of the field, the value is cast to the
     * right type, and the right type of constraint is created.
     * This is used in taglib implementation, but could be useful more generally.
     *
     * @param query      The query to create the constraint for
     * @param fieldName  The field to create the constraint on (as a string, so it can include the step), e.g. 'news.number'
     * @param operator   The operator to use. This constant can be produces from a string using {@link #getOperator(String)}.
     * @param value      The value to compare with, which must be of the right type. If field is number it might also be an alias.
     * @param value2     The other value (only relevant if operator is BETWEEN, the only terniary operator)
     * @param caseSensitive  Whether it should happen case sensitively (not relevant for number fields)
     * @param datePart       The part of a DATETIME value that is to be checked
     * @return The new constraint, or <code>null</code> it by chance the specified arguments did not lead to a new actual constraint (e.g. if value is an empty set)
     */
    public static Constraint createConstraint(final Query query, final String fieldName, final int operator, final Object originalValue, final Object value2, final boolean caseSensitive, final int datePart) {

        Object value = originalValue;
        StepField stepField = query.createStepField(fieldName);
        if (stepField == null) {
            throw new BridgeException("Could not create stepfield with '" + fieldName + "'");
        }

        Cloud cloud = query.getCloud();
        FieldConstraint newConstraint;

        if (value instanceof StepField) {
            newConstraint = query.createConstraint(stepField, operator, (StepField)value);
        } else if (operator == OPERATOR_NULL || value == null) {
            newConstraint = query.createConstraint(stepField);
        } else {
            Field field = cloud.getNodeManager(stepField.getStep().getTableName()).getField(stepField.getFieldName());
            int fieldType = field.getType();

            if (fieldName.equals("number") || fieldType == Field.TYPE_NODE) {
                if (value instanceof String) { // it might be an alias!
                    if (cloud.hasNode((String) value)) {
                        Node node = cloud.getNode((String)value);
                        value = Integer.valueOf(node.getNumber());
                    } else {
                        value = -1; // non existing node number. Integer.parseInt((String) value);
                    }
                } else if (value instanceof Collection) {  // or even more aliases!
                    Collection col  = (Collection) value;
                    value = new ArrayList();
                    List<Object> list = (List<Object>) value;
                    for (Object v : col) {
                        if (v instanceof Number) {
                            list.add(v);
                        } else {
                            String s = Casting.toString(v);
                            if (cloud.hasNode(s)) {
                                Node node = cloud.getNode(s);
                                list.add(node.getNumber());
                            } else {
                                list.add(-1);
                            }

                        }
                    }

                }
            }
            if (operator != OPERATOR_IN) { // should the elements of the collection then not be cast?

                if (fieldType == Field.TYPE_XML) {
                    // XML's are treated as String in the query-handler so, let's anticipate that here...
                    // a bit of a hack, perhaps we need something like a 'searchCast' or so.
                    value = Casting.toString(value);
                } else {
                    Object castedValue = field.getDataType().cast(value, null, field);
                    if (castedValue == null && value != null && fieldType == Field.TYPE_NODE) {
                        // non existing node-number, like e.g. -1 are csated to null,
                        // but that is incorrect when e..g the operator is GREATER
                        castedValue = Casting.toInteger(value);
                    }
                    value = castedValue;

                }
            }

            Object compareValue = getCompareValue(fieldType, operator, value, datePart, cloud);

            if (log.isDebugEnabled()) {
                log.debug(" " + originalValue + " -> " + value + " -> " + compareValue);
            }

            if (operator > 0 && operator < OPERATOR_IN) {
                if (fieldType == Field.TYPE_DATETIME && datePart> -1) {
                    newConstraint = query.createConstraint(stepField, operator, compareValue, datePart);
                } else {
                    if (operator == FieldCompareConstraint.EQUAL  && compareValue == null) {
                        newConstraint = query.createConstraint(stepField);
                    } else {
                        newConstraint = query.createConstraint(stepField, operator, compareValue);
                    }
                }
            } else {
                if (fieldType == Field.TYPE_DATETIME && datePart> -1) {
                    throw new RuntimeException("Cannot apply IN or BETWEEN to a partial date field");
                }
                switch (operator) {
                case OPERATOR_BETWEEN :
                    Object compareValue2 = getCompareValue(fieldType, operator, value2);
                    newConstraint = query.createConstraint(stepField, compareValue, compareValue2);
                    break;
                case OPERATOR_IN :
                    newConstraint = query.createConstraint(stepField, (SortedSet)compareValue);
                    break;
                default :
                    throw new RuntimeException("Unknown value for operation " + operator);
                }
            }
        }
        query.setCaseSensitive(newConstraint, caseSensitive);
        return newConstraint;

    }

    /**
     * Takes a Constraint of a query, and takes al constraints on 'sourceStep' of it, and copies
     * those Constraints to the given step of the receiving query.
     *
     * Constraints on different steps then the given 'sourceStep' are ignored. CompositeConstraints
     * cause recursion and would work too (but same limitation are valid for the childs).
     *
     * @param c          The constrain to be copied (for example the result of sourceQuery.getConstraint()).
     * @param sourceStep The step in the 'source' query.
     * @param query      The receiving query
     * @param step       The step of the receiving query which must 'receive' the sort orders.
     * @since MMBase-1.7.1
     * @see   org.mmbase.storage.search.implementation.BasicSearchQuery#copyConstraint Functions are similar
     * @throws IllegalArgumentException If the given constraint is not compatible with the given step.
     * @throws UnsupportedOperationException If CompareFieldsConstraints or LegacyConstraints are encountered.
     * @return The new constraint or null
     */
    public static Constraint copyConstraint(Constraint c, Step sourceStep, Query query, Step step) {
        if (c == null) return null;

        if (c instanceof CompositeConstraint) {
            CompositeConstraint constraint = (CompositeConstraint) c;
            List<Constraint> constraints = new ArrayList<Constraint>();
            for (Constraint child :  constraint.getChilds()) {
                Constraint cons = copyConstraint(child, sourceStep, query, step);
                if (cons != null) constraints.add(cons);
            }
            int size = constraints.size();
            if (size == 0) return null;
            if (size == 1) return constraints.get(0);
            Iterator<Constraint> i = constraints.iterator();
            int op = constraint.getLogicalOperator();
            Constraint newConstraint    = query.createConstraint(i.next(), op, i.next());
            while (i.hasNext()) {
                newConstraint = query.createConstraint(newConstraint, op, i.next());
            }
            query.setInverse(newConstraint, constraint.isInverse());
            return newConstraint;
        } else if (c instanceof CompareFieldsConstraint) {
            throw new UnsupportedOperationException("Cannot copy comparison between fields"); // at least not from different steps
        }


        FieldConstraint fieldConstraint = (FieldConstraint) c;
        if (! fieldConstraint.getField().getStep().equals(sourceStep)) return null; // constraint is not for the request step, so don't copy.

        StepField field = query.createStepField(step, fieldConstraint.getField().getFieldName());

        FieldConstraint newConstraint;
        if (c instanceof FieldValueConstraint) {
            newConstraint = query.createConstraint(field, ((FieldValueConstraint) c).getOperator(), ((FieldValueConstraint) c).getValue());
        } else if (c instanceof FieldNullConstraint) {
            newConstraint = query.createConstraint(field);
        } else if (c instanceof FieldValueBetweenConstraint) {
            FieldValueBetweenConstraint constraint = (FieldValueBetweenConstraint) c;
            try {
                newConstraint = query.createConstraint(field, constraint.getLowerLimit(), constraint.getUpperLimit());
            } catch (NumberFormatException e) {
                newConstraint = query.createConstraint(field, constraint.getLowerLimit(), constraint.getUpperLimit());
            }
        } else if (c instanceof FieldValueInConstraint) {
            FieldValueInConstraint constraint = (FieldValueInConstraint) c;

            // sigh
            SortedSet<Object> set = new TreeSet<Object>();
            int type   =  field.getType();
            for (Object value : constraint.getValues()) {
                switch(type) {
                case Field.TYPE_INTEGER:
                case Field.TYPE_LONG:
                case Field.TYPE_NODE:
                    value = Long.valueOf(Casting.toLong(value));
                    break;
                case Field.TYPE_FLOAT:
                case Field.TYPE_DOUBLE:
                    value = Double.valueOf(Casting.toDouble(value));
                    break;
                case Field.TYPE_DATETIME:
                    value = new Date((long) 1000 * Integer.parseInt("" + value));
                    break;
                default:
                    log.debug("Unknown type " + type);
                    break;
                }
                set.add(value);
            }
            newConstraint = query.createConstraint(field, set);
        } else if (c instanceof LegacyConstraint) {
            throw new UnsupportedOperationException("Cannot copy legacy constraint to other step");
        } else {
            throw new RuntimeException("Could not copy constraint " + c);
        }
        query.setInverse(newConstraint, fieldConstraint.isInverse());
        query.setCaseSensitive(newConstraint, fieldConstraint.isCaseSensitive());
        return newConstraint;

    }
    /**
     * Copies SortOrders to a given step of another query. SortOrders which do not sort the given
     * 'sourceStep' are ignored.
     * @param sortOrders A list of SortOrders (for example the result of sourceQuery.getSortOrders()).
     * @param sourceStep The step in the 'source' query.
     * @param query      The receiving query
     * @param step       The step of the receiving query which must 'receive' the sort orders.
     * @since MMBase-1.7.1

     */
    public static void copySortOrders(List<SortOrder> sortOrders, Step sourceStep,  Query query, Step step) {
        for (SortOrder sortOrder : sortOrders) {
            StepField sourceField = sortOrder.getField();
            if (! sourceField.getStep().equals(sourceStep)) continue; // for another step
            if (sortOrder instanceof DateSortOrder) {
                query.addSortOrder(query.createStepField(step, sourceField.getFieldName()), sortOrder.getDirection(),
                                   sortOrder.isCaseSensitive(),
                                   ((DateSortOrder)sortOrder).getPart());
            } else {
                query.addSortOrder(query.createStepField(step, sourceField.getFieldName()), sortOrder.getDirection(),
                                   sortOrder.isCaseSensitive());
            }
        }
    }

    /**
     * Converts a String to a SortOrder constant
     * @param dir string representation of direction of sortorder
     * @return SortOrder constant
     * @since MMBase-1.7.1
     */
    public static int getSortOrder(String dir) {
        dir = dir.toUpperCase();
        if (dir.equals("")) {
            return  SortOrder.ORDER_ASCENDING;
        } else if (dir.equals("DOWN")) {
            return SortOrder.ORDER_DESCENDING;
        } else if (dir.equals("UP")) {
            return SortOrder.ORDER_ASCENDING;
        } else if (dir.equals("ASCENDING")) {
            return SortOrder.ORDER_ASCENDING;
        } else if (dir.equals("DESCENDING")) {
            return SortOrder.ORDER_DESCENDING;
        } else {
            throw new BridgeException("Unknown sort-order '" + dir + "'");
        }
    }

    /**
     * Adds sort orders to the query, using two strings. Like in 'getList' of Cloud. Several tag-attributes need this.
     * @param query query to add the sortorders to
     * @param sorted string with comma-separated fields
     * @param directions string with comma-separated directions
     *
     * @todo implement for normal query.
     * @return The new sort orders
     */
    public static List<SortOrder> addSortOrders(Query query, String sorted, String directions) {
        // following code was copied from MMObjectBuilder.setSearchQuery (bit ugly)
        if (sorted == null) {
            return query.getSortOrders().subList(0, 0);
        }
        if (directions == null) {
            directions = "";
        }
        List<SortOrder> list = query.getSortOrders();
        int initialSize = list.size();

        StringTokenizer sortedTokenizer = new StringTokenizer(sorted, ",");
        StringTokenizer directionsTokenizer = new StringTokenizer(directions, ",");

        while (sortedTokenizer.hasMoreTokens()) {
            String fieldName = sortedTokenizer.nextToken().trim();
            int dot = fieldName.indexOf('.');

            StepField sf;
            if (dot == -1 && query instanceof NodeQuery) {
                NodeManager nodeManager = ((NodeQuery)query).getNodeManager();
                sf = ((NodeQuery)query).getStepField(nodeManager.getField(fieldName));
            } else {
                sf = query.createStepField(fieldName);
            }

            int dir = SortOrder.ORDER_ASCENDING;
            if (directionsTokenizer.hasMoreTokens()) {
                String direction = directionsTokenizer.nextToken().trim();
                dir = getSortOrder(direction);
            }
            query.addSortOrder(sf, dir);
        }

        return list.subList(initialSize, list.size());
    }

    /**
     * Returns substring of given string without the leading digits (used in 'paths')
     * @param complete string with leading digits
     * @return string with digits removed
     */
    public static String removeDigits(String complete) {
        int end = complete.length() - 1;
        while (Character.isDigit(complete.charAt(end))) {
            --end;
        }
        return complete.substring(0, end + 1);
    }

    /**
     * Adds path of steps to an existing query. The query may contain steps already. Per step also
     * the 'search direction' may be specified.
     * @param query extend this query
     * @param path create steps from this path
     * @param searchDirs add steps with these relation directions
     * @return The new steps.
     */
    public static List<Step> addPath(Query query, String path, String searchDirs) {
        if (path == null || path.equals("")) {
            return query.getSteps().subList(0, 0);
        }
        if (searchDirs == null) {
            searchDirs = "";
        }

        List<Step> list = query.getSteps();
        int initialSize = list.size();

        StringTokenizer pathTokenizer       = new StringTokenizer(path, ",");
        StringTokenizer searchDirsTokenizer = new StringTokenizer(searchDirs, ",");

        Cloud cloud = query.getCloud();

        if (query.getSteps().size() == 0) { // if no steps yet, first step must be added with addStep
            String completeFirstToken = pathTokenizer.nextToken().trim();
            String firstToken = removeDigits(completeFirstToken);
            //if (cloud.hasRole(firstToken)) {
            // you cannot start with a role.., should we throw exception?
            // naa, the following code will throw exception that node type does not exist.
            //}
            Step step = query.addStep(cloud.getNodeManager(firstToken));
            if (!firstToken.equals(completeFirstToken)) {
                query.setAlias(step, completeFirstToken);
            }
        }

        String searchDir = null; // outside the loop, so defaulting to previous searchDir
        while (pathTokenizer.hasMoreTokens()) {
            String completeToken = pathTokenizer.nextToken().trim();
            String token = removeDigits(completeToken);

            if (searchDirsTokenizer.hasMoreTokens()) {
                searchDir = searchDirsTokenizer.nextToken();
            }

            if (cloud.hasRole(token) && pathTokenizer.hasMoreTokens()) {
                if (cloud.hasNodeManager(token)) {
                    // Ambigious path element '" + token + "', is both a role and a nodemanager
                    // This is pretty common though. E.g. 'posrel'.
                }
                String nodeManagerAlias = pathTokenizer.nextToken().trim();
                String nodeManagerName = removeDigits(nodeManagerAlias);
                NodeManager nodeManager = cloud.getNodeManager(nodeManagerName);
                RelationStep relationStep = query.addRelationStep(nodeManager, token, searchDir);

                /// make it possible to postfix with numbers manually
                if (!cloud.hasRole(completeToken)) {
                    query.setAlias(relationStep, completeToken);
                }
                if (!nodeManagerName.equals(nodeManagerAlias)) {
                    Step next = relationStep.getNext();
                    query.setAlias(next, nodeManagerAlias);
                }
            } else {
                NodeManager nodeManager = cloud.getNodeManager(token);
                RelationStep step = query.addRelationStep(nodeManager, null /* role */ , searchDir);
                if (!completeToken.equals(nodeManager.getName())) {
                    Step next = step.getNext();
                    query.setAlias(next, completeToken);
                }
            }
        }
        if (searchDirsTokenizer.hasMoreTokens()) {
            throw new BridgeException("Too many search directions (" + path + "/" + searchDirs + ")");
        }
        return list.subList(initialSize, list.size());
    }

    /**
     * Adds a number of fields. Fields is represented as a comma separated string.
     * @param query The query where the fields should be added to
     * @param fields a comma separated string of fields
     * @return The new stepfields
     */
    public static List<StepField> addFields(Query query, String fields) {
        List<StepField> result = new ArrayList<StepField>();
        if (fields == null || fields.equals("")) {
            return result;
        }

        for (String fieldName : StringSplitter.split(fields)) {
            result.add(query.addField(fieldName));
        }
        return result;

    }



    /**
     * Add startNodes to the first step with the correct type to the given query. The nodes are identified
     * by a String, which could be prefixed with a step-alias, if you want to add the nodes to
     * another then this found step.
     *
     * Furthermore may the nodes by identified by their alias, if they have one.
     * @param query query to add the startnodes
     * @param startNodes start nodes
     *
     * @see org.mmbase.module.core.ClusterBuilder#getMultiLevelSearchQuery(List, List, String, List, String, List, List, int)
     * (this is essentially a 'bridge' version of the startnodes part)
     */
    public static void addStartNodes(Query query, String startNodes) {
        if (startNodes == null || "".equals(startNodes) || "-1".equals(startNodes)) {
            return;
        }

        Step firstStep = null; // the 'default' step to which nodes are added. It is the first step which corresponds with the type of the first node.

        for (String nodeAlias : StringSplitter.split(startNodes)) {
                                                     // can be a string, prefixed with the step alias.
            Step step;                                // the step to which the node must be added (defaults to 'firstStep').
            String nodeNumber;                        // a node number or perhaps also a node alias.
            {
                int dot = nodeAlias.indexOf('.'); // this feature is not in core. It should be considered experimental
                if (dot == -1) {
                    step = firstStep;
                    nodeNumber = nodeAlias;
                } else {
                    step = query.getStep(nodeAlias.substring(0, dot));
                    nodeNumber = nodeAlias.substring(dot + 1);
                }
            }

            if (firstStep == null) { // firstStep not yet determined, do that now.
                Node node;
                try {
                    node = query.getCloud().getNode(nodeNumber);
                } catch (NotFoundException nfe) { // alias with dot?
                    node = query.getCloud().getNode(nodeAlias);
                }
                NodeManager nodeManager = node.getNodeManager();
                for (Step queryStep : query.getSteps()) {
                    NodeManager queryNodeManager = query.getCloud().getNodeManager(queryStep.getTableName());
                    if (queryNodeManager.equals(nodeManager) || queryNodeManager.getDescendants().contains(nodeManager)) {
                        // considering inheritance. ClusterBuilder is not doing that, but I think it is a bug.
                        firstStep = queryStep;
                        break;
                    }
                }
                if (firstStep == null) {
                    // odd..
                    // See also org.mmbase.module.core.ClusterBuilder#getMultiLevelSearchQuery
                    // specified a node which is not of the type of one of the steps.
                    // take as default the 'first' step (which will make the result empty, compatible with 1.6, bug #6440).
                    firstStep = query.getSteps().get(0);
                }
            }

            if (step == null) {
                step = firstStep;
            }

            try {
                try {
                    query.addNode(step, Integer.parseInt(nodeNumber));
                } catch (NumberFormatException nfe) {
                    query.addNode(step, query.getCloud().getNode(nodeNumber));          // node was specified by alias.
                }
            } catch (NotFoundException nnfe) {
                query.addNode(step, query.getCloud().getNode(nodeAlias)); // perhas an alias containing a dot?
            }
        }
    }

    /**
     * Takes the query, and does a count with the same constraints (so ignoring 'offset' and 'max')
     * @param query query as base for the count
     * @return number of results
     */
    public static int count(Query query) {
        Cloud cloud = query.getCloud();
        Query count = query.aggregatingClone();
        int type = query.isDistinct() ? AggregatedField.AGGREGATION_TYPE_COUNT_DISTINCT : AggregatedField.AGGREGATION_TYPE_COUNT;

        String resultName;
        if (query instanceof NodeQuery) {
            // all fields are present of the node-step, so, we could use the number field simply.
            resultName = "number";
            NodeQuery nq = (NodeQuery) query;
            count.addAggregatedField(nq.getNodeStep(), nq.getNodeManager().getField(resultName), type);
        } else {
            List<StepField> fields = query.getFields();
            if (fields.size() == 0) { // for non-distinct queries always the number fields would be available
                throw new IllegalArgumentException("Cannot count queries with less than one field: " + query);
            }

            if (query.isDistinct() && fields.size() > 1) {
                // aha hmm. Well, we also find it ok if all fields are of one step, and 'number' is present
                resultName = null;
                Step step = null;
                for (StepField sf : fields) {
                    if (step == null) {
                        step = sf.getStep();
                    } else {
                        if (! step.equals(sf.getStep())) {
                            throw new UnsupportedOperationException("Cannot count distinct queries with fields of more than one step. Current fields: " + fields);
                        }
                    }
                    if (sf.getFieldName().equals("number")) {
                        resultName = sf.getFieldName();
                    }
                }
                if (resultName == null) {
                    throw new UnsupportedOperationException("Cannot count distinct queries with more than one field if 'number' field is missing. Current fields: " + fields);
                }
                count.addAggregatedField(step, cloud.getNodeManager(step.getTableName()).getField(resultName), type);
            } else {
                // simply take this one field
                StepField sf = fields.get(0);
                Step step = sf.getStep();
                resultName = sf.getFieldName();
                count.addAggregatedField(step, cloud.getNodeManager(step.getTableName()).getField(resultName), type);
            }
        }
        NodeList r = cloud.getList(count);
        if (r.size() != 1) throw new RuntimeException("Count query " + query + " did not give one result but " + r);
        Node result = r.getNode(0);
        return result.getIntValue(resultName);
    }

    /**
     * @since MMBase-1.8
     */
    protected static Object aggregate(Query query, StepField field, int type) {
        Cloud cloud = query.getCloud();
        Query aggregate = query.aggregatingClone();
        String resultName = field.getFieldName();
        Step step = field.getStep();
        aggregate.addAggregatedField(step, cloud.getNodeManager(step.getTableName()).getField(resultName), type);
        NodeList r = cloud.getList(aggregate);
        if (r.size() != 1) throw new RuntimeException("Aggregated query " + query + " did not give one result but " + r);
        Node result = r.getNode(0);
        return result.getValue(resultName);
    }

    /**
     * @since MMBase-1.8
     */
    public static Object min(Query query, StepField field) {
        return aggregate(query, field, AggregatedField.AGGREGATION_TYPE_MIN);
    }
    /**
     * @since MMBase-1.8
     */
    public static Object max(Query query, StepField field) {
        return aggregate(query, field, AggregatedField.AGGREGATION_TYPE_MAX);
    }

    /**
     * Searches a list of Steps for a step with a certain name. (alias or tableName)
     * @param steps steps to search through
     * @param stepAlias alias to search for
     * @return The Step if found, otherwise null
     * @throws ClassCastException if list does not contain only Steps
     */
    public static Step searchStep(List<Step> steps, String stepAlias) {
        if (log.isDebugEnabled()) {
            log.debug("Searching '" + stepAlias + "' in " + steps);
        }
        // first try aliases
        for (Step step : steps) {
            if (stepAlias.equals(step.getAlias())) {
                return step;
            }
        }
        // if no aliases found, try table names
        for (Step step : steps) {
            if (stepAlias.equals(step.getTableName())) {
                return step;
            }
        }
        return null;
    }

    /**
     * Returns the NodeQuery returning the given Node. This query itself is not very useful, because
     * you already have its result (the node), but it is convenient as a base query for many other
     * goals.
     *
     * If the node is uncommited, it cannot be queried, and the node query returning all nodes from
     * the currect type will be returned.
     *
     * @param node Node to create the query from
     * @return A new NodeQuery object
     */
    public static NodeQuery createNodeQuery(Node node) {
        NodeManager nm = node.getNodeManager();
        NodeQuery query = node.getCloud().createNodeQuery(); // use the version which can accept more steps
        Step step       = query.addStep(nm);
        query.setNodeStep(step);
        if (! node.isNew()) {
            query.addNode(step, node);
        }
        return query;
    }

    /**
     * Returns a query to find the nodes related to the given node.
     * @param node start node
     * @param otherNodeManager node manager on the other side of the relation
     * @param role role of the relation
     * @param direction direction of the relation
     * @return A new NodeQuery object
     */
    public static NodeQuery createRelatedNodesQuery(Node node, NodeManager otherNodeManager, String role, String direction) {
        NodeQuery query = createNodeQuery(node);
        if (otherNodeManager == null) otherNodeManager = node.getCloud().getNodeManager("object");
        RelationStep step = query.addRelationStep(otherNodeManager, role, direction);
        query.setNodeStep(step.getNext());
        return query;
    }

    /**
     * Returns a query to find the relations nodes of the given node.
     * @param node start node
     * @param otherNodeManager node manager on the other side of the relation
     * @param role role of the relation
     * @param direction direction of the relation
     * @return A new NodeQuery object
     */
    public static NodeQuery createRelationNodesQuery(Node node, NodeManager otherNodeManager, String role, String direction) {
        NodeQuery query = createNodeQuery(node);
        if (otherNodeManager == null) otherNodeManager = node.getCloud().getNodeManager("object");
        RelationStep step = query.addRelationStep(otherNodeManager, role, direction);
        query.setNodeStep(step);
        return query;
    }

    /**
     * Returns a query to find the relations nodes between two given nodes.
     *
     * To test <em>whether</em> to nodes are related you can use e.g.:
     * <code>
     *  if (Queries.count(Queries.createRelationNodesQuery(node1, node2, "posrel", null)) > 0) {
     *    ..
     *  }
     * </code>
     * @param node start node
     * @param otherNode node on the other side of the relation
     * @param role role of the relation
     * @param direction direction of the relation
     * @return A new NodeQuery object
     * @since MMBase-1.8
     */
    public static NodeQuery createRelationNodesQuery(Node node, Node otherNode, String role, String direction) {
        NodeQuery query = createNodeQuery(node);
        NodeManager otherNodeManager = otherNode.getNodeManager();
        RelationStep step = query.addRelationStep(otherNodeManager, role, direction);
        Step nextStep = step.getNext();
        query.addNode(nextStep, otherNode.getNumber());
        query.setNodeStep(step);
        return query;
    }

    /**
     * Queries a list of cluster nodes, using a {@link org.mmbase.bridge.NodeQuery} (so al fields of
     * one step are available), plus some fields of the relation step.  The actual node can be got
     * from the node cache by doing a {@link org.mmbase.bridge.Node#getNodeValue} with the {@link
     * org.mmbase.bridge.NodeList#NODESTEP_PROPERTY} property.  The fields of the relation can be got by
     * prefixing their names by the role and a dot (as normal in multilevel results).
     * @param node start node
     * @param otherNodeManager node manager on the other side of the relation
     * @param role role of the relation
     * @param direction direction of the relation
     * @param relationFields  Comma separated string of fields which must be queried from the relation step
     * @param sortOrders      Comma separated string of fields of sortorders, or the empty string or <code>null</code>
     *                        So, this methods is targeted at the use of 'posrel' and similar fields, because sorting on other fields isn't possible right now.
     * @since MMBase-1.8
     * @todo  EXPERIMENTAL
     */
    public static NodeList getRelatedNodes(Node node, NodeManager otherNodeManager, String role, String direction, String relationFields, String sortOrders) {
        NodeQuery q = Queries.createRelatedNodesQuery(node, otherNodeManager, role, direction);
        addRelationFields(q, role, relationFields, sortOrders);
        return q.getCloud().getList(q);
    }

    /**
     * @since MMBase-1.8
     */
    public static NodeQuery addRelationFields(NodeQuery q, String role, String relationFields, String sortOrders) {
        List<String> list = StringSplitter.split(relationFields);
        List<String> orders = StringSplitter.split(sortOrders);
        Iterator<String> j = orders.iterator();
        for (String fieldName : list) {
            StepField sf = q.addField(role + "." + fieldName);
            if (j.hasNext()) {
                String so = j.next();
                q.addSortOrder(sf, getSortOrder(so));
            }
        }
        return q;
    }

    /**
     * Add a sortorder (DESCENDING) on al the'number' fields of the query, on which there is not yet a
     * sortorder. This ensures that the query result is ordered uniquely.
     * @param q query to change
     * @return The changed Query
     */
    public static Query sortUniquely(final Query q) {
        List<Step> steps = null;

        // remove the ones which are already sorted
        for (SortOrder sortOrder : q.getSortOrders()) {
            if (sortOrder.getField().getFieldName().equals("number")) {
                Step step = sortOrder.getField().getStep();
                if (steps == null) {
                    // instantiate new ArrayList only if really necessary
                    steps = new ArrayList<Step>(q.getSteps());
                }
                steps.remove(step);
            }
        }
        if (steps == null) {
            steps = q.getSteps();
        }
        // add sort order on the remaining ones:
        for (Step step : steps) {
            StepField sf = q.createStepField(step, "number");
            if (sf == null) {
                throw new RuntimeException("Create stepfield for 'number' field returned null!");
            }
            q.addSortOrder(sf, SortOrder.ORDER_DESCENDING);
        }
        return q;
    }

    /**
     * Make sure all sorted fields are queried
     * @since MMBase-1.8
     */
    public static Query addSortedFields(Query q) {
        List<StepField> fields = q.getFields();
        for (SortOrder order : q.getSortOrders()) {
            StepField field = order.getField();
            Step s = field.getStep();
            StepField sf = q.createStepField(s, q.getCloud().getNodeManager(s.getTableName()).getField(field.getFieldName()));
            if (! fields.contains(sf)) {
                q.addField(s, q.getCloud().getNodeManager(s.getTableName()).getField(field.getFieldName()));
            }
        }
        return q;
    }

    /**
     * Obtains a value for the field of a sortorder from a given node.
     * Used to set constraints based on sortorder.
     * @since MMBase-1.8
     */
    public static Object getSortOrderFieldValue(Node node, SortOrder sortOrder) {
        String fieldName = sortOrder.getField().getFieldName();
        if (node == null) throw new IllegalArgumentException("No node given");
        Object value = node.getValue(fieldName);
        if (value == null) {
            Step step = sortOrder.getField().getStep();
            String pref = step.getAlias();
            if (pref == null) {
                pref = step.getTableName();
            }
            value = node.getValue(pref+ "." + fieldName);

        }
        if (value instanceof Node) {
            value = ((Node)value).getNumber();
        }
        return value;
    }


    /**
     * Compare tho nodes, with a SortOrder. This determins where a certain node is smaller or bigger than a certain other node, with respect to some SortOrder.
     * This is used by {@link #compare(Node, Node, List)}
     *
     * If node2 is only 'longer' then node1, but otherwise equal, then it is bigger.
     *
     * @since MMBase-1.8
     */
    public static int compare(Node node1, Node node2, SortOrder sortOrder) {
        return compare(getSortOrderFieldValue(node1, sortOrder),
                       getSortOrderFieldValue(node2, sortOrder),
                       sortOrder);

    }
    /**
     * @since MMBase-1.8
     */
    public static int compare(Object value, Object value2, SortOrder sortOrder) {
        int result;
        // compare values - if they differ, detemrine whether
        // they are bigger or smaller and return the result
        // remaining fields are not of interest ionce a difference is found
        if (value == null) {
            if (value2 != null) {
                return 1;
            } else {
                result = 0;
            }
        } else if (value2 == null) {
            return -1;
        } else {
            // compare the results
            try {
                result = ((Comparable<Object>)value).compareTo(value2);
            } catch (ClassCastException cce) {
                // This should not occur, and indicates very odd values are being sorted on (i.e. byte arrays).
                // warn and ignore this sortorder
                log.warn("Cannot compare values " + value +" and " + value2 + " in sortorder field " +
                         sortOrder.getField().getFieldName() + " in step " + sortOrder.getField().getStep().getAlias());
                result = 0;
            }
        }
        // if the order of this field is descending,
        // then the result of the comparison is the reverse (the node is 'greater' if the value is 'less' )
        if (sortOrder.getDirection() == SortOrder.ORDER_DESCENDING) {
            result = -result;
        }
        return result;
    }

    /**
     * Does a field-by-field compare of two Node objects, on the fields used to order the nodes.
     * This is used to determine whether a node comes after or before another, in a certain query result.
     *
     * @return -1 if node1 is smaller than node 2, 0 if both nodes are equals, and +1 is node 1 is greater than node 2.
     * @since MMBase-1.8
     */
    public static int compare(Node node1, Node node2, List<SortOrder> sortOrders) {
        if (node1 == null) return -1;
        if (node2 == null) return +1;
        int result = 0;
        Iterator<SortOrder> i = sortOrders.iterator();
        while (result == 0 && i.hasNext()) {
            SortOrder order = i.next();
            result = compare(node1, node2, order);
        }
        // if all fields match - return 0 as if equal
        return result;
    }

    public static void main(String[] argv) {
        System.out.println(ConstraintParser.convertClauseToDBS("(([cpsettings.status]='[A]' OR [cpsettings.status]='I') AND [users.account] != '') and (lower([users.account]) LIKE '%t[est%' OR lower([users.email]) LIKE '%te]st%' OR lower([users.firstname]) LIKE '%t[e]st%' OR lower([users.lastname]) LIKE '%]test%')"));
    }

}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.