From ec21c12c7cdc3700770cbd08e04083fa85251ca2 Mon Sep 17 00:00:00 2001
From: Jukka Zitting <jukka@apache.org>
Date: Fri, 12 Aug 2011 17:06:23 +0200
Subject: [PATCH] JCR-3047: OperandEvaluator should be able to handle Nodes as
 well, not just Rows

Move the generic Row implementations from -core to -jcr-commons and use them to implement Node support in OperandEvaluator.
---
 .../core/query/lucene/LuceneQueryFactory.java      |    7 +-
 .../core/query/lucene/join/AbstractRow.java        |   80 ---
 .../query/lucene/join/ChildNodeJoinMerger.java     |    6 +-
 .../lucene/join/DescendantNodeJoinMerger.java      |    6 +-
 .../core/query/lucene/join/EquiJoinMerger.java     |    5 +-
 .../core/query/lucene/join/JoinMerger.java         |   11 +-
 .../jackrabbit/core/query/lucene/join/JoinRow.java |  120 -----
 .../core/query/lucene/join/QueryEngine.java        |    9 +-
 .../core/query/lucene/join/SameNodeJoinMerger.java |    6 +-
 .../core/query/lucene/join/SelectorRow.java        |   78 ---
 .../jackrabbit/commons/query/AbstractRow.java      |  119 +++++
 .../apache/jackrabbit/commons/query/JoinRow.java   |  177 +++++++
 .../jackrabbit/commons/query/OperandEvaluator.java |  404 +++++++++++++++
 .../jackrabbit/commons/query/SelectorRow.java      |  127 +++++
 .../commons/query/qom/OperandEvaluator.java        |  533 --------------------
 15 files changed, 853 insertions(+), 835 deletions(-)
 delete mode 100644 jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractRow.java
 delete mode 100644 jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinRow.java
 delete mode 100644 jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SelectorRow.java
 create mode 100644 jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/AbstractRow.java
 create mode 100644 jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/JoinRow.java
 create mode 100644 jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/OperandEvaluator.java
 create mode 100644 jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SelectorRow.java
 delete mode 100644 jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/OperandEvaluator.java

diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryFactory.java
index 33d714f..b7c0921 100644
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryFactory.java
+++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryFactory.java
@@ -81,6 +81,7 @@ import javax.jcr.query.qom.NodeLocalName;
 import javax.jcr.query.qom.NodeName;
 import javax.jcr.query.qom.Not;
 import javax.jcr.query.qom.Or;
+import javax.jcr.query.qom.Operand;
 import javax.jcr.query.qom.PropertyExistence;
 import javax.jcr.query.qom.PropertyValue;
 import javax.jcr.query.qom.SameNode;
@@ -91,9 +92,9 @@ import javax.jcr.query.qom.UpperCase;
 import org.apache.jackrabbit.commons.predicate.Predicate;
 import org.apache.jackrabbit.commons.predicate.Predicates;
 import org.apache.jackrabbit.commons.predicate.RowPredicate;
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.SelectorRow;
 import org.apache.jackrabbit.core.SessionImpl;
-import org.apache.jackrabbit.core.query.lucene.join.SelectorRow;
 import org.apache.jackrabbit.core.query.lucene.join.ValueComparator;
 import org.apache.jackrabbit.spi.Name;
 import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
