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 2010/11/02 14:32:44 UTC

svn commit: r1030038 - in /jackrabbit/trunk: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ ja...

Author: mreutegg
Date: Tue Nov  2 13:32:43 2010
New Revision: 1030038

URL: http://svn.apache.org/viewvc?rev=1030038&view=rev
Log:
JCR-2790: jcr:like on node name

Added:
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java   (with props)
Modified:
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java
    jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java?rev=1030038&r1=1030037&r2=1030038&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java Tue Nov  2 13:32:43 2010
@@ -44,6 +44,7 @@ import org.apache.jackrabbit.spi.PathFac
 import org.apache.jackrabbit.spi.commons.conversion.NameException;
 import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
 import org.apache.jackrabbit.spi.commons.name.NameConstants;
+import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
 import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
 import org.apache.jackrabbit.spi.commons.query.AndQueryNode;
 import org.apache.jackrabbit.spi.commons.query.DefaultQueryNodeVisitor;
@@ -69,9 +70,7 @@ import org.apache.lucene.analysis.Analyz
 import org.apache.lucene.index.Term;
 import org.apache.lucene.queryParser.ParseException;
 import org.apache.lucene.queryParser.QueryParser;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.Query;
+import org.apache.lucene.search.*;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -100,6 +99,11 @@ public class LuceneQueryBuilder implemen
     private static final Name PARENT_ELEMENT_NAME = PATH_FACTORY.getParentElement().getName();
 
     /**
+     * Name constant for fn:name()
+     */
+    private static final Name FN_NAME = NameFactoryImpl.getInstance().create(SearchManager.NS_FN_URI, "name()");
+
+    /**
      * Root node of the abstract query tree
      */
     private final QueryRootNode root;
@@ -709,35 +713,44 @@ public class LuceneQueryBuilder implemen
         }
 
         // support for fn:name()
