You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by mr...@apache.org on 2006/11/16 11:48:21 UTC

svn commit: r475677 [1/2] - in /jackrabbit/trunk/jackrabbit/src: main/java/org/apache/jackrabbit/core/query/ main/java/org/apache/jackrabbit/core/query/lucene/ main/java/org/apache/jackrabbit/core/query/sql/ main/java/org/apache/jackrabbit/core/query/x...

Author: mreutegg
Date: Thu Nov 16 02:48:20 2006
New Revision: 475677

URL: http://svn.apache.org/viewvc?view=rev&rev=475677
Log:
JCR-638: Support lower-/upper-case functions

Added:
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/PropertyFunctionQueryNode.java   (with props)
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java   (with props)
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java   (with props)
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java   (with props)
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java   (with props)
    jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/query/UpperLowerCaseQueryTest.java   (with props)
Modified:
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/DefaultQueryNodeVisitor.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNode.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNodeVisitor.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryTreeDump.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/RelationQueryNode.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/DefaultParserVisitor.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/JCRSQLQueryBuilder.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/QueryFormat.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/QueryFormat.java
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/XPathQueryBuilder.java
    jackrabbit/trunk/jackrabbit/src/main/javacc/sql/JCRSQL.jjt
    jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/query/TestAll.java

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/DefaultQueryNodeVisitor.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/DefaultQueryNodeVisitor.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/DefaultQueryNodeVisitor.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/DefaultQueryNodeVisitor.java Thu Nov 16 02:48:20 2006
@@ -69,4 +69,8 @@
     public Object visit(DerefQueryNode node, Object data) {
         return data;
     }
+
+    public Object visit(PropertyFunctionQueryNode node, Object data) {
+        return data;
+    }
 }

