Author: thomasm Date: Tue May 15 08:47:06 2012 New Revision: 1338599 URL: http://svn.apache.org/viewvc?rev=1338599&view=rev Log: OAK-28 Query implementation (support parsing and evaluating fulltext search conditions)
Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/query/FullTextTest.java Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java?rev=1338599&r1=1338598&r2=1338599&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/Query.java Tue May 15 08:47:06 2012 @@ -33,6 +33,7 @@ import org.apache.jackrabbit.oak.query.a import org.apache.jackrabbit.oak.query.ast.DescendantNodeImpl; import org.apache.jackrabbit.oak.query.ast.DescendantNodeJoinConditionImpl; import org.apache.jackrabbit.oak.query.ast.EquiJoinConditionImpl; +import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl; import org.apache.jackrabbit.oak.query.ast.FullTextSearchScoreImpl; import org.apache.jackrabbit.oak.query.ast.LengthImpl; import org.apache.jackrabbit.oak.query.ast.LiteralImpl; @@ -136,8 +137,16 @@ public class Query { } @Override + public boolean visit(FullTextSearchImpl node) { + node.setQuery(query); + node.bindSelector(source); + return true; + } + + @Override public boolean visit(FullTextSearchScoreImpl node) { node.setQuery(query); + node.bindSelector(source); return true; } Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java?rev=1338599&r1=1338598&r2=1338599&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java Tue May 15 08:47:06 2012 @@ -18,6 +18,10 @@ */ package org.apache.jackrabbit.oak.query.ast; +import java.text.ParseException; +import java.util.ArrayList; +import org.apache.jackrabbit.mk.simple.NodeImpl; +import org.apache.jackrabbit.oak.api.CoreValue; import org.apache.jackrabbit.oak.query.index.FilterImpl; public class FullTextSearchImpl extends ConstraintImpl { @@ -25,6 +29,8 @@ public class FullTextSearchImpl extends private final String selectorName; private final String propertyName; private final StaticOperandImpl fullTextSearchExpression; + private SelectorImpl selector; + private FullTextExpression expr; public FullTextSearchImpl(String selectorName, String propertyName, StaticOperandImpl fullTextSearchExpression) { @@ -68,15 +74,229 @@ public class FullTextSearchImpl extends return builder.toString(); } + private boolean evaluateContains(CoreValue value) { + String v = value.getString(); + return expr.evaluate(v); + } + @Override public boolean evaluate() { - // TODO support evaluating fulltext conditions + if (propertyName != null) { + CoreValue v = selector.currentProperty(propertyName); + if (v == null) { + return false; + } + return evaluateContains(v); + } + NodeImpl n = selector.currentNode(); + for (int i = 0; i < n.getPropertyCount(); i++) { + String p = n.getProperty(i); + if (p == null) { + break; + } + CoreValue v = selector.currentProperty(p); + if (evaluateContains(v)) { + return true; + } + } return false; } + public void bindSelector(SourceImpl source) { + selector = source.getSelector(selectorName); + if (selector == null) { + throw new IllegalArgumentException("Unknown selector: " + selectorName); + } + CoreValue v = fullTextSearchExpression.currentValue(); + try { + expr = FullTextParser.parse(v.getString()); + } catch (ParseException e) { + throw new IllegalArgumentException("Invalid expression: " + fullTextSearchExpression, e); + } + } + @Override public void apply(FilterImpl f) { + if (propertyName != null) { + if (f.getSelector() == selector) { + f.restrictProperty(propertyName, Operator.NOT_EQUAL, (CoreValue) null); + } + } // TODO support fulltext index conditions } + public static class FullTextParser { + + String text; + int parseIndex; + + public static FullTextExpression parse(String text) throws ParseException { + FullTextParser p = new FullTextParser(); + p.text = text; + FullTextExpression e = p.parseOr(); + return e; + } + + FullTextExpression parseOr() throws ParseException { + FullTextOr or = new FullTextOr(); + or.list.add(parseAnd()); + while (parseIndex < text.length()) { + if (text.substring(parseIndex).startsWith("OR ")) { + parseIndex += 3; + or.list.add(parseAnd()); + } else { + break; + } + } + return or.simplify(); + } + + FullTextExpression parseAnd() throws ParseException { + FullTextAnd and = new FullTextAnd(); + and.list.add(parseTerm()); + while (parseIndex < text.length()) { + if (text.substring(parseIndex).startsWith("OR ")) { + break; + } + and.list.add(parseTerm()); + } + return and.simplify(); + } + + FullTextExpression parseTerm() throws ParseException { + if (parseIndex >= text.length()) { + throw getSyntaxError("term"); + } + FullTextTerm term = new FullTextTerm(); + StringBuilder buff = new StringBuilder(); + char c = text.charAt(parseIndex); + if (c == '-') { + if (++parseIndex >= text.length()) { + throw getSyntaxError("term"); + } + term.not = true; + } + if (c == '\"') { + parseIndex++; + while (true) { + if (parseIndex >= text.length()) { + throw getSyntaxError("double quote"); + } + c = text.charAt(parseIndex++); + if (c == '\\') { + // escape + if (parseIndex >= text.length()) { + throw getSyntaxError("escaped char"); + } + c = text.charAt(parseIndex++); + buff.append(c); + } else if (c == '\"') { + if (parseIndex < text.length() && text.charAt(parseIndex) != ' ') { + throw getSyntaxError("space"); + } + parseIndex++; + break; + } else { + buff.append(c); + } + } + } else { + do { + c = text.charAt(parseIndex++); + if (c == '\\') { + // escape + if (parseIndex >= text.length()) { + throw getSyntaxError("escaped char"); + } + c = text.charAt(parseIndex++); + buff.append(c); + } else if (c == ' ') { + break; + } else { + buff.append(c); + } + } while (parseIndex < text.length()); + } + if (buff.length() == 0) { + throw getSyntaxError("term"); + } + term.text = buff.toString(); + return term.simplify(); + } + + private ParseException getSyntaxError(String expected) { + int index = Math.max(0, Math.min(parseIndex, text.length() - 1)); + String query = text.substring(0, index) + "(*)" + text.substring(index).trim(); + if (expected != null) { + query += "; expected: " + expected; + } + return new ParseException("FullText expression: " + query, index); + } + + } + + public abstract static class FullTextExpression { + public abstract boolean evaluate(String value); + abstract FullTextExpression simplify(); + } + + static class FullTextAnd extends FullTextExpression { + ArrayList<FullTextExpression> list = new ArrayList<FullTextExpression>(); + + @Override + public boolean evaluate(String value) { + for (FullTextExpression e : list) { + if (!e.evaluate(value)) { + return false; + } + } + return true; + } + + @Override + FullTextExpression simplify() { + return list.size() == 1 ? list.get(0) : this; + } + + } + + static class FullTextOr extends FullTextExpression { + ArrayList<FullTextExpression> list = new ArrayList<FullTextExpression>(); + + @Override + public boolean evaluate(String value) { + for (FullTextExpression e : list) { + if (e.evaluate(value)) { + return true; + } + } + return false; + } + + @Override + FullTextExpression simplify() { + return list.size() == 1 ? list.get(0).simplify() : this; + } + + } + + static class FullTextTerm extends FullTextExpression { + boolean not; + String text; + + @Override + public boolean evaluate(String value) { + if (not) { + return value.indexOf(text) < 0; + } + return value.indexOf(text) >= 0; + } + + @Override + FullTextExpression simplify() { + return this; + } + + } + } Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/query/FullTextTest.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/query/FullTextTest.java?rev=1338599&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/query/FullTextTest.java (added) +++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/query/FullTextTest.java Tue May 15 08:47:06 2012 @@ -0,0 +1,99 @@ +/* + * 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.oak.query; + +import static org.junit.Assert.*; +import java.text.ParseException; +import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl; +import org.junit.Test; + +/** + * Test the fulltext parsing and evaluation. + */ +public class FullTextTest { + + @Test + public void and() throws ParseException { + assertFalse(test("hello world", "hello")); + assertFalse(test("hello world", "world")); + assertTrue(test("hello world", "world hello")); + assertTrue(test("hello world ", "hello world")); + } + + @Test + public void or() throws ParseException { + assertTrue(test("hello OR world", "hello")); + assertTrue(test("hello OR world", "world")); + assertFalse(test("hello OR world", "hi")); + } + + @Test + public void not() throws ParseException { + assertTrue(test("hello -world", "hello")); + assertFalse(test("hello -world", "hello world")); + } + + @Test + public void quoted() throws ParseException { + assertTrue(test("\"hello world\"", "hello world")); + assertFalse(test("\"hello world\"", "world hello")); + assertTrue(test("\"hello-world\"", "hello-world")); + assertTrue(test("\"hello\\-world\"", "hello-world")); + assertTrue(test("\"hello \\\"world\\\"\"", "hello \"world\"")); + assertTrue(test("\"hello world\" -hallo", "hello world")); + assertFalse(test("\"hello world\" -hallo", "hallo hello world")); + } + + @Test + public void escaped() throws ParseException { + assertFalse(test("\\\"hello\\\"", "hello")); + assertTrue(test("\"hello\"", "\"hello\"")); + assertTrue(test("\\\"hello\\\"", "\"hello\"")); + assertFalse(test("\\-1 2 3", "1 2 3")); + assertTrue(test("\\-1 2 3", "-1 2 3")); + } + + @Test + public void invalid() throws ParseException { + testInvalid("", "(*); expected: term"); + testInvalid("x OR ", "x OR(*); expected: term"); + testInvalid("\"", "(*)\"; expected: double quote"); + testInvalid("-", "(*)-; expected: term"); + testInvalid("- x", "- (*)x; expected: term"); + testInvalid("\\", "(*)\\; expected: escaped char"); + testInvalid("\"\\", "\"(*)\\; expected: escaped char"); + testInvalid("\"x\"y", "\"x\"(*)y; expected: space"); + } + + private void testInvalid(String pattern, String expectedMessage) { + try { + test(pattern, ""); + fail("Expected exception " + expectedMessage); + } catch (ParseException e) { + String msg = e.getMessage(); + assertTrue(msg.startsWith("FullText expression: ")); + msg = msg.substring("FullText expression: ".length()); + assertEquals(expectedMessage, msg); + } + } + + private boolean test(String pattern, String value) throws ParseException { + FullTextSearchImpl.FullTextExpression e = FullTextSearchImpl.FullTextParser.parse(pattern); + return e.evaluate(value); + } + +}