-        if (propertyName.getNamespaceURI().equals(SearchManager.NS_FN_URI)
-                && propertyName.getLocalName().equals("name()")) {
+        if (propertyName.equals(FN_NAME)) {
             if (node.getValueType() != QueryConstants.TYPE_STRING) {
                 exceptions.add(new InvalidQueryException("Name function can "
                         + "only be used in conjunction with a string literal"));
                 return data;
             }
-            if (node.getOperation() != QueryConstants.OPERATION_EQ_VALUE
-                    && node.getOperation() != QueryConstants.OPERATION_EQ_GENERAL) {
-                exceptions.add(new InvalidQueryException("Name function can "
-                        + "only be used in conjunction with an equals operator"));
-                return data;
-            }
-            // check if string literal is a valid XML Name
-            if (XMLChar.isValidName(node.getStringValue())) {
-                // parse string literal as JCR Name
-                try {
-                    Name n = session.getQName(ISO9075.decode(node.getStringValue()));
-                    query = new NameQuery(n, indexFormatVersion, nsMappings);
-                } catch (NameException e) {
-                    exceptions.add(e);
-                    return data;
-                } catch (NamespaceException e) {
-                    exceptions.add(e);
-                    return data;
+            if (node.getOperation() == QueryConstants.OPERATION_EQ_VALUE
+                    || node.getOperation() == QueryConstants.OPERATION_EQ_GENERAL) {
+                // check if string literal is a valid XML Name
+                if (XMLChar.isValidName(node.getStringValue())) {
+                    // parse string literal as JCR Name
+                    try {
+                        Name n = session.getQName(ISO9075.decode(node.getStringValue()));
+                        query = new NameQuery(n, indexFormatVersion, nsMappings);
+                    } catch (NameException e) {
+                        exceptions.add(e);
+                        return data;
+                    } catch (NamespaceException e) {
+                        exceptions.add(e);
+                        return data;
+                    }
+                } else {
+                    // will never match -> create dummy query
+                    query = new BooleanQuery();
+                }
+            } else if (node.getOperation() == QueryConstants.OPERATION_LIKE) {
+                // the like operation always has one string value.
+                // no coercing, see above
+                if (stringValues[0].equals("%")) {
+                    query = new org.apache.lucene.search.MatchAllDocsQuery();
+                } else {
+                    query = new WildcardNameQuery(stringValues[0], 
+                            transform[0], session, nsMappings);
                 }
             } else {
-                // will never match -> create dummy query
-                query = new BooleanQuery();
+                exceptions.add(new InvalidQueryException("Name function can "
+                        + "only be used in conjunction with the following operators: equals, like"));
+                return data;
             }
         } else {
             switch (node.getOperation()) {

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java?rev=1030038&r1=1030037&r2=1030038&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java Tue Nov  2 13:32:43 2010
@@ -306,6 +306,8 @@ public abstract class QueryResultImpl im
                     log.warn("Unable to close query result: " + e);
                 }
             }
+            // make sure PerQueryCache is disposed
+            PerQueryCache.getInstance().dispose();
         }
     }
 

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java?rev=1030038&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java Tue Nov  2 13:32:43 2010
@@ -0,0 +1,84 @@
+/*
+ * 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 java.io.IOException;
+
+import javax.jcr.NamespaceException;
+
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.FilteredTermEnum;
+import org.apache.lucene.search.MultiTermQuery;
+import org.apache.lucene.search.Query;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements a wildcard query on the name field.
+ * <p/>
+ * Wildcards are:
+ * <ul>
+ * <li><code>%</code> : matches zero or more characters</li>
+ * <li><code>_</code> : matches exactly one character</li>
+ * </ul>
+ * Wildcards in the namespace prefix are not supported and will not match.
+ */
+public class WildcardNameQuery extends WildcardQuery {
+
+    private static final long serialVersionUID = -4705104992551930918L;
+
+    /**
+     * Logger instance for this class.
+     */
+    private static final Logger log = LoggerFactory.getLogger(WildcardNameQuery.class);
+
+    public WildcardNameQuery(String pattern,
+                             int transform,
+                             NamespaceResolver resolver,
+                             NamespaceMappings nsMappings) {
+        super(FieldNames.LABEL, null,
+                convertPattern(pattern, resolver, nsMappings), transform);
+    }
+
+    private static String convertPattern(String pattern,
+                                         NamespaceResolver resolver,
+                                         NamespaceMappings nsMappings) {
+        String prefix = "";
+        int idx = pattern.indexOf(':');
+        if (idx != -1) {
+            prefix = pattern.substring(0, idx);
+        }
+        StringBuffer sb = new StringBuffer();
+        // translate prefix
+        try {
+            sb.append(nsMappings.getPrefix(resolver.getURI(prefix)));
+        } catch (NamespaceException e) {
+            // prefix in pattern is probably unknown
+            log.debug("unknown namespace prefix in pattern: " + pattern);
+            // -> ignore and use empty string for index internal prefix
+            //    this will not match anything
+        }
+        sb.append(":");
+        // remaining pattern, may also be whole pattern
+        sb.append(pattern.substring(idx + 1));
+        return sb.toString();
+    }
+}

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

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java?rev=1030038&r1=1030037&r2=1030038&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java Tue Nov  2 13:32:43 2010
@@ -61,9 +61,9 @@ public class WildcardQuery extends Query
     private final String field;
 
     /**
-     * Name of the property to search.
+     * Creates a term value for a given string.
      */
-    private final String propName;
+    private final WildcardTermEnum.TermValueFactory tvf;
 
     /**
      * The wildcard pattern.
@@ -71,7 +71,7 @@ public class WildcardQuery extends Query
     private final String pattern;
 
     /**
-     * How property values are tranformed before they are matched using the
+     * How property values are transformed before they are matched using the
      * provided pattern.
      */
     private int transform = TRANSFORM_NONE;
@@ -91,11 +91,20 @@ public class WildcardQuery extends Query
      * @param transform how property values are transformed before they are
      *                  matched using the <code>pattern</code>.
      */
-    public WildcardQuery(String field, String propName, String pattern, int transform) {
+    public WildcardQuery(String field, final String propName, String pattern, int transform) {
         this.field = field.intern();
-        this.propName = propName;
         this.pattern = pattern;
         this.transform = transform;
+        if (propName != null) {
+            tvf = new WildcardTermEnum.TermValueFactory() {
+                @Override
+                public String createValue(String s) {
+                    return FieldNames.createNamedValue(propName, s);
+                }
+            };
+        } else {
+            tvf = new WildcardTermEnum.TermValueFactory();
+        }
     }
 
     /**
@@ -128,7 +137,7 @@ public class WildcardQuery extends Query
     public Query rewrite(IndexReader reader) throws IOException {
         Query stdWildcardQuery = new MultiTermQuery(new Term(field, pattern)) {
             protected FilteredTermEnum getEnum(IndexReader reader) throws IOException {
-                return new WildcardTermEnum(reader, field, propName, pattern, transform);
+                return new WildcardTermEnum(reader, field, tvf, pattern, transform);
             }
         };
         try {
@@ -158,7 +167,7 @@ public class WildcardQuery extends Query
      * @return a string representation of this query.
      */
     public String toString(String field) {
-        return propName + ":" + pattern;
+        return field + ":" + tvf.createValue(pattern);
     }
 
     /**
@@ -277,7 +286,7 @@ public class WildcardQuery extends Query
         WildcardQueryScorer(Similarity similarity, IndexReader reader) {
             super(similarity);
             this.reader = reader;
-            this.cacheKey = field + '\uFFFF' + propName + '\uFFFF' + transform + '\uFFFF' + pattern;
+            this.cacheKey = field + '\uFFFF' + tvf.createValue('\uFFFF' + pattern) + '\uFFFF' + transform;
             // check cache
             PerQueryCache cache = PerQueryCache.getInstance();
             Map<String, BitSet> m = (Map<String, BitSet>) cache.get(WildcardQueryScorer.class, reader);
@@ -344,7 +353,7 @@ public class WildcardQuery extends Query
             if (hitsCalculated) {
                 return;
             }
-            TermEnum terms = new WildcardTermEnum(reader, field, propName, pattern, transform);
+            TermEnum terms = new WildcardTermEnum(reader, field, tvf, pattern, transform);
             try {
                 // use unpositioned TermDocs
                 TermDocs docs = reader.termDocs();

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java?rev=1030038&r1=1030037&r2=1030038&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java Tue Nov  2 13:32:43 2010
@@ -46,6 +46,11 @@ class WildcardTermEnum extends FilteredT
     private final String field;
 
     /**
+     * Factory for term values.
+     */
+    private final TermValueFactory tvf;
+
+    /**
      * The term prefix without wildcards
      */
     private final String prefix;
@@ -70,8 +75,7 @@ class WildcardTermEnum extends FilteredT
      *
      * @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 tvf       the term value factory.
      * @param pattern   the pattern to match the values.
      * @param transform the transformation that should be applied to the term
      *                  enum from the index reader.
@@ -82,7 +86,7 @@ class WildcardTermEnum extends FilteredT
      */
     public WildcardTermEnum(IndexReader reader,
                             String field,
-                            String propName,
+                            TermValueFactory tvf,
                             String pattern,
                             int transform) throws IOException {
         if (transform < TRANSFORM_NONE || transform > TRANSFORM_UPPER_CASE) {
@@ -90,6 +94,7 @@ class WildcardTermEnum extends FilteredT
         }
         this.field = field;
         this.transform = transform;
+        this.tvf = tvf;
 
         int idx = 0;
 
@@ -97,17 +102,13 @@ class WildcardTermEnum extends FilteredT
             // optimize the term comparison by removing the prefix from the pattern
             // and therefore use a more precise range scan
             while (idx < pattern.length()
-                    && Character.isLetterOrDigit(pattern.charAt(idx))) {
+                    && (Character.isLetterOrDigit(pattern.charAt(idx)) || pattern.charAt(idx) == ':')) {
                 idx++;
             }
 
-            if (propName == null) {
-                prefix = pattern.substring(0, idx);
-            } else {
-                prefix = FieldNames.createNamedValue(propName, pattern.substring(0, idx));
-            }
+            prefix = tvf.createValue(pattern.substring(0, idx));
         } else {
-            prefix = FieldNames.createNamedValue(propName, "");
+            prefix = tvf.createValue("");
         }
 
         // initialize with prefix as dummy value
@@ -117,7 +118,7 @@ class WildcardTermEnum extends FilteredT
         if (transform == TRANSFORM_NONE) {
             setEnum(reader.terms(new Term(field, prefix)));
         } else {
-            setEnum(new LowerUpperCaseTermEnum(reader, field, propName, pattern, transform));
+            setEnum(new LowerUpperCaseTermEnum(reader, field, pattern, transform));
         }
     }
 
@@ -172,7 +173,6 @@ class WildcardTermEnum extends FilteredT
 
         public LowerUpperCaseTermEnum(IndexReader reader,
                                       String field,
-                                      String propName,
                                       String pattern,
                                       int transform) throws IOException {
             if (transform != TRANSFORM_LOWER_CASE && transform != TRANSFORM_UPPER_CASE) {
@@ -201,28 +201,28 @@ class WildcardTermEnum extends FilteredT
                     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");
+                        String prefix = tvf.createValue("");
+                        String limit = tvf.createValue("\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());
+                        String prefix = tvf.createValue(lowerLimit.toString());
 
                         StringBuffer upperLimit = new StringBuffer(patternPrefix.toLowerCase());
                         upperLimit.append('\uFFFF');
-                        String limit = FieldNames.createNamedValue(propName, upperLimit.toString());
+                        String limit = tvf.createValue(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());
+                        prefix = tvf.createValue(patternPrefix.toUpperCase());
                         upperLimit = new StringBuffer(patternPrefix.toLowerCase());
                         upperLimit.setCharAt(0, Character.toUpperCase(upperLimit.charAt(0)));
                         upperLimit.append('\uFFFF');
-                        limit = FieldNames.createNamedValue(propName, upperLimit.toString());
+                        limit = tvf.createValue(upperLimit.toString());
                         rangeScans.add(new RangeScan(reader,
                                 new Term(field, prefix), new Term(field, limit)));
                     }
@@ -299,4 +299,19 @@ class WildcardTermEnum extends FilteredT
             current = it.hasNext() ? it.next() : null;
         }
     }
+
+    public static class TermValueFactory {
+
+        /**
+         * Creates a term value from the given string. This implementation
+         * simply returns the given string. Sub classes my apply some
+         * transformation to the string.
+         *
+         * @param s the string.
+         * @return the term value to use in the query.
+         */
+        public String createValue(String s) {
+            return s;
+        }
+    }
 }

Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java?rev=1030038&r1=1030037&r2=1030038&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java Tue Nov  2 13:32:43 2010
@@ -297,6 +297,50 @@ public class GQLTest extends AbstractQue
         checkResultSequence(rows, new Node[]{n2});
     }
 