Added: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/PropertyFunctionQueryNode.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/PropertyFunctionQueryNode.java?view=auto&rev=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/PropertyFunctionQueryNode.java (added)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/PropertyFunctionQueryNode.java Thu Nov 16 02:48:20 2006
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.core.query;
+
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Collections;
+
+/**
+ * <code>PropertyFunctionQueryNode</code> allows to place function calls on properties
+ * in a query. Supported function names are:
+ * <ul>
+ * <li><code>upper-case</code> as specified in <a href="http://www.w3.org/TR/xquery-operators/#func-upper-case">fn:upper-case()</a></li>
+ * <li><code>lower-case</code> as specified in <a href="http://www.w3.org/TR/xquery-operators/#func-lower-case">fn:lower-case()</a></li>
+ * </ul>
+ */
+public class PropertyFunctionQueryNode extends QueryNode {
+
+    /**
+     * Requests that property values in a {@link RelationQueryNode} are
+     * converted to upper case before they are matched with the literal.
+     */
+    public static final String UPPER_CASE = "upper-case";
+
+    /**
+     * Requests that property values in a {@link RelationQueryNode} are
+     * converted to lower case before they are matched with the literal.
+     */
+    public static final String LOWER_CASE = "lower-case";
+
+    /**
+     * The set of supported function names.
+     */
+    private static final Set SUPPORTED_FUNCTION_NAMES;
+
+    static {
+        Set tmp = new HashSet();
+        tmp.add(UPPER_CASE);
+        tmp.add(LOWER_CASE);
+        SUPPORTED_FUNCTION_NAMES = Collections.unmodifiableSet(tmp);
+    }
+
+    /**
+     * The function name.
+     */
+    private final String functionName;
+
+    /**
+     * Creates a property function query node. This query node describes a
+     * function which is applied to a property parameter of the
+     * <code>parent</code> query node.
+     *
+     * @param parent       the query node where this function is applied to.
+     * @param functionName the name of the function which is applied to
+     *                     <code>parent</code>.
+     * @throws IllegalArgumentException if <code>functionName</code> is not a
+     *                                  supported function.
+     */
+    public PropertyFunctionQueryNode(QueryNode parent, String functionName)
+            throws IllegalArgumentException {
+        super(parent);
+        if (!SUPPORTED_FUNCTION_NAMES.contains(functionName)) {
+            throw new IllegalArgumentException("unknown function name");
+        }
+        this.functionName = functionName;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object accept(QueryNodeVisitor visitor, Object data) {
+        return visitor.visit(this, data);
+    }
+
+    /**
+     * Returns the type of this node.
+     *
+     * @return the type of this node.
+     */
+    public int getType() {
+        return QueryNode.TYPE_PROP_FUNCTION;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean equals(Object obj) {
+        if (obj instanceof PropertyFunctionQueryNode) {
+            PropertyFunctionQueryNode other = (PropertyFunctionQueryNode) obj;
+            return functionName.equals(other.functionName);
+        }
+        return false;
+    }
+
+    /**
+     * @return the name of this function.
+     */
+    public String getFunctionName() {
+        return functionName;
+    }
+}

Propchange: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/PropertyFunctionQueryNode.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNode.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNode.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNode.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNode.java Thu Nov 16 02:48:20 2006
@@ -59,6 +59,9 @@
     /** Type value for {@link DerefQueryNode} */
     public static final int TYPE_DEREF = 12;
 
+    /** Type value for {@link PropertyFunctionQueryNode} */
+    public static final int TYPE_PROP_FUNCTION = 13;
+
     /**
      * References the parent of this <code>QueryNode</code>. If this is the root
      * of a query tree, then <code>parent</code> is <code>null</code>.

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNodeVisitor.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNodeVisitor.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNodeVisitor.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryNodeVisitor.java Thu Nov 16 02:48:20 2006
@@ -44,4 +44,6 @@
     Object visit(OrderQueryNode node, Object data);
 
     Object visit(DerefQueryNode node, Object data);
+
+    Object visit(PropertyFunctionQueryNode node, Object data);
 }

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryTreeDump.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryTreeDump.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryTreeDump.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/QueryTreeDump.java Thu Nov 16 02:48:20 2006
@@ -235,6 +235,7 @@
             buffer.append(" Type=TIMESTAMP Value=").append(node.getDateValue());
         }
         buffer.append("\n");
+        traverse(node.getOperands(), buffer);
         return buffer;
     }
 
@@ -274,6 +275,15 @@
         }
         buffer.append("\n");
         traverse(node.getOperands(), buffer);
+        return buffer;
+    }
+
+    public Object visit(PropertyFunctionQueryNode node, Object data) {
+        StringBuffer buffer = (StringBuffer) data;
+        buffer.append(PADDING, 0, indent);
+        buffer.append("+ PropertyFunctionQueryNode: ");
+        buffer.append(node.getFunctionName());
+        buffer.append("()\n");
         return buffer;
     }
 

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/RelationQueryNode.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/RelationQueryNode.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/RelationQueryNode.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/RelationQueryNode.java Thu Nov 16 02:48:20 2006
@@ -23,7 +23,7 @@
 /**
  * Implements a query node that defines property value relation.
  */
-public class RelationQueryNode extends QueryNode implements QueryConstants {
+public class RelationQueryNode extends NAryQueryNode implements QueryConstants {
 
     /**
      * The name of the property

Added: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java?view=auto&rev=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java (added)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java Thu Nov 16 02:48:20 2006
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.core.query.lucene;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.TermEnum;
+import org.apache.lucene.search.MultiTermQuery;
+import org.apache.lucene.search.FilteredTermEnum;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * <code>CaseTermQuery</code> implements a term query which convert the term
+ * from the index either to upper or lower case before it is matched.
+ */
+abstract class CaseTermQuery extends MultiTermQuery implements TransformConstants {
+
+    /**
+     * Indicates whether terms from the index should be lower-cased or
+     * upper-cased.
+     */
+    protected final int transform;
+
+    CaseTermQuery(Term term, int transform) {
+        super(term);
+        this.transform = transform;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected FilteredTermEnum getEnum(IndexReader reader) throws IOException {
+        return new CaseTermEnum(reader);
+    }
+
+    static final class Upper extends CaseTermQuery {
+
+        Upper(Term term) {
+            super(term, TRANSFORM_UPPER_CASE);
+        }
+    }
+
+    static final class Lower extends CaseTermQuery {
+
+        Lower(Term term) {
+            super(term, TRANSFORM_LOWER_CASE);
+        }
+
+    }
+
+    private final class CaseTermEnum extends FilteredTermEnum {
+
+        private final int nameLength;
+
+        private final OffsetCharSequence termText;
+
+        private final OffsetCharSequence currentTerm;
+
+        CaseTermEnum(IndexReader reader) throws IOException {
+            // gather all terms that match
+            // keep them in order and remember the doc frequency as value
+            final Map orderedTerms = new LinkedHashMap();
+
+            Term term = getTerm();
+
+            // there are always two range scanse: one with an initial
+            // lower case character and another one with an initial upper case
+            // character
+            List rangeScans = new ArrayList(2);
+            nameLength = FieldNames.getNameLength(term.text());
+            String propName = term.text().substring(0, nameLength);
+            this.termText = new OffsetCharSequence(nameLength, term.text());
+            this.currentTerm = new OffsetCharSequence(nameLength, term.text(), transform);
+
+            try {
+                // start with a term using the lower case character for the first
+                // character of the value.
+                if (term.text().length() > nameLength) {
+                    // start with initial lower case
+                    StringBuffer lowerLimit = new StringBuffer(propName);
+                    lowerLimit.append(termText.toString().toUpperCase());
+                    lowerLimit.setCharAt(nameLength, Character.toLowerCase(lowerLimit.charAt(nameLength)));
+                    StringBuffer upperLimit = new StringBuffer(propName);
+                    upperLimit.append(termText.toString().toLowerCase());
+                    rangeScans.add(new RangeScan(reader,
+                            new Term(term.field(), lowerLimit.toString()),
+                            new Term(term.field(), upperLimit.toString())));
+
+                    // second scan with upper case start
+                    lowerLimit = new StringBuffer(propName);
+                    lowerLimit.append(termText.toString().toUpperCase());
+                    upperLimit = new StringBuffer(propName);
+                    upperLimit.append(termText.toString().toLowerCase());
+                    upperLimit.setCharAt(nameLength, Character.toUpperCase(upperLimit.charAt(nameLength)));
+                    rangeScans.add(new RangeScan(reader,
+                            new Term(term.field(), lowerLimit.toString()),
+                            new Term(term.field(), upperLimit.toString())));
+
+                } else {
+                    // use term as is
+                    rangeScans.add(new RangeScan(reader, term, term));
+                }
+
+                for (Iterator it = rangeScans.iterator(); it.hasNext(); ) {
+                    TermEnum terms = (TermEnum) it.next();
+                    do {
+                        Term t = terms.term();
+                        if (t != null) {
+                            currentTerm.setBase(t.text());
+                            int compare = currentTerm.compareTo(termText);
+                            if (compare == 0) {
+                                orderedTerms.put(t, new Integer(terms.docFreq()));
+                            } else if (compare < 0) {
+                                // try next one
+                            } else {
+                                // compare > 0
+                            }
+                        } else {
+                            break;
+                        }
+                    } while (terms.next());
+                }
+            } finally {
+                for (Iterator it = rangeScans.iterator(); it.hasNext(); ) {
+                    TermEnum terms = (TermEnum) it.next();
+                    try {
+                        terms.close();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                }
+            }
+
+            final Iterator it = orderedTerms.keySet().iterator();
+
+            setEnum(new TermEnum() {
+
+                private Term current;
+
+                {
+                    getNext();
+                }
+
+                public boolean next() {
+                    getNext();
+                    return current != null;
+                }
+
+                public Term term() {
+                    return current;
+                }
+
+                public int docFreq() {
+                    Integer docFreq = (Integer) orderedTerms.get(current);
+                    return docFreq != null ? docFreq.intValue() : 0;
+                }
+
+                public void close() {
+                    // nothing to close
+                }
+
+                private void getNext() {
+                    current = it.hasNext() ? (Term) it.next() : null;
+                }
+            });
+        }
+
+        protected boolean termCompare(Term term) {
+            // they all match
+            return true;
+        }
+
+        protected float difference() {
+            return 1.0f;
+        }
+
+        protected boolean endEnum() {
+            // todo correct?
+            return false;
+        }
+    }
+}

Propchange: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java Thu Nov 16 02:48:20 2006
@@ -82,4 +82,16 @@
         return fieldName + '\uFFFF' + value;
     }
 
+    /**
+     * Returns the length of the field prefix in <code>namedValue</code>. See
+     * also {@link #createNamedValue(String, String)}. If <code>namedValue</code>
+     * does not contain a name prefix, this method return 0.
+     *
+     * @param namedValue the named value as created by {@link #createNamedValue(String, String)}.
+     * @return the length of the field prefix including the separator char
+     *         (\uFFFF).
+     */
+    public static int getNameLength(String namedValue) {
+        return namedValue.indexOf('\uFFFF') + 1;
+    }
 }

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java Thu Nov 16 02:48:20 2006
@@ -33,6 +33,8 @@
 import org.apache.jackrabbit.core.query.QueryRootNode;
 import org.apache.jackrabbit.core.query.RelationQueryNode;
 import org.apache.jackrabbit.core.query.TextsearchQueryNode;
+import org.apache.jackrabbit.core.query.PropertyFunctionQueryNode;
+import org.apache.jackrabbit.core.query.DefaultQueryNodeVisitor;
 import org.apache.jackrabbit.core.query.lucene.fulltext.QueryParser;
 import org.apache.jackrabbit.core.query.lucene.fulltext.ParseException;
 import org.apache.jackrabbit.core.state.ItemStateManager;
@@ -585,6 +587,19 @@
             return data;
         }
 
+        // get property transformation
+        final int[] transform = new int[]{TransformConstants.TRANSFORM_NONE};
+        node.acceptOperands(new DefaultQueryNodeVisitor() {
+            public Object visit(PropertyFunctionQueryNode node, Object data) {
+                if (node.getFunctionName().equals(PropertyFunctionQueryNode.LOWER_CASE)) {
+                    transform[0] = TransformConstants.TRANSFORM_LOWER_CASE;
+                } else if (node.getFunctionName().equals(PropertyFunctionQueryNode.UPPER_CASE)) {
+                    transform[0] = TransformConstants.TRANSFORM_UPPER_CASE;
+                }
+                return data;
+            }
+        }, null);
+
         String field = "";
         try {
             field = NameFormat.format(node.getProperty(), nsMappings);
@@ -598,8 +613,17 @@
             case QueryConstants.OPERATION_EQ_GENERAL:
                 BooleanQuery or = new BooleanQuery();
                 for (int i = 0; i < stringValues.length; i++) {
-                    or.add(new TermQuery(new Term(FieldNames.PROPERTIES,
-                            FieldNames.createNamedValue(field, stringValues[i]))), false, false);
+                    Term t = new Term(FieldNames.PROPERTIES,
+                                FieldNames.createNamedValue(field, stringValues[i]));
+                    Query q;
+                    if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE) {
+                        q = new CaseTermQuery.Upper(t);
+                    } else if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE) {
+                        q = new CaseTermQuery.Lower(t);
+                    } else {
+                        q = new TermQuery(t);
+                    }
+                    or.add(q, false, false);
                 }
                 query = or;
                 if (node.getOperation() == QueryConstants.OPERATION_EQ_VALUE) {
@@ -612,7 +636,7 @@
                 for (int i = 0; i < stringValues.length; i++) {
                     Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                     Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "\uFFFF"));
-                    or.add(new RangeQuery(lower, upper, true), false, false);
+                    or.add(new RangeQuery(lower, upper, true, transform[0]), false, false);
                 }
                 query = or;
                 if (node.getOperation() == QueryConstants.OPERATION_GE_VALUE) {
@@ -625,7 +649,7 @@
                 for (int i = 0; i < stringValues.length; i++) {
                     Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                     Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "\uFFFF"));
-                    or.add(new RangeQuery(lower, upper, false), false, false);
+                    or.add(new RangeQuery(lower, upper, false, transform[0]), false, false);
                 }
                 query = or;
                 if (node.getOperation() == QueryConstants.OPERATION_GT_VALUE) {
@@ -638,7 +662,7 @@
                 for (int i = 0; i < stringValues.length; i++) {
                     Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, ""));
                     Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
