You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by th...@apache.org on 2012/05/15 10:47:06 UTC

svn commit: r1338599 - in /jackrabbit/oak/trunk/oak-core/src: main/java/org/apache/jackrabbit/oak/query/Query.java main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java test/java/org/apache/jackrabbit/oak/query/FullTextTest.java

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);
+    }
+
+}