+    public void testName() throws RepositoryException {
+        Node file1 = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT);
+        superuser.save();
+
+        String stmt = createStatement("\"quick brown\" name:file1.txt");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:file?.txt");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:?ile1.txt");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:file1.tx?");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:file1.???");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:fil*xt");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:*.txt");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:file1.*");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:*");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:fIlE1.*");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{file1});
+
+        stmt = createStatement("\"quick brown\" name:file2.txt");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{});
+
+        stmt = createStatement("\"quick brown\" name:file1.t?");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{});
+
+        stmt = createStatement("\"quick brown\" name:?le1.txt");
+        checkResultWithRetries(stmt, "jcr:content", new Node[]{});
+    }
+
     public void XXXtestQueryDestruction() throws RepositoryException {
         char[] stmt = createStatement("title:jackrabbit \"apache software\" type:file order:+title limit:10..20").toCharArray();
         for (char c = 0; c < 255; c++) {
@@ -343,11 +387,15 @@ public class GQLTest extends AbstractQue
      */
     protected void checkResultWithRetries(String gql, String cpp, Node[] nodes)
             throws RepositoryException {
-        for (int i = 0; i < 10; i++) {
+        int retries = 10;
+        for (int i = 0; i < retries; i++) {
             try {
                 checkResult(GQL.execute(gql, superuser, cpp), nodes);
                 break;
             } catch (AssertionFailedError e) {
+                if (i + 1 == retries) {
+                    throw e;
+                }
                 try {
                     // sleep for a second and retry
                     Thread.sleep(1000);

Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java?rev=1030038&r1=1030037&r2=1030038&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java Tue Nov  2 13:32:43 2010
@@ -24,7 +24,7 @@ import javax.jcr.Node;
  */
 public class FnNameQueryTest extends AbstractQueryTest {
 
-    public void testFnName() throws RepositoryException {
+    public void testSimple() throws RepositoryException {
         Node n1 = testRootNode.addNode(nodeName1);
         n1.setProperty(propertyName1, 1);
         Node n2 = testRootNode.addNode(nodeName2);
@@ -32,7 +32,7 @@ public class FnNameQueryTest extends Abs
         Node n3 = testRootNode.addNode(nodeName3);
         n3.setProperty(propertyName1, 3);
 
-        testRootNode.save();
+        superuser.save();
 
         String base = testPath + "/*[@" + propertyName1;
         executeXPathQuery(base + " = 1 and fn:name() = '" + nodeName1 + "']",
@@ -47,11 +47,11 @@ public class FnNameQueryTest extends Abs
                 new Node[]{n2, n3});
     }
 
-    public void testFnNameWithSpace() throws RepositoryException {
+    public void testWithSpace() throws RepositoryException {
         Node n1 = testRootNode.addNode("My Documents");
         n1.setProperty(propertyName1, 1);
 
-        testRootNode.save();
+        superuser.save();
 
         String base = testPath + "/*[@" + propertyName1;
         executeXPathQuery(base + " = 1 and fn:name() = 'My Documents']",
@@ -59,4 +59,92 @@ public class FnNameQueryTest extends Abs
         executeXPathQuery(base + " = 1 and fn:name() = 'My_x0020_Documents']",
                 new Node[]{n1});
     }
+
+    public void testLikeWithWildcard() throws RepositoryException {
+        Node n1 = testRootNode.addNode("Foo");
+        n1.setProperty(propertyName1, 1);
+
+        superuser.save();
+
+        String prefix = testPath + "/*[@" + propertyName1 + " = 1 and jcr:like(fn:name(), '";
+        String suffix = "')]";
+        executeXPathQuery(prefix + "F%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "Fo%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "Foo%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "Fooo%" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%o" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%oo" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%Foo" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%Foo%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%oo%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%" + suffix, new Node[]{n1});
+
+        executeXPathQuery(prefix + "F__" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "Fo_" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "F_o" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "Foo_" + suffix, new Node[]{});
+    }
+
+    public void testLikeWithWildcardAndLowerCase()
+            throws RepositoryException {
+        Node n1 = testRootNode.addNode("Foo");
+        n1.setProperty(propertyName1, 1);
+
+        superuser.save();
+
+        String prefix = testPath + "/*[@" + propertyName1 + " = 1 and jcr:like(fn:lower-case(fn:name()), '";
+        String suffix = "')]";
+
+        executeXPathQuery(prefix + "f%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "fo%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "foo%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "fooo%" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%o" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%oo" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%foo" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%foo%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "%oo%" + suffix, new Node[]{n1});
+
+        executeXPathQuery(prefix + "f__" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "fo_" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "f_o" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "_oo" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "foo_" + suffix, new Node[]{});
+
+        // all non-matching
+        executeXPathQuery(prefix + "F%" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "fO%" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "foO%" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%O" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%Oo" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%Foo" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%FOO%" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%oO%" + suffix, new Node[]{});
+
+        executeXPathQuery(prefix + "F__" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "fO_" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "F_o" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "_oO" + suffix, new Node[]{});
+    }
+
+    public void testLikeWithPrefix() throws RepositoryException {
+        Node n1 = testRootNode.addNode("jcr:content");
+        n1.setProperty(propertyName1, 1);
+
+        superuser.save();
+
+        String prefix = testPath + "/*[@" + propertyName1 + " = 1 and jcr:like(fn:name(), '";
+        String suffix = "')]";
+
+        executeXPathQuery(prefix + "jcr:%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "jcr:c%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "jcr:%ten%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "jcr:c_nt%" + suffix, new Node[]{n1});
+        executeXPathQuery(prefix + "jcr:%nt" + suffix, new Node[]{n1});
+
+        // non-matching
+        executeXPathQuery(prefix + "invalid:content" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%:content" + suffix, new Node[]{});
+        executeXPathQuery(prefix + "%:%" + suffix, new Node[]{});
+    }
 }

Modified: jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java?rev=1030038&r1=1030037&r2=1030038&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java (original)
+++ jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java Tue Nov  2 13:32:43 2010
@@ -74,6 +74,9 @@ import org.apache.jackrabbit.util.Text;
  * are omitted and only one value is specified GQL will return at most this
  * number of results. E.g. <code>limit:10</code> (will return the first 10
  * results)</li>
+ * <li><code><b>name:</b></code> a constraint on the name of the returned nodes.
+ * The following wild cards are allowed: '*', matching any character sequence of
+ * length 0..n; '?', matching any single character.</li>
  * </ul>
  * <p/>
  * <b>Property name</b>
@@ -150,6 +153,11 @@ public final class GQL {
     private static final String LIMIT = "limit";
 
     /**
+     * Constant for <code>name</code> keyword.
+     */
+    private static final String NAME = "name";
+
+    /**
      * Constant for <code>OR</code> operator.
      */
     private static final String OR = "OR";
@@ -699,6 +707,11 @@ public final class GQL {
                     // noise
                 case '*':
                 case '?':
+                    if (property.toString().equals(NAME)) {
+                        // allow wild cards in name
+                        value.append(c);
+                        break;
+                    }
                 case '\'':
                 case '~':
                 case '^':
@@ -775,7 +788,13 @@ public final class GQL {
                 }
             }
         } else {
-            ContainsExpression expr = new ContainsExpression(property, value);
+            Expression expr;
+            if (property.equals(NAME)) {
+                expr = new NameExpression(value);
+            } else {
+                expr = new ContainsExpression(property, value);
+            }
+
             if (optional) {
                 Expression last = conditions.get(conditions.size() - 1);
                 if (last instanceof OptionalExpression) {
@@ -868,6 +887,30 @@ public final class GQL {
     }
 
     /**
+     * A name expression.
+     */
+    private class NameExpression implements Expression {
+
+        private final String value;
+
+        NameExpression(String value) {
+            String tmp = value;
+            tmp = tmp.replaceAll("'", "''");
+            tmp = tmp.replaceAll("\\*", "\\%");
+            tmp = tmp.replaceAll("\\?", "\\_");
+            tmp = tmp.toLowerCase();
+            this.value = tmp;
+        }
+
+        public void toString(StringBuffer buffer)
+                throws RepositoryException {
+            buffer.append("jcr:like(fn:lower-case(fn:name()), '");
+            buffer.append(value);
+            buffer.append("')");
+        }
+    }
+
+    /**
      * A single contains expression.
      */
     private final class ContainsExpression extends PropertyExpression {