-                    or.add(new RangeQuery(lower, upper, true), false, false);
+                    or.add(new RangeQuery(lower, upper, true, transform[0]), false, false);
                 }
                 query = or;
                 if (node.getOperation() == QueryConstants.OPERATION_LE_VALUE) {
@@ -651,7 +675,7 @@
                 if (stringValues[0].equals("%")) {
                     query = new MatchAllQuery(field);
                 } else {
-                    query = new WildcardQuery(FieldNames.PROPERTIES, field, stringValues[0]);
+                    query = new WildcardQuery(FieldNames.PROPERTIES, field, stringValues[0], transform[0]);
                 }
                 break;
             case QueryConstants.OPERATION_LT_VALUE:      // <
@@ -660,7 +684,7 @@
                 for (int i = 0; i < stringValues.length; i++) {
                     Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, ""));
                     Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
-                    or.add(new RangeQuery(lower, upper, false), false, false);
+                    or.add(new RangeQuery(lower, upper, false, transform[0]), false, false);
                 }
                 query = or;
                 if (node.getOperation() == QueryConstants.OPERATION_LT_VALUE) {
@@ -674,7 +698,15 @@
                 // exclude all nodes where 'field' has the term in question
                 for (int i = 0; i < stringValues.length; i++) {
                     Term t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
-                    notQuery.add(new TermQuery(t), false, true);
+                    Query q;
+                    if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE) {
+                        q = new CaseTermQuery.Upper(t);
+                    } else if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE) {
+                        q = new CaseTermQuery.Lower(t);
+                    } else {
+                        q = new TermQuery(t);
+                    }
+                    notQuery.add(q, false, true);
                 }
                 // and exclude all nodes where 'field' is multi valued
                 notQuery.add(new TermQuery(new Term(FieldNames.MVP, field)), false, true);
@@ -694,7 +726,15 @@
                     Term t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                     Query svp = new NotQuery(new TermQuery(new Term(FieldNames.MVP, field)));
                     BooleanQuery and = new BooleanQuery();
-                    and.add(new TermQuery(t), true, false);
+                    Query q;
+                    if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE) {
+                        q = new CaseTermQuery.Upper(t);
+                    } else if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE) {
+                        q = new CaseTermQuery.Lower(t);
+                    } else {
+                        q = new TermQuery(t);
+                    }
+                    and.add(q, true, false);
                     and.add(svp, true, false);
                     notQuery.add(and, false, true);
                 }
@@ -716,6 +756,10 @@
     }
 
     public Object visit(OrderQueryNode node, Object data) {
+        return data;
+    }
+
+    public Object visit(PropertyFunctionQueryNode node, Object data) {
         return data;
     }
 