@@ -167,7 +168,7 @@ public class LuceneQueryFactory {
     }
 
     public List<Row> execute(
-            Map<String, PropertyValue> columns, Selector selector,
+            Map<String, Operand> columns, Selector selector,
             Constraint constraint) throws RepositoryException, IOException {
         final IndexReader reader = index.getIndexReader(true);
         QueryHits hits = null;
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractRow.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractRow.java
deleted file mode 100644
index 9e0a0e1..0000000
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractRow.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.join;
-
-import java.util.Map;
-
-import javax.jcr.ItemNotFoundException;
-import javax.jcr.Node;
-import javax.jcr.RepositoryException;
-import javax.jcr.Value;
-import javax.jcr.query.Row;
-import javax.jcr.query.qom.Operand;
-import javax.jcr.query.qom.PropertyValue;
-
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
-
-abstract class AbstractRow implements Row {
-
-    private final Map<String, PropertyValue> columns;
-
-    private final OperandEvaluator evaluator;
-
-    protected AbstractRow(
-            Map<String, PropertyValue> columns, OperandEvaluator evaluator) {
-        this.columns = columns;
-        this.evaluator = evaluator;
-    }
-
-    public Value[] getValues() throws RepositoryException {
-        Value[] values = new Value[columns.size()];
-        for (Operand operand : columns.values()) {
-            values = evaluator.getValues(operand, this);
-        }
-        return values;
-    }
-
-    public Value getValue(String columnName)
-            throws ItemNotFoundException, RepositoryException {
-        Operand operand = columns.get(columnName);
-        if (operand != null) {
-            return evaluator.getValue(operand, this);
-        } else {
-            throw new ItemNotFoundException(
-                    "Column " + columnName + " is not included in this row");
-        }
-    }
-
-    public String getPath() throws RepositoryException {
-        Node node = getNode();
-        if (node != null) {
-            return node.getPath();
-        } else {
-            return null;
-        }
-    }
-
-    public String getPath(String selectorName) throws RepositoryException {
-        Node node = getNode(selectorName);
-        if (node != null) {
-            return node.getPath();
-        } else {
-            return null;
-        }
-    }
-
-}
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java
index 62db16d..21597c4 100644
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java
+++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java
@@ -30,10 +30,10 @@ import javax.jcr.query.Row;
 import javax.jcr.query.qom.ChildNodeJoinCondition;
 import javax.jcr.query.qom.Constraint;
 import javax.jcr.query.qom.Join;
-import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.Operand;
 import javax.jcr.query.qom.QueryObjectModelFactory;
 
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.OperandEvaluator;
 
 class ChildNodeJoinMerger extends JoinMerger {
 
@@ -42,7 +42,7 @@ class ChildNodeJoinMerger extends JoinMerger {
     private final String parentSelector;
 
     public ChildNodeJoinMerger(
-            Join join, Map<String, PropertyValue> columns,
+            Join join, Map<String, Operand> columns,
             OperandEvaluator evaluator, QueryObjectModelFactory factory,
             ChildNodeJoinCondition condition)
             throws RepositoryException {
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java
index cb1babe..4f0b64e 100644
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java
+++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java
@@ -30,10 +30,10 @@ import javax.jcr.query.Row;
 import javax.jcr.query.qom.Constraint;
 import javax.jcr.query.qom.DescendantNodeJoinCondition;
 import javax.jcr.query.qom.Join;
-import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.Operand;
 import javax.jcr.query.qom.QueryObjectModelFactory;
 
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.OperandEvaluator;
 
 class DescendantNodeJoinMerger extends JoinMerger {
 
@@ -42,7 +42,7 @@ class DescendantNodeJoinMerger extends JoinMerger {
     private final String ancestorSelector;
 
     public DescendantNodeJoinMerger(
-            Join join, Map<String, PropertyValue> columns,
+            Join join, Map<String, Operand> columns,
             OperandEvaluator evaluator, QueryObjectModelFactory factory,
             DescendantNodeJoinCondition condition)
             throws RepositoryException {
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java
index c4a0a6f..c7e412d 100644
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java
+++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java
@@ -33,10 +33,11 @@ import javax.jcr.query.qom.Constraint;
 import javax.jcr.query.qom.EquiJoinCondition;
 import javax.jcr.query.qom.Join;
 import javax.jcr.query.qom.Literal;
+import javax.jcr.query.qom.Operand;
 import javax.jcr.query.qom.PropertyValue;
 import javax.jcr.query.qom.QueryObjectModelFactory;
 
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.OperandEvaluator;
 
 class EquiJoinMerger extends JoinMerger {
 
@@ -45,7 +46,7 @@ class EquiJoinMerger extends JoinMerger {
     private final PropertyValue rightProperty;
 
     public EquiJoinMerger(
-            Join join, Map<String, PropertyValue> columns,
+            Join join, Map<String, Operand> columns,
             OperandEvaluator evaluator, QueryObjectModelFactory factory,
             EquiJoinCondition condition) throws RepositoryException {
         super(join, columns, evaluator, factory);
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java
index 82b11c1..c8fe83f 100644
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java
+++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java
@@ -40,7 +40,7 @@ import javax.jcr.query.qom.DescendantNodeJoinCondition;
 import javax.jcr.query.qom.EquiJoinCondition;
 import javax.jcr.query.qom.Join;
 import javax.jcr.query.qom.JoinCondition;
-import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.Operand;
 import javax.jcr.query.qom.QueryObjectModelFactory;
 import javax.jcr.query.qom.SameNodeJoinCondition;
 import javax.jcr.query.qom.Selector;
@@ -48,7 +48,8 @@ import javax.jcr.query.qom.Source;
 
 import org.apache.jackrabbit.commons.iterator.RowIterable;
 import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter;
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.JoinRow;
+import org.apache.jackrabbit.commons.query.OperandEvaluator;
 
 /**
  * A join merger is used by the {@link QueryEngine} class to efficiently
@@ -73,7 +74,7 @@ abstract class JoinMerger {
      * @throws RepositoryException if the merger can not be created
      */
     public static JoinMerger getJoinMerger(
-            Join join, Map<String, PropertyValue> columns,
+            Join join, Map<String, Operand> columns,
             OperandEvaluator evaluator, QueryObjectModelFactory factory)
             throws RepositoryException {
         JoinCondition condition = join.getJoinCondition();
@@ -107,7 +108,7 @@ abstract class JoinMerger {
 
     private final String[] selectorNames;
 
-    private final Map<String, PropertyValue> columns;
+    private final Map<String, Operand> columns;
 
     private final String[] columnNames;
 
@@ -116,7 +117,7 @@ abstract class JoinMerger {
     protected final QueryObjectModelFactory factory;
 
     protected JoinMerger(
-            Join join, Map<String, PropertyValue> columns,
+            Join join, Map<String, Operand> columns,
             OperandEvaluator evaluator, QueryObjectModelFactory factory)
             throws RepositoryException {
         this.type = join.getJoinType();
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinRow.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinRow.java
deleted file mode 100644
index 71a2945..0000000
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinRow.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.join;
-
-import java.util.Map;
-import java.util.Set;
-
-import javax.jcr.Node;
-import javax.jcr.RepositoryException;
-import javax.jcr.query.Row;
-import javax.jcr.query.qom.PropertyValue;
-
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
-
-public class JoinRow extends AbstractRow {
-
-    private final Row leftRow;
-
-    private final Set<String> leftSelectors;
-
-    private final Row rightRow;
-
-    private final Set<String> rightSelectors;
-
-    public JoinRow(
-            Map<String, PropertyValue> columns, OperandEvaluator evaluator,
-            Row leftRow, Set<String> leftSelectors,
-            Row rightRow, Set<String> rightSelectors) {
-        super(columns, evaluator);
-        this.leftRow = leftRow;
-        this.leftSelectors = leftSelectors;
-        this.rightRow = rightRow;
-        this.rightSelectors = rightSelectors;
-    }
-
-    public Node getNode() throws RepositoryException {
-        throw new RepositoryException();
-    }
-
-    public Node getNode(String selectorName) throws RepositoryException {
-        Row row = getRow(selectorName);
-        if (row != null) {
-            return row.getNode(selectorName);
-        } else {
-            return null;
-        }
-    }
-
-    public double getScore() throws RepositoryException {
-        throw new RepositoryException();
-    }
-
-    public double getScore(String selectorName) throws RepositoryException {
-        Row row = getRow(selectorName);
-        if (row != null) {
-            return row.getScore(selectorName);
-        } else {
-            return 0.0;
-        }
-    }
-
-    private Row getRow(String selector) throws RepositoryException {
-        if (leftSelectors.contains(selector)) {
-            return leftRow;
-        } else if (rightSelectors.contains(selector)) {
-            return rightRow;
-        } else {
-            throw new RepositoryException(
-                    "Selector " + selector + " is not included in this row");
-        }
-    }
-
-    //--------------------------------------------------------------< Object >
-
-    public String toString() {
-        StringBuilder builder = new StringBuilder();
-        builder.append("{ ");
-        for (String selector : leftSelectors) {
-            builder.append(selector);
-            builder.append("=");
-            try {
-                builder.append(leftRow.getNode(selector));
-            } catch (RepositoryException e) {
-                builder.append(e.getMessage());
-            }
-            builder.append(" ");
-        }
-        for (String selector : rightSelectors) {
-            builder.append(selector);
-            builder.append("=");
-            if(rightRow != null){
-                try {
-                    builder.append(rightRow.getNode(selector));
-                } catch (RepositoryException e) {
-                    builder.append(e.getMessage());
-                }
-            }else{
-                builder.append("null");
-            }
-            builder.append(" ");
-        }
-        builder.append("}");
-        return builder.toString();
-    }
-
-}
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java
index 4fe24e5..78434f7 100644
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java
+++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java
@@ -58,7 +58,7 @@ import javax.jcr.query.qom.Source;
 import org.apache.commons.io.IOUtils;
 import org.apache.jackrabbit.commons.JcrUtils;
 import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter;
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.OperandEvaluator;
 import org.apache.jackrabbit.core.query.lucene.LuceneQueryFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -478,8 +478,7 @@ public class QueryEngine {
         String[] selectorNames = selectorMap.keySet().toArray(
                 new String[selectorMap.size()]);
 
-        Map<String, PropertyValue> columnMap = getColumnMap(columns,
-                selectorMap);
+        Map<String, Operand> columnMap = getColumnMap(columns, selectorMap);
         String[] columnNames = columnMap.keySet().toArray(
                 new String[columnMap.size()]);
 
@@ -502,9 +501,9 @@ public class QueryEngine {
         }
     }
 
-    private Map<String, PropertyValue> getColumnMap(Column[] columns,
+    private Map<String, Operand> getColumnMap(Column[] columns,
             Map<String, NodeType> selectors) throws RepositoryException {
-        Map<String, PropertyValue> map = new LinkedHashMap<String, PropertyValue>();
+        Map<String, Operand> map = new LinkedHashMap<String, Operand>();
         if (columns != null && columns.length > 0) {
             for (int i = 0; i < columns.length; i++) {
                 String name = columns[i].getColumnName();
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.java
index 52cb553..db1c26d 100644
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.java
+++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.java
@@ -30,11 +30,11 @@ import javax.jcr.RepositoryException;
 import javax.jcr.query.Row;
 import javax.jcr.query.qom.Constraint;
 import javax.jcr.query.qom.Join;
-import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.Operand;
 import javax.jcr.query.qom.QueryObjectModelFactory;
 import javax.jcr.query.qom.SameNodeJoinCondition;
 
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
+import org.apache.jackrabbit.commons.query.OperandEvaluator;
 
 class SameNodeJoinMerger extends JoinMerger {
 
@@ -45,7 +45,7 @@ class SameNodeJoinMerger extends JoinMerger {
     private final String path;
 
     public SameNodeJoinMerger(
-            Join join, Map<String, PropertyValue> columns,
+            Join join, Map<String, Operand> columns,
             OperandEvaluator evaluator, QueryObjectModelFactory factory,
             SameNodeJoinCondition condition) throws RepositoryException {
         super(join, columns, evaluator, factory);
diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SelectorRow.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SelectorRow.java
deleted file mode 100644
index 901cec1..0000000
--- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SelectorRow.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.join;
-
-import java.util.Map;
-
-import javax.jcr.Node;
-import javax.jcr.RepositoryException;
-import javax.jcr.query.qom.PropertyValue;
-
-import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
-
-/**
- * A row implementation for a query with just a single selector.
- */
-public class SelectorRow extends AbstractRow {
-
-    private final String selector;
-
-    private final Node node;
-
-    private final double score;
-
-    public SelectorRow(
-            Map<String, PropertyValue> columns, OperandEvaluator evaluator,
-            String selector, Node node, double score) {
-        super(columns, evaluator);
-        this.selector = selector;
-        this.node = node;
-        this.score = score;
-    }
-
-    public Node getNode() {
-        return node;
-    }
-
-    public Node getNode(String selectorName) throws RepositoryException {
-        checkSelectorName(selectorName);
-        return node;
-    }
-
-    public double getScore() {
-        return score;
-    }
-
-    public double getScore(String selectorName) throws RepositoryException {
-        checkSelectorName(selectorName);
-        return score;
-    }
-
-    private void checkSelectorName(String name) throws RepositoryException {
-        if (!selector.equals(name)) {
-            throw new RepositoryException(
-                    "Selector " + name + " is not included in this row");
-        }
-    }
-
-    //--------------------------------------------------------------< Object >
-
-    public String toString() {
-        return "{ " + selector + ": " + node + " }";
-    }
-
-}
diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/AbstractRow.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/AbstractRow.java
new file mode 100644
index 0000000..486f05b
--- /dev/null
+++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/AbstractRow.java
@@ -0,0 +1,119 @@
+/*
+ * 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.commons.query;
+
+import java.util.Map;
+
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.query.Row;
+import javax.jcr.query.qom.Operand;
+
+/**
+ * Abstract base class for {@link Row} implementations. A subclass just
+ * needs to implement the getNode() and getScore() methods to properly
+ * implement the Row interface. The column definitions and the operand
+ * evaluator given to the constructor are used together with the getNode()
+ * and getScore() methods to implement the rest of the Row interface.
+ *
+ * @since Apache Jackrabbit 2.3
+ */
+public abstract class AbstractRow implements Row {
+
+    /** Column definitions */
+    private final Map<String, Operand> columns;
+
+    /** Evaluator for column operands */
+    private final OperandEvaluator evaluator;
+
+    /**
+     * Creates a new row based on the given columns definitions and the
+     * means to evaluate those definitions.
+     *
+     * @param columns column definitions
+     * @param evaluator evaluator for column operands
+     */
+    protected AbstractRow(
+            Map<String, Operand> columns, OperandEvaluator evaluator) {
+        this.columns = columns;
+        this.evaluator = evaluator;
+    }
+
+    /**
+     * Returns the evaluated values of all the defined columns.
+     *
+     * @return column values
+     * @throws RepositoryException if the columns can not be evaluated
+     */
+    public Value[] getValues() throws RepositoryException {
+        Value[] values = new Value[columns.size()];
+        for (Operand operand : columns.values()) {
+            values = evaluator.getValues(operand, this);
+        }
+        return values;
+    }
+
+    /**
+     * Returns the evaluated value of all the given column.
+     *
+     * @param columnName column name
+     * @return column value
+     * @throws ItemNotFoundException if the column does not exist
+     * @throws RepositoryException if the column can not be evaluated
+     */
+    public Value getValue(String columnName)
+            throws ItemNotFoundException, RepositoryException {
+        Operand operand = columns.get(columnName);
+        if (operand != null) {
+            return evaluator.getValue(operand, this);
+        } else {
+            throw new ItemNotFoundException(
+                    "Column " + columnName + " is not included in this row");
+        }
+    }
+
+    /**
+     * Gets the single node of this row and returns its path.
+     *
+     * @return node path
+     * @throws RepositoryException if the node or its path can not be accessed
+     */
+    public String getPath() throws RepositoryException {
+        return getNode().getPath();
+    }
+
+    /**
+     * Gets the identified node of this row and returns its path.
+     * Returns <code>null</code> if the identified node is absent because of
+     * an outer join.
+     *
+     * @param selectorName selector name
+     * @return path of the identified node, or <code>null</code>
+     * @throws RepositoryException if the node or its path can not be accessed
+     */
+    public String getPath(String selectorName) throws RepositoryException {
+        Node node = getNode(selectorName);
+        if (node != null) {
+            return node.getPath();
+        } else {
+            return null;
+        }
+    }
+
+}
diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/JoinRow.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/JoinRow.java
new file mode 100644
index 0000000..257228c
--- /dev/null
+++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/JoinRow.java
@@ -0,0 +1,177 @@
+/*
+ * 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.commons.query;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+import javax.jcr.query.qom.Operand;
+
+/**
+ * Row with more than one selector.
+ *
+ * @since Apache Jackrabbit 2.3
+ */
+public class JoinRow extends AbstractRow {
+
+    /** Left part of the join, possibly <code>null</code> */
+    private final Row leftRow;
+
+    /** Selectors of the left part of the join */
+    private final Set<String> leftSelectors;
+
+    /** Right part of the join, possibly <code>null</code> */
+    private final Row rightRow;
+
+    /** Selectors of the right part of the join */
+    private final Set<String> rightSelectors;
+
+    /**
+     * Creates a row from the given left and right parts of a join.
+     * Either the left or the right part could be null for an outer join.
+     *
+     * @param columns column definitions
+     * @param evaluator evaluator for column operands
+     * @param leftRow left part of the join, or <code>null</code>
+     * @param leftSelectors selectors of the left part of the join
+     * @param rightRow right part of the join, or <code>null</code>
+     * @param rightSelectors selectors of the right part of the join
+     */
+    public JoinRow(
+            Map<String, Operand> columns, OperandEvaluator evaluator,
+            Row leftRow, Set<String> leftSelectors,
+            Row rightRow, Set<String> rightSelectors) {
+        super(columns, evaluator);
+        this.leftRow = leftRow;
+        this.leftSelectors = leftSelectors;
+        this.rightRow = rightRow;
+        this.rightSelectors = rightSelectors;
+    }
+
+    /**
+     * Throws an exception since this row has more than one selector.
+     *
+     * @return nothing
+     * @throws RepositoryException always thrown
+     */
+    public Node getNode() throws RepositoryException {
+        throw new RepositoryException("This row has more than one selector");
+    }
+
+    /**
+     * Returns the node of the given selector, or <code>null</code> if
+     * the node is absent due to an outer join.
+     *
+     * @param selector selector name
+     * @return the node of the given selector, or <code>null</code>
+     * @throws RepositoryException if the given selector is unknown or
+     *                             if the node can not be accessed
+     */
+    public Node getNode(String selectorName) throws RepositoryException {
+        Row row = getRow(selectorName);
+        if (row != null) {
+            return row.getNode(selectorName);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Throws an exception since this row has more than one selector.
+     *
+     * @return nothing
+     * @throws RepositoryException always thrown
+     */
+    public double getScore() throws RepositoryException {
+        throw new RepositoryException("This row has more than one selector");
+    }
+
+    /**
+     * Returns the full text search score of the given selector,
+     * or 0.0 as the default value if the selector value is absent due
+     * to an outer join.
+     *
+     * @param selector selector name
+     * @return full text search score of the given selector, or 0.0
+     * @throws RepositoryException if the given selector is unknown or
+     *                             if the search score can not be accessed
+     */
+    public double getScore(String selectorName) throws RepositoryException {
+        Row row = getRow(selectorName);
+        if (row != null) {
+            return row.getScore(selectorName);
+        } else {
+            return 0.0;
+        }
+    }
+
+    /**
+     * Returns the left or right part of the join, depending on which side
+     * contains the given selector.
+     *
+     * @param selector selector name
+     * @return the row that contains the given selector, or <code>null</code>
+     * @throws RepositoryException if the given selector is unknown
+     */
+    private Row getRow(String selector) throws RepositoryException {
+        if (leftSelectors.contains(selector)) {
+            return leftRow;
+        } else if (rightSelectors.contains(selector)) {
+            return rightRow;
+        } else {
+            throw new RepositoryException(
+                    "Selector " + selector + " is not included in this row");
+        }
+    }
+
+    //--------------------------------------------------------------< Object >
+
+    /**
+     * Returns a string representation of this row.
+     *
+     * @return string representation of this row
+     */
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("{ ");
+        appendSelectors(builder, leftSelectors, leftRow);
+        appendSelectors(builder, rightSelectors, rightRow);
+        return builder.toString();
+    }
+
+    private static void appendSelectors(
+            StringBuilder builder, Set<String> selectors, Row row) {
+        for (String selector : selectors) {
+            builder.append(selector);
+            builder.append("=");
+            if (row != null) {
+                try {
+                    builder.append(row.getNode(selector));
+                } catch (RepositoryException e) {
+                    builder.append(e.getMessage());
+                }
+            } else {
+                builder.append("null");
+            }
+            builder.append(" ");
+        }
+    }
+
+}
diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/OperandEvaluator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/OperandEvaluator.java
new file mode 100644
index 0000000..5b5e92d
--- /dev/null
+++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/OperandEvaluator.java
@@ -0,0 +1,404 @@
+/*
+ * 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.commons.query;
+
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+import javax.jcr.query.Row;
+import javax.jcr.query.qom.BindVariableValue;
+import javax.jcr.query.qom.FullTextSearchScore;
+import javax.jcr.query.qom.Length;
+import javax.jcr.query.qom.Literal;
+import javax.jcr.query.qom.LowerCase;
+import javax.jcr.query.qom.NodeLocalName;
+import javax.jcr.query.qom.NodeName;
+import javax.jcr.query.qom.Operand;
+import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.StaticOperand;
+import javax.jcr.query.qom.UpperCase;
+
+/**
+ * Evaluator of QOM {@link Operand operands}. This class evaluates operands
+ * in the context of a {@link ValueFactory value factory}, a set of bind
+ * variables and possibly a query result row. It is also possible to specify
+ * a locale to be used for upper and lower case conversion. The default
+ * locale is {@link Locale#ENGLISH}.
+ *
+ * @since Apache Jackrabbit 2.3
+ */
+public class OperandEvaluator {
+
+    /** Value factory */
+    private final ValueFactory factory;
+
+    /** Bind variables */
+    private final Map<String, Value> variables;
+
+    /** The locale to use in upper- and lower-case conversion. */
+    private final Locale locale;
+
+    /**
+     * Creates an operand evaluator for the given value factory and set of
+     * bind variables. Upper- and lower-case conversions are performed using
+     * the given locale.
+     *
+     * @param factory value factory
+     * @param variables bind variables
+     * @param locale locale to use in upper- and lower-case conversions
+     */
+    public OperandEvaluator(
+            ValueFactory factory, Map<String, Value> variables, Locale locale) {
+        this.factory = factory;
+        this.variables = variables;
+        this.locale = locale;
+    }
+
+    /**
+     * Creates an operand evaluator for the given value factory and set of
+     * bind variables. Upper- and lower-case conversions are performed using
+     * the {@link Locale#ENGLISH}.
+     *
+     * @param factory value factory
+     * @param variables bind variables
+     * @param locale locale to use in upper- and lower-case conversions
+     */
+    public OperandEvaluator(
+            ValueFactory factory, Map<String, Value> variables) {
+        this(factory, variables, Locale.ENGLISH);
+    }
+
+    /**
+     * Returns the value of the given static operand
+     * ({@link Literal literal} or {@link BindVariableValue bind variable})
+     * casted to the given type.
+     *
+     * @param operand static operand to be evaluated
+     * @param type expected value type
+     * @return evaluated value, casted to the given type
+     * @throws RepositoryException if a named bind variable is not found,
+     *                             if the operand type is unknown, or
+     *                             if the type conversion fails
+     */
+    public Value getValue(StaticOperand operand, int type)
+            throws RepositoryException {
+        Value value = getValue(operand);
+        if (type == PropertyType.UNDEFINED || type == value.getType()) {
+            return value;
+        } if (type == PropertyType.LONG) {
+            return factory.createValue(value.getLong());
+        } if (type == PropertyType.DOUBLE) {
+            return factory.createValue(value.getDouble());
+        } if (type == PropertyType.DATE) {
+            return factory.createValue(value.getDate());
+        } else {
+            return factory.createValue(value.getString(), type);
+        }
+    }
+
+    /**
+     * Returns the value of the given static operand
+     * ({@link Literal literal} or {@link BindVariableValue bind variable}).
+     *
+     * @param operand static operand to be evaluated
+     * @return evaluated value
+     * @throws RepositoryException if a named bind variable is not found,
+     *                             or if the operand type is unknown
+     */
+    public Value getValue(StaticOperand operand) throws RepositoryException {
+        if (operand instanceof Literal) {
+            Literal literal = (Literal) operand;
+            return literal.getLiteralValue();
+        } else if (operand instanceof BindVariableValue) {
+            BindVariableValue bvv = (BindVariableValue) operand;
+            Value value = variables.get(bvv.getBindVariableName());
+            if (value != null) {
+                return value;
+            } else {
+                throw new RepositoryException(
+                        "Unknown bind variable: " + bvv.getBindVariableName());
+            }
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unknown static operand type: " + operand);
+        }
+    }
+
+    /**
+     * Returns the value of the given operand in the context of the given row.
+     * This is a convenience method that uses a somewhat lossy best-effort
+     * mapping to evaluate multi-valued operands to a single value. Use the
+     * {@link #getValues(Operand, Row)} method for more accurate results.
+     *
+     * @param operand operand to be evaluated
+     * @param row query result row
+     * @return evaluated value
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    public Value getValue(Operand operand, Row row) throws RepositoryException {
+        Value[] values = getValues(operand, row);
+        if (values.length == 1) {
+            return values[0];
+        } else {
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < values.length; i++) {
+                if (i > 0) {
+                    builder.append(' ');
+                }
+                builder.append(values[i].getString());
+            }
+            return factory.createValue(builder.toString());
+        }
+    }
+
+    /**
+     * Evaluates the given operand in the context of the given row.
+     *
+     * @param operand operand to be evaluated
+     * @param row query result row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    public Value[] getValues(Operand operand, Row row)
+            throws RepositoryException {
+        if (operand instanceof StaticOperand) {
+            StaticOperand so = (StaticOperand) operand;
+            return new Value[] { getValue(so) };
+        } else if (operand instanceof FullTextSearchScore) {
+            FullTextSearchScore ftss = (FullTextSearchScore) operand;
+            double score = row.getScore(ftss.getSelectorName());
+            return new Value[] { factory.createValue(score) };
+        } else if (operand instanceof NodeName) {
+            NodeName nn = (NodeName) operand;
+            Value value = factory.createValue(
+                    row.getNode(nn.getSelectorName()).getName(),
+                    PropertyType.NAME);
+            return new Value[] { value };
+        } else if (operand instanceof Length) {
+            return getLengthValues((Length) operand, row);
+        } else if (operand instanceof LowerCase) {
+            return getLowerCaseValues((LowerCase) operand, row);
+        } else if (operand instanceof UpperCase) {
+            return getUpperCaseValues((UpperCase) operand, row);
+        } else if (operand instanceof NodeLocalName) {
+            return getNodeLocalNameValues((NodeLocalName) operand, row);
+        } else if (operand instanceof PropertyValue) {
+            return getPropertyValues((PropertyValue) operand, row);
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unknown operand type: " + operand);
+        }
+    }
+
+    /**
+     * Evaluates the given operand in the context of the given node
+     * and full text search score.
+     *
+     * @param operand operand to be evaluated
+     * @param node node
+     * @param score full text search score
+     * @return values of the operand at the given node
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    public Value[] getValues(Operand operand, Node node, double score)
+            throws RepositoryException {
+        Map<String, Operand> columns = Collections.emptyMap();
+        return getValues(operand, new SelectorRow(
+                columns, this, "node", node, score));
+    }
+
+    /**
+     * Evaluates the given operand in the context of the given node.
+     * A default full text search score of 0.0 is used for a possible
+     * {@link FullTextSearchScore} operand.
+     *
+     * @param operand operand to be evaluated
+     * @param node node
+     * @return values of the operand at the given node
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    public Value[] getValues(Operand operand, Node node)
+            throws RepositoryException {
+        return getValues(operand, node, 0.0);
+    }
+
+    /**
+     * Returns the name of the property that would be used when evaluating
+     * the given operand. Returns <code>null</code> if the operand uses no
+     * specific property.
+     * 
+     * @param operand operand
+     * @return property name, or <code>null</code>
+     */
+    public String getAffectedPropertyName(Operand operand) {
+        if (operand instanceof Length) {
+            return ((Length) operand).getPropertyValue().getPropertyName();
+        } else if (operand instanceof LowerCase) {
+            return getAffectedPropertyName(((LowerCase) operand).getOperand());
+        } else if (operand instanceof UpperCase) {
+            return getAffectedPropertyName(((UpperCase) operand).getOperand());
+        } else if (operand instanceof PropertyValue) {
+            return ((PropertyValue) operand).getPropertyName();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the values of the given value length operand at the given row.
+     *
+     * @see #getProperty(PropertyValue, Row)
+     * @param operand value length operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getLengthValues(Length operand, Row row)
+            throws RepositoryException {
+        Property property = getProperty(operand.getPropertyValue(), row);
+        if (property == null) {
+            return new Value[0];
+        } else if (property.isMultiple()) {
+            long[] lengths = property.getLengths();
+            Value[] values = new Value[lengths.length];
+            for (int i = 0; i < lengths.length; i++) {
+                values[i] = factory.createValue(lengths[i]);
+            }
+            return values;
+        } else {
+            long length = property.getLength();
+            return new Value[] { factory.createValue(length) };
+        }
+    }
+
+    /**
+     * Returns the values of the given lower case operand at the given row.
+     *
+     * @param operand lower case operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getLowerCaseValues(LowerCase operand, Row row)
+            throws RepositoryException {
+        Value[] values = getValues(operand.getOperand(), row);
+        for (int i = 0; i < values.length; i++) {
+            String value = values[i].getString();
+            String lower = value.toLowerCase(locale);
+            if (!value.equals(lower)) {
+                values[i] = factory.createValue(lower);
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Returns the values of the given upper case operand at the given row.
+     *
+     * @param operand upper case operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getUpperCaseValues(UpperCase operand, Row row)
+            throws RepositoryException {
+        Value[] values = getValues(operand.getOperand(), row);
+        for (int i = 0; i < values.length; i++) {
+            String value = values[i].getString();
+            String upper = value.toUpperCase(locale);
+            if (!value.equals(upper)) {
+                values[i] = factory.createValue(upper);
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Returns the value of the given local name operand at the given row.
+     *
+     * @param operand local name operand
+     * @param row row
+     * @return value of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getNodeLocalNameValues(NodeLocalName operand, Row row)
+            throws RepositoryException {
+        Node node = row.getNode(operand.getSelectorName());
+        String name = node.getName();
+        int colon = name.indexOf(':');
+        if (colon != -1) {
+            name = name.substring(colon + 1);
+        }
+        return new Value[] { factory.createValue(name, PropertyType.NAME) };
+    }
+
+    /**
+     * Returns the values of the given property value operand at the given row.
+     *
+     * @see #getProperty(PropertyValue, Row)
+     * @param operand property value operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getPropertyValues(PropertyValue operand, Row row)
+            throws RepositoryException {
+        Property property = getProperty(operand, row);
+        if (property == null) {
+            return new Value[0];
+        } else if (property.isMultiple()) {
+            return property.getValues();
+        } else {
+            return new Value[] { property.getValue() };
+        }
+    }
+
+    /**
+     * Returns the identified property from the given row. This method
+     * is used by both the {@link #getValue(Length, Row)} and the
+     * {@link #getValue(PropertyValue, Row)} methods to access properties.
+     *
+     * @param operand property value operand
+     * @param row row
+     * @return the identified property,
+     *         or <code>null</code> if the property does not exist
+     * @throws RepositoryException if the property can't be accessed
+     */
+    private Property getProperty(PropertyValue operand, Row row)
+            throws RepositoryException {
+        Node node = row.getNode(operand.getSelectorName());
+        if (node != null) {
+            try {
+                return node.getProperty(operand.getPropertyName());
+            } catch (PathNotFoundException e) {
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+}
diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SelectorRow.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SelectorRow.java
new file mode 100644
index 0000000..e3d8f0f
--- /dev/null
+++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SelectorRow.java
@@ -0,0 +1,127 @@
+/*
+ * 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.commons.query;
+
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.qom.Operand;
+
+/**
+ * Row with just a single selector.
+ *
+ * @since Apache Jackrabbit 2.3
+ */
+public class SelectorRow extends AbstractRow {
+
+    /** Name of the single selector of this row */
+    private final String selector;
+
+    /** Node of this row */
+    private final Node node;
+
+    /** Full text search score of this row */
+    private final double score;
+
+    /**
+     * Creates a row with a single selector.
+     *
+     * @param columns column definitions
+     * @param evaluator evaluator for column operands
+     * @param selector name of the single selector
+     * @param node node of this row
+     * @param score full text search score of this row
+     */
+    public SelectorRow(
+            Map<String, Operand> columns, OperandEvaluator evaluator,
+            String selector, Node node, double score) {
+        super(columns, evaluator);
+        this.selector = selector;
+        this.node = node;
+        this.score = score;
+    }
+
+    /**
+     * Returns the node of this row.
+     *
+     * @return node of this row
+     */
+    public Node getNode() {
+        return node;
+    }
+
+    /**
+     * Returns the node of this row if given the correct selector name.
+     * Otherwise throws an exception.
+     *
+     * @param selectorName selector name
+     * @return node of this row
+     * @throws RepositoryException if an unknown selector name was given
+     */
+    public Node getNode(String selectorName) throws RepositoryException {
+        checkSelectorName(selectorName);
+        return node;
+    }
+
+    /**
+     * Returns the full text search score of this row.
+     *
+     * @return full text search score of this row
+     */
+    public double getScore() {
+        return score;
+    }
+
+    /**
+     * Returns the full text search score of this row if given the correct
+     * selector name. Otherwise throws an exception.
+     *
+     * @param selectorName selector name
+     * @return full text search score of this row
+     * @throws RepositoryException if an unknown selector name was given
+     */
+    public double getScore(String selectorName) throws RepositoryException {
+        checkSelectorName(selectorName);
+        return score;
+    }
+
+    /**
+     * Checks that the given selector name matches the one of this row.
+     *
+     * @param name selector name
+     * @throws RepositoryException if an unknown selector name was given
+     */
+    private void checkSelectorName(String name) throws RepositoryException {
+        if (!selector.equals(name)) {
+            throw new RepositoryException(
+                    "Selector " + name + " is not included in this row");
+        }
+    }
+
+    //--------------------------------------------------------------< Object >
+
+    /**
+     * Returns a string representation of this row.
+     *
+     * @return string representation of this row
+     */
+    public String toString() {
+        return "{ " + selector + ": " + node + " }";
+    }
+
+}
diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/OperandEvaluator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/OperandEvaluator.java
deleted file mode 100644
index c1b5a85..0000000
--- a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/OperandEvaluator.java
+++ /dev/null
@@ -1,533 +0,0 @@
-/*
- * 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.commons.query.qom;
-
-import java.util.Locale;
-import java.util.Map;
-
-import javax.jcr.Node;
-import javax.jcr.PathNotFoundException;
-import javax.jcr.Property;
-import javax.jcr.PropertyType;
-import javax.jcr.RepositoryException;
-import javax.jcr.UnsupportedRepositoryOperationException;
-import javax.jcr.Value;
-import javax.jcr.ValueFactory;
-import javax.jcr.query.Row;
-import javax.jcr.query.qom.BindVariableValue;
-import javax.jcr.query.qom.FullTextSearchScore;
-import javax.jcr.query.qom.Length;
-import javax.jcr.query.qom.Literal;
-import javax.jcr.query.qom.LowerCase;
-import javax.jcr.query.qom.NodeLocalName;
-import javax.jcr.query.qom.NodeName;
-import javax.jcr.query.qom.Operand;
-import javax.jcr.query.qom.PropertyValue;
-import javax.jcr.query.qom.StaticOperand;
-import javax.jcr.query.qom.UpperCase;
-
-/**
- * Evaluator of QOM {@link Operand operands}. This class evaluates operands
- * in the context of a {@link ValueFactory value factory}, a set of bind
- * variables and possibly a query result row.
- */
-public class OperandEvaluator {
-
-    /** Value factory */
-    private final ValueFactory factory;
-
-    /** Bind variables */
-    private final Map<String, Value> variables;
-
-    /** The locale to use in upper- and lower-case conversion. */
-    private final Locale locale;
-
-    /**
-     * Creates an operand evaluator for the given value factory and set of
-     * bind variables. Upper- and lower-case conversions are performed using
-     * the given locale.
-     *
-     * @param factory value factory
-     * @param variables bind variables
-     * @param locale locale to use in upper- and lower-case conversions
-     */
-    public OperandEvaluator(
-            ValueFactory factory, Map<String, Value> variables, Locale locale) {
-        this.factory = factory;
-        this.variables = variables;
-        this.locale = locale;
-    }
-
-    /**
-     * Creates an operand evaluator for the given value factory and set of
-     * bind variables. Upper- and lower-case conversions are performed using
-     * the {@link Locale#ENGLISH}.
-     *
-     * @param factory value factory
-     * @param variables bind variables
-     * @param locale locale to use in upper- and lower-case conversions
-     */
-    public OperandEvaluator(
-            ValueFactory factory, Map<String, Value> variables) {
-        this(factory, variables, Locale.ENGLISH);
-    }
-
-    /**
-     * Returns the value of the given static operand
-     * ({@link Literal literal} or {@link BindVariableValue bind variable})
-     * casted to the given type.
-     *
-     * @param operand static operand to be evaluated
-     * @param type expected value type
-     * @return evaluated value, casted to the given type
-     * @throws RepositoryException if a named bind variable is not found,
-     *                             if the operand type is unknown, or
-     *                             if the type conversion fails
-     */
-    public Value getValue(StaticOperand operand, int type)
-            throws RepositoryException {
-        Value value = getValue(operand);
-        if (type == PropertyType.UNDEFINED || type == value.getType()) {
-            return value;
-        } if (type == PropertyType.LONG) {
-            return factory.createValue(value.getLong());
-        } if (type == PropertyType.DOUBLE) {
-            return factory.createValue(value.getDouble());
-        } if (type == PropertyType.DATE) {
-            return factory.createValue(value.getDate());
-        } else {
-            return factory.createValue(value.getString(), type);
-        }
-    }
-
-    /**
-     * Returns the value of the given static operand
-     * ({@link Literal literal} or {@link BindVariableValue bind variable}).
-     *
-     * @param operand static operand to be evaluated
-     * @return evaluated value
-     * @throws RepositoryException if a named bind variable is not found,
-     *                             or if the operand type is unknown
-     */
-    public Value getValue(StaticOperand operand) throws RepositoryException {
-        if (operand instanceof Literal) {
-            Literal literal = (Literal) operand;
-            return literal.getLiteralValue();
-        } else if (operand instanceof BindVariableValue) {
-            BindVariableValue bvv = (BindVariableValue) operand;
-            Value value = variables.get(bvv.getBindVariableName());
-            if (value != null) {
-                return value;
-            } else {
-                throw new RepositoryException(
-                        "Unknown bind variable: " + bvv.getBindVariableName());
-            }
-        } else {
-            throw new UnsupportedRepositoryOperationException(
-                    "Unknown static operand type: " + operand);
-        }
-    }
-
-    /**
-     * Returns the value of the given operand in the context of the given row.
-     * This is a convenience method that uses a somewhat lossy best-effort
-     * mapping to evaluate multi-valued operands to a single value. Use the
-     * {@link #getValues(Operand, Row)} method for more accurate results.
-     *
-     * @param operand operand to be evaluated
-     * @param row query result row
-     * @return evaluated value
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    public Value getValue(Operand operand, Row row) throws RepositoryException {
-        Value[] values = getValues(operand, row);
-        if (values.length == 1) {
-            return values[0];
-        } else {
-            StringBuilder builder = new StringBuilder();
-            for (int i = 0; i < values.length; i++) {
-                if (i > 0) {
-                    builder.append(' ');
-                }
-                builder.append(values[i].getString());
-            }
-            return factory.createValue(builder.toString());
-        }
-    }
-
-    /**
-     * Evaluates the given operand in the context of the given row.
-     *
-     * @param operand operand to be evaluated
-     * @param row query result row
-     * @return values of the operand at the given row
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    public Value[] getValues(Operand operand, Row row)
-            throws RepositoryException {
-        if (operand instanceof StaticOperand) {
-            StaticOperand so = (StaticOperand) operand;
-            return new Value[] { getValue(so) };
-        } else if (operand instanceof FullTextSearchScore) {
-            FullTextSearchScore ftss = (FullTextSearchScore) operand;
-            double score = row.getScore(ftss.getSelectorName());
-            return new Value[] { factory.createValue(score) };
-        } else if (operand instanceof NodeName) {
-            NodeName nn = (NodeName) operand;
-            Value value = factory.createValue(
-                    row.getNode(nn.getSelectorName()).getName(),
-                    PropertyType.NAME);
-            return new Value[] { value };
-        } else if (operand instanceof Length) {
-            return getLengthValues((Length) operand, row);
-        } else if (operand instanceof LowerCase) {
-            return getLowerCaseValues((LowerCase) operand, row);
-        } else if (operand instanceof UpperCase) {
-            return getUpperCaseValues((UpperCase) operand, row);
-        } else if (operand instanceof NodeLocalName) {
-            return getNodeLocalNameValues((NodeLocalName) operand, row);
-        } else if (operand instanceof PropertyValue) {
-            return getPropertyValues((PropertyValue) operand, row);
-        } else {
-            throw new UnsupportedRepositoryOperationException(
-                    "Unknown operand type: " + operand);
-        }
-    }
-
-    /**
-     * Evaluates the given operand in the context of the given node.
-     *
-     * @param operand operand to be evaluated
-     * @param node node
-     * @return values of the operand at the given node
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    public Value[] getValues(Operand operand, Node node)
-            throws RepositoryException {
-        if (operand instanceof StaticOperand) {
-            StaticOperand so = (StaticOperand) operand;
-            return new Value[] { getValue(so) };
-        }
-        if (operand instanceof FullTextSearchScore) {
-            final double defaultScore = 0.0;
-            return new Value[] { factory.createValue(defaultScore) };
-        }
-        if (operand instanceof NodeName) {
-            Value value = factory
-                    .createValue(node.getName(), PropertyType.NAME);
-            return new Value[] { value };
-        }
-        if (operand instanceof Length) {
-            return getLengthValues((Length) operand, node);
-        }
-        if (operand instanceof LowerCase) {
-            return getLowerCaseValues((LowerCase) operand, node);
-        }
-        if (operand instanceof UpperCase) {
-            return getUpperCaseValues((UpperCase) operand, node);
-        }
-        if (operand instanceof NodeLocalName) {
-            return getNodeLocalNameValues((NodeLocalName) operand, node);
-        }
-        if (operand instanceof PropertyValue) {
-            return getPropertyValues((PropertyValue) operand, node);
-        }
-        throw new UnsupportedRepositoryOperationException(
-                "Unknown operand type: " + operand);
-    }
-
-    /**
-     * Evaluates the operand and extracts the node property that is supposed to
-     * be used for evaluation.
-     * 
-     * Can be <code>null</code> if there is no possible value
-     * 
-     * @param operand
-     * @return the node's property name
-     */
-    public String getAffectedPropertyName(Operand operand) {
-        if (operand instanceof StaticOperand) {
-            return null;
-        }
-        if (operand instanceof FullTextSearchScore) {
-            return null;
-        }
-        if (operand instanceof NodeName) {
-            return null;
-        }
-        if (operand instanceof Length) {
-            return ((Length) operand).getPropertyValue().getPropertyName();
-        }
-        if (operand instanceof LowerCase) {
-            return getAffectedPropertyName(((LowerCase) operand).getOperand());
-        }
-        if (operand instanceof UpperCase) {
-            return getAffectedPropertyName(((UpperCase) operand).getOperand());
-        }
-        if (operand instanceof NodeLocalName) {
-            return null;
-        }
-        if (operand instanceof PropertyValue) {
-            return ((PropertyValue) operand).getPropertyName();
-        }
-        return null;
-    }
-
-    /**
-     * Returns the values of the given value length operand at the given row.
-     *
-     * @see #getProperty(PropertyValue, Row)
-     * @param operand value length operand
-     * @param row row
-     * @return values of the operand at the given row
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getLengthValues(Length operand, Row row)
-            throws RepositoryException {
-        Property property = getProperty(operand.getPropertyValue(), row);
-        if (property == null) {
-            return new Value[0];
-        } else if (property.isMultiple()) {
-            long[] lengths = property.getLengths();
-            Value[] values = new Value[lengths.length];
-            for (int i = 0; i < lengths.length; i++) {
-                values[i] = factory.createValue(lengths[i]);
-            }
-            return values;
-        } else {
-            long length = property.getLength();
-            return new Value[] { factory.createValue(length) };
-        }
-    }
-
-    /**
-     * Returns the values of the given value length operand for the given node.
-     *
-     * @see #getProperty(PropertyValue, Node)
-     * @param operand value length operand
-     * @param node node
-     * @return values of the operand for the given node
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getLengthValues(Length operand, Node n)
-            throws RepositoryException {
-        Property property = getProperty(operand.getPropertyValue(), n);
-        if (property == null) {
-            return new Value[0];
-        }
-        if (property.isMultiple()) {
-            long[] lengths = property.getLengths();
-            Value[] values = new Value[lengths.length];
-            for (int i = 0; i < lengths.length; i++) {
-                values[i] = factory.createValue(lengths[i]);
-            }
-            return values;
-        }
-        long length = property.getLength();
-        return new Value[] { factory.createValue(length) };
-    }
-
-    /**
-     * Returns the values of the given lower case operand at the given row.
-     *
-     * @param operand lower case operand
-     * @param row row
-     * @return values of the operand at the given row
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getLowerCaseValues(LowerCase operand, Row row)
-            throws RepositoryException {
-        Value[] values = getValues(operand.getOperand(), row);
-        for (int i = 0; i < values.length; i++) {
-            String value = values[i].getString();
-            String lower = value.toLowerCase(locale);
-            if (!value.equals(lower)) {
-                values[i] = factory.createValue(lower);
-            }
-        }
-        return values;
-    }
-
-    /**
-     * Returns the values of the given lower case operand for the given node.
-     *
-     * @param operand lower case operand
-     * @param node node
-     * @return values of the operand for the given node
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getLowerCaseValues(LowerCase operand, Node node)
-            throws RepositoryException {
-        Value[] values = getValues(operand.getOperand(), node);
-        for (int i = 0; i < values.length; i++) {
-            String value = values[i].getString();
-            String lower = value.toLowerCase(locale);
-            if (!value.equals(lower)) {
-                values[i] = factory.createValue(lower);
-            }
-        }
-        return values;
-    }
-
-    /**
-     * Returns the values of the given upper case operand at the given row.
-     *
-     * @param operand upper case operand
-     * @param row row
-     * @return values of the operand at the given row
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getUpperCaseValues(UpperCase operand, Row row)
-            throws RepositoryException {
-        Value[] values = getValues(operand.getOperand(), row);
-        for (int i = 0; i < values.length; i++) {
-            String value = values[i].getString();
-            String upper = value.toUpperCase(locale);
-            if (!value.equals(upper)) {
-                values[i] = factory.createValue(upper);
-            }
-        }
-        return values;
-    }
-
-    /**
-     * Returns the values of the given upper case operand for the given node.
-     *
-     * @param operand upper case operand
-     * @param node node
-     * @return values of the operand for the given node
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getUpperCaseValues(UpperCase operand, Node node)
-            throws RepositoryException {
-        Value[] values = getValues(operand.getOperand(), node);
-        for (int i = 0; i < values.length; i++) {
-            String value = values[i].getString();
-            String upper = value.toUpperCase(locale);
-            if (!value.equals(upper)) {
-                values[i] = factory.createValue(upper);
-            }
-        }
-        return values;
-    }
-
-    /**
-     * Returns the value of the given local name operand at the given row.
-     *
-     * @param operand local name operand
-     * @param row row
-     * @return value of the operand at the given row
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getNodeLocalNameValues(NodeLocalName operand, Row row)
-            throws RepositoryException {
-        return getNodeLocalNameValues(operand,
-                row.getNode(operand.getSelectorName()));
-    }
-
-    /**
-     * Returns the value of the given local name operand for the given node.
-     * 
-     * @param operand
-     *            local name operand
-     * @param node
-     *            node
-     * @return value of the operand for the given node
-     * @throws RepositoryException
-     */
-    private Value[] getNodeLocalNameValues(NodeLocalName operand, Node node)
-            throws RepositoryException {
-        String name = node.getName();
-        int colon = name.indexOf(':');
-        if (colon != -1) {
-            name = name.substring(colon + 1);
-        }
-        return new Value[] { factory.createValue(name, PropertyType.NAME) };
-    }
-
-    /**
-     * Returns the values of the given property value operand at the given row.
-     *
-     * @see #getProperty(PropertyValue, Row)
-     * @param operand property value operand
-     * @param row row
-     * @return values of the operand at the given row
-     * @throws RepositoryException if the operand can't be evaluated
-     */
-    private Value[] getPropertyValues(PropertyValue operand, Row row)
-            throws RepositoryException {
-        Property property = getProperty(operand, row);
-        if (property == null) {
-            return new Value[0];
-        } else if (property.isMultiple()) {
-            return property.getValues();
-        } else {
-            return new Value[] { property.getValue() };
-        }
-    }
-
-    private Value[] getPropertyValues(PropertyValue operand, Node node)
-            throws RepositoryException {
-        Property property = getProperty(operand, node);
-        if (property == null) {
-            return new Value[0];
-        } else if (property.isMultiple()) {
-            return property.getValues();
-        } else {
-            return new Value[] { property.getValue() };
-        }
-    }
-
-    /**
-     * Returns the identified property from the given row. This method
-     * is used by both the {@link #getValue(Length, Row)} and the
-     * {@link #getValue(PropertyValue, Row)} methods to access properties.
-     *
-     * @param operand property value operand
-     * @param row row
-     * @return the identified property,
-     *         or <code>null</code> if the property does not exist
-     * @throws RepositoryException if the property can't be accessed
-     */
-    private Property getProperty(PropertyValue operand, Row row)
-            throws RepositoryException {
-        return getProperty(operand, row.getNode(operand.getSelectorName()));
-    }
-
-    /**
-     * Returns the identified property from the given node.
-     * 
-     * Can return <code>null</code> is the property doesn't exist or it is not
-     * accessible.
-     * 
-     * @param operand
-     * @param node
-     * @return identified property
-     * @throws RepositoryException
-     */
-    private Property getProperty(PropertyValue operand, Node node)
-            throws RepositoryException {
-        if (node == null) {
-            return null;
-        }
-        try {
-            return node.getProperty(operand.getPropertyName());
-        } catch (PathNotFoundException e) {
-            return null;
-        }
-    }
-}
-- 
1.7.4.4

