Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java (revision 801565) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java Thu Oct 14 17:46:53 CEST 2010 @@ -46,6 +46,11 @@ 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 @@ * * @param reader the index reader. * @param field the lucene field to search. - * @param propName the embedded jcr property name or null 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 @@ */ 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 @@ } this.field = field; this.transform = transform; + this.tvf = tvf; int idx = 0; @@ -97,18 +102,14 @@ // 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); + prefix = tvf.createValue(pattern.substring(0, idx)); - } else { + } else { - prefix = FieldNames.createNamedValue(propName, pattern.substring(0, idx)); + prefix = tvf.createValue(""); - } + } - } else { - prefix = FieldNames.createNamedValue(propName, ""); - } // initialize with prefix as dummy value input = new OffsetCharSequence(prefix.length(), prefix, transform); @@ -117,7 +118,7 @@ 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 @@ 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 @@ 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 @@ 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; -} + } + } +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java (revision 531159) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java Mon Nov 01 22:08:25 CET 2010 @@ -24,7 +24,7 @@ */ 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 @@ 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 @@ 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 @@ 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[]{}); + } +} Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java (revision 983909) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java Mon Oct 25 17:44:05 CEST 2010 @@ -306,6 +306,8 @@ log.warn("Unable to close query result: " + e); } } + // make sure PerQueryCache is disposed + PerQueryCache.getInstance().dispose(); } } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java (revision 801565) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java Wed Oct 13 17:31:30 CEST 2010 @@ -61,9 +61,9 @@ 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 @@ 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,12 +91,21 @@ * @param transform how property values are transformed before they are * matched using the pattern. */ - 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(); + } + } /** * Creates a new WildcardQuery. @@ -128,7 +137,7 @@ 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 @@ * @return a string representation of this query. */ public String toString(String field) { - return propName + ":" + pattern; + return field + ":" + tvf.createValue(pattern); } /** @@ -277,7 +286,7 @@ 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 m = (Map) cache.get(WildcardQueryScorer.class, reader); @@ -344,7 +353,7 @@ 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(); Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java Wed Oct 13 17:50:51 CEST 2010 +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java Wed Oct 13 17:50:51 CEST 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. + *

+ * Wildcards are: + *

+ * 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(); + } +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java (revision 829019) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java Mon Nov 01 22:34:59 CET 2010 @@ -297,6 +297,50 @@ 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 @@ */ 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); Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java (revision 1002596) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java Mon Nov 01 22:13:52 CET 2010 @@ -44,6 +44,7 @@ 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.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 @@ 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,37 +713,46 @@ } // 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; - } + 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(); - } + // 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 { + } else { + query = new WildcardNameQuery(stringValues[0], + transform[0], session, nsMappings); + } + } else { + exceptions.add(new InvalidQueryException("Name function can " + + "only be used in conjunction with the following operators: equals, like")); + return data; + } + } else { switch (node.getOperation()) { case QueryConstants.OPERATION_EQ_VALUE: // = case QueryConstants.OPERATION_EQ_GENERAL: Index: jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java =================================================================== --- jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java (revision 828996) +++ jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java Mon Nov 01 21:53:29 CET 2010 @@ -74,6 +74,9 @@ * are omitted and only one value is specified GQL will return at most this * number of results. E.g. limit:10 (will return the first 10 * results) + *
  • name: 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.
  • * *

    * Property name @@ -150,6 +153,11 @@ private static final String LIMIT = "limit"; /** + * Constant for name keyword. + */ + private static final String NAME = "name"; + + /** * Constant for OR operator. */ private static final String OR = "OR"; @@ -699,6 +707,11 @@ // noise case '*': case '?': + if (property.toString().equals(NAME)) { + // allow wild cards in name + value.append(c); + break; + } case '\'': case '~': case '^': @@ -775,7 +788,13 @@ } } } 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 @@ } /** + * 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 {