Added: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java?view=auto&rev=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java (added)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java Thu Nov 16 02:48:20 2006
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.core.query.lucene;
+
+/**
+ * CharSequence that applies an offset to a base CharSequence. The base
+ * CharSequence can be replaced without creating a new CharSequence.
+ */
+final class OffsetCharSequence implements CharSequence, Comparable, TransformConstants {
+
+    /**
+     * Indicates how the underlying char sequence is exposed / tranformed.
+     */
+    private final int transform;
+
+    /**
+     * The offset to apply to the base CharSequence
+     */
+    private final int offset;
+
+    /**
+     * The base character sequence
+     */
+    private CharSequence base;
+
+    /**
+     * Creates a new OffsetCharSequence with an <code>offset</code>.
+     *
+     * @param offset    the offset
+     * @param base      the base CharSequence
+     * @param transform how the <code>base</code> char sequence is
+     *                  tranformed.
+     */
+    OffsetCharSequence(int offset, CharSequence base, int transform) {
+        this.offset = offset;
+        this.base = base;
+        this.transform = transform;
+    }
+
+    /**
+     * Creates a new OffsetCharSequence with an <code>offset</code>.
+     *
+     * @param offset    the offset
+     * @param base      the base CharSequence
+     */
+    OffsetCharSequence(int offset, CharSequence base) {
+        this(offset, base, TRANSFORM_NONE);
+    }
+
+    /**
+     * Sets a new base sequence.
+     *
+     * @param base the base character sequence
+     */
+    public void setBase(CharSequence base) {
+        this.base = base;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public int length() {
+        return base.length() - offset;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public char charAt(int index) {
+        if (transform == TRANSFORM_NONE) {
+            return base.charAt(index + offset);
+        } else if (transform == TRANSFORM_LOWER_CASE) {
+            return Character.toLowerCase(base.charAt(index + offset));
+        } else if (transform == TRANSFORM_UPPER_CASE) {
+            return Character.toUpperCase(base.charAt(index + offset));
+        }
+        // shouldn't get here. return plain character
+        return base.charAt(index + offset);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public CharSequence subSequence(int start, int end) {
+        CharSequence seq = base.subSequence(start + offset, end + offset);
+        if (transform != TRANSFORM_NONE) {
+            seq = new OffsetCharSequence(0, seq, transform);
+        }
+        return seq;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public String toString() {
+        if (transform == TRANSFORM_NONE) {
+            return base.subSequence(offset, base.length()).toString();
+        } else {
+            int len = length();
+            StringBuffer buf = new StringBuffer(len);
+            for (int i = 0; i < len; i++) {
+                buf.append(charAt(i));
+            }
+            return buf.toString();
+        }
+    }
+
+    //-----------------------------< Comparable >-------------------------------
+
+    /**
+     * Compares this char sequence to another char sequence <code>o</code>.
+     *
+     * @param o the other char sequence.
+     * @return as defined in {@link String#compareTo(Object)} but also takes
+     *         {@link #transform} into account.
+     */
+    public int compareTo(Object o) {
+        OffsetCharSequence other = (OffsetCharSequence) o;
+        int len1 = length();
+        int len2 = other.length();
+        int lim = Math.min(len1, len2);
+
+        for (int i = 0; i  < lim; i++) {
+            char c1 = charAt(i);
+            char c2 = other.charAt(i);
+            if (c1 != c2) {
+                return c1 - c2;
+            }
+        }
+        return len1 - len2;
+    }
+}

Propchange: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java Thu Nov 16 02:48:20 2006
@@ -35,6 +35,9 @@
 import java.util.BitSet;
 import java.util.Map;
 import java.util.WeakHashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Iterator;
 
 /**
  * Implements a variant of the lucene class {@link org.apache.lucene.search.RangeQuery}.
@@ -42,7 +45,7 @@
  * but will calculate the matching documents itself. That way a
  * <code>TooManyClauses</code> can be avoided.
  */
-public class RangeQuery extends Query {
+public class RangeQuery extends Query implements TransformConstants {
 
     /**
      * Logger instance for this class.
@@ -73,14 +76,21 @@
     private boolean inclusive;
 
     /**
+     * How the term enum is transformed before it is compared to lower and upper
+     * term.
+     */
+    private final int transform;
+
+    /**
      * Creates a new RangeQuery. The lower or the upper term may be
      * <code>null</code>, but not both!
      *
      * @param lowerTerm the lower term of the interval, or <code>null</code>
      * @param upperTerm the upper term of the interval, or <code>null</code>.
      * @param inclusive if <code>true</code> the interval is inclusive.
+     * @param transform how term enums are transformed when read from the index.
      */
-    public RangeQuery(Term lowerTerm, Term upperTerm, boolean inclusive) {
+    public RangeQuery(Term lowerTerm, Term upperTerm, boolean inclusive, int transform) {
         if (lowerTerm == null && upperTerm == null) {
             throw new IllegalArgumentException("At least one term must be non-null");
         }
@@ -97,6 +107,7 @@
 
         this.upperTerm = upperTerm;
         this.inclusive = inclusive;
+        this.transform = transform;
     }
 
     /**
@@ -109,13 +120,19 @@
      * @throws IOException if an error occurs.
      */
     public Query rewrite(IndexReader reader) throws IOException {
-        Query stdRangeQueryImpl
-                = new org.apache.lucene.search.RangeQuery(lowerTerm, upperTerm, inclusive);
-        try {
-            return stdRangeQueryImpl.rewrite(reader);
-        } catch (BooleanQuery.TooManyClauses e) {
-            log.debug("Too many terms to enumerate, using custom RangeQuery");
-            // failed, use own implementation
+        if (transform == TRANSFORM_NONE) {
+            Query stdRangeQueryImpl
+                    = new org.apache.lucene.search.RangeQuery(lowerTerm, upperTerm, inclusive);
+            try {
+                return stdRangeQueryImpl.rewrite(reader);
+            } catch (BooleanQuery.TooManyClauses e) {
+                log.debug("Too many terms to enumerate, using custom RangeQuery");
+                // failed, use own implementation
+                return this;
+            }
+        } else {
+            // always use our implementation when we need to transform the
+            // term enum
             return this;
         }
     }
@@ -283,6 +300,8 @@
             key.append(upperTerm != null ? upperTerm.text() : "");
             key.append('\uFFFF');
             key.append(inclusive);
+            key.append('\uFFFF');
+            key.append(transform);
             this.cacheKey = key.toString();
             // check cache
             synchronized (cache) {
@@ -352,53 +371,121 @@
                 return;
             }
 
-            TermEnum enumerator = reader.terms(lowerTerm);
+            String testField = getField();
 
-            try {
-                boolean checkLower = false;
-                if (!inclusive) {
-                    // make adjustments to set to exclusive
-                    checkLower = true;
-                }
+            boolean checkLower = false;
+            if (!inclusive || transform != TRANSFORM_NONE) {
+                // make adjustments to set to exclusive
+                checkLower = true;
+            }
 
-                String testField = getField();
+            int propNameLength = FieldNames.getNameLength(lowerTerm.text());
+            String namePrefix = "";
+            if (propNameLength > 0) {
+                namePrefix = lowerTerm.text().substring(0, propNameLength);
+            }
+            List startTerms = new ArrayList(2);
 
-                TermDocs docs = reader.termDocs();
+            if (transform == TRANSFORM_NONE || lowerTerm.text().length() <= propNameLength) {
+                // use lowerTerm as is
+                startTerms.add(lowerTerm);
+            } else {
+                // first enumerate terms using lower case start character
+                StringBuffer termText = new StringBuffer(propNameLength + 1);
+                termText.append(lowerTerm.text().subSequence(0, propNameLength));
+                char startCharacter = lowerTerm.text().charAt(propNameLength);
+                termText.append(Character.toLowerCase(startCharacter));
+                startTerms.add(new Term(lowerTerm.field(), termText.toString()));
+                // second enumerate terms using upper case start character
+                termText.setCharAt(termText.length() - 1, Character.toUpperCase(startCharacter));
+                startTerms.add(new Term(lowerTerm.field(), termText.toString()));
+            }
+
+            for (Iterator it = startTerms.iterator(); it.hasNext(); ) {
+                Term startTerm = (Term) it.next();
+
+                TermEnum terms = reader.terms(startTerm);
                 try {
-                    do {
-                        Term term = enumerator.term();
-                        if (term != null && term.field() == testField) {
-                            if (!checkLower || term.text().compareTo(lowerTerm.text()) > 0) {
-                                checkLower = false;
+                    TermDocs docs = reader.termDocs();
+                    try {
+                        do {
+                            Term term = terms.term();
+                            if (term != null && term.field() == testField) {
+                                if (checkLower) {
+                                    int compare = termCompare(term.text(), lowerTerm.text(), propNameLength);
+                                    if (compare > 0 || compare == 0 && inclusive) {
+                                        // do not check lower term anymore if no
+                                        // transformation is done on the term enum
+                                        checkLower = transform == TRANSFORM_NONE ? false : true;
+                                    } else {
+                                        // continue with next term
+                                        continue;
+                                    }
+                                }
                                 if (upperTerm != null) {
-                                    int compare = upperTerm.text().compareTo(term.text());
+                                    int compare = termCompare(term.text(), upperTerm.text(), propNameLength);
                                     // if beyond the upper term, or is exclusive and
-                                    // this is equal to the upper term, break out
-                                    if ((compare < 0) || (!inclusive && compare == 0)) {
-                                        break;
+                                    // this is equal to the upper term
+                                    if ((compare > 0) || (!inclusive && compare == 0)) {
+                                        // only break out if no transformation
+                                        // was done on the term from the enum
+                                        if (transform == TRANSFORM_NONE) {
+                                            break;
+                                        } else {
+                                            // because of the transformation
+                                            // it is possible that the next
+                                            // term will be included again if
+                                            // we still enumerate on the same
+                                            // property name
+                                            if (term.text().startsWith(namePrefix)) {
+                                                continue;
+                                            } else {
+                                                break;
+                                            }
+                                        }
                                     }
                                 }
 
-                                docs.seek(enumerator);
+                                docs.seek(terms);
                                 while (docs.next()) {
                                     hits.set(docs.doc());
                                 }
+                            } else {
+                                break;
                             }
-                        } else {
-                            break;
-                        }
-                    } while (enumerator.next());
+                        } while(terms.next());
+                    } finally {
+                        docs.close();
+                    }
                 } finally {
-                    docs.close();
+                    terms.close();
                 }
-            } finally {
-                enumerator.close();
             }
+
             hitsCalculated = true;
             // put to cache
             synchronized (resultMap) {
                 resultMap.put(cacheKey, hits);
             }
+        }
+
+        /**
+         * Compares the <code>text</code> with the <code>other</code> String. This
+         * implementation behaves like {@link String#compareTo(Object)} but also
+         * respects the {@link RangeQuery#transform} property.
+         *
+         * @param text   the text to compare to <code>other</code>. The
+         *               transformation function is applied to this parameter before
+         *               it is compared to <code>other</code>.
+         * @param other  the other String.
+         * @param offset start comparing the two strings at <code>offset</code>.
+         * @return see {@link String#compareTo(Object)}. But also respects {@link
+         *         #transform}.
+         */
+        private int termCompare(String text, String other, int offset) {
+            OffsetCharSequence seq1 = new OffsetCharSequence(offset, text, transform);
+            OffsetCharSequence seq2 = new OffsetCharSequence(offset, other);
+            return seq1.compareTo(seq2);
         }
     }
 }

Added: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java?view=auto&rev=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java (added)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java Thu Nov 16 02:48:20 2006
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.core.query.lucene;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.FilteredTermEnum;
+
+import java.io.IOException;
+
+/**
+ * <code>RangeScan</code> implements a range scan on terms.
+ */
+class RangeScan extends FilteredTermEnum {
+
+    private final Term upper;
+
+    private boolean endEnum = false;
+
+    /**
+     * Scans the index beginning at <code>lower</code> Term to <code>upper</code>.
+     * @param reader the index reader;
+     * @param lower the lower limit.
+     * @param upper the upper limit.
+     */
+    RangeScan(IndexReader reader, Term lower, Term upper) throws IOException {
+        this.upper = upper;
+        setEnum(reader.terms(lower));
+    }
+
+    protected boolean termCompare(Term term) {
+        int compare = term.compareTo(upper);
+        if (compare > 0) {
+            endEnum = true;
+        }
+        return compare <= 0;
+    }
+
+    protected float difference() {
+        return 1.0f;
+    }
+
+    protected boolean endEnum() {
+        return endEnum;
+    }
+}

Propchange: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java?view=auto&rev=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java (added)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java Thu Nov 16 02:48:20 2006
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.core.query.lucene;
+
+/**
+ * <code>TransformConstants</code> defines constants for query processing.
+ */
+public interface TransformConstants {
+
+    /**
+     * No transformation is done on the term enum.
+     */
+    static final int TRANSFORM_NONE = 0;
+
+    /**
+     * The underlying term enum is transformed to lower case characters.
+     */
+    static final int TRANSFORM_LOWER_CASE = 1;
+
+    /**
+     * The underlying term enum is transformed to upper case characters.
+     */
+    static final int TRANSFORM_UPPER_CASE = 2;
+
+}

Propchange: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java Thu Nov 16 02:48:20 2006
@@ -48,7 +48,7 @@
  * <li><code>_</code> : matches exactly one character</li>
  * </ul>
  */
-public class WildcardQuery extends Query {
+public class WildcardQuery extends Query implements TransformConstants {
 
     /**
      * Logger instance for this class.
@@ -71,6 +71,12 @@
     private final String pattern;
 
     /**
+     * How property values are tranformed before they are matched using the
+     * provided pattern.
+     */
+    private final int transform;
+
+    /**
      * Simple result cache for previously calculated hits.
      * key=IndexReader value=Map{key=String:pattern,value=BitSet:hits}
      */
@@ -79,14 +85,28 @@
     /**
      * Creates a new <code>WildcardQuery</code>.
      *
-     * @param field the name of the field to search.
-     * @param propName name of the property to search.
-     * @param pattern the wildcard pattern.
+     * @param field     the name of the field to search.
+     * @param propName  name of the property to search.
+     * @param pattern   the wildcard pattern.
+     * @param transform how property values are transformed before they are
+     *                  matched using the <code>pattern</code>.
      */
-    public WildcardQuery(String field, String propName, String pattern) {
+    public WildcardQuery(String field, String propName, String pattern, int transform) {
         this.field = field.intern();
         this.propName = propName;
         this.pattern = pattern;
+        this.transform = transform;
+    }
+
+    /**
+     * Creates a new <code>WildcardQuery</code>.
+     *
+     * @param field    the name of the field to search.
+     * @param propName name of the property to search.
+     * @param pattern  the wildcard pattern.
+     */
+    public WildcardQuery(String field, String propName, String pattern) {
+        this(field, propName, pattern, TRANSFORM_NONE);
     }
 
     /**
@@ -101,7 +121,7 @@
     public Query rewrite(IndexReader reader) throws IOException {
         Query stdWildcardQuery = new MultiTermQuery(new Term(FieldNames.PROPERTIES, pattern)) {
             protected FilteredTermEnum getEnum(IndexReader reader) throws IOException {
-                return new WildcardTermEnum(reader, field, propName, pattern);
+                return new WildcardTermEnum(reader, field, propName, pattern, transform);
             }
         };
         try {
@@ -245,7 +265,7 @@
         WildcardQueryScorer(Similarity similarity, IndexReader reader) {
             super(similarity);
             this.reader = reader;
-            this.cacheKey = field + '\uFFFF' + propName + '\uFFFF' + pattern;
+            this.cacheKey = field + '\uFFFF' + propName + '\uFFFF' + transform +'\uFFFF' + pattern;
             // check cache
             synchronized (cache) {
                 Map m = (Map) cache.get(reader);
@@ -313,7 +333,7 @@
             if (hitsCalculated) {
                 return;
             }
-            TermEnum terms = new WildcardTermEnum(reader, field, propName, pattern);
+            TermEnum terms = new WildcardTermEnum(reader, field, propName, pattern, transform);
             try {
                 // use unpositioned TermDocs
                 TermDocs docs = reader.termDocs();

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java Thu Nov 16 02:48:20 2006
@@ -18,17 +18,23 @@
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.Term;
+import org.apache.lucene.index.TermEnum;
 import org.apache.lucene.search.FilteredTermEnum;
 
 import java.io.IOException;
 import java.util.regex.Pattern;
 import java.util.regex.Matcher;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Iterator;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Implements a wildcard term enum that optionally supports embedded property
  * names in lucene term texts.
  */
-class WildcardTermEnum extends FilteredTermEnum {
+class WildcardTermEnum extends FilteredTermEnum implements TransformConstants {
 
     /**
      * The pattern matcher.
@@ -56,20 +62,35 @@
     private final OffsetCharSequence input;
 
     /**
+     * How terms from the index are transformed.
+     */
+    private final int transform;
+
+    /**
      * Creates a new <code>WildcardTermEnum</code>.
      *
-     * @param reader the index reader.
-     * @param field the lucene field to search.
-     * @param propName the embedded jcr property name or <code>null</code> if
-     *   there is not embedded property name.
-     * @param pattern the pattern to match the values.
-     * @throws IOException if an error occurs while reading from the index.
+     * @param reader    the index reader.
+     * @param field     the lucene field to search.
+     * @param propName  the embedded jcr property name or <code>null</code> if
+     *                  there is not embedded property name.
+     * @param pattern   the pattern to match the values.
+     * @param transform the transformation that should be applied to the term
+     *                  enum from the index reader.
+     * @throws IOException              if an error occurs while reading from
+     *                                  the index.
+     * @throws IllegalArgumentException if <code>transform</code> is not a valid
+     *                                  value.
      */
     public WildcardTermEnum(IndexReader reader,
                             String field,
                             String propName,
-                            String pattern) throws IOException {
+                            String pattern,
+                            int transform) throws IOException {
+        if (transform < TRANSFORM_NONE || transform > TRANSFORM_UPPER_CASE) {
+            throw new IllegalArgumentException("invalid transform parameter");
+        }
         this.field = field;
+        this.transform = transform;
 
         int idx = 0;
         while (idx < pattern.length()
@@ -84,22 +105,31 @@
         }
 
         // initialize with prefix as dummy value
-        input = new OffsetCharSequence(prefix.length(), prefix);
+        input = new OffsetCharSequence(prefix.length(), prefix, transform);
         this.pattern = createRegexp(pattern.substring(idx)).matcher(input);
 
-        setEnum(reader.terms(new Term(field, prefix)));
+        if (transform == TRANSFORM_NONE) {
+            setEnum(reader.terms(new Term(field, prefix)));
+        } else {
+            setEnum(new LowerUpperCaseTermEnum(reader, field, propName, pattern, transform));
+        }
     }
 
     /**
      * @inheritDoc
      */
     protected boolean termCompare(Term term) {
-        if (term.field() == field && term.text().startsWith(prefix)) {
-            input.setBase(term.text());
-            return pattern.reset().matches();
+        if (transform == TRANSFORM_NONE) {
+            if (term.field() == field && term.text().startsWith(prefix)) {
+                input.setBase(term.text());
+                return pattern.reset().matches();
+            }
+            endEnum = true;
+            return false;
+        } else {
+            // pre filtered, no need to check
+            return true;
         }
-        endEnum = true;
-        return false;
     }
 
     /**
@@ -169,67 +199,143 @@
     }
 
     /**
-     * CharSequence that applies an offset to a base CharSequence. The base
-     * CharSequence can be replaced without creating a new CharSequence.
+     * Implements a term enum which respects the transformation flag and
+     * matches a pattern on the enumerated terms.
      */
-    private static final class OffsetCharSequence implements CharSequence {
+    private class LowerUpperCaseTermEnum extends TermEnum {
 
         /**
-         * The offset to apply to the base CharSequence
+         * The matching terms
          */
-        private final int offset;
+        private final Map orderedTerms = new LinkedHashMap();
 
         /**
-         * The base character sequence
+         * Iterator over all matching terms
          */
-        private CharSequence base;
+        private final Iterator it;
+
+        public LowerUpperCaseTermEnum(IndexReader reader,
+                                      String field,
+                                      String propName,
+                                      String pattern,
+                                      int transform) throws IOException {
+            if (transform != TRANSFORM_LOWER_CASE && transform != TRANSFORM_UPPER_CASE) {
+                throw new IllegalArgumentException("transform");
+            }
+
+            // create range scans
+            List rangeScans = new ArrayList(2);
+            try {
+                int idx = 0;
+                while (idx < pattern.length()
+                        && Character.isLetterOrDigit(pattern.charAt(idx))) {
+                    idx++;
+                }
+                String patternPrefix = pattern.substring(0, idx);
+                if (patternPrefix.length() == 0) {
+                    // scan full property range
+                    String prefix = FieldNames.createNamedValue(propName, "");
+                    String limit = FieldNames.createNamedValue(propName, "\uFFFF");
+                    rangeScans.add(new RangeScan(reader,
+                            new Term(field, prefix), new Term(field, limit)));
+                } else {
+                    // start with initial lower case
+                    StringBuffer lowerLimit = new StringBuffer(patternPrefix.toUpperCase());
+                    lowerLimit.setCharAt(0, Character.toLowerCase(lowerLimit.charAt(0)));
+                    String prefix = FieldNames.createNamedValue(propName, lowerLimit.toString());
+
+                    StringBuffer upperLimit = new StringBuffer(patternPrefix.toLowerCase());
+                    upperLimit.append('\uFFFF');
+                    String limit = FieldNames.createNamedValue(propName, upperLimit.toString());
+                    rangeScans.add(new RangeScan(reader,
+                            new Term(field, prefix), new Term(field, limit)));
+
+                    // second scan with upper case start
+                    prefix = FieldNames.createNamedValue(propName, patternPrefix.toUpperCase());
+                    upperLimit = new StringBuffer(patternPrefix.toLowerCase());
+                    upperLimit.setCharAt(0, Character.toUpperCase(upperLimit.charAt(0)));
+                    upperLimit.append('\uFFFF');
+                    limit = FieldNames.createNamedValue(propName, upperLimit.toString());
+                    rangeScans.add(new RangeScan(reader,
+                            new Term(field, prefix), new Term(field, limit)));
+                }
+
+                String prefix = FieldNames.createNamedValue(propName, patternPrefix);
+                // initialize with prefix as dummy value
+                OffsetCharSequence input = new OffsetCharSequence(prefix.length(), prefix, transform);
+                Matcher matcher = createRegexp(pattern.substring(idx)).matcher(input);
+
+                // do range scans with patter matcher
+                for (Iterator it = rangeScans.iterator(); it.hasNext(); ) {
+                    RangeScan scan = (RangeScan) it.next();
+                    do {
+                        Term t = scan.term();
+                        if (t != null) {
+                            input.setBase(t.text());
+                            if (matcher.reset().matches()) {
+                                orderedTerms.put(t, new Integer(scan.docFreq()));
+                            }
+                        }
+                    } while (scan.next());
+                }
+
+            } finally {
+                // close range scans
+                for (Iterator it = rangeScans.iterator(); it.hasNext(); ) {
+                    RangeScan scan = (RangeScan) it.next();
+                    try {
+                        scan.close();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                }
+            }
+
+            it = orderedTerms.keySet().iterator();
+            getNext();
+        }
 
         /**
-         * Creates a new OffsetCharSequence with an <code>offset</code>.
-         *
-         * @param offset the offset
-         * @param base the base CharSequence
+         * The current term in this enum.
          */
-        OffsetCharSequence(int offset, CharSequence base) {
-            this.offset = offset;
-            this.base = base;
-        }
+        private Term current;
 
         /**
-         * Sets a new base sequence.
-         *
-         * @param base the base character sequence
+         * {@inheritDoc}
          */
-        public void setBase(CharSequence base) {
-            this.base = base;
+        public boolean next() {
+            getNext();
+            return current != null;
         }
 
         /**
-         * @inheritDoc
+         * {@inheritDoc}
          */
-        public int length() {
-            return base.length() - offset;
+        public Term term() {
+            return current;
         }
 
         /**
-         * @inheritDoc
+         * {@inheritDoc}
          */
-        public char charAt(int index) {
-            return base.charAt(index + offset);
+        public int docFreq() {
+            Integer docFreq = (Integer) orderedTerms.get(current);
+            return docFreq != null ? docFreq.intValue() : 0;
         }
 
         /**
-         * @inheritDoc
+         * {@inheritDoc}
          */
-        public CharSequence subSequence(int start, int end) {
-            return base.subSequence(start + offset, end + offset);
+        public void close() {
+            // nothing to do here
         }
 
         /**
-         * @inheritDoc
+         * Sets the current field to the next term in this enum or to
+         * <code>null</code> if there is no next.
          */
-        public String toString() {
-            return base.subSequence(offset, base.length()).toString();
+        private void getNext() {
+            current = it.hasNext() ? (Term) it.next() : null;
         }
     }
 }

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/DefaultParserVisitor.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/DefaultParserVisitor.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/DefaultParserVisitor.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/DefaultParserVisitor.java Thu Nov 16 02:48:20 2006
@@ -89,4 +89,12 @@
     public Object visit(ASTDescendingOrderSpec node, Object data) {
         return data;
     }
+
+    public Object visit(ASTLowerFunction node, Object data) {
+        return data;
+    }
+
+    public Object visit(ASTUpperFunction node, Object data) {
+        return data;
+    }
 }

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/JCRSQLQueryBuilder.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/JCRSQLQueryBuilder.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/JCRSQLQueryBuilder.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/JCRSQLQueryBuilder.java Thu Nov 16 02:48:20 2006
@@ -29,6 +29,7 @@
 import org.apache.jackrabbit.core.query.QueryRootNode;
 import org.apache.jackrabbit.core.query.RelationQueryNode;
 import org.apache.jackrabbit.core.query.TextsearchQueryNode;
+import org.apache.jackrabbit.core.query.PropertyFunctionQueryNode;
 import org.apache.jackrabbit.name.IllegalNameException;
 import org.apache.jackrabbit.name.NamespaceResolver;
 import org.apache.jackrabbit.name.QName;
@@ -295,6 +296,30 @@
                     value[0] = node;
                     return data;
                 }
+
+                public Object visit(ASTLowerFunction node, Object data) {
+                    getIdentifier(node);
+                    return data;
+                }
+
+                public Object visit(ASTUpperFunction node, Object data) {
+                    getIdentifier(node);
+                    return data;
+                }
+
+                private void getIdentifier(SimpleNode node) {
+                    if (node.jjtGetNumChildren() > 0) {
+                        Node n = node.jjtGetChild(0);
+                        if (n instanceof ASTIdentifier) {
+                            ASTIdentifier identifier = (ASTIdentifier) n;
+                            if (tmp[0] == null) {
+                                tmp[0] = identifier.getName();
+                            } else if (tmp[1] == null) {
+                                tmp[1] = identifier.getName();
+                            }
+                        }
+                    }
+                }
             }, data);
             QName identifier = tmp[0];
 
@@ -312,9 +337,11 @@
                 AndQueryNode between = new AndQueryNode(parent);
                 RelationQueryNode rel = createRelationQueryNode(between,
                         identifier, QueryConstants.OPERATION_GE_GENERAL, (ASTLiteral) node.children[1]);
+                node.childrenAccept(this, rel);
                 between.addOperand(rel);
                 rel = createRelationQueryNode(between,
                         identifier, QueryConstants.OPERATION_LE_GENERAL, (ASTLiteral) node.children[2]);
+                node.childrenAccept(this, rel);
                 between.addOperand(rel);
                 predicateNode = between;
             } else if (type == QueryConstants.OPERATION_GE_GENERAL
@@ -325,6 +352,7 @@
                     || type == QueryConstants.OPERATION_EQ_GENERAL) {
                 predicateNode = createRelationQueryNode(parent,
                         identifier, type, value[0]);
+                node.childrenAccept(this, predicateNode);
             } else if (type == QueryConstants.OPERATION_LIKE) {
                 ASTLiteral pattern = value[0];
                 if (node.getEscapeString() != null) {
@@ -342,11 +370,13 @@
                 }
                 predicateNode = createRelationQueryNode(parent,
                         identifier, type, pattern);
+                node.childrenAccept(this, predicateNode);
             } else if (type == QueryConstants.OPERATION_IN) {
                 OrQueryNode in = new OrQueryNode(parent);
                 for (int i = 1; i < node.children.length; i++) {
                     RelationQueryNode rel = createRelationQueryNode(in,
                             identifier, QueryConstants.OPERATION_EQ_VALUE, (ASTLiteral) node.children[i]);
+                    node.childrenAccept(this, rel);
                     in.addOperand(rel);
                 }
                 predicateNode = in;
@@ -412,12 +442,12 @@
     }
 
     public Object visit(ASTLiteral node, Object data) {
-        // do nothing, should never be called actually
+        // do nothing
         return data;
     }
 
     public Object visit(ASTIdentifier node, Object data) {
-        // do nothing, should never be called actually
+        // do nothing
         return data;
     }
 
@@ -465,6 +495,26 @@
     public Object visit(ASTContainsExpression node, Object data) {
         NAryQueryNode parent = (NAryQueryNode) data;
         parent.addOperand(new TextsearchQueryNode(parent, node.getQuery(), node.getPropertyName()));
+        return parent;
+    }
+
+    public Object visit(ASTLowerFunction node, Object data) {
+        RelationQueryNode parent = (RelationQueryNode) data;
+        if (parent.getValueType() != QueryConstants.TYPE_STRING) {
+            String msg = "LOWER() function is only supported for String literal";
+            throw new IllegalArgumentException(msg);
+        }
+        parent.addOperand(new PropertyFunctionQueryNode(parent, PropertyFunctionQueryNode.LOWER_CASE));
+        return parent;
+    }
+
+    public Object visit(ASTUpperFunction node, Object data) {
+        RelationQueryNode parent = (RelationQueryNode) data;
+        if (parent.getValueType() != QueryConstants.TYPE_STRING) {
+            String msg = "UPPER() function is only supported for String literal";
+            throw new IllegalArgumentException(msg);
+        }
+        parent.addOperand(new PropertyFunctionQueryNode(parent, PropertyFunctionQueryNode.UPPER_CASE));
         return parent;
     }
 

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/QueryFormat.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/QueryFormat.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/QueryFormat.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/sql/QueryFormat.java Thu Nov 16 02:48:20 2006
@@ -31,6 +31,7 @@
 import org.apache.jackrabbit.core.query.QueryRootNode;
 import org.apache.jackrabbit.core.query.RelationQueryNode;
 import org.apache.jackrabbit.core.query.TextsearchQueryNode;
+import org.apache.jackrabbit.core.query.PropertyFunctionQueryNode;
 import org.apache.jackrabbit.name.NamespaceResolver;
 import org.apache.jackrabbit.name.NoPrefixDeclaredException;
 import org.apache.jackrabbit.name.QName;
@@ -382,39 +383,36 @@
     public Object visit(RelationQueryNode node, Object data) {
         StringBuffer sb = (StringBuffer) data;
         try {
+            StringBuffer propName = new StringBuffer();
+            appendName(node.getProperty(), resolver, propName);
+            // surround name with property function
+            node.acceptOperands(this, propName);
+
+            sb.append(propName);
             if (node.getOperation() == OPERATION_EQ_VALUE || node.getOperation() == OPERATION_EQ_GENERAL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" = ");
                 appendValue(node, sb);
             } else if (node.getOperation() == OPERATION_GE_VALUE || node.getOperation() == OPERATION_GE_GENERAL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" >= ");
                 appendValue(node, sb);
             } else if (node.getOperation() == OPERATION_GT_VALUE || node.getOperation() == OPERATION_GT_GENERAL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" > ");
                 appendValue(node, sb);
             } else if (node.getOperation() == OPERATION_LE_VALUE || node.getOperation() == OPERATION_LE_GENERAL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" <= ");
                 appendValue(node, sb);
             } else if (node.getOperation() == OPERATION_LIKE) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" LIKE ");
                 appendValue(node, sb);
             } else if (node.getOperation() == OPERATION_LT_VALUE || node.getOperation() == OPERATION_LT_GENERAL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" < ");
                 appendValue(node, sb);
             } else if (node.getOperation() == OPERATION_NE_VALUE || node.getOperation() == OPERATION_NE_GENERAL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" <> ");
                 appendValue(node, sb);
             } else if (node.getOperation() == OPERATION_NULL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" IS NULL");
             } else if (node.getOperation() == OPERATION_NOT_NULL) {
-                appendName(node.getProperty(), resolver, sb);
                 sb.append(" IS NOT NULL");
             } else {
                 exceptions.add(new InvalidQueryException("Invalid operation: " + node.getOperation()));
@@ -449,6 +447,19 @@
             }
         } else {
             sb.append(" SCORE");
+        }
+        return sb;
+    }
+
+    public Object visit(PropertyFunctionQueryNode node, Object data) {
+        StringBuffer sb = (StringBuffer) data;
+        String functionName = node.getFunctionName();
+        if (functionName.equals(PropertyFunctionQueryNode.LOWER_CASE)) {
+            sb.insert(0, "LOWER(").append(")");
+        } else if (functionName.equals(PropertyFunctionQueryNode.UPPER_CASE)) {
+            sb.insert(0, "UPPER(").append(")");
+        } else {
+            exceptions.add(new InvalidQueryException("Unsupported function: " + functionName));
         }
         return sb;
     }

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/QueryFormat.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/QueryFormat.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/QueryFormat.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/QueryFormat.java Thu Nov 16 02:48:20 2006
@@ -31,6 +31,7 @@
 import org.apache.jackrabbit.core.query.QueryRootNode;
 import org.apache.jackrabbit.core.query.RelationQueryNode;
 import org.apache.jackrabbit.core.query.TextsearchQueryNode;
+import org.apache.jackrabbit.core.query.PropertyFunctionQueryNode;
 import org.apache.jackrabbit.name.NamespaceResolver;
 import org.apache.jackrabbit.name.NoPrefixDeclaredException;
 import org.apache.jackrabbit.name.QName;
@@ -294,14 +295,18 @@
         StringBuffer sb = (StringBuffer) data;
         try {
 
-            String propName = "@";
+            StringBuffer propName = new StringBuffer();
             // only encode if not position function
             if (node.getProperty().equals(XPathQueryBuilder.FN_POSITION_FULL)) {
-                propName += NameFormat.format(node.getProperty(), resolver);
+                NameFormat.format(node.getProperty(), resolver, propName);
             } else {
-                propName += NameFormat.format(ISO9075.encode(node.getProperty()), resolver);
+                propName.append("@");
+                NameFormat.format(ISO9075.encode(node.getProperty()), resolver, propName);
             }
 
+            // surround name with property function
+            node.acceptOperands(this, propName);
+
             if (node.getOperation() == OPERATION_EQ_VALUE) {
                 sb.append(propName).append(" eq ");
                 appendValue(node, sb);
@@ -376,6 +381,25 @@
             exceptions.add(e);
         }
         return data;
+    }
+
+    public Object visit(PropertyFunctionQueryNode node, Object data) {
+        StringBuffer sb = (StringBuffer) data;
+        String functionName = node.getFunctionName();
+        try {
+            if (functionName.equals(PropertyFunctionQueryNode.LOWER_CASE)) {
+                sb.insert(0, NameFormat.format(XPathQueryBuilder.FN_LOWER_CASE, resolver) + "(");
+                sb.append(")");
+            } else if (functionName.equals(PropertyFunctionQueryNode.UPPER_CASE)) {
+                sb.insert(0, NameFormat.format(XPathQueryBuilder.FN_UPPER_CASE, resolver) + "(");
+                sb.append(")");
+            } else {
+                exceptions.add(new InvalidQueryException("Unsupported function: " + functionName));
+            }
+        } catch (NoPrefixDeclaredException e) {
+            exceptions.add(e);
+        }
+        return sb;
     }
 
     //----------------------------< internal >----------------------------------

Modified: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/XPathQueryBuilder.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/XPathQueryBuilder.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/XPathQueryBuilder.java (original)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/core/query/xpath/XPathQueryBuilder.java Thu Nov 16 02:48:20 2006
@@ -30,6 +30,8 @@
 import org.apache.jackrabbit.core.query.QueryRootNode;
 import org.apache.jackrabbit.core.query.RelationQueryNode;
 import org.apache.jackrabbit.core.query.TextsearchQueryNode;
+import org.apache.jackrabbit.core.query.PropertyFunctionQueryNode;
+import org.apache.jackrabbit.core.query.DefaultQueryNodeVisitor;
 import org.apache.jackrabbit.name.IllegalNameException;
 import org.apache.jackrabbit.name.NamespaceResolver;
 import org.apache.jackrabbit.name.NoPrefixDeclaredException;
@@ -53,9 +55,24 @@
 public class XPathQueryBuilder implements XPathVisitor, XPathTreeConstants {
 
     /**
+     * Namespace uri for xpath functions. See also class SearchManager
+     */
+    static final String NS_FN_URI = "http://www.w3.org/2004/10/xpath-functions";
+
+    /**
      * QName for 'fn:not'
      */
-    static final QName FN_NOT = new QName("http://www.w3.org/2004/10/xpath-functions", "not");
+    static final QName FN_NOT = new QName(NS_FN_URI, "not");
+
+    /**
+     * QName for 'fn:lower-case'
+     */
+    static final QName FN_LOWER_CASE = new QName(NS_FN_URI, "lower-case");
+
+    /**
+     * QName for 'fn:upper-case'
+     */
+    static final QName FN_UPPER_CASE = new QName(NS_FN_URI, "upper-case");
 
     /**
      * QName for 'not' as defined in XPath 1.0 (no prefix)
@@ -593,11 +610,25 @@
             exceptions.add(new InvalidQueryException("Unsupported ComparisonExpr type:" + node.getValue()));
         }
 
-        RelationQueryNode rqn = new RelationQueryNode(queryNode, type);
+        final RelationQueryNode rqn = new RelationQueryNode(queryNode, type);
 
         // traverse
         node.childrenAccept(this, rqn);
 
+        // check if string transformation is valid
+        rqn.acceptOperands(new DefaultQueryNodeVisitor() {
+            public Object visit(PropertyFunctionQueryNode node, Object data) {
+                String functionName = node.getFunctionName();
+                if ((functionName.equals(PropertyFunctionQueryNode.LOWER_CASE)
+                        || functionName.equals(PropertyFunctionQueryNode.UPPER_CASE))
+                            && rqn.getValueType() != QueryConstants.TYPE_STRING) {
+                    String msg = "Upper and lower case function are only supported with String literals";
+                    exceptions.add(new InvalidQueryException(msg));
+                }
+                return data;
+            }
+        }, null);
+
         queryNode.addOperand(rqn);
     }
 
@@ -850,6 +881,32 @@
                     createOrderSpec(node, (OrderQueryNode) queryNode);
                 } else {
                     exceptions.add(new InvalidQueryException("Unsupported location for jcr:score()"));
+                }
+            } else if (NameFormat.format(FN_LOWER_CASE, resolver).equals(fName)) {
+                if (node.jjtGetNumChildren() == 2) {
+                    if (queryNode.getType() == QueryNode.TYPE_RELATION) {
+                        RelationQueryNode relNode = (RelationQueryNode) queryNode;
+                        relNode.addOperand(new PropertyFunctionQueryNode(relNode, PropertyFunctionQueryNode.LOWER_CASE));
+                        // get property name
+                        node.jjtGetChild(1).jjtAccept(this, relNode);
+                    } else {
+                        exceptions.add(new InvalidQueryException("Unsupported location for fn:lower-case()"));
+                    }
+                } else {
+                    exceptions.add(new InvalidQueryException("Wrong number of argument for fn:lower-case()"));
+                }
+            } else if (NameFormat.format(FN_UPPER_CASE, resolver).equals(fName)) {
+                if (node.jjtGetNumChildren() == 2) {
+                    if (queryNode.getType() == QueryNode.TYPE_RELATION) {
+                        RelationQueryNode relNode = (RelationQueryNode) queryNode;
+                        relNode.addOperand(new PropertyFunctionQueryNode(relNode, PropertyFunctionQueryNode.UPPER_CASE));
+                        // get property name
+                        node.jjtGetChild(1).jjtAccept(this, relNode);
+                    } else {
+                        exceptions.add(new InvalidQueryException("Unsupported location for fn:upper-case()"));
+                    }
+                } else {
+                    exceptions.add(new InvalidQueryException("Unsupported location for fn:upper-case()"));
                 }
             } else {
                 exceptions.add(new InvalidQueryException("Unsupported function: " + fName));

Modified: jackrabbit/trunk/jackrabbit/src/main/javacc/sql/JCRSQL.jjt
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/main/javacc/sql/JCRSQL.jjt?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/javacc/sql/JCRSQL.jjt (original)
+++ jackrabbit/trunk/jackrabbit/src/main/javacc/sql/JCRSQL.jjt Thu Nov 16 02:48:20 2006
@@ -100,7 +100,9 @@
 | < LIKE: "LIKE" >
 | < NULL: "NULL" >
 | < FROM: "FROM" >
+| < LOWER: "LOWER" >
 | < ORDER: "ORDER" >
+| < UPPER: "UPPER" >
 | < WHERE: "WHERE" >
 | < ESCAPE: "ESCAPE" >
 | < SELECT: "SELECT" >
@@ -304,7 +306,15 @@
 {
   (
     (
-      identifier = Identifier() { jjtThis.setIdentifier(identifier); } ( <PERIOD> identifier = Identifier() { Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); jjtThis.setIdentifier(identifier); } )?
+      (
+        (
+          identifier = Identifier() { jjtThis.setIdentifier(identifier); } ( <PERIOD> identifier = Identifier() { Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); jjtThis.setIdentifier(identifier); } )?
+        )
+      |
+        (
+          identifier = PropertyFunction() { jjtThis.setIdentifier(identifier); }
+        )
+      )
       (
         (
           operationType = ComparisonOperation() { jjtThis.setOperationType(operationType); }
@@ -339,7 +349,10 @@
     (
       Literal() (<NOT> { jjtThis.setNegate(true); })? <IN>
       (
-        identifier = Identifier()
+        (
+            identifier = Identifier()
+          | identifier = PropertyFunction()
+        )
         {
           jjtThis.setIdentifier(identifier);
           jjtThis.setOperationType(jjtThis.isNegate() ? QueryConstants.OPERATION_NE_GENERAL : QueryConstants.OPERATION_EQ_GENERAL);
@@ -349,6 +362,45 @@
   )
 }
 
+QName PropertyFunction() #void :
+{
+  QName identifier;
+}
+{
+  (
+      identifier = LowerFunction()
+    | identifier = UpperFunction()
+  )
+  {
+    return identifier;
+  }
+}
+
+QName LowerFunction() :
+{
+  QName identifier;
+}
+{
+  (
+    <LOWER> "(" identifier = Identifier() ")"
+  )
+  {
+    return identifier;
+  }
+}
+
+QName UpperFunction() :
+{
+  QName identifier;
+}
+{
+  (
+    <UPPER> "(" identifier = Identifier() ")"
+  )
+  {
+    return identifier;
+  }
+}
 
 int ComparisonOperation() #void :
 {

Modified: jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/query/TestAll.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/query/TestAll.java?view=diff&rev=475677&r1=475676&r2=475677
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/query/TestAll.java (original)
+++ jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/query/TestAll.java Thu Nov 16 02:48:20 2006
@@ -45,6 +45,7 @@
         suite.addTestSuite(MixinTest.class);
         suite.addTestSuite(DerefTest.class);
         suite.addTestSuite(VersionStoreQueryTest.class);
+        suite.addTestSuite(UpperLowerCaseQueryTest.class);
 
         // exclude long running tests per default
         //suite.addTestSuite(MassiveRangeTest.class);