[dna-commits] DNA SVN: r1320 - in trunk: dna-graph/src/main/java/org/jboss/dna/graph/property and 17 other directories.

dna-commits at lists.jboss.org dna-commits at lists.jboss.org
Mon Nov 16 18:25:37 EST 2009


Author: rhauch
Date: 2009-11-16 18:25:36 -0500 (Mon, 16 Nov 2009)
New Revision: 1320

Added:
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Validator.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexLayout.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/IndexLayout.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/IndexSession.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexLayout.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/filters/
   trunk/dna-search/src/main/java/org/jboss/dna/search/filters/ResultFilter.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareNameQuery.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/ComparePathQuery.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareQuery.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareStringQuery.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/MatchNoneQuery.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/NotQuery.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/ScoreQuery.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/query/UuidsQuery.java
   trunk/dna-search/src/test/java/org/jboss/dna/search/query/
   trunk/dna-search/src/test/java/org/jboss/dna/search/query/LuceneNotQueryTest.java
Removed:
   trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexStrategy.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/IndexContext.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/IndexStrategy.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexStrategy.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/SearchContext.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/WorkspaceSearchEngine.java
   trunk/dna-search/src/test/java/org/jboss/dna/search/WorkspaceSearchEngineTest.java
Modified:
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/ValueComparators.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryContext.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryResults.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/FullTextSearch.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/Visitors.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/parse/FullTextSearchParser.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/plan/CanonicalPlanner.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/AbstractAccessComponent.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/QueryProcessor.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableColumn.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableSchemata.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableTable.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Schemata.java
   trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/optimize/RuleBasedOptimizerTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/FullTextSearchParserTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/SqlQueryParserTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/plan/CanonicalPlannerTest.java
   trunk/dna-search/pom.xml
   trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfiguration.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfigurations.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/IndexRules.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/SearchEngine.java
   trunk/dna-search/src/main/java/org/jboss/dna/search/SearchI18n.java
   trunk/dna-search/src/main/resources/org/jboss/dna/search/SearchI18n.properties
   trunk/dna-search/src/test/java/org/jboss/dna/search/SearchEngineTest.java
Log:
DNA-467 Continued implementation of the dna-search components.  Current status is that the SearchEngine is nearly complete (feature-wise), but has had little testing.

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -133,6 +133,8 @@
     public static I18n tableDoesNotExist;
     public static I18n columnDoesNotExistOnTable;
     public static I18n columnDoesNotExistInQuery;
+    public static I18n columnIsNotFullTextSearchable;
+    public static I18n tableIsNotFullTextSearchable;
     public static I18n selectorDoesNotExistInQuery;
     public static I18n propertyOnSelectorIsNotUsedInQuery;
     public static I18n errorResolvingNodesFromLocationsUsingSourceAndWorkspace;

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/ValueComparators.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/ValueComparators.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/ValueComparators.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -261,6 +261,19 @@
         }
     };
     /**
+     * A comparator of path segment values.
+     */
+    public static final Comparator<Path.Segment> PATH_SEGMENT_COMPARATOR = new Comparator<Path.Segment>() {
+
+        public int compare( Path.Segment o1,
+                            Path.Segment o2 ) {
+            if (o1 == o2) return 0;
+            if (o1 == null) return -1;
+            if (o2 == null) return 1;
+            return o1.compareTo(o2);
+        }
+    };
+    /**
      * A comparator of URI values.
      */
     public static final Comparator<URI> URI_COMPARATOR = new Comparator<URI>() {

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryContext.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryContext.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryContext.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -165,6 +165,24 @@
     }
 
     /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof QueryContext) {
+            QueryContext that = (QueryContext)obj;
+            if (!this.context.equals(that.getExecutionContext())) return false;
+            if (!this.schemata.equals(that.getSchemata())) return false;
+            if (!this.variables.equals(that.getVariables())) return false;
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Obtain a copy of this context, except that the copy uses the supplied execution context.
      * 
      * @param context the execution context that should be used in the new query context

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryResults.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryResults.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/QueryResults.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -179,6 +179,7 @@
      * {@link QueryResults#getTuples() tuples} in the results, and which can be used to access the individual values in each of
      * the tuples.
      */
+    @Immutable
     public interface Columns {
         /**
          * Get the columns.

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/FullTextSearch.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/FullTextSearch.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/FullTextSearch.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -190,25 +190,77 @@
     }
 
     /**
+     * A {@link Term} that represents a search term that requires another term to not appear.
+     */
+    public static class NegationTerm implements Term {
+        private final Term negated;
+
+        public NegationTerm( Term negatedTerm ) {
+            assert negatedTerm != null;
+            this.negated = negatedTerm;
+        }
+
+        /**
+         * Get the term that is negated.
+         * 
+         * @return the negated term; never null
+         */
+        public Term getNegatedTerm() {
+            return negated;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#hashCode()
+         */
+        @Override
+        public int hashCode() {
+            return negated.hashCode();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#equals(java.lang.Object)
+         */
+        @Override
+        public boolean equals( Object obj ) {
+            if (obj == this) return true;
+            if (obj instanceof NegationTerm) {
+                NegationTerm that = (NegationTerm)obj;
+                return this.getNegatedTerm().equals(that.getNegatedTerm());
+            }
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#toString()
+         */
+        @Override
+        public String toString() {
+            return "-" + negated.toString();
+        }
+    }
+
+    /**
      * A {@link Term} that represents a single search term. The term may be comprised of multiple words.
      */
     public static class SimpleTerm implements Term {
         private final String value;
-        private final boolean excluded;
         private final boolean quoted;
 
         /**
          * Create a simple term with the value and whether the term is excluded or included.
          * 
          * @param value the value that makes up the term
-         * @param excluded true if the term should not appear, or false if the term is required
          */
-        public SimpleTerm( String value,
-                           boolean excluded ) {
+        public SimpleTerm( String value ) {
             assert value != null;
             assert value.trim().length() > 0;
             this.value = value;
-            this.excluded = excluded;
             this.quoted = this.value.indexOf(' ') != -1;
         }
 
@@ -223,12 +275,12 @@
         }
 
         /**
-         * Get whether or not this term is expected to appear in the results.
+         * Get the values of this term if the term is quoted.
          * 
-         * @return true if the term is expected to not appear, or false if the term is expected to appear
+         * @return the array of terms; never null
          */
-        public boolean isExcluded() {
-            return excluded;
+        public String[] getValues() {
+            return value.split("/w");
         }
 
         /**
@@ -260,7 +312,6 @@
             if (obj == this) return true;
             if (obj instanceof SimpleTerm) {
                 SimpleTerm that = (SimpleTerm)obj;
-                if (this.isExcluded() != that.isExcluded()) return false;
                 return this.getValue().equals(that.getValue());
             }
             return false;
@@ -273,8 +324,7 @@
          */
         @Override
         public String toString() {
-            String value = quoted ? "\"" + this.value + "\"" : this.value;
-            return excluded ? "-" + value : value;
+            return quoted ? "\"" + this.value + "\"" : this.value;
         }
     }
 
@@ -352,7 +402,7 @@
     }
 
     /**
-     * A set of {@link Term}s that are ANDed together.
+     * A set of {@link Term}s that are ORed together.
      */
     public static class Disjunction extends CompoundTerm {
 

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/Visitors.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/Visitors.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/model/Visitors.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -511,18 +511,23 @@
         }
 
         protected final void enqueue( Visitable objectToBeVisited ) {
-            itemQueue.add(objectToBeVisited);
+            if (objectToBeVisited != null) {
+                itemQueue.add(objectToBeVisited);
+            }
         }
 
         protected final void enqueue( Iterable<? extends Visitable> objectsToBeVisited ) {
             for (Visitable objectToBeVisited : objectsToBeVisited) {
-                itemQueue.add(objectToBeVisited);
+                if (objectToBeVisited != null) {
+                    itemQueue.add(objectToBeVisited);
+                }
             }
         }
 
         protected final void visitNext() {
             if (!itemQueue.isEmpty()) {
                 Visitable first = (Visitable)itemQueue.removeFirst();
+                assert first != null;
                 first.accept(this);
             }
         }

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/parse/FullTextSearchParser.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/parse/FullTextSearchParser.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/parse/FullTextSearchParser.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -31,6 +31,7 @@
 import org.jboss.dna.common.util.CheckArg;
 import org.jboss.dna.graph.query.model.FullTextSearch.Conjunction;
 import org.jboss.dna.graph.query.model.FullTextSearch.Disjunction;
+import org.jboss.dna.graph.query.model.FullTextSearch.NegationTerm;
 import org.jboss.dna.graph.query.model.FullTextSearch.SimpleTerm;
 import org.jboss.dna.graph.query.model.FullTextSearch.Term;
 
@@ -88,10 +89,9 @@
     }
 
     protected Term parseTerm( TokenStream tokens ) {
-        if (tokens.canConsume('-')) {
-            return new SimpleTerm(removeQuotes(tokens.consume()), true);
-        }
-        return new SimpleTerm(removeQuotes(tokens.consume()), false);
+        boolean negated = tokens.canConsume('-');
+        Term result = new SimpleTerm(removeQuotes(tokens.consume()));
+        return negated ? new NegationTerm(result) : result;
     }
 
     /**

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/plan/CanonicalPlanner.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/plan/CanonicalPlanner.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/plan/CanonicalPlanner.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -53,6 +53,7 @@
 import org.jboss.dna.graph.query.plan.PlanNode.Property;
 import org.jboss.dna.graph.query.plan.PlanNode.Type;
 import org.jboss.dna.graph.query.validate.Schemata;
+import org.jboss.dna.graph.query.validate.Validator;
 import org.jboss.dna.graph.query.validate.Schemata.Table;
 import org.jboss.dna.graph.query.validate.Schemata.View;
 
@@ -143,10 +144,28 @@
         // Process the orderings and limits ...
         plan = attachSorting(context, plan, query.getOrderings());
         plan = attachLimits(context, plan, query.getLimits());
+
+        // Validate that all the parts of the query are resolvable ...
+        validate(context, query, usedSources);
+
         return plan;
     }
 
     /**
+     * Validate the supplied query.
+     * 
+     * @param context the context in which the query is being planned
+     * @param query the set query to be planned
+     * @param usedSelectors the map of {@link SelectorName}s (aliases or names) used in the query.
+     */
+    protected void validate( QueryContext context,
+                             QueryCommand query,
+                             Map<SelectorName, Table> usedSelectors ) {
+        // Resolve everything ...
+        Visitors.visitAll(query, new Validator(context, usedSelectors));
+    }
+
+    /**
      * Create a canonical query plan for the given set query.
      * 
      * @param context the context in which the query is being planned

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/AbstractAccessComponent.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/AbstractAccessComponent.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/AbstractAccessComponent.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.query.process;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.property.Name;
@@ -31,7 +32,6 @@
 import org.jboss.dna.graph.query.QueryContext;
 import org.jboss.dna.graph.query.QueryResults.Columns;
 import org.jboss.dna.graph.query.model.AllNodes;
-import org.jboss.dna.graph.query.model.And;
 import org.jboss.dna.graph.query.model.Column;
 import org.jboss.dna.graph.query.model.Constraint;
 import org.jboss.dna.graph.query.model.Limit;
@@ -50,7 +50,7 @@
     protected final PlanNode accessNode;
     protected final SelectorName sourceName;
     protected final List<Column> projectedColumns;
-    protected final Constraint constraint;
+    protected final List<Constraint> andedConstraints;
     protected final Limit limit;
 
     protected AbstractAccessComponent( QueryContext context,
@@ -94,16 +94,13 @@
         }
 
         // Add the criteria ...
-        Constraint constraint = null;
+        List<Constraint> andedConstraints = null;
         for (PlanNode select : accessNode.findAllAtOrBelow(Type.SELECT)) {
             Constraint selectConstraint = select.getProperty(Property.SELECT_CRITERIA, Constraint.class);
-            if (constraint != null) {
-                constraint = new And(constraint, selectConstraint);
-            } else {
-                constraint = selectConstraint;
-            }
+            if (andedConstraints == null) andedConstraints = new ArrayList<Constraint>();
+            andedConstraints.add(selectConstraint);
         }
-        this.constraint = constraint;
+        this.andedConstraints = andedConstraints != null ? andedConstraints : Collections.<Constraint>emptyList();
 
         // Find the limit ...
         Limit limit = Limit.NONE;

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/QueryProcessor.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/QueryProcessor.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/process/QueryProcessor.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -51,8 +51,8 @@
 /**
  * An abstract {@link Processor} implementation that builds a tree of {@link ProcessingComponent} objects to perform the different
  * parts of the query processing logic. Subclasses are required to only implement one method: the
- * {@link #createAccessComponent(QueryContext, PlanNode, Columns, Analyzer)} should create a ProcessorComponent object that will
- * perform the (low-level access) query described by the {@link PlanNode plan} given as a parameter.
+ * {@link #createAccessComponent(QueryCommand,QueryContext, PlanNode, Columns, Analyzer)} should create a ProcessorComponent
+ * object that will perform the (low-level access) query described by the {@link PlanNode plan} given as a parameter.
  */
 public abstract class QueryProcessor implements Processor {
 
@@ -81,18 +81,24 @@
 
             // Go through the plan and create the corresponding ProcessingComponents ...
             Analyzer analyzer = createAnalyzer(context);
-            ProcessingComponent component = createComponent(context, plan, columns, analyzer);
+            ProcessingComponent component = createComponent(command, context, plan, columns, analyzer);
             long nanos2 = System.nanoTime();
             statistics = statistics.withResultsFormulationTime(nanos2 - nanos);
-
-            // Now execute the component ...
             nanos = nanos2;
-            tuples = component.execute();
 
+            if (component != null) {
+                // Now execute the component ...
+                tuples = component.execute();
+            } else {
+                // There must have been an error ...
+                assert context.getProblems().hasErrors();
+                tuples = Collections.emptyList();
+            }
+
         } finally {
             statistics = statistics.withExecutionTime(System.nanoTime() - nanos);
         }
-        if (tuples == null) tuples = Collections.emptyList();
+        assert tuples != null;
         return new org.jboss.dna.graph.query.process.QueryResults(context, command, columns, statistics, tuples);
     }
 
@@ -100,7 +106,7 @@
      * Create an {@link Analyzer} implementation that should be used by the non-access {@link ProcessingComponent}s that evaluate
      * criteria. By default, this method returns null, which means that any criteria evaluation will likely be pushed down under
      * an {@link Type#ACCESS ACCESS} node (and thus handled by an
-     * {@link #createAccessComponent(QueryContext, PlanNode, Columns, Analyzer) access component}.
+     * {@link #createAccessComponent(QueryCommand,QueryContext, PlanNode, Columns, Analyzer) access component}.
      * <p>
      * However, for more simple access components that are not capable of handling joins and other non-trivial criteria, simply
      * return an Analyzer implementation that implements the methods using the source.
@@ -116,13 +122,15 @@
     /**
      * Create the {@link ProcessingComponent} that processes a single {@link Type#ACCESS} branch of a query plan.
      * 
+     * @param originalQuery the original query that is being executed; never null
      * @param context the context in which query is being evaluated; never null
      * @param accessNode the node in the query plan that represents the {@link Type#ACCESS} plan; never null
      * @param resultColumns the columns that are to be returned; never null
      * @param analyzer the criteria analyzer; never null
      * @return the processing component; may not be null
      */
-    protected abstract ProcessingComponent createAccessComponent( QueryContext context,
+    protected abstract ProcessingComponent createAccessComponent( QueryCommand originalQuery,
+                                                                  QueryContext context,
                                                                   PlanNode accessNode,
                                                                   Columns resultColumns,
                                                                   Analyzer analyzer );
@@ -133,17 +141,20 @@
      * {@link PlanNode} objects in the optimized query plan, and the method is actually recursive (since the optimized query plan
      * forms a tree). However, whenever this call structure reaches the {@link Type#ACCESS ACCESS} nodes in the query plan (which
      * each represents a separate atomic low-level query to the underlying system), the
-     * {@link #createAccessComponent(QueryContext, PlanNode, Columns, Analyzer)} method is called. Subclasses should create an
-     * appropriate ProcessingComponent implementation that performs this atomic low-level query.
+     * {@link #createAccessComponent(QueryCommand,QueryContext, PlanNode, Columns, Analyzer)} method is called. Subclasses should
+     * create an appropriate ProcessingComponent implementation that performs this atomic low-level query.
      * 
+     * @param originalQuery the original query that is being executed; never null
      * @param context the context in which query is being evaluated
      * @param node the plan node for which the ProcessingComponent is to be created
      * @param columns the definition of the result columns for this portion of the query
      * @param analyzer the analyzer (returned from {@link #createAnalyzer(QueryContext)}) that should be used on the components
      *        that evaluate criteria; may be null if a best-effort should be made for the evaluation
-     * @return the processing component for this plan node; never null
+     * @return the processing component for this plan node; or null if there was an error recorded in the
+     *         {@link QueryContext#getProblems() problems}
      */
-    protected ProcessingComponent createComponent( QueryContext context,
+    protected ProcessingComponent createComponent( QueryCommand originalQuery,
+                                                   QueryContext context,
                                                    PlanNode node,
                                                    Columns columns,
                                                    Analyzer analyzer ) {
@@ -152,14 +163,18 @@
             case ACCESS:
                 // Create the component to handle the ACCESS node ...
                 assert node.getChildCount() == 1;
-                component = createAccessComponent(context, node, columns, analyzer);
+                component = createAccessComponent(originalQuery, context, node, columns, analyzer);
                 // // Don't do anything special with an access node at the moment ...
                 // component = createComponent(context, node.getFirstChild(), columns, analyzer);
                 break;
             case DUP_REMOVE:
                 // Create the component under the DUP_REMOVE ...
                 assert node.getChildCount() == 1;
-                ProcessingComponent distinctDelegate = createComponent(context, node.getFirstChild(), columns, analyzer);
+                ProcessingComponent distinctDelegate = createComponent(originalQuery,
+                                                                       context,
+                                                                       node.getFirstChild(),
+                                                                       columns,
+                                                                       analyzer);
                 component = new DistinctComponent(distinctDelegate);
                 break;
             case GROUP:
@@ -167,8 +182,8 @@
             case JOIN:
                 // Create the components under the JOIN ...
                 assert node.getChildCount() == 2;
-                ProcessingComponent left = createComponent(context, node.getFirstChild(), columns, analyzer);
-                ProcessingComponent right = createComponent(context, node.getLastChild(), columns, analyzer);
+                ProcessingComponent left = createComponent(originalQuery, context, node.getFirstChild(), columns, analyzer);
+                ProcessingComponent right = createComponent(originalQuery, context, node.getLastChild(), columns, analyzer);
                 // Create the join component ...
                 JoinAlgorithm algorithm = node.getProperty(Property.JOIN_ALGORITHM, JoinAlgorithm.class);
                 JoinType joinType = node.getProperty(Property.JOIN_TYPE, JoinType.class);
@@ -202,7 +217,11 @@
             case LIMIT:
                 // Create the component under the LIMIT ...
                 assert node.getChildCount() == 1;
-                ProcessingComponent limitDelegate = createComponent(context, node.getFirstChild(), columns, analyzer);
+                ProcessingComponent limitDelegate = createComponent(originalQuery,
+                                                                    context,
+                                                                    node.getFirstChild(),
+                                                                    columns,
+                                                                    analyzer);
                 // Then create the limit component ...
                 Integer rowLimit = node.getProperty(Property.LIMIT_COUNT, Integer.class);
                 Integer offset = node.getProperty(Property.LIMIT_OFFSET, Integer.class);
@@ -217,7 +236,11 @@
             case PROJECT:
                 // Create the component under the PROJECT ...
                 assert node.getChildCount() == 1;
-                ProcessingComponent projectDelegate = createComponent(context, node.getFirstChild(), columns, analyzer);
+                ProcessingComponent projectDelegate = createComponent(originalQuery,
+                                                                      context,
+                                                                      node.getFirstChild(),
+                                                                      columns,
+                                                                      analyzer);
                 // Then create the project component ...
                 List<Column> projectedColumns = node.getPropertyAsList(Property.PROJECT_COLUMNS, Column.class);
                 component = new ProjectComponent(projectDelegate, projectedColumns);
@@ -225,7 +248,11 @@
             case SELECT:
                 // Create the component under the SELECT ...
                 assert node.getChildCount() == 1;
-                ProcessingComponent selectDelegate = createComponent(context, node.getFirstChild(), columns, analyzer);
+                ProcessingComponent selectDelegate = createComponent(originalQuery,
+                                                                     context,
+                                                                     node.getFirstChild(),
+                                                                     columns,
+                                                                     analyzer);
                 // Then create the select component ...
                 Constraint constraint = node.getProperty(Property.SELECT_CRITERIA, Constraint.class);
                 component = new SelectComponent(selectDelegate, constraint, context.getVariables(), analyzer);
@@ -234,7 +261,7 @@
                 // Create the components under the SET_OPERATION ...
                 List<ProcessingComponent> setDelegates = new LinkedList<ProcessingComponent>();
                 for (PlanNode child : node) {
-                    setDelegates.add(createComponent(context, child, columns, analyzer));
+                    setDelegates.add(createComponent(originalQuery, context, child, columns, analyzer));
                 }
                 // Then create the select component ...
                 Operation operation = node.getProperty(Property.SET_OPERATION, Operation.class);
@@ -255,7 +282,11 @@
             case SORT:
                 // Create the component under the SORT ...
                 assert node.getChildCount() == 1;
-                ProcessingComponent sortDelegate = createComponent(context, node.getFirstChild(), columns, analyzer);
+                ProcessingComponent sortDelegate = createComponent(originalQuery,
+                                                                   context,
+                                                                   node.getFirstChild(),
+                                                                   columns,
+                                                                   analyzer);
                 // Then create the sort component ...
                 List<Object> orderBys = node.getPropertyAsList(Property.SORT_ORDER_BY, Object.class);
                 if (orderBys.isEmpty()) {

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableColumn.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableColumn.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableColumn.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -29,13 +29,24 @@
 
 @Immutable
 class ImmutableColumn implements Column {
+
+    public static final boolean DEFAULT_FULL_TEXT_SEARCHABLE = false;
+
+    private final boolean fullTextSearchable;
     private final String name;
     private final PropertyType type;
 
     protected ImmutableColumn( String name,
                                PropertyType type ) {
+        this(name, type, DEFAULT_FULL_TEXT_SEARCHABLE);
+    }
+
+    protected ImmutableColumn( String name,
+                               PropertyType type,
+                               boolean fullTextSearchable ) {
         this.name = name;
         this.type = type != null ? type : PropertyType.STRING;
+        this.fullTextSearchable = fullTextSearchable;
     }
 
     /**
@@ -59,6 +70,15 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.query.validate.Schemata.Column#isFullTextSearchable()
+     */
+    public boolean isFullTextSearchable() {
+        return fullTextSearchable;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#toString()
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableSchemata.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableSchemata.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableSchemata.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -328,12 +328,34 @@
             CheckArg.isNotEmpty(tableName, "tableName");
             CheckArg.isNotEmpty(columnName, "columnName");
             CheckArg.isNotNull(type, "type");
+            return addColumn(tableName, columnName, type, ImmutableColumn.DEFAULT_FULL_TEXT_SEARCHABLE);
+        }
+
+        /**
+         * Add a column with the supplied name and type to the named table. Any existing column with that name will be replaced
+         * with the new column. If the table does not yet exist, it will be added.
+         * 
+         * @param tableName the name of the new table
+         * @param columnName the names of the column
+         * @param type the type for the column
+         * @param fullTextSearchable true if the column should be full-text searchable, or false if not
+         * @return this builder, for convenience in method chaining; never null
+         * @throws IllegalArgumentException if the table name is null or empty, the column name is null or empty, or if the
+         *         property type is null
+         */
+        public Builder addColumn( String tableName,
+                                  String columnName,
+                                  PropertyType type,
+                                  boolean fullTextSearchable ) {
+            CheckArg.isNotEmpty(tableName, "tableName");
+            CheckArg.isNotEmpty(columnName, "columnName");
+            CheckArg.isNotNull(type, "type");
             SelectorName selector = new SelectorName(tableName);
             ImmutableTable existing = tables.get(selector);
             ImmutableTable table = null;
             if (existing == null) {
                 List<Column> columns = new ArrayList<Column>();
-                columns.add(new ImmutableColumn(columnName, type));
+                columns.add(new ImmutableColumn(columnName, type, fullTextSearchable));
                 table = new ImmutableTable(selector, columns);
             } else {
                 table = existing.withColumn(columnName, type);
@@ -343,6 +365,37 @@
         }
 
         /**
+         * Make sure the column on the named table is searchable.
+         * 
+         * @param tableName the name of the new table
+         * @param columnName the names of the column
+         * @return this builder, for convenience in method chaining; never null
+         * @throws IllegalArgumentException if the table name is null or empty or if the column name is null or empty
+         */
+        public Builder makeSearchable( String tableName,
+                                       String columnName ) {
+            CheckArg.isNotEmpty(tableName, "tableName");
+            CheckArg.isNotEmpty(columnName, "columnName");
+            SelectorName selector = new SelectorName(tableName);
+            ImmutableTable existing = tables.get(selector);
+            ImmutableTable table = null;
+            if (existing == null) {
+                List<Column> columns = new ArrayList<Column>();
+                columns.add(new ImmutableColumn(columnName, PropertyType.STRING, true));
+                table = new ImmutableTable(selector, columns);
+            } else {
+                Column column = existing.getColumn(columnName);
+                PropertyType type = PropertyType.STRING;
+                if (column != null) {
+                    type = column.getPropertyType();
+                }
+                table = existing.withColumn(columnName, type, true);
+            }
+            tables.put(table.getName(), table);
+            return this;
+        }
+
+        /**
          * Add to the specified table a key that references the existing named columns.
          * 
          * @param tableName the name of the new table
@@ -419,7 +472,8 @@
                                                             "The view references a non-existant column '"
                                                             + column.getColumnName() + "' in '" + source.getName() + "'");
                         }
-                        viewColumns.add(new ImmutableColumn(viewColumnName, sourceColumn.getPropertyType()));
+                        viewColumns.add(new ImmutableColumn(viewColumnName, sourceColumn.getPropertyType(),
+                                                            sourceColumn.isFullTextSearchable()));
                     }
                     if (viewColumns.size() != columns.size()) {
                         // We weren't able to resolve all of the columns,

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableTable.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableTable.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/ImmutableTable.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -187,10 +187,18 @@
         return new ImmutableTable(getName(), newColumns);
     }
 
+    public ImmutableTable withColumn( String name,
+                                      PropertyType type,
+                                      boolean fullTextSearchable ) {
+        List<Column> newColumns = new LinkedList<Column>(columns);
+        newColumns.add(new ImmutableColumn(name, type, fullTextSearchable));
+        return new ImmutableTable(getName(), newColumns);
+    }
+
     public ImmutableTable withColumns( Iterable<Column> columns ) {
         List<Column> newColumns = new LinkedList<Column>(this.getColumns());
         for (Column column : columns) {
-            newColumns.add(new ImmutableColumn(column.getName(), column.getPropertyType()));
+            newColumns.add(new ImmutableColumn(column.getName(), column.getPropertyType(), column.isFullTextSearchable()));
         }
         return new ImmutableTable(getName(), newColumns);
     }

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Schemata.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Schemata.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Schemata.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -160,6 +160,13 @@
          * @return the property type; never null
          */
         PropertyType getPropertyType();
+
+        /**
+         * Get whether the column can be used in a full-text search.
+         * 
+         * @return true if the column is full-text searchable, or false otherwise
+         */
+        boolean isFullTextSearchable();
     }
 
     /**

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Validator.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Validator.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Validator.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,339 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.graph.query.validate;
+
+import java.util.Map;
+import org.jboss.dna.common.collection.Problems;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.query.QueryContext;
+import org.jboss.dna.graph.query.model.AllNodes;
+import org.jboss.dna.graph.query.model.ChildNode;
+import org.jboss.dna.graph.query.model.ChildNodeJoinCondition;
+import org.jboss.dna.graph.query.model.Column;
+import org.jboss.dna.graph.query.model.DescendantNode;
+import org.jboss.dna.graph.query.model.DescendantNodeJoinCondition;
+import org.jboss.dna.graph.query.model.EquiJoinCondition;
+import org.jboss.dna.graph.query.model.FullTextSearch;
+import org.jboss.dna.graph.query.model.FullTextSearchScore;
+import org.jboss.dna.graph.query.model.Length;
+import org.jboss.dna.graph.query.model.LowerCase;
+import org.jboss.dna.graph.query.model.NamedSelector;
+import org.jboss.dna.graph.query.model.NodeDepth;
+import org.jboss.dna.graph.query.model.NodeLocalName;
+import org.jboss.dna.graph.query.model.NodeName;
+import org.jboss.dna.graph.query.model.NodePath;
+import org.jboss.dna.graph.query.model.PropertyExistence;
+import org.jboss.dna.graph.query.model.PropertyValue;
+import org.jboss.dna.graph.query.model.SameNode;
+import org.jboss.dna.graph.query.model.SameNodeJoinCondition;
+import org.jboss.dna.graph.query.model.SelectorName;
+import org.jboss.dna.graph.query.model.Visitor;
+import org.jboss.dna.graph.query.model.Visitors.AbstractVisitor;
+import org.jboss.dna.graph.query.validate.Schemata.Table;
+
+/**
+ * A {@link Visitor} implementation that validates a query's used of a {@link Schemata} and records any problems as errors.
+ */
+public class Validator extends AbstractVisitor {
+
+    private final QueryContext context;
+    private final Problems problems;
+    private final Map<SelectorName, Table> selectorsByName;
+
+    /**
+     * @param context the query context
+     * @param selectorsByName the {@link Table tables} by their name or alias, as defined by the selectors
+     */
+    public Validator( QueryContext context,
+                      Map<SelectorName, Table> selectorsByName ) {
+        this.context = context;
+        this.problems = this.context.getProblems();
+        this.selectorsByName = selectorsByName;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.AllNodes)
+     */
+    @Override
+    public void visit( AllNodes obj ) {
+        // this table doesn't have to be in the list of selected tables
+        verifyTable(obj.getName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.ChildNode)
+     */
+    @Override
+    public void visit( ChildNode obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.ChildNodeJoinCondition)
+     */
+    @Override
+    public void visit( ChildNodeJoinCondition obj ) {
+        verify(obj.getParentSelectorName());
+        verify(obj.getChildSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.Column)
+     */
+    @Override
+    public void visit( Column obj ) {
+        verify(obj.getSelectorName(), obj.getPropertyName()); // don't care about the alias
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.DescendantNode)
+     */
+    @Override
+    public void visit( DescendantNode obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.DescendantNodeJoinCondition)
+     */
+    @Override
+    public void visit( DescendantNodeJoinCondition obj ) {
+        verify(obj.getAncestorSelectorName());
+        verify(obj.getDescendantSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.EquiJoinCondition)
+     */
+    @Override
+    public void visit( EquiJoinCondition obj ) {
+        verify(obj.getSelector1Name(), obj.getProperty1Name());
+        verify(obj.getSelector2Name(), obj.getProperty2Name());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.FullTextSearch)
+     */
+    @Override
+    public void visit( FullTextSearch obj ) {
+        SelectorName selectorName = obj.getSelectorName();
+        if (obj.getPropertyName() != null) {
+            Schemata.Column column = verify(selectorName, obj.getPropertyName());
+            if (column != null) {
+                // Make sure the column is full-text searchable ...
+                if (!column.isFullTextSearchable()) {
+                    problems.addError(GraphI18n.columnIsNotFullTextSearchable, column.getName(), selectorName);
+                }
+            }
+        } else {
+            Table table = verify(selectorName);
+            if (table != null) {
+                // Make sure there is at least one column on the table that is full-text searchable ...
+                boolean searchable = false;
+                for (Schemata.Column column : table.getColumns()) {
+                    if (column.isFullTextSearchable()) {
+                        searchable = true;
+                        break;
+                    }
+                }
+                if (!searchable) {
+                    problems.addError(GraphI18n.tableIsNotFullTextSearchable, selectorName);
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.FullTextSearchScore)
+     */
+    @Override
+    public void visit( FullTextSearchScore obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.Length)
+     */
+    @Override
+    public void visit( Length obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.LowerCase)
+     */
+    @Override
+    public void visit( LowerCase obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.NamedSelector)
+     */
+    @Override
+    public void visit( NamedSelector obj ) {
+        verify(obj.getAliasOrName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.NodeDepth)
+     */
+    @Override
+    public void visit( NodeDepth obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.NodeLocalName)
+     */
+    @Override
+    public void visit( NodeLocalName obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.NodeName)
+     */
+    @Override
+    public void visit( NodeName obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.NodePath)
+     */
+    @Override
+    public void visit( NodePath obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.PropertyExistence)
+     */
+    @Override
+    public void visit( PropertyExistence obj ) {
+        verify(obj.getSelectorName(), obj.getPropertyName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.PropertyValue)
+     */
+    @Override
+    public void visit( PropertyValue obj ) {
+        verify(obj.getSelectorName(), obj.getPropertyName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.SameNode)
+     */
+    @Override
+    public void visit( SameNode obj ) {
+        verify(obj.getSelectorName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.query.model.Visitors.AbstractVisitor#visit(org.jboss.dna.graph.query.model.SameNodeJoinCondition)
+     */
+    @Override
+    public void visit( SameNodeJoinCondition obj ) {
+        verify(obj.getSelector1Name());
+        verify(obj.getSelector2Name());
+    }
+
+    protected String string( Name name ) {
+        return context.getExecutionContext().getValueFactories().getStringFactory().create(name);
+    }
+
+    protected Table verify( SelectorName selectorName ) {
+        Table table = selectorsByName.get(selectorName);
+        if (table == null) {
+            problems.addError(GraphI18n.tableDoesNotExist, selectorName.getName());
+        }
+        return table;
+    }
+
+    protected Table verifyTable( SelectorName tableName ) {
+        Table table = selectorsByName.get(tableName);
+        if (table == null) {
+            problems.addError(GraphI18n.tableDoesNotExist, tableName.getName());
+        }
+        return table;
+    }
+
+    protected Schemata.Column verify( SelectorName selectorName,
+                                      Name propertyName ) {
+        Table table = selectorsByName.get(selectorName);
+        if (table == null) {
+            problems.addError(GraphI18n.tableDoesNotExist, selectorName.getName());
+            return null;
+        }
+        Schemata.Column column = table.getColumn(string(propertyName));
+        if (column == null) {
+            problems.addError(GraphI18n.columnDoesNotExistOnTable, string(propertyName), selectorName.getName());
+        }
+        return column;
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/query/validate/Validator.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Modified: trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties
===================================================================
--- trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties	2009-11-16 23:25:36 UTC (rev 1320)
@@ -121,6 +121,8 @@
 tableDoesNotExist = Table '{0}' does not exist
 columnDoesNotExistOnTable = Column '{0}' does not exist on the table '{1}'
 columnDoesNotExistInQuery = Column '{0}' does not exist in query
+columnIsNotFullTextSearchable = Column '{0}' on the table '{1}' does not support full-text searching
+tableIsNotFullTextSearchable = Table '{0}' has no columns that support full-text searching
 selectorDoesNotExistInQuery = Selector '{0}' does not exist in query
 propertyOnSelectorIsNotUsedInQuery = Property '{0}' on selector '{1}' is not used in query
 errorResolvingNodesFromLocationsUsingSourceAndWorkspace = Error resolving nodes from locations using '{1}' workspace in '{0}'

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/optimize/RuleBasedOptimizerTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/optimize/RuleBasedOptimizerTest.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/optimize/RuleBasedOptimizerTest.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -77,6 +77,8 @@
         builder.addTable("t1", "c11", "c12", "c13");
         builder.addTable("t2", "c21", "c22", "c23");
         builder.addTable("all", "a1", "a2", "a3", "a4", "primaryType", "mixins");
+        builder.makeSearchable("all", "a2");
+        builder.makeSearchable("all", "a1");
         builder.addKey("all", "a1");
         builder.addKey("all", "a3");
         builder.addView("v1", "SELECT c11, c12 AS c2 FROM t1 WHERE c13 < CAST('3' AS LONG)");

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/FullTextSearchParserTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/FullTextSearchParserTest.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/FullTextSearchParserTest.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -34,6 +34,7 @@
 import org.jboss.dna.graph.query.model.FullTextSearch.CompoundTerm;
 import org.jboss.dna.graph.query.model.FullTextSearch.Conjunction;
 import org.jboss.dna.graph.query.model.FullTextSearch.Disjunction;
+import org.jboss.dna.graph.query.model.FullTextSearch.NegationTerm;
 import org.jboss.dna.graph.query.model.FullTextSearch.SimpleTerm;
 import org.jboss.dna.graph.query.model.FullTextSearch.Term;
 import org.junit.Before;
@@ -159,9 +160,9 @@
         Disjunction disjunction = (Disjunction)result;
         assertThat(disjunction.getTerms().size(), is(4));
         Conjunction conjunction1 = (Conjunction)disjunction.getTerms().get(0);
-        SimpleTerm term3 = (SimpleTerm)disjunction.getTerms().get(1);
-        SimpleTerm term4 = (SimpleTerm)disjunction.getTerms().get(2);
-        SimpleTerm term5 = (SimpleTerm)disjunction.getTerms().get(3);
+        Term term3 = disjunction.getTerms().get(1);
+        Term term4 = disjunction.getTerms().get(2);
+        Term term5 = disjunction.getTerms().get(3);
         assertHasSimpleTerms(conjunction1, "term1", "term2");
         assertSimpleTerm(term3, "term3", true, false);
         assertSimpleTerm(term4, "term4", true, false);
@@ -172,12 +173,12 @@
                                              String... terms ) {
         List<Term> expectedTerms = new ArrayList<Term>();
         for (String term : terms) {
-            SimpleTerm expected = new SimpleTerm(term, false);
             if (term.startsWith("-")) {
                 term = term.substring(1);
-                expected = new SimpleTerm(term, true);
+                expectedTerms.add(new NegationTerm(new SimpleTerm(term)));
+            } else {
+                expectedTerms.add(new SimpleTerm(term));
             }
-            expectedTerms.add(expected);
         }
         assertHasTerms(compoundTerm, expectedTerms.toArray(new Term[expectedTerms.size()]));
     }
@@ -187,11 +188,20 @@
                                          boolean excluded,
                                          boolean quotingRequired ) {
         assertThat(term, is(notNullValue()));
-        assertThat(term, is(instanceOf(SimpleTerm.class)));
-        SimpleTerm simpleTerm = (SimpleTerm)term;
-        assertThat(simpleTerm.getValue(), is(value));
-        assertThat(simpleTerm.isExcluded(), is(excluded));
-        assertThat(simpleTerm.isQuotingRequired(), is(quotingRequired));
+        if (excluded) {
+            assertThat(term, is(instanceOf(NegationTerm.class)));
+            NegationTerm negationTerm = (NegationTerm)term;
+            Term negated = negationTerm.getNegatedTerm();
+            assertThat(negated, is(instanceOf(SimpleTerm.class)));
+            SimpleTerm simpleTerm = (SimpleTerm)negated;
+            assertThat(simpleTerm.getValue(), is(value));
+            assertThat(simpleTerm.isQuotingRequired(), is(quotingRequired));
+        } else {
+            assertThat(term, is(instanceOf(SimpleTerm.class)));
+            SimpleTerm simpleTerm = (SimpleTerm)term;
+            assertThat(simpleTerm.getValue(), is(value));
+            assertThat(simpleTerm.isQuotingRequired(), is(quotingRequired));
+        }
     }
 
     public static void assertHasTerms( CompoundTerm compoundTerm,

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/SqlQueryParserTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/SqlQueryParserTest.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/parse/SqlQueryParserTest.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -70,7 +70,7 @@
 import org.jboss.dna.graph.query.model.UpperCase;
 import org.jboss.dna.graph.query.model.FullTextSearch.Conjunction;
 import org.jboss.dna.graph.query.model.FullTextSearch.Disjunction;
-import org.jboss.dna.graph.query.model.FullTextSearch.SimpleTerm;
+import org.jboss.dna.graph.query.model.FullTextSearch.Term;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -680,15 +680,15 @@
     @Test
     public void shouldParseFullTextSearchExpressionFromStringWithValidExpression() {
         Position pos = new Position(100, 13);
-        FullTextSearch.Term result = parser.parseFullTextSearchExpression("term1 term2 OR -term3 OR -term4 OR term5", pos);
+        Term result = parser.parseFullTextSearchExpression("term1 term2 OR -term3 OR -term4 OR term5", pos);
         assertThat(result, is(notNullValue()));
         assertThat(result, is(instanceOf(Disjunction.class)));
         Disjunction disjunction = (Disjunction)result;
         assertThat(disjunction.getTerms().size(), is(4));
         Conjunction conjunction1 = (Conjunction)disjunction.getTerms().get(0);
-        SimpleTerm term3 = (SimpleTerm)disjunction.getTerms().get(1);
-        SimpleTerm term4 = (SimpleTerm)disjunction.getTerms().get(2);
-        SimpleTerm term5 = (SimpleTerm)disjunction.getTerms().get(3);
+        Term term3 = disjunction.getTerms().get(1);
+        Term term4 = disjunction.getTerms().get(2);
+        Term term5 = disjunction.getTerms().get(3);
         FullTextSearchParserTest.assertHasSimpleTerms(conjunction1, "term1", "term2");
         FullTextSearchParserTest.assertSimpleTerm(term3, "term3", true, false);
         FullTextSearchParserTest.assertSimpleTerm(term4, "term4", true, false);

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/plan/CanonicalPlannerTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/plan/CanonicalPlannerTest.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/query/plan/CanonicalPlannerTest.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -159,4 +159,66 @@
         assertThat(source.getChildCount(), is(0));
     }
 
+    @Test
+    public void shouldProduceErrorWhenFullTextSearchingTableWithNoSearchableColumns() {
+        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
+        // Make sure the query without the search criteria does not have an error
+        query = builder.select("column1", "column2").from("someTable").query();
+        queryContext = new QueryContext(context, schemata, hints, problems);
+        plan = planner.createPlan(queryContext, query);
+        assertThat(problems.hasErrors(), is(false));
+
+        query = builder.select("column1", "column2").from("someTable").where().search("someTable", "term1").end().query();
+        queryContext = new QueryContext(context, schemata, hints, problems);
+        plan = planner.createPlan(queryContext, query);
+        assertThat(problems.hasErrors(), is(true));
+    }
+
+    @Test
+    public void shouldProducePlanWhenFullTextSearchingTableWithAtLeastOneSearchableColumn() {
+        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3")
+                                  .makeSearchable("someTable", "column1")
+                                  .build();
+        query = builder.select("column1", "column4").from("someTable").where().search("someTable", "term1").end().query();
+        queryContext = new QueryContext(context, schemata, hints, problems);
+        plan = planner.createPlan(queryContext, query);
+        assertThat(problems.hasErrors(), is(true));
+    }
+
+    @Test
+    public void shouldProduceErrorWhenFullTextSearchingColumnThatIsNotSearchable() {
+        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
+        // Make sure the query without the search criteria does not have an error
+        query = builder.select("column1", "column2").from("someTable").query();
+        queryContext = new QueryContext(context, schemata, hints, problems);
+        plan = planner.createPlan(queryContext, query);
+        assertThat(problems.hasErrors(), is(false));
+
+        query = builder.select("column1", "column2")
+                       .from("someTable")
+                       .where()
+                       .search("someTable", "column2", "term1")
+                       .end()
+                       .query();
+        queryContext = new QueryContext(context, schemata, hints, problems);
+        plan = planner.createPlan(queryContext, query);
+        assertThat(problems.hasErrors(), is(true));
+    }
+
+    @Test
+    public void shouldProducePlanWhenFullTextSearchingColumnThatIsSearchable() {
+        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3")
+                                  .makeSearchable("someTable", "column1")
+                                  .build();
+        query = builder.select("column1", "column4")
+                       .from("someTable")
+                       .where()
+                       .search("someTable", "column1", "term1")
+                       .end()
+                       .query();
+        queryContext = new QueryContext(context, schemata, hints, problems);
+        plan = planner.createPlan(queryContext, query);
+        assertThat(problems.hasErrors(), is(true));
+    }
+
 }

Modified: trunk/dna-search/pom.xml
===================================================================
--- trunk/dna-search/pom.xml	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/pom.xml	2009-11-16 23:25:36 UTC (rev 1320)
@@ -91,6 +91,20 @@
       <artifactId>log4j</artifactId>
       <scope>test</scope>
     </dependency>
+    
+    
+    
+    
+    
+    <dependency>
+      <groupId>org.apache.jackrabbit</groupId>
+      <artifactId>jackrabbit-core</artifactId>
+      <version>2.0-beta1</version>
+    </dependency>
+
+
+
+
     <!-- 
     Java Concurrency in Practice annotations
     -->

Modified: trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfiguration.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfiguration.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfiguration.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -28,7 +28,8 @@
 
 /**
  * Interface used to obtain the Lucene {@link Directory} instance that should be used for a workspace given the name of the
- * workspace. There are several implementations (see {@link DirectoryConfigurations}), but custom implementations can always be used.
+ * workspace. There are several implementations (see {@link DirectoryConfigurations}), but custom implementations can always be
+ * used.
  */
 @ThreadSafe
 public interface DirectoryConfiguration {
@@ -43,4 +44,16 @@
      */
     Directory getDirectory( String workspaceName,
                             String indexName ) throws SearchEngineException;
+
+    /**
+     * Destroy the {@link Directory} that is used for the workspace with the supplied name.
+     * 
+     * @param workspaceName the workspace name
+     * @param indexName the name of the index to be created
+     * @return true if the directory existed and was destroyed, or false if the directory didn't exist
+     * @throws IllegalArgumentException if the workspace name is null
+     * @throws SearchEngineException if there is a problem creating the directory
+     */
+    boolean destroyDirectory( String workspaceName,
+                              String indexName ) throws SearchEngineException;
 }

Modified: trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfigurations.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfigurations.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/DirectoryConfigurations.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -36,6 +36,7 @@
 import org.jboss.dna.common.text.NoOpEncoder;
 import org.jboss.dna.common.text.TextEncoder;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.FileUtil;
 import org.jboss.dna.common.util.HashCode;
 
 /**
@@ -146,6 +147,19 @@
         }
 
         /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.DirectoryConfiguration#destroyDirectory(java.lang.String, java.lang.String)
+         */
+        public boolean destroyDirectory( String workspaceName,
+                                         String indexName ) throws SearchEngineException {
+            CheckArg.isNotNull(workspaceName, "workspaceName");
+            IndexId id = new IndexId(workspaceName, indexName);
+            DirectoryType result = directories.remove(id);
+            return result != null ? doDestroy(result) : false;
+        }
+
+        /**
          * Method implemented by subclasses to create a new Directory implementation.
          * 
          * @param workspaceName the name of the workspace for which the {@link Directory} is to be created; never null
@@ -155,6 +169,8 @@
          */
         protected abstract DirectoryType createDirectory( String workspaceName,
                                                           String indexName ) throws SearchEngineException;
+
+        protected abstract boolean doDestroy( DirectoryType directory ) throws SearchEngineException;
     }
 
     /**
@@ -172,6 +188,16 @@
                                                 String indexName ) {
             return new RAMDirectory();
         }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.DirectoryConfigurations.PoolingDirectoryFactory#doDestroy(org.apache.lucene.store.Directory)
+         */
+        @Override
+        protected boolean doDestroy( RAMDirectory directory ) throws SearchEngineException {
+            return directory != null;
+        }
     }
 
     /**
@@ -304,6 +330,20 @@
         }
 
         /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.DirectoryConfigurations.PoolingDirectoryFactory#doDestroy(org.apache.lucene.store.Directory)
+         */
+        @Override
+        protected boolean doDestroy( FSDirectory directory ) throws SearchEngineException {
+                File file = directory.getFile();
+                if (file.exists()) {
+                    return FileUtil.delete(file);
+                }
+                return false;
+        }
+
+        /**
          * Override this method to define which subclass of {@link FSDirectory} should be created.
          * 
          * @param directory the file system directory; never null

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexLayout.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexLayout.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexLayout.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,1503 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import net.jcip.annotations.ThreadSafe;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldSelector;
+import org.apache.lucene.document.FieldSelectorResult;
+import org.apache.lucene.document.NumericField;
+import org.apache.lucene.document.Field.Index;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.IndexWriter.MaxFieldLength;
+import org.apache.lucene.queryParser.ParseException;
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Collector;
+import org.apache.lucene.search.FieldCache;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.WildcardQuery;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.regex.JavaUtilRegexCapabilities;
+import org.apache.lucene.search.regex.RegexQuery;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.Version;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.common.text.NoOpEncoder;
+import org.jboss.dna.common.text.TextEncoder;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.Node;
+import org.jboss.dna.graph.property.Binary;
+import org.jboss.dna.graph.property.DateTime;
+import org.jboss.dna.graph.property.DateTimeFactory;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.property.PropertyType;
+import org.jboss.dna.graph.property.ValueFactories;
+import org.jboss.dna.graph.property.ValueFactory;
+import org.jboss.dna.graph.query.QueryContext;
+import org.jboss.dna.graph.query.QueryEngine;
+import org.jboss.dna.graph.query.QueryResults;
+import org.jboss.dna.graph.query.QueryResults.Columns;
+import org.jboss.dna.graph.query.model.NodeDepth;
+import org.jboss.dna.graph.query.model.NodeLocalName;
+import org.jboss.dna.graph.query.model.NodeName;
+import org.jboss.dna.graph.query.model.NodePath;
+import org.jboss.dna.graph.query.model.Operator;
+import org.jboss.dna.graph.query.model.PropertyValue;
+import org.jboss.dna.graph.query.model.QueryCommand;
+import org.jboss.dna.graph.query.model.Visitors;
+import org.jboss.dna.graph.query.optimize.Optimizer;
+import org.jboss.dna.graph.query.optimize.OptimizerRule;
+import org.jboss.dna.graph.query.optimize.RuleBasedOptimizer;
+import org.jboss.dna.graph.query.plan.CanonicalPlanner;
+import org.jboss.dna.graph.query.plan.PlanHints;
+import org.jboss.dna.graph.query.plan.PlanNode;
+import org.jboss.dna.graph.query.plan.Planner;
+import org.jboss.dna.graph.query.process.ProcessingComponent;
+import org.jboss.dna.graph.query.process.QueryProcessor;
+import org.jboss.dna.graph.request.ChangeRequest;
+import org.jboss.dna.search.IndexRules.Rule;
+import org.jboss.dna.search.query.CompareNameQuery;
+import org.jboss.dna.search.query.ComparePathQuery;
+import org.jboss.dna.search.query.CompareStringQuery;
+import org.jboss.dna.search.query.MatchNoneQuery;
+import org.jboss.dna.search.query.NotQuery;
+import org.jboss.dna.search.query.UuidsQuery;
+
+/**
+ * A simple {@link IndexLayout} implementation that relies upon two separate indexes: one for the node content and a second one
+ * for paths and UUIDs.
+ */
+ at ThreadSafe
+public abstract class DualIndexLayout implements IndexLayout {
+
+    protected static final long MIN_DATE = 0;
+    protected static final long MAX_DATE = Long.MAX_VALUE;
+    protected static final long MIN_LONG = Long.MIN_VALUE;
+    protected static final long MAX_LONG = Long.MAX_VALUE;
+    protected static final double MIN_DOUBLE = Double.MIN_VALUE;
+    protected static final double MAX_DOUBLE = Double.MAX_VALUE;
+    protected static final int MIN_DEPTH = 0;
+    protected static final int MAX_DEPTH = 100;
+
+    protected static final String PATHS_INDEX_NAME = "paths";
+    protected static final String CONTENT_INDEX_NAME = "content";
+
+    protected static final String UUID_FIELD = "uuid";
+    protected static final String FULL_TEXT_SUFFIX = "/fs"; // the slash character is not allowed in a property name unescaped
+
+    static class PathIndex {
+        public static final String PATH = "path";
+        public static final String LOCAL_NAME = "name";
+        public static final String SNS_INDEX = "sns";
+        public static final String UUID = UUID_FIELD;
+        public static final String DEPTH = "depth";
+    }
+
+    static class ContentIndex {
+        public static final String UUID = UUID_FIELD;
+        public static final String FULL_TEXT = "fts";
+    }
+
+    /**
+     * The number of results that should be returned when performing queries while deleting entire branches of content. The
+     * current value is {@value} .
+     */
+    protected static final int SIZE_OF_DELETE_BATCHES = 1000;
+
+    private ThreadLocal<DateFormat> dateFormatter = new ThreadLocal<DateFormat>() {
+        @Override
+        protected DateFormat initialValue() {
+            return new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+        }
+    };
+
+    /**
+     * Obtain an immutable {@link FieldSelector} instance that accesses the UUID field.
+     */
+    protected static final FieldSelector UUID_FIELD_SELECTOR = new FieldSelector() {
+        private static final long serialVersionUID = 1L;
+
+        public FieldSelectorResult accept( String fieldName ) {
+            return PathIndex.UUID.equals(fieldName) ? FieldSelectorResult.LOAD_AND_BREAK : FieldSelectorResult.NO_LOAD;
+        }
+    };
+
+    /**
+     * Get the date formatter that can be reused safely within the current thread.
+     * 
+     * @return the date formatter; never null
+     */
+    protected DateFormat dateFormatter() {
+        return dateFormatter.get();
+    }
+
+    /**
+     * Get the text encoder that should be used to encode namespaces in the search index.
+     * 
+     * @return the namespace text encoder; never null
+     */
+    protected TextEncoder getNamespaceEncoder() {
+        return new NoOpEncoder();
+    }
+
+    /**
+     * Create a Lucene {@link Analyzer} analyzer that should be used for indexing and searching.
+     * 
+     * @return the analyzer; never null
+     */
+    protected Analyzer createAnalyzer() {
+        return new StandardAnalyzer(Version.LUCENE_CURRENT);
+    }
+
+    protected abstract class LuceneSession implements IndexSession {
+        protected final ExecutionContext context;
+        protected final String sourceName;
+        protected final String workspaceName;
+        protected final IndexRules rules;
+        private final QueryEngine queryEngine;
+        private final Analyzer analyzer;
+        private final Directory pathsIndexDirectory;
+        private final Directory contentIndexDirectory;
+        protected final boolean overwrite;
+        protected final boolean readOnly;
+        protected final ValueFactory<String> stringFactory;
+        protected final DateTimeFactory dateFactory;
+        protected final PathFactory pathFactory;
+        private int changeCount;
+        private IndexReader pathsReader;
+        private IndexWriter pathsWriter;
+        private IndexSearcher pathsSearcher;
+        private IndexReader contentReader;
+        private IndexWriter contentWriter;
+        private IndexSearcher contentSearcher;
+
+        protected LuceneSession( ExecutionContext context,
+                                 String sourceName,
+                                 String workspaceName,
+                                 IndexRules rules,
+                                 Directory pathsIndexDirectory,
+                                 Directory contentIndexDirectory,
+                                 boolean overwrite,
+                                 boolean readOnly ) {
+            this.context = context;
+            this.sourceName = sourceName;
+            this.workspaceName = workspaceName;
+            this.rules = rules;
+            this.overwrite = overwrite;
+            this.readOnly = readOnly;
+            this.pathsIndexDirectory = pathsIndexDirectory;
+            this.contentIndexDirectory = contentIndexDirectory;
+            this.analyzer = createAnalyzer();
+            this.stringFactory = context.getValueFactories().getStringFactory();
+            this.dateFactory = context.getValueFactories().getDateFactory();
+            this.pathFactory = context.getValueFactories().getPathFactory();
+            assert this.context != null;
+            assert this.sourceName != null;
+            assert this.workspaceName != null;
+            assert this.rules != null;
+            assert this.analyzer != null;
+            assert this.pathsIndexDirectory != null;
+            assert this.contentIndexDirectory != null;
+            assert this.stringFactory != null;
+            assert this.dateFactory != null;
+            // do this last ...
+            this.queryEngine = createQueryProcessor();
+            assert this.queryEngine != null;
+        }
+
+        /**
+         * Create the field name that will be used to store the full-text searchable property values.
+         * 
+         * @param propertyName the name of the property; may not be null
+         * @return the field name for the full-text searchable property values; never null
+         */
+        protected String fullTextFieldName( String propertyName ) {
+            return propertyName + FULL_TEXT_SUFFIX;
+        }
+
+        protected IndexReader getPathsReader() throws IOException {
+            if (pathsReader == null) {
+                pathsReader = IndexReader.open(pathsIndexDirectory, readOnly);
+            }
+            return pathsReader;
+        }
+
+        protected IndexReader getContentReader() throws IOException {
+            if (contentReader == null) {
+                contentReader = IndexReader.open(contentIndexDirectory, readOnly);
+            }
+            return contentReader;
+        }
+
+        protected IndexWriter getPathsWriter() throws IOException {
+            assert !readOnly;
+            if (pathsWriter == null) {
+                pathsWriter = new IndexWriter(pathsIndexDirectory, analyzer, overwrite, MaxFieldLength.UNLIMITED);
+            }
+            return pathsWriter;
+        }
+
+        protected IndexWriter getContentWriter() throws IOException {
+            assert !readOnly;
+            if (contentWriter == null) {
+                contentWriter = new IndexWriter(contentIndexDirectory, analyzer, overwrite, MaxFieldLength.UNLIMITED);
+            }
+            return contentWriter;
+        }
+
+        protected IndexSearcher getPathsSearcher() throws IOException {
+            if (pathsSearcher == null) {
+                pathsSearcher = new IndexSearcher(getPathsReader());
+            }
+            return pathsSearcher;
+        }
+
+        protected IndexSearcher getContentSearcher() throws IOException {
+            if (contentSearcher == null) {
+                contentSearcher = new IndexSearcher(getContentReader());
+            }
+            return contentSearcher;
+        }
+
+        protected boolean hasWriters() {
+            return pathsWriter != null || contentWriter != null;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#getContext()
+         */
+        public final ExecutionContext getContext() {
+            return context;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#getSourceName()
+         */
+        public final String getSourceName() {
+            return sourceName;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#getWorkspaceName()
+         */
+        public String getWorkspaceName() {
+            return workspaceName;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#hasChanges()
+         */
+        public boolean hasChanges() {
+            return changeCount > 0;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#index(org.jboss.dna.graph.Node)
+         */
+        public void index( Node node ) throws IOException {
+            assert !readOnly;
+            Location location = node.getLocation();
+            UUID uuid = location.getUuid();
+            if (uuid == null) uuid = UUID.randomUUID();
+            Path path = location.getPath();
+            String uuidStr = stringFactory.create(uuid);
+            String pathStr = pathAsString(path, stringFactory);
+            String nameStr = path.isRoot() ? "" : stringFactory.create(path.getLastSegment().getName());
+            int sns = path.isRoot() ? 1 : path.getLastSegment().getIndex();
+
+            Logger logger = Logger.getLogger(getClass());
+            if (logger.isTraceEnabled()) {
+                logger.trace("indexing {0}", pathStr);
+            }
+
+            // Create a separate document for the path, which makes it easier to handle moves since the path can
+            // be changed without changing any other content fields ...
+            Document doc = new Document();
+            doc.add(new Field(PathIndex.PATH, pathStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
+            doc.add(new Field(PathIndex.LOCAL_NAME, nameStr, Field.Store.YES, Field.Index.ANALYZED));
+            doc.add(new NumericField(PathIndex.LOCAL_NAME, Field.Store.YES, true).setIntValue(sns));
+            doc.add(new Field(PathIndex.UUID, uuidStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
+            doc.add(new NumericField(PathIndex.DEPTH, Field.Store.YES, true).setIntValue(path.size()));
+            getPathsWriter().addDocument(doc);
+
+            // Create the document for the content (properties) ...
+            doc = new Document();
+            doc.add(new Field(ContentIndex.UUID, uuidStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
+            String stringValue = null;
+            StringBuilder fullTextSearchValue = null;
+            for (Property property : node.getProperties()) {
+                Name name = property.getName();
+                Rule rule = rules.getRule(name);
+                if (rule.isSkipped()) continue;
+                String nameString = stringFactory.create(name);
+                if (rule.isDate()) {
+                    for (Object value : property) {
+                        if (value == null) continue;
+                        DateTime dateValue = dateFactory.create(value);
+                        // Add a separate field for each property value ...
+                        doc.add(new NumericField(nameString, rule.getStoreOption(), true).setLongValue(dateValue.getMillisecondsInUtc()));
+                        // Dates are not added to the full-text search field (since this wouldn't make sense)
+                    }
+                    continue;
+                }
+                for (Object value : property) {
+                    if (value == null) continue;
+                    if (value instanceof Binary) {
+                        // don't include binary values as individual fields but do include them in the full-text search ...
+                        // TODO : add to full-text search ...
+                        continue;
+                    }
+                    stringValue = stringFactory.create(value);
+                    // Add a separate field for each property value ...
+                    doc.add(new Field(nameString, stringValue, rule.getStoreOption(), rule.getIndexOption()));
+
+                    if (rule.isFullText()) {
+                        // Add this text to the full-text field ...
+                        if (fullTextSearchValue == null) {
+                            fullTextSearchValue = new StringBuilder();
+                        } else {
+                            fullTextSearchValue.append(' ');
+                        }
+                        fullTextSearchValue.append(stringValue);
+
+                        // Also create a full-text-searchable field ...
+                        String fullTextNameString = fullTextFieldName(nameString);
+                        doc.add(new Field(fullTextNameString, stringValue, Store.NO, Index.ANALYZED));
+                    }
+                }
+            }
+            // Add the full-text-search field ...
+            if (fullTextSearchValue != null) {
+                doc.add(new Field(ContentIndex.FULL_TEXT, fullTextSearchValue.toString(), Field.Store.NO, Field.Index.ANALYZED));
+            }
+            getContentWriter().addDocument(doc);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#optimize()
+         */
+        public void optimize() throws IOException {
+            getContentWriter().optimize();
+            getPathsWriter().optimize();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#apply(java.lang.Iterable)
+         */
+        public int apply( Iterable<ChangeRequest> changes ) /*throws IOException*/{
+            for (ChangeRequest change : changes) {
+                if (change != null) continue;
+            }
+            return 0;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * Because this strategy uses multiple indexes, and since there's no correlation between the documents in those indexes,
+         * we need to perform the delete in multiple steps. First, we need to perform a query to find out which nodes exist below
+         * a certain path. Then, we need to delete those nodes from the paths index. Finally, we need to delete the corresponding
+         * documents in the content index that represent those same nodes.
+         * </p>
+         * <p>
+         * Since we don't know how many documents there will be, we perform these steps in batches, where each batch limits the
+         * number of results to a maximum number. We repeat batches as long as we find more results. This approach has the
+         * advantage that we'll never bring in a large number of results, and it allows us to delete the documents from the
+         * content node using a query.
+         * </p>
+         * 
+         * @see org.jboss.dna.search.IndexSession#deleteBelow(org.jboss.dna.graph.property.Path)
+         */
+        public int deleteBelow( Path path ) throws IOException {
+            assert !readOnly;
+            // Perform a query using the reader to find those nodes at/below the path ...
+            try {
+                IndexReader pathReader = getPathsReader();
+                IndexSearcher pathSearcher = new IndexSearcher(pathReader);
+                String pathStr = stringFactory.create(path) + "/";
+                PrefixQuery query = new PrefixQuery(new Term(PathIndex.PATH, pathStr));
+                int numberDeleted = 0;
+                while (true) {
+                    // Execute the query and get the results ...
+                    TopDocs results = pathSearcher.search(query, SIZE_OF_DELETE_BATCHES);
+                    int numResultsInBatch = results.scoreDocs.length;
+                    // Walk the results, delete the doc, and add to the query that we'll use against the content index ...
+                    IndexReader contentReader = getContentReader();
+                    for (ScoreDoc result : results.scoreDocs) {
+                        int docId = result.doc;
+                        // Find the UUID of the node ...
+                        Document doc = pathReader.document(docId, UUID_FIELD_SELECTOR);
+                        String uuid = doc.get(PathIndex.UUID);
+                        // Delete the document from the paths index ...
+                        pathReader.deleteDocument(docId);
+                        // Delete the corresponding document from the content index ...
+                        contentReader.deleteDocuments(new Term(ContentIndex.UUID, uuid));
+                    }
+                    numberDeleted += numResultsInBatch;
+                    if (numResultsInBatch < SIZE_OF_DELETE_BATCHES) break;
+                }
+                return numberDeleted;
+            } catch (FileNotFoundException e) {
+                // There are no index files yet, so nothing to delete ...
+                return 0;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#search(org.jboss.dna.graph.ExecutionContext, java.lang.String, int, int,
+         *      java.util.List)
+         */
+        public void search( ExecutionContext context,
+                            String fullTextString,
+                            int maxResults,
+                            int offset,
+                            List<Location> results ) throws IOException, ParseException {
+            assert fullTextString != null;
+            assert fullTextString.length() > 0;
+            assert offset >= 0;
+            assert maxResults > 0;
+            assert results != null;
+
+            // Parse the full-text search and search against the 'fts' field ...
+            QueryParser parser = new QueryParser(ContentIndex.FULL_TEXT, createAnalyzer());
+            Query query = parser.parse(fullTextString);
+            TopDocs docs = getContentSearcher().search(query, maxResults + offset);
+
+            // Collect the results ...
+            IndexReader contentReader = getContentReader();
+            IndexReader pathReader = getPathsReader();
+            IndexSearcher pathSearcher = getPathsSearcher();
+            ScoreDoc[] scoreDocs = docs.scoreDocs;
+            int numberOfResults = scoreDocs.length;
+            if (numberOfResults > offset) {
+                // There are enough results to satisfy the offset ...
+                PathFactory pathFactory = context.getValueFactories().getPathFactory();
+                for (int i = offset, num = scoreDocs.length; i != num; ++i) {
+                    ScoreDoc result = scoreDocs[i];
+                    int docId = result.doc;
+                    // Find the UUID of the node (this UUID might be artificial, so we have to find the path) ...
+                    Document doc = contentReader.document(docId, UUID_FIELD_SELECTOR);
+                    String uuid = doc.get(ContentIndex.UUID);
+                    // Find the path for this node (is there a better way to do this than one search per UUID?) ...
+                    TopDocs pathDocs = pathSearcher.search(new TermQuery(new Term(PathIndex.UUID, uuid)), 1);
+                    if (pathDocs.scoreDocs.length < 1) {
+                        // No path record found ...
+                        continue;
+                    }
+                    Document pathDoc = pathReader.document(pathDocs.scoreDocs[0].doc);
+                    String pathString = pathDoc.get(PathIndex.PATH);
+                    Path path = pathFactory.create(pathString);
+                    // Now add the location ...
+                    results.add(Location.create(path, UUID.fromString(uuid)));
+                }
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#query(org.jboss.dna.graph.query.QueryContext,
+         *      org.jboss.dna.graph.query.model.QueryCommand)
+         */
+        public QueryResults query( QueryContext queryContext,
+                                   QueryCommand query ) {
+            return queryEngine.execute(queryContext, query);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#commit()
+         */
+        public void commit() throws IOException {
+            IOException ioError = null;
+            RuntimeException runtimeError = null;
+            if (pathsReader != null) {
+                try {
+                    pathsReader.close();
+                } catch (IOException e) {
+                    ioError = e;
+                } catch (RuntimeException e) {
+                    runtimeError = e;
+                } finally {
+                    pathsReader = null;
+                }
+            }
+            if (contentReader != null) {
+                try {
+                    contentReader.close();
+                } catch (IOException e) {
+                    if (ioError == null) ioError = e;
+                } catch (RuntimeException e) {
+                    if (runtimeError == null) runtimeError = e;
+                } finally {
+                    contentReader = null;
+                }
+            }
+            if (pathsWriter != null) {
+                try {
+                    pathsWriter.commit();
+                } catch (IOException e) {
+                    ioError = e;
+                } catch (RuntimeException e) {
+                    runtimeError = e;
+                } finally {
+                    try {
+                        pathsWriter.close();
+                    } catch (IOException e) {
+                        ioError = e;
+                    } catch (RuntimeException e) {
+                        runtimeError = e;
+                    } finally {
+                        pathsWriter = null;
+                    }
+                }
+            }
+            if (contentWriter != null) {
+                try {
+                    contentWriter.commit();
+                } catch (IOException e) {
+                    if (ioError == null) ioError = e;
+                } catch (RuntimeException e) {
+                    if (runtimeError == null) runtimeError = e;
+                } finally {
+                    try {
+                        contentWriter.close();
+                    } catch (IOException e) {
+                        ioError = e;
+                    } catch (RuntimeException e) {
+                        runtimeError = e;
+                    } finally {
+                        contentWriter = null;
+                    }
+                }
+            }
+            if (ioError != null) throw ioError;
+            if (runtimeError != null) throw runtimeError;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.IndexSession#rollback()
+         */
+        public void rollback() throws IOException {
+            IOException ioError = null;
+            RuntimeException runtimeError = null;
+            if (pathsReader != null) {
+                try {
+                    pathsReader.close();
+                } catch (IOException e) {
+                    ioError = e;
+                } catch (RuntimeException e) {
+                    runtimeError = e;
+                } finally {
+                    pathsReader = null;
+                }
+            }
+            if (contentReader != null) {
+                try {
+                    contentReader.close();
+                } catch (IOException e) {
+                    if (ioError == null) ioError = e;
+                } catch (RuntimeException e) {
+                    if (runtimeError == null) runtimeError = e;
+                } finally {
+                    contentReader = null;
+                }
+            }
+            if (pathsWriter != null) {
+                try {
+                    pathsWriter.rollback();
+                } catch (IOException e) {
+                    ioError = e;
+                } catch (RuntimeException e) {
+                    runtimeError = e;
+                } finally {
+                    try {
+                        pathsWriter.close();
+                    } catch (IOException e) {
+                        ioError = e;
+                    } catch (RuntimeException e) {
+                        runtimeError = e;
+                    } finally {
+                        pathsWriter = null;
+                    }
+                }
+            }
+            if (contentWriter != null) {
+                try {
+                    contentWriter.rollback();
+                } catch (IOException e) {
+                    if (ioError == null) ioError = e;
+                } catch (RuntimeException e) {
+                    if (runtimeError == null) runtimeError = e;
+                } finally {
+                    try {
+                        contentWriter.close();
+                    } catch (IOException e) {
+                        ioError = e;
+                    } catch (RuntimeException e) {
+                        runtimeError = e;
+                    } finally {
+                        contentWriter = null;
+                    }
+                }
+            }
+            if (ioError != null) throw ioError;
+            if (runtimeError != null) throw runtimeError;
+        }
+
+        protected QueryEngine createQueryProcessor() {
+            // Create the query engine ...
+            Planner planner = new CanonicalPlanner();
+            Optimizer optimizer = new RuleBasedOptimizer() {
+                /**
+                 * {@inheritDoc}
+                 * 
+                 * @see org.jboss.dna.graph.query.optimize.RuleBasedOptimizer#populateRuleStack(java.util.LinkedList,
+                 *      org.jboss.dna.graph.query.plan.PlanHints)
+                 */
+                @Override
+                protected void populateRuleStack( LinkedList<OptimizerRule> ruleStack,
+                                                  PlanHints hints ) {
+                    super.populateRuleStack(ruleStack, hints);
+                    // Add any custom rules here, either at the front of the stack or at the end
+                }
+            };
+            QueryProcessor processor = new QueryProcessor() {
+                /**
+                 * {@inheritDoc}
+                 * 
+                 * @see org.jboss.dna.graph.query.process.QueryProcessor#createAccessComponent(org.jboss.dna.graph.query.model.QueryCommand,
+                 *      org.jboss.dna.graph.query.QueryContext, org.jboss.dna.graph.query.plan.PlanNode,
+                 *      org.jboss.dna.graph.query.QueryResults.Columns,
+                 *      org.jboss.dna.graph.query.process.SelectComponent.Analyzer)
+                 */
+                @Override
+                protected ProcessingComponent createAccessComponent( QueryCommand originalQuery,
+                                                                     QueryContext context,
+                                                                     PlanNode accessNode,
+                                                                     Columns resultColumns,
+                                                                     org.jboss.dna.graph.query.process.SelectComponent.Analyzer analyzer ) {
+                    try {
+                        return LuceneSession.this.createAccessComponent(originalQuery,
+                                                                        context,
+                                                                        accessNode,
+                                                                        resultColumns,
+                                                                        analyzer);
+                    } catch (IOException e) {
+                        I18n msg = SearchI18n.errorWhilePerformingQuery;
+                        context.getProblems().addError(e,
+                                                       msg,
+                                                       Visitors.readable(originalQuery),
+                                                       getWorkspaceName(),
+                                                       getSourceName(),
+                                                       e.getMessage());
+                        return null;
+                    }
+                }
+            };
+
+            return new QueryEngine(planner, optimizer, processor);
+        }
+
+        protected abstract ProcessingComponent createAccessComponent( QueryCommand originalQuery,
+                                                                      QueryContext context,
+                                                                      PlanNode accessNode,
+                                                                      Columns resultColumns,
+                                                                      org.jboss.dna.graph.query.process.SelectComponent.Analyzer analyzer )
+            throws IOException;
+
+        /**
+         * Get the set of UUIDs for the children of the node at the given path.
+         * 
+         * @param parentPath the path to the parent node; may not be null
+         * @return the UUIDs of the child nodes; never null but possibly empty
+         * @throws IOException if there is an error accessing the indexes
+         */
+        protected Set<UUID> getUuidsForChildrenOf( Path parentPath ) throws IOException {
+            // Find the path of the parent ...
+            String stringifiedPath = pathAsString(parentPath, stringFactory);
+            // Append a '/' to the parent path, so we'll only get decendants ...
+            stringifiedPath = stringifiedPath + '/';
+
+            // Create a query to find all the nodes below the parent path ...
+            Query query = new PrefixQuery(new Term(PathIndex.PATH, stringifiedPath));
+            // Include only the children ...
+            int childrenDepth = parentPath.size() + 1;
+            Query depthQuery = NumericRangeQuery.newIntRange(PathIndex.DEPTH, childrenDepth, childrenDepth, true, true);
+            // And combine ...
+            BooleanQuery combinedQuery = new BooleanQuery();
+            combinedQuery.add(query, Occur.MUST);
+            combinedQuery.add(depthQuery, Occur.MUST);
+            query = combinedQuery;
+
+            // Now execute and collect the UUIDs ...
+            UuidCollector uuidCollector = new UuidCollector();
+            IndexSearcher searcher = getPathsSearcher();
+            searcher.search(query, uuidCollector);
+            return uuidCollector.getUuids();
+        }
+
+        /**
+         * Get the set of UUIDs for the nodes that are descendants of the node at the given path.
+         * 
+         * @param parentPath the path to the parent node; may not be null and <i>may not be the root node</i>
+         * @param includeParent true if the parent node should be included in the results, or false if only the descendants should
+         *        be included
+         * @return the UUIDs of the nodes; never null but possibly empty
+         * @throws IOException if there is an error accessing the indexes
+         */
+        protected Set<UUID> getUuidsForDescendantsOf( Path parentPath,
+                                                      boolean includeParent ) throws IOException {
+            assert !parentPath.isRoot();
+
+            // Find the path of the parent ...
+            String stringifiedPath = pathAsString(parentPath, stringFactory);
+            if (!includeParent) {
+                // Append a '/' to the parent path, and we'll only get decendants ...
+                stringifiedPath = stringifiedPath + '/';
+            }
+
+            // Create a prefix query ...
+            Query query = new PrefixQuery(new Term(PathIndex.PATH, stringifiedPath));
+
+            // Now execute and collect the UUIDs ...
+            UuidCollector uuidCollector = new UuidCollector();
+            IndexSearcher searcher = getPathsSearcher();
+            searcher.search(query, uuidCollector);
+            return uuidCollector.getUuids();
+        }
+
+        /**
+         * Get the set containing the single UUID for the node at the given path.
+         * 
+         * @param path the path to the node; may not be null
+         * @return the UUID of the supplied node; or null if the node cannot be found
+         * @throws IOException if there is an error accessing the indexes
+         */
+        protected UUID getUuidFor( Path path ) throws IOException {
+            // Create a query to find all the nodes below the parent path ...
+            IndexSearcher searcher = getPathsSearcher();
+            String stringifiedPath = pathAsString(path, stringFactory);
+            TermQuery query = new TermQuery(new Term(PathIndex.PATH, stringifiedPath));
+
+            // Now execute and collect the UUIDs ...
+            TopDocs topDocs = searcher.search(query, 1);
+            if (topDocs.totalHits == 0) return null;
+            Document pathDoc = getPathsReader().document(topDocs.scoreDocs[0].doc);
+            String uuidString = pathDoc.get(PathIndex.UUID);
+            return UUID.fromString(uuidString);
+        }
+
+        /**
+         * Utility method to create a query to find all of the documents representing nodes with the supplied UUIDs.
+         * 
+         * @param uuids the UUIDs of the nodes that are to be found; may not be null
+         * @return the query; never null
+         */
+        protected Query findAllNodesWithUuids( Set<UUID> uuids ) {
+            if (uuids.isEmpty()) {
+                // There are no children, so return a null query ...
+                return new MatchNoneQuery();
+            }
+            if (uuids.size() == 1) {
+                UUID uuid = uuids.iterator().next();
+                if (uuid == null) return new MatchNoneQuery();
+                return new TermQuery(new Term(ContentIndex.UUID, uuid.toString()));
+            }
+            if (uuids.size() < 50) {
+                // Create an OR boolean query for all the UUIDs, since this is probably more efficient ...
+                BooleanQuery query = new BooleanQuery();
+                for (UUID uuid : uuids) {
+                    Query uuidQuery = new TermQuery(new Term(ContentIndex.UUID, uuid.toString()));
+                    query.add(uuidQuery, Occur.SHOULD);
+                }
+                return query;
+            }
+            // Returna query that will always find all of the UUIDs ...
+            return new UuidsQuery(ContentIndex.UUID, uuids, getContext().getValueFactories().getUuidFactory());
+        }
+
+        protected Query findAllNodesBelow( Path ancestorPath ) throws IOException {
+            if (ancestorPath.isRoot()) {
+                return new MatchAllDocsQuery();
+            }
+            Set<UUID> uuids = getUuidsForDescendantsOf(ancestorPath, false);
+            return findAllNodesWithUuids(uuids);
+        }
+
+        /**
+         * Return a query that can be used to find all of the documents that represent nodes that are children of the node at the
+         * supplied path.
+         * 
+         * @param parentPath the path of the parent node.
+         * @return the query; never null
+         * @throws IOException if there is an error finding the UUIDs of the child nodes
+         */
+        protected Query findChildNodes( Path parentPath ) throws IOException {
+            if (parentPath.isRoot()) {
+                return new MatchAllDocsQuery();
+            }
+            Set<UUID> childUuids = getUuidsForChildrenOf(parentPath);
+            return findAllNodesWithUuids(childUuids);
+        }
+
+        /**
+         * Create a query that can be used to find the one document (or node) that exists at the exact path supplied. This method
+         * first queries the {@link PathIndex path index} to find the UUID of the node at the supplied path, and then returns a
+         * query that matches the UUID.
+         * 
+         * @param path the path of the node
+         * @return the query; never null
+         * @throws IOException if there is an error finding the UUID for the supplied path
+         */
+        protected Query findNodeAt( Path path ) throws IOException {
+            UUID uuid = getUuidFor(path);
+            if (uuid == null) return null;
+            return new TermQuery(new Term(ContentIndex.UUID, uuid.toString()));
+        }
+
+        /**
+         * Create a query that can be used to find documents (or nodes) that have a field value that satisfies the supplied LIKE
+         * expression.
+         * 
+         * @param fieldName the name of the document field to search
+         * @param likeExpression the JCR like expression
+         * @return the query; never null
+         */
+        protected Query findNodesLike( String fieldName,
+                                       String likeExpression ) {
+            assert likeExpression != null;
+            assert likeExpression.length() > 0;
+
+            // '%' matches 0 or more characters
+            // '_' matches any single character
+            // '\x' matches 'x'
+            // all other characters match themselves
+
+            // Wildcard queries are a better match, but they can be slow and should not be used
+            // if the first character of the expression is a '%' or '_' ...
+            char firstChar = likeExpression.charAt(0);
+            if (firstChar != '%' && firstChar != '_') {
+                // Create a wildcard query ...
+                String expression = toWildcardExpression(likeExpression);
+                return new WildcardQuery(new Term(fieldName, expression));
+            }
+            // Create a regex query,
+            String regex = toRegularExpression(likeExpression);
+            RegexQuery query = new RegexQuery(new Term(fieldName, regex));
+            query.setRegexImplementation(new JavaUtilRegexCapabilities());
+            return query;
+        }
+
+        protected Query findNodesWith( PropertyValue propertyValue,
+                                       Operator operator,
+                                       Object value,
+                                       boolean caseSensitive ) {
+            String field = stringFactory.create(propertyValue.getPropertyName());
+            PropertyType valueType = PropertyType.discoverType(value);
+            ValueFactories factories = context.getValueFactories();
+            switch (valueType) {
+                case NAME:
+                case PATH:
+                case REFERENCE:
+                case URI:
+                case UUID:
+                case STRING:
+                    String stringValue = stringFactory.create(value);
+                    if (valueType == PropertyType.PATH) {
+                        stringValue = pathAsString(pathFactory.create(value), stringFactory);
+                    }
+                    if (!caseSensitive) stringValue = stringValue.toLowerCase();
+                    switch (operator) {
+                        case EQUAL_TO:
+                            return new TermQuery(new Term(field, stringValue));
+                        case NOT_EQUAL_TO:
+                            Query query = new TermQuery(new Term(field, stringValue));
+                            return new NotQuery(query);
+                        case GREATER_THAN:
+                            return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue,
+                                                                                              field,
+                                                                                              factories,
+                                                                                              caseSensitive);
+                        case GREATER_THAN_OR_EQUAL_TO:
+                            return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue,
+                                                                                                       field,
+                                                                                                       factories,
+                                                                                                       caseSensitive);
+                        case LESS_THAN:
+                            return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue,
+                                                                                           field,
+                                                                                           factories,
+                                                                                           caseSensitive);
+                        case LESS_THAN_OR_EQUAL_TO:
+                            return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue,
+                                                                                                    field,
+                                                                                                    factories,
+                                                                                                    caseSensitive);
+                        case LIKE:
+                            return findNodesLike(field, stringValue);
+                    }
+                    break;
+                case DATE:
+                    long date = factories.getLongFactory().create(value);
+                    switch (operator) {
+                        case EQUAL_TO:
+                            return NumericRangeQuery.newLongRange(field, date, date, true, true);
+                        case NOT_EQUAL_TO:
+                            Query query = NumericRangeQuery.newLongRange(field, date, date, true, true);
+                            return new NotQuery(query);
+                        case GREATER_THAN:
+                            return NumericRangeQuery.newLongRange(field, date, MAX_DATE, false, true);
+                        case GREATER_THAN_OR_EQUAL_TO:
+                            return NumericRangeQuery.newLongRange(field, date, MAX_DATE, true, true);
+                        case LESS_THAN:
+                            return NumericRangeQuery.newLongRange(field, MIN_DATE, date, true, false);
+                        case LESS_THAN_OR_EQUAL_TO:
+                            return NumericRangeQuery.newLongRange(field, MIN_DATE, date, true, true);
+                        case LIKE:
+                            // This is not allowed ...
+                            assert false;
+                            return null;
+                    }
+                    break;
+                case LONG:
+                    long longValue = factories.getLongFactory().create(value);
+                    switch (operator) {
+                        case EQUAL_TO:
+                            return NumericRangeQuery.newLongRange(field, longValue, longValue, true, true);
+                        case NOT_EQUAL_TO:
+                            Query query = NumericRangeQuery.newLongRange(field, longValue, longValue, true, true);
+                            return new NotQuery(query);
+                        case GREATER_THAN:
+                            return NumericRangeQuery.newLongRange(field, longValue, MAX_LONG, false, true);
+                        case GREATER_THAN_OR_EQUAL_TO:
+                            return NumericRangeQuery.newLongRange(field, longValue, MAX_LONG, true, true);
+                        case LESS_THAN:
+                            return NumericRangeQuery.newLongRange(field, MIN_LONG, longValue, true, false);
+                        case LESS_THAN_OR_EQUAL_TO:
+                            return NumericRangeQuery.newLongRange(field, MIN_LONG, longValue, true, true);
+                        case LIKE:
+                            // This is not allowed ...
+                            assert false;
+                            return null;
+                    }
+                    break;
+                case DECIMAL:
+                case DOUBLE:
+                    double doubleValue = factories.getDoubleFactory().create(value);
+                    switch (operator) {
+                        case EQUAL_TO:
+                            return NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true);
+                        case NOT_EQUAL_TO:
+                            Query query = NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true);
+                            return new NotQuery(query);
+                        case GREATER_THAN:
+                            return NumericRangeQuery.newDoubleRange(field, doubleValue, MAX_DOUBLE, false, true);
+                        case GREATER_THAN_OR_EQUAL_TO:
+                            return NumericRangeQuery.newDoubleRange(field, doubleValue, MAX_DOUBLE, true, true);
+                        case LESS_THAN:
+                            return NumericRangeQuery.newDoubleRange(field, MIN_DOUBLE, doubleValue, true, false);
+                        case LESS_THAN_OR_EQUAL_TO:
+                            return NumericRangeQuery.newDoubleRange(field, MIN_DOUBLE, doubleValue, true, true);
+                        case LIKE:
+                            // This is not allowed ...
+                            assert false;
+                            return null;
+                    }
+                    break;
+                case BOOLEAN:
+                    boolean booleanValue = factories.getBooleanFactory().create(value);
+                    stringValue = stringFactory.create(value);
+                    switch (operator) {
+                        case EQUAL_TO:
+                            return new TermQuery(new Term(field, stringValue));
+                        case NOT_EQUAL_TO:
+                            return new TermQuery(new Term(field, stringFactory.create(!booleanValue)));
+                        case GREATER_THAN:
+                            if (!booleanValue) {
+                                return new TermQuery(new Term(field, stringFactory.create(true)));
+                            }
+                            // Can't be greater than 'true', per JCR spec
+                            return new MatchNoneQuery();
+                        case GREATER_THAN_OR_EQUAL_TO:
+                            return new TermQuery(new Term(field, stringFactory.create(true)));
+                        case LESS_THAN:
+                            if (booleanValue) {
+                                return new TermQuery(new Term(field, stringFactory.create(false)));
+                            }
+                            // Can't be less than 'false', per JCR spec
+                            return new MatchNoneQuery();
+                        case LESS_THAN_OR_EQUAL_TO:
+                            return new TermQuery(new Term(field, stringFactory.create(false)));
+                        case LIKE:
+                            // This is not allowed ...
+                            assert false;
+                            return null;
+                    }
+                    break;
+                case OBJECT:
+                case BINARY:
+                    // This is not allowed ...
+                    assert false;
+                    return null;
+            }
+            return null;
+
+        }
+
+        protected Query findNodesWith( NodePath nodePath,
+                                       Operator operator,
+                                       Object value,
+                                       boolean caseSensitive ) throws IOException {
+            if (!caseSensitive) value = stringFactory.create(value).toLowerCase();
+            Path pathValue = operator != Operator.LIKE ? pathFactory.create(value) : null;
+            Query query = null;
+            switch (operator) {
+                case EQUAL_TO:
+                    return findNodeAt(pathValue);
+                case NOT_EQUAL_TO:
+                    return new NotQuery(findNodeAt(pathValue));
+                case LIKE:
+                    String likeExpression = stringFactory.create(value);
+                    return findNodesLike(PathIndex.PATH, likeExpression);
+                case GREATER_THAN:
+                    query = ComparePathQuery.createQueryForNodesWithPathGreaterThan(pathValue,
+                                                                                    PathIndex.PATH,
+                                                                                    context.getValueFactories(),
+                                                                                    caseSensitive);
+                    break;
+                case GREATER_THAN_OR_EQUAL_TO:
+                    query = ComparePathQuery.createQueryForNodesWithPathGreaterThanOrEqualTo(pathValue,
+                                                                                             PathIndex.PATH,
+                                                                                             context.getValueFactories(),
+                                                                                             caseSensitive);
+                    break;
+                case LESS_THAN:
+                    query = ComparePathQuery.createQueryForNodesWithPathLessThan(pathValue,
+                                                                                 PathIndex.PATH,
+                                                                                 context.getValueFactories(),
+                                                                                 caseSensitive);
+                    break;
+                case LESS_THAN_OR_EQUAL_TO:
+                    query = ComparePathQuery.createQueryForNodesWithPathLessThanOrEqualTo(pathValue,
+                                                                                          PathIndex.PATH,
+                                                                                          context.getValueFactories(),
+                                                                                          caseSensitive);
+                    break;
+            }
+            // Now execute and collect the UUIDs ...
+            UuidCollector uuidCollector = new UuidCollector();
+            IndexSearcher searcher = getPathsSearcher();
+            searcher.search(query, uuidCollector);
+            return findAllNodesWithUuids(uuidCollector.getUuids());
+        }
+
+        protected Query findNodesWith( NodeName nodeName,
+                                       Operator operator,
+                                       Object value,
+                                       boolean caseSensitive ) throws IOException {
+            String stringValue = stringFactory.create(value);
+            if (!caseSensitive) stringValue = stringValue.toLowerCase();
+            Path.Segment segment = operator != Operator.LIKE ? pathFactory.createSegment(stringValue) : null;
+            int snsIndex = operator != Operator.LIKE ? segment.getIndex() : 0;
+            Query query = null;
+            switch (operator) {
+                case EQUAL_TO:
+                    BooleanQuery booleanQuery = new BooleanQuery();
+                    booleanQuery.add(new TermQuery(new Term(PathIndex.LOCAL_NAME, stringValue)), Occur.MUST);
+                    booleanQuery.add(NumericRangeQuery.newIntRange(PathIndex.SNS_INDEX, snsIndex, snsIndex, true, false),
+                                     Occur.MUST);
+                    return booleanQuery;
+                case NOT_EQUAL_TO:
+                    booleanQuery = new BooleanQuery();
+                    booleanQuery.add(new TermQuery(new Term(PathIndex.LOCAL_NAME, stringValue)), Occur.MUST);
+                    booleanQuery.add(NumericRangeQuery.newIntRange(PathIndex.SNS_INDEX, snsIndex, snsIndex, true, false),
+                                     Occur.MUST);
+                    return new NotQuery(booleanQuery);
+                case GREATER_THAN:
+                    query = CompareNameQuery.createQueryForNodesWithNameGreaterThan(segment,
+                                                                                    PathIndex.LOCAL_NAME,
+                                                                                    PathIndex.SNS_INDEX,
+                                                                                    context.getValueFactories(),
+                                                                                    caseSensitive);
+                    break;
+                case GREATER_THAN_OR_EQUAL_TO:
+                    query = CompareNameQuery.createQueryForNodesWithNameGreaterThanOrEqualTo(segment,
+                                                                                             PathIndex.LOCAL_NAME,
+                                                                                             PathIndex.SNS_INDEX,
+                                                                                             context.getValueFactories(),
+                                                                                             caseSensitive);
+                    break;
+                case LESS_THAN:
+                    query = CompareNameQuery.createQueryForNodesWithNameLessThan(segment,
+                                                                                 PathIndex.LOCAL_NAME,
+                                                                                 PathIndex.SNS_INDEX,
+                                                                                 context.getValueFactories(),
+                                                                                 caseSensitive);
+                    break;
+                case LESS_THAN_OR_EQUAL_TO:
+                    query = CompareNameQuery.createQueryForNodesWithNameLessThanOrEqualTo(segment,
+                                                                                          PathIndex.LOCAL_NAME,
+                                                                                          PathIndex.SNS_INDEX,
+                                                                                          context.getValueFactories(),
+                                                                                          caseSensitive);
+                    break;
+                case LIKE:
+                    // See whether the like expression has brackets ...
+                    String likeExpression = stringValue;
+                    int openBracketIndex = likeExpression.indexOf('[');
+                    if (openBracketIndex != -1) {
+                        String localNameExpression = likeExpression.substring(0, openBracketIndex);
+                        String snsIndexExpression = likeExpression.substring(openBracketIndex);
+                        Query localNameQuery = createLocalNameQuery(localNameExpression);
+                        Query snsQuery = createSnsIndexQuery(snsIndexExpression);
+                        if (localNameQuery == null) {
+                            if (snsQuery == null) {
+                                query = new MatchNoneQuery();
+                            } else {
+                                // There is just an SNS part ...
+                                query = snsQuery;
+                            }
+                        } else {
+                            // There is a local name part ...
+                            if (snsQuery == null) {
+                                query = localNameQuery;
+                            } else {
+                                // There is both a local name part and a SNS part ...
+                                booleanQuery = new BooleanQuery();
+                                booleanQuery.add(localNameQuery, Occur.MUST);
+                                booleanQuery.add(snsQuery, Occur.MUST);
+                                query = booleanQuery;
+                            }
+                        }
+                    } else {
+                        // There is no SNS expression ...
+                        query = createLocalNameQuery(likeExpression);
+                    }
+                    assert query != null;
+                    break;
+            }
+
+            // Now execute and collect the UUIDs ...
+            UuidCollector uuidCollector = new UuidCollector();
+            IndexSearcher searcher = getPathsSearcher();
+            searcher.search(query, uuidCollector);
+            return findAllNodesWithUuids(uuidCollector.getUuids());
+        }
+
+        protected Query findNodesWith( NodeLocalName nodeName,
+                                       Operator operator,
+                                       Object value,
+                                       boolean caseSensitive ) throws IOException {
+            String nameValue = stringFactory.create(value);
+            Query query = null;
+            switch (operator) {
+                case LIKE:
+                    String likeExpression = stringFactory.create(value);
+                    return findNodesLike(PathIndex.LOCAL_NAME, likeExpression); // already is a query with UUIDs
+                case EQUAL_TO:
+                    query = new TermQuery(new Term(PathIndex.LOCAL_NAME, nameValue));
+                    break;
+                case NOT_EQUAL_TO:
+                    query = new NotQuery(new TermQuery(new Term(PathIndex.LOCAL_NAME, nameValue)));
+                    break;
+                case GREATER_THAN:
+                    query = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(nameValue,
+                                                                                       PathIndex.LOCAL_NAME,
+                                                                                       context.getValueFactories(),
+                                                                                       caseSensitive);
+                    break;
+                case GREATER_THAN_OR_EQUAL_TO:
+                    query = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(nameValue,
+                                                                                                PathIndex.LOCAL_NAME,
+                                                                                                context.getValueFactories(),
+                                                                                                caseSensitive);
+                    break;
+                case LESS_THAN:
+                    query = CompareStringQuery.createQueryForNodesWithFieldLessThan(nameValue,
+                                                                                    PathIndex.LOCAL_NAME,
+                                                                                    context.getValueFactories(),
+                                                                                    caseSensitive);
+                    break;
+                case LESS_THAN_OR_EQUAL_TO:
+                    query = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(nameValue,
+                                                                                             PathIndex.LOCAL_NAME,
+                                                                                             context.getValueFactories(),
+                                                                                             caseSensitive);
+                    break;
+            }
+
+            // Now execute and collect the UUIDs ...
+            UuidCollector uuidCollector = new UuidCollector();
+            IndexSearcher searcher = getPathsSearcher();
+            searcher.search(query, uuidCollector);
+            return findAllNodesWithUuids(uuidCollector.getUuids());
+        }
+
+        protected Query findNodesWith( NodeDepth depthConstraint,
+                                       Operator operator,
+                                       Object value ) throws IOException {
+            int depth = context.getValueFactories().getLongFactory().create(value).intValue();
+            Query query = null;
+            switch (operator) {
+                case EQUAL_TO:
+                    query = NumericRangeQuery.newIntRange(PathIndex.DEPTH, depth, depth, true, true);
+                    break;
+                case NOT_EQUAL_TO:
+                    query = NumericRangeQuery.newIntRange(PathIndex.DEPTH, depth, depth, true, true);
+                    query = new NotQuery(query);
+                    break;
+                case GREATER_THAN:
+                    query = NumericRangeQuery.newIntRange(PathIndex.DEPTH, depth, MAX_DEPTH, false, true);
+                    break;
+                case GREATER_THAN_OR_EQUAL_TO:
+                    query = NumericRangeQuery.newIntRange(PathIndex.DEPTH, depth, MAX_DEPTH, true, true);
+                    break;
+                case LESS_THAN:
+                    query = NumericRangeQuery.newIntRange(PathIndex.DEPTH, MIN_DEPTH, depth, true, false);
+                    break;
+                case LESS_THAN_OR_EQUAL_TO:
+                    query = NumericRangeQuery.newIntRange(PathIndex.DEPTH, MIN_DEPTH, depth, true, true);
+                    break;
+                case LIKE:
+                    // This is not allowed ...
+                    return null;
+            }
+
+            // Now execute and collect the UUIDs ...
+            UuidCollector uuidCollector = new UuidCollector();
+            IndexSearcher searcher = getPathsSearcher();
+            searcher.search(query, uuidCollector);
+            return findAllNodesWithUuids(uuidCollector.getUuids());
+        }
+
+        protected Query createLocalNameQuery( String likeExpression ) {
+            if (likeExpression == null) return null;
+            likeExpression = likeExpression.trim();
+            if (likeExpression.length() == 0) return null;
+            if (likeExpression.indexOf('?') != -1 || likeExpression.indexOf('*') != -1) {
+                // The local name is a like ...
+                return findNodesLike(PathIndex.LOCAL_NAME, likeExpression);
+            }
+            // The local name is an exact match ...
+            return new TermQuery(new Term(PathIndex.LOCAL_NAME, likeExpression));
+        }
+
+        protected Query createSnsIndexQuery( String likeExpression ) {
+            if (likeExpression == null) return null;
+            likeExpression = likeExpression.trim();
+            if (likeExpression.length() == 0) return null;
+
+            // Remove the leading '[' ...
+            assert likeExpression.charAt(0) == '[';
+            likeExpression = likeExpression.substring(1);
+
+            // Remove the trailing ']' if it exists ...
+            int closeBracketIndex = likeExpression.indexOf(']');
+            if (closeBracketIndex != -1) {
+                likeExpression = likeExpression.substring(0, closeBracketIndex);
+            }
+            // If SNS expression contains '?' or '*' ...
+            if (likeExpression.indexOf('?') != -1 || likeExpression.indexOf('*') != -1) {
+                // There is a LIKE expression for the SNS ...
+                return findNodesLike(PathIndex.SNS_INDEX, likeExpression);
+            }
+            // This is not a LIKE expression but an exact value specification and should be a number ...
+            try {
+                // This SNS is just a number ...
+                int sns = Integer.parseInt(likeExpression);
+                return NumericRangeQuery.newIntRange(PathIndex.SNS_INDEX, sns, sns, true, false);
+            } catch (NumberFormatException e) {
+                // It's not a number but it's in the SNS field, so there will be no results ...
+                return new MatchNoneQuery();
+            }
+        }
+
+    }
+
+    /**
+     * Convert the JCR like expression to a Lucene wildcard expression. The JCR like expression uses '%' to match 0 or more
+     * characters, '_' to match any single character, '\x' to match the 'x' character, and all other characters to match
+     * themselves.
+     * 
+     * @param likeExpression the like expression; may not be null
+     * @return the expression that can be used with a WildcardQuery; never null
+     */
+    protected static String toWildcardExpression( String likeExpression ) {
+        assert likeExpression != null;
+        assert likeExpression.length() > 0;
+        return likeExpression.replace('%', '*').replace('_', '?').replaceAll("\\\\(.)", "$1");
+    }
+
+    /**
+     * Convert the JCR like expression to a regular expression. The JCR like expression uses '%' to match 0 or more characters,
+     * '_' to match any single character, '\x' to match the 'x' character, and all other characters to match themselves. Note that
+     * if any regex metacharacters appear in the like expression, they will be escaped within the resulting regular expression.
+     * 
+     * @param likeExpression the like expression; may not be null
+     * @return the expression that can be used with a WildcardQuery; never null
+     */
+    protected static String toRegularExpression( String likeExpression ) {
+        assert likeExpression != null;
+        assert likeExpression.length() > 0;
+        // Replace all '\x' with 'x' ...
+        String result = likeExpression.replaceAll("\\\\(.)", "$1");
+        // Escape characters used as metacharacters in regular expressions, including
+        // '[', '^', '\', '$', '.', '|', '?', '*', '+', '(', and ')'
+        result = result.replaceAll("([[^\\\\$.|?*+()])", "\\$1");
+        // Replace '%'->'[.]+' and '_'->'[.]
+        result = likeExpression.replace("%", "[.]+").replace("_", "[.]");
+        return result;
+    }
+
+    protected static String pathAsString( Path path,
+                                          ValueFactory<String> stringFactory ) {
+        assert path != null;
+        if (path.isRoot()) return "/";
+        String pathStr = stringFactory.create(path);
+        if (!pathStr.endsWith("]")) {
+            pathStr = pathStr + '[' + Path.DEFAULT_INDEX + ']';
+        }
+        return pathStr;
+    }
+
+    /**
+     * A {@link Collector} implementation that only captures the UUID of the documents returned by a query. Score information is
+     * not recorded. This is often used when querying the {@link PathIndex} to collect the UUIDs of a set of nodes satisfying some
+     * path constraint.
+     * 
+     * @see DualIndexLayout.LuceneSession#findChildNodes(Path)
+     */
+    protected static class UuidCollector extends Collector {
+        private final Set<UUID> uuids = new HashSet<UUID>();
+        private String[] uuidsByDocId;
+        private int baseDocId;
+
+        protected UuidCollector() {
+        }
+
+        /**
+         * Get the UUIDs that have been collected.
+         * 
+         * @return the set of UUIDs; never null
+         */
+        public Set<UUID> getUuids() {
+            return uuids;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#acceptsDocsOutOfOrder()
+         */
+        @Override
+        public boolean acceptsDocsOutOfOrder() {
+            return true;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#setScorer(org.apache.lucene.search.Scorer)
+         */
+        @Override
+        public void setScorer( Scorer scorer ) {
+            // we don't care about scoring
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#collect(int)
+         */
+        @Override
+        public void collect( int doc ) {
+            int index = doc - baseDocId;
+            assert index >= 0;
+            String uuidString = uuidsByDocId[index];
+            assert uuidString != null;
+            uuids.add(UUID.fromString(uuidString));
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#setNextReader(org.apache.lucene.index.IndexReader, int)
+         */
+        @Override
+        public void setNextReader( IndexReader reader,
+                                   int docBase ) throws IOException {
+            this.uuidsByDocId = FieldCache.DEFAULT.getStrings(reader, UUID_FIELD);
+            this.baseDocId = docBase;
+        }
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexLayout.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Deleted: trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexStrategy.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexStrategy.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/DualIndexStrategy.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -1,399 +0,0 @@
-/*
- * JBoss DNA (http://www.jboss.org/dna)
- * See the COPYRIGHT.txt file distributed with this work for information
- * regarding copyright ownership.  Some portions may be licensed
- * to Red Hat, Inc. under one or more contributor license agreements.
- * See the AUTHORS.txt file in the distribution for a full listing of 
- * individual contributors.
- *
- * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
- * is licensed to you under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- * 
- * JBoss DNA is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.search;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.UUID;
-import net.jcip.annotations.ThreadSafe;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.Field;
-import org.apache.lucene.document.FieldSelector;
-import org.apache.lucene.document.FieldSelectorResult;
-import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.queryParser.ParseException;
-import org.apache.lucene.queryParser.QueryParser;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.PrefixQuery;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.search.TopDocs;
-import org.apache.lucene.util.Version;
-import org.jboss.dna.common.text.NoOpEncoder;
-import org.jboss.dna.common.text.TextEncoder;
-import org.jboss.dna.common.util.Logger;
-import org.jboss.dna.graph.Location;
-import org.jboss.dna.graph.Node;
-import org.jboss.dna.graph.property.Binary;
-import org.jboss.dna.graph.property.DateTime;
-import org.jboss.dna.graph.property.DateTimeFactory;
-import org.jboss.dna.graph.property.Name;
-import org.jboss.dna.graph.property.Path;
-import org.jboss.dna.graph.property.Property;
-import org.jboss.dna.graph.property.ValueFactory;
-import org.jboss.dna.graph.query.QueryContext;
-import org.jboss.dna.graph.query.QueryEngine;
-import org.jboss.dna.graph.query.QueryResults;
-import org.jboss.dna.graph.query.QueryResults.Columns;
-import org.jboss.dna.graph.query.model.QueryCommand;
-import org.jboss.dna.graph.query.optimize.Optimizer;
-import org.jboss.dna.graph.query.optimize.OptimizerRule;
-import org.jboss.dna.graph.query.optimize.RuleBasedOptimizer;
-import org.jboss.dna.graph.query.plan.CanonicalPlanner;
-import org.jboss.dna.graph.query.plan.PlanHints;
-import org.jboss.dna.graph.query.plan.PlanNode;
-import org.jboss.dna.graph.query.plan.Planner;
-import org.jboss.dna.graph.query.process.ProcessingComponent;
-import org.jboss.dna.graph.query.process.QueryProcessor;
-import org.jboss.dna.search.IndexRules.Rule;
-
-/**
- * A simple {@link IndexStrategy} implementation that relies upon two separate indexes: one for the node content and a second
- * one for paths and UUIDs.
- */
- at ThreadSafe
-abstract class DualIndexStrategy implements IndexStrategy {
-
-    static class PathIndex {
-        public static final String PATH = "path";
-        public static final String UUID = "uuid";
-    }
-
-    static class ContentIndex {
-        public static final String UUID = PathIndex.UUID;
-        public static final String FULL_TEXT = "fts";
-    }
-
-    /**
-     * The number of results that should be returned when performing queries while deleting entire branches of content. The
-     * current value is {@value} .
-     */
-    protected static final int SIZE_OF_DELETE_BATCHES = 1000;
-
-    private ThreadLocal<DateFormat> dateFormatter = new ThreadLocal<DateFormat>() {
-        @Override
-        protected DateFormat initialValue() {
-            return new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
-        }
-    };
-
-    /**
-     * Obtain an immutable {@link FieldSelector} instance that accesses the UUID field.
-     */
-    protected static final FieldSelector UUID_FIELD_SELECTOR = new FieldSelector() {
-        private static final long serialVersionUID = 1L;
-
-        public FieldSelectorResult accept( String fieldName ) {
-            return PathIndex.UUID.equals(fieldName) ? FieldSelectorResult.LOAD_AND_BREAK : FieldSelectorResult.NO_LOAD;
-        }
-    };
-
-    private final IndexRules rules;
-    private final Logger logger;
-    private final QueryEngine queryEngine;
-
-    /**
-     * Create a new indexing strategy instance.
-     * 
-     * @param rules the indexing rules that govern how properties are to be index; may not be null
-     */
-    protected DualIndexStrategy( IndexRules rules ) {
-        assert rules != null;
-        this.rules = rules;
-        this.logger = Logger.getLogger(getClass());
-        // Create the query engine ...
-        Planner planner = new CanonicalPlanner();
-        Optimizer optimizer = new RuleBasedOptimizer() {
-            /**
-             * {@inheritDoc}
-             * 
-             * @see org.jboss.dna.graph.query.optimize.RuleBasedOptimizer#populateRuleStack(java.util.LinkedList,
-             *      org.jboss.dna.graph.query.plan.PlanHints)
-             */
-            @Override
-            protected void populateRuleStack( LinkedList<OptimizerRule> ruleStack,
-                                              PlanHints hints ) {
-                super.populateRuleStack(ruleStack, hints);
-                // Add any custom rules here, either at the front of the stack or at the end
-            }
-        };
-        QueryProcessor processor = new QueryProcessor() {
-            /**
-             * {@inheritDoc}
-             * 
-             * @see org.jboss.dna.graph.query.process.QueryProcessor#createAccessComponent(org.jboss.dna.graph.query.QueryContext,
-             *      org.jboss.dna.graph.query.plan.PlanNode, org.jboss.dna.graph.query.QueryResults.Columns,
-             *      org.jboss.dna.graph.query.process.SelectComponent.Analyzer)
-             */
-            @Override
-            protected ProcessingComponent createAccessComponent( QueryContext context,
-                                                                 PlanNode accessNode,
-                                                                 Columns resultColumns,
-                                                                 org.jboss.dna.graph.query.process.SelectComponent.Analyzer analyzer ) {
-                return DualIndexStrategy.this.createAccessComponent((SearchContext)context, accessNode, resultColumns, analyzer);
-            }
-        };
-
-        this.queryEngine = new QueryEngine(planner, optimizer, processor);
-    }
-
-    protected abstract ProcessingComponent createAccessComponent( SearchContext context,
-                                                                  PlanNode accessNode,
-                                                                  Columns resultColumns,
-                                                                  org.jboss.dna.graph.query.process.SelectComponent.Analyzer analyzer );
-
-    /**
-     * Utility method to obtain a {@link DateFormat} instance that can be used safely within a single thread.
-     * 
-     * @return the date formatter; never null
-     */
-    protected final DateFormat dateFormatter() {
-        return dateFormatter.get();
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#getNamespaceEncoder()
-     */
-    public TextEncoder getNamespaceEncoder() {
-        return new NoOpEncoder();
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#getChangeCountForAutomaticOptimization()
-     */
-    public int getChangeCountForAutomaticOptimization() {
-        return 0;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#createAnalyzer()
-     */
-    public Analyzer createAnalyzer() {
-        return new StandardAnalyzer(Version.LUCENE_CURRENT);
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * Because this strategy uses multiple indexes, and since there's no correlation between the documents in those indexes, we
-     * need to perform the delete in multiple steps. First, we need to perform a query to find out which nodes exist below a
-     * certain path. Then, we need to delete those nodes from the paths index. Finally, we need to delete the corresponding
-     * documents in the content index that represent those same nodes.
-     * </p>
-     * <p>
-     * Since we don't know how many documents there will be, we perform these steps in batches, where each batch limits the number
-     * of results to a maximum number. We repeat batches as long as we find more results. This approach has the advantage that
-     * we'll never bring in a large number of results, and it allows us to delete the documents from the content node using a
-     * query.
-     * </p>
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#deleteBelow(Path, IndexContext)
-     */
-    public int deleteBelow( Path path,
-                            IndexContext indexes ) throws IOException {
-        // Perform a query using the reader to find those nodes at/below the path ...
-        try {
-            IndexReader pathReader = indexes.getPathsReader();
-            IndexSearcher pathSearcher = new IndexSearcher(pathReader);
-            String pathStr = indexes.stringFactory().create(path) + "/";
-            PrefixQuery query = new PrefixQuery(new Term(PathIndex.PATH, pathStr));
-            int numberDeleted = 0;
-            while (true) {
-                // Execute the query and get the results ...
-                TopDocs results = pathSearcher.search(query, SIZE_OF_DELETE_BATCHES);
-                int numResultsInBatch = results.scoreDocs.length;
-                // Walk the results, delete the doc, and add to the query that we'll use against the content index ...
-                IndexReader contentReader = indexes.getContentReader();
-                for (ScoreDoc result : results.scoreDocs) {
-                    int docId = result.doc;
-                    // Find the UUID of the node ...
-                    Document doc = pathReader.document(docId, UUID_FIELD_SELECTOR);
-                    String uuid = doc.get(PathIndex.UUID);
-                    // Delete the document from the paths index ...
-                    pathReader.deleteDocument(docId);
-                    // Delete the corresponding document from the content index ...
-                    contentReader.deleteDocuments(new Term(ContentIndex.UUID, uuid));
-                }
-                numberDeleted += numResultsInBatch;
-                if (numResultsInBatch < SIZE_OF_DELETE_BATCHES) break;
-            }
-            indexes.commit();
-            return numberDeleted;
-        } catch (FileNotFoundException e) {
-            // There are no index files yet, so nothing to delete ...
-            return 0;
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#index(Node, IndexContext)
-     */
-    public void index( Node node,
-                       IndexContext indexes ) throws IOException {
-        ValueFactory<String> strings = indexes.stringFactory();
-        Location location = node.getLocation();
-        UUID uuid = location.getUuid();
-        if (uuid == null) uuid = UUID.randomUUID();
-        Path path = location.getPath();
-        String pathStr = path.isRoot() ? "/" : strings.create(location.getPath()) + "/";
-        String uuidStr = uuid.toString();
-
-        if (logger.isTraceEnabled()) {
-            logger.trace("indexing {0}", pathStr);
-        }
-
-        // Create a separate document for the path, which makes it easier to handle moves since the path can
-        // be changed without changing any other content fields ...
-        Document doc = new Document();
-        doc.add(new Field(PathIndex.PATH, pathStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
-        doc.add(new Field(PathIndex.UUID, uuidStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
-        indexes.getPathsWriter().addDocument(doc);
-
-        // Create the document for the content (properties) ...
-        doc = new Document();
-        doc.add(new Field(ContentIndex.UUID, uuidStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
-        String stringValue = null;
-        StringBuilder fullTextSearchValue = null;
-        for (Property property : node.getProperties()) {
-            Name name = property.getName();
-            Rule rule = rules.getRule(name);
-            if (rule.isSkipped()) continue;
-            String nameString = strings.create(name);
-            if (rule.isDate()) {
-                DateTimeFactory dateFactory = indexes.dateFactory();
-                for (Object value : property) {
-                    if (value == null) continue;
-                    DateTime dateValue = dateFactory.create(value);
-                    stringValue = dateFormatter().format(dateValue.toDate());
-                    // Add a separate field for each property value ...
-                    doc.add(new Field(nameString, stringValue, rule.getStoreOption(), rule.getIndexOption()));
-                    // Dates are not added to the full-text search field (since this wouldn't make sense)
-                }
-                continue;
-            }
-            for (Object value : property) {
-                if (value == null) continue;
-                if (value instanceof Binary) {
-                    // don't include binary values as individual fields but do include them in the full-text search ...
-                    // TODO : add to full-text search ...
-                    continue;
-                }
-                stringValue = strings.create(value);
-                // Add a separate field for each property value ...
-                doc.add(new Field(nameString, stringValue, rule.getStoreOption(), rule.getIndexOption()));
-                // And add to the full-text field ...
-                if (rule.isFullText()) {
-                    if (fullTextSearchValue == null) {
-                        fullTextSearchValue = new StringBuilder();
-                    } else {
-                        fullTextSearchValue.append(' ');
-                    }
-                    fullTextSearchValue.append(stringValue);
-                }
-            }
-        }
-        // Add the full-text-search field ...
-        if (fullTextSearchValue != null) {
-            doc.add(new Field(ContentIndex.FULL_TEXT, fullTextSearchValue.toString(), Field.Store.NO, Field.Index.ANALYZED));
-        }
-        indexes.getContentWriter().addDocument(doc);
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#search(String, int, int, IndexContext, List)
-     */
-    public void search( String fullTextString,
-                        int maxResults,
-                        int offset,
-                        IndexContext indexes,
-                        List<Location> results ) throws IOException, ParseException {
-        assert fullTextString != null;
-        assert fullTextString.length() > 0;
-        assert offset >= 0;
-        assert maxResults > 0;
-        assert indexes != null;
-        assert results != null;
-
-        // Parse the full-text search and search against the 'fts' field ...
-        QueryParser parser = new QueryParser(ContentIndex.FULL_TEXT, createAnalyzer());
-        Query query = parser.parse(fullTextString);
-        TopDocs docs = indexes.getContentSearcher().search(query, maxResults + offset);
-
-        // Collect the results ...
-        IndexReader contentReader = indexes.getContentReader();
-        IndexReader pathReader = indexes.getPathsReader();
-        IndexSearcher pathSearcher = indexes.getPathsSearcher();
-        ScoreDoc[] scoreDocs = docs.scoreDocs;
-        int numberOfResults = scoreDocs.length;
-        if (numberOfResults > offset) {
-            // There are enough results to satisfy the offset ...
-            for (int i = offset, num = scoreDocs.length; i != num; ++i) {
-                ScoreDoc result = scoreDocs[i];
-                int docId = result.doc;
-                // Find the UUID of the node (this UUID might be artificial, so we have to find the path) ...
-                Document doc = contentReader.document(docId, UUID_FIELD_SELECTOR);
-                String uuid = doc.get(ContentIndex.UUID);
-                // Find the path for this node (is there a better way to do this than one search per UUID?) ...
-                TopDocs pathDocs = pathSearcher.search(new TermQuery(new Term(PathIndex.UUID, uuid)), 1);
-                if (pathDocs.scoreDocs.length < 1) {
-                    // No path record found ...
-                    continue;
-                }
-                Document pathDoc = pathReader.document(pathDocs.scoreDocs[0].doc);
-                Path path = indexes.pathFactory().create(pathDoc.get(PathIndex.PATH));
-                // Now add the location ...
-                results.add(Location.create(path, UUID.fromString(uuid)));
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#query(org.jboss.dna.search.SearchContext,
-     *      org.jboss.dna.graph.query.model.QueryCommand)
-     */
-    public QueryResults query( SearchContext context,
-                               QueryCommand query ) {
-        return this.queryEngine.execute(context, query);
-    }
-}

Deleted: trunk/dna-search/src/main/java/org/jboss/dna/search/IndexContext.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/IndexContext.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/IndexContext.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -1,294 +0,0 @@
-/*
- * JBoss DNA (http://www.jboss.org/dna)
- * See the COPYRIGHT.txt file distributed with this work for information
- * regarding copyright ownership.  Some portions may be licensed
- * to Red Hat, Inc. under one or more contributor license agreements.
- * See the AUTHORS.txt file in the distribution for a full listing of 
- * individual contributors.
- *
- * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
- * is licensed to you under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- * 
- * JBoss DNA is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.search;
-
-import java.io.IOException;
-import net.jcip.annotations.NotThreadSafe;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriter.MaxFieldLength;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.store.Directory;
-import org.jboss.dna.common.util.CheckArg;
-import org.jboss.dna.graph.ExecutionContext;
-import org.jboss.dna.graph.property.DateTimeFactory;
-import org.jboss.dna.graph.property.PathFactory;
-import org.jboss.dna.graph.property.ValueFactory;
-
-/**
- * A context for working with the index readers and writers.
- */
- at NotThreadSafe
-final class IndexContext {
-
-    private final ExecutionContext context;
-    private final Directory pathsIndexDirectory;
-    private final Directory contentIndexDirectory;
-    private final Analyzer analyzer;
-    private final boolean overwrite;
-    private final boolean readOnly;
-    private final ValueFactory<String> stringFactory;
-    private final DateTimeFactory dateFactory;
-    private IndexReader pathsReader;
-    private IndexWriter pathsWriter;
-    private IndexSearcher pathsSearcher;
-    private IndexReader contentReader;
-    private IndexWriter contentWriter;
-    private IndexSearcher contentSearcher;
-
-    IndexContext( ExecutionContext context,
-                  Directory pathsIndexDirectory,
-                  Directory contentIndexDirectory,
-                  Analyzer analyzer,
-                  boolean overwrite,
-                  boolean readOnly ) {
-        assert context != null;
-        assert pathsIndexDirectory != null;
-        assert contentIndexDirectory != null;
-        this.context = context;
-        this.pathsIndexDirectory = pathsIndexDirectory;
-        this.contentIndexDirectory = contentIndexDirectory;
-        this.analyzer = analyzer;
-        this.overwrite = overwrite;
-        this.stringFactory = context.getValueFactories().getStringFactory();
-        this.dateFactory = context.getValueFactories().getDateFactory();
-        this.readOnly = readOnly;
-    }
-
-    /**
-     * @return context
-     */
-    public ExecutionContext context() {
-        return context;
-    }
-
-    /**
-     * @return stringFactory
-     */
-    public ValueFactory<String> stringFactory() {
-        return stringFactory;
-    }
-
-    public DateTimeFactory dateFactory() {
-        return dateFactory;
-    }
-
-    public PathFactory pathFactory() {
-        return context.getValueFactories().getPathFactory();
-    }
-
-    public IndexReader getPathsReader() throws IOException {
-        if (pathsReader == null) {
-            pathsReader = IndexReader.open(pathsIndexDirectory, readOnly);
-        }
-        return pathsReader;
-    }
-
-    public IndexReader getContentReader() throws IOException {
-        if (contentReader == null) {
-            contentReader = IndexReader.open(contentIndexDirectory, readOnly);
-        }
-        return contentReader;
-    }
-
-    public IndexWriter getPathsWriter() throws IOException {
-        if (pathsWriter == null) {
-            pathsWriter = new IndexWriter(pathsIndexDirectory, analyzer, overwrite, MaxFieldLength.UNLIMITED);
-        }
-        return pathsWriter;
-    }
-
-    public IndexWriter getContentWriter() throws IOException {
-        if (contentWriter == null) {
-            contentWriter = new IndexWriter(contentIndexDirectory, analyzer, overwrite, MaxFieldLength.UNLIMITED);
-        }
-        return contentWriter;
-    }
-
-    public IndexSearcher getPathsSearcher() throws IOException {
-        if (pathsSearcher == null) {
-            pathsSearcher = new IndexSearcher(getPathsReader());
-        }
-        return pathsSearcher;
-    }
-
-    public IndexSearcher getContentSearcher() throws IOException {
-        if (contentSearcher == null) {
-            contentSearcher = new IndexSearcher(getContentReader());
-        }
-        return contentSearcher;
-    }
-
-    public boolean hasWriters() {
-        return pathsWriter != null || contentWriter != null;
-    }
-
-    public void commit() throws IOException {
-        IOException ioError = null;
-        RuntimeException runtimeError = null;
-        if (pathsReader != null) {
-            try {
-                pathsReader.close();
-            } catch (IOException e) {
-                ioError = e;
-            } catch (RuntimeException e) {
-                runtimeError = e;
-            } finally {
-                pathsReader = null;
-            }
-        }
-        if (contentReader != null) {
-            try {
-                contentReader.close();
-            } catch (IOException e) {
-                if (ioError == null) ioError = e;
-            } catch (RuntimeException e) {
-                if (runtimeError == null) runtimeError = e;
-            } finally {
-                contentReader = null;
-            }
-        }
-        if (pathsWriter != null) {
-            try {
-                pathsWriter.commit();
-            } catch (IOException e) {
-                ioError = e;
-            } catch (RuntimeException e) {
-                runtimeError = e;
-            } finally {
-                try {
-                    pathsWriter.close();
-                } catch (IOException e) {
-                    ioError = e;
-                } catch (RuntimeException e) {
-                    runtimeError = e;
-                } finally {
-                    pathsWriter = null;
-                }
-            }
-        }
-        if (contentWriter != null) {
-            try {
-                contentWriter.commit();
-            } catch (IOException e) {
-                if (ioError == null) ioError = e;
-            } catch (RuntimeException e) {
-                if (runtimeError == null) runtimeError = e;
-            } finally {
-                try {
-                    contentWriter.close();
-                } catch (IOException e) {
-                    ioError = e;
-                } catch (RuntimeException e) {
-                    runtimeError = e;
-                } finally {
-                    contentWriter = null;
-                }
-            }
-        }
-        if (ioError != null) throw ioError;
-        if (runtimeError != null) throw runtimeError;
-    }
-
-    public void rollback() throws IOException {
-        IOException ioError = null;
-        RuntimeException runtimeError = null;
-        if (pathsReader != null) {
-            try {
-                pathsReader.close();
-            } catch (IOException e) {
-                ioError = e;
-            } catch (RuntimeException e) {
-                runtimeError = e;
-            } finally {
-                pathsReader = null;
-            }
-        }
-        if (contentReader != null) {
-            try {
-                contentReader.close();
-            } catch (IOException e) {
-                if (ioError == null) ioError = e;
-            } catch (RuntimeException e) {
-                if (runtimeError == null) runtimeError = e;
-            } finally {
-                contentReader = null;
-            }
-        }
-        if (pathsWriter != null) {
-            try {
-                pathsWriter.rollback();
-            } catch (IOException e) {
-                ioError = e;
-            } catch (RuntimeException e) {
-                runtimeError = e;
-            } finally {
-                try {
-                    pathsWriter.close();
-                } catch (IOException e) {
-                    ioError = e;
-                } catch (RuntimeException e) {
-                    runtimeError = e;
-                } finally {
-                    pathsWriter = null;
-                }
-            }
-        }
-        if (contentWriter != null) {
-            try {
-                contentWriter.rollback();
-            } catch (IOException e) {
-                if (ioError == null) ioError = e;
-            } catch (RuntimeException e) {
-                if (runtimeError == null) runtimeError = e;
-            } finally {
-                try {
-                    contentWriter.close();
-                } catch (IOException e) {
-                    ioError = e;
-                } catch (RuntimeException e) {
-                    runtimeError = e;
-                } finally {
-                    contentWriter = null;
-                }
-            }
-        }
-        if (ioError != null) throw ioError;
-        if (runtimeError != null) throw runtimeError;
-    }
-
-    /**
-     * Create a copy of this index context, except that it uses the supplied execution context.
-     * 
-     * @param context the new execution context that should be used in the copy
-     * @return the new context; never null
-     * @throws IllegalArgumentException if the context is null
-     */
-    public IndexContext with( ExecutionContext context ) {
-        CheckArg.isNotNull(context, "context");
-        return new IndexContext(context, pathsIndexDirectory, contentIndexDirectory, analyzer, overwrite, readOnly);
-    }
-
-}

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/IndexLayout.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/IndexLayout.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/IndexLayout.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,66 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search;
+
+import java.io.IOException;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.graph.ExecutionContext;
+
+/**
+ * The representation of a single layout of one or more Lucene indexes.
+ */
+ at ThreadSafe
+public interface IndexLayout {
+
+    /**
+     * Create a new session to the indexes.
+     * 
+     * @param context the execution context for which this session is to be established; may not be null
+     * @param sourceName the name of the source; may not be null
+     * @param workspaceName the name of the workspace; may not be null
+     * @param overwrite true if the existing indexes should be overwritten, or false if they should be used
+     * @param readOnly true if the resulting session can be optimized for use in read-only situations, or false if the session
+     *        needs to allow calling the write methods
+     * @return the session to the indexes; never null
+     */
+    IndexSession createSession( ExecutionContext context,
+                                String sourceName,
+                                String workspaceName,
+                                boolean overwrite,
+                                boolean readOnly );
+
+    /**
+     * Destroy the indexes for the workspace with the supplied name.
+     * 
+     * @param context the execution context in which the destruction should be performed; may not be null
+     * @param sourceName the name of the source; may not be null
+     * @param workspaceName the name of the workspace; may not be null
+     * @return true if the indexes for the workspace were destroyed, or false if there was no such workspace index
+     * @throws IOException if there is a problem destroying the indexes
+     */
+    boolean destroyIndexes( ExecutionContext context,
+                            String sourceName,
+                            String workspaceName ) throws IOException;
+
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/IndexLayout.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Modified: trunk/dna-search/src/main/java/org/jboss/dna/search/IndexRules.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/IndexRules.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/IndexRules.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -39,7 +39,7 @@
  * The set of rules that dictate how properties should be indexed.
  */
 @Immutable
-class IndexRules {
+public class IndexRules {
 
     public static final int INDEX = 2 << 0;
     public static final int ANALYZE = 2 << 1;

Copied: trunk/dna-search/src/main/java/org/jboss/dna/search/IndexSession.java (from rev 1319, trunk/dna-search/src/main/java/org/jboss/dna/search/IndexStrategy.java)
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/IndexSession.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/IndexSession.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,162 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.queryParser.ParseException;
+import org.apache.lucene.search.IndexSearcher;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.Node;
+import org.jboss.dna.graph.connector.RepositorySource;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.query.QueryContext;
+import org.jboss.dna.graph.query.QueryResults;
+import org.jboss.dna.graph.query.model.QueryCommand;
+import org.jboss.dna.graph.query.validate.Schemata;
+import org.jboss.dna.graph.request.ChangeRequest;
+
+/**
+ * A stateful session that maintains {@link IndexReader}, {@link IndexWriter} and {@link IndexSearcher} resources to the indexes
+ * of a particular source and workspace.
+ */
+public interface IndexSession {
+
+    /**
+     * Get the name of the {@link RepositorySource repository source} for which this session exists. A session instance will
+     * always return the same name.
+     * 
+     * @return the source name; never null
+     */
+    String getSourceName();
+
+    /**
+     * Get the name of the workspace for which this session exists. A session instance will always return the same name.
+     * 
+     * @return the workspace name; never null
+     */
+    String getWorkspaceName();
+
+    /**
+     * Get the execution context in which this session is operating.
+     * 
+     * @return the execution context; never null
+     */
+    ExecutionContext getContext();
+
+    /**
+     * Return whether this session made changes to the indexed state.
+     * 
+     * @return true if change were made, or false otherwise
+     */
+    boolean hasChanges();
+
+    /**
+     * Perform a full-text search given the supplied query.
+     * 
+     * @param context the context in which the search should be executed; may not be null
+     * @param fullTextString the full-text query; never null or blank
+     * @param maxResults the maximum number of results that are to be returned; always positive
+     * @param offset the number of initial results to skip, or 0 if the first results are to be returned
+     * @param results the list where the results should be accumulated; never null
+     * @throws IOException if there is a problem indexing or using the indexes
+     * @throws ParseException if there is a problem parsing the query
+     */
+    void search( ExecutionContext context,
+                 String fullTextString,
+                 int maxResults,
+                 int offset,
+                 List<Location> results ) throws IOException, ParseException;
+
+    /**
+     * Perform a query of the content. The {@link QueryCommand query} is supplied in the form of the Abstract Query Model, with
+     * the {@link Schemata} that defines the tables and views that are available to the query, and the set of index readers (and
+     * writers) that should be used.
+     * 
+     * @param queryContext the context in which the query should be executed; may not be null
+     * @param query the query; never null
+     * @return the results of the query; never null
+     * @throws IOException if there is a problem indexing or using the indexes
+     * @throws ParseException if there is a problem parsing the query
+     */
+    QueryResults query( QueryContext queryContext,
+                        QueryCommand query ) throws IOException, ParseException;
+
+    /**
+     * Index the node given the index writers. Note that implementors should simply just use the writers to add documents to the
+     * index(es), and should never call any of the writer lifecycle methods (e.g., {@link IndexWriter#commit()},
+     * {@link IndexWriter#rollback()}, etc.).
+     * 
+     * @param node the node to be indexed; never null
+     * @throws IOException if there is a problem indexing or using the writers
+     */
+    void index( Node node ) throws IOException;
+
+    /**
+     * Update the indexes to reflect the supplied changes to the graph content. Note that implementors should simply just use the
+     * writers to add documents to the index(es), and should never call any of the writer lifecycle methods (e.g.,
+     * {@link IndexWriter#commit()}, {@link IndexWriter#rollback()}, etc.).
+     * 
+     * @param changes the set of changes to the content
+     * @return the (approximate) number of nodes that were affected by the changes
+     * @throws IOException if there is a problem indexing or using the writers
+     */
+    int apply( Iterable<ChangeRequest> changes ) throws IOException;
+
+    /**
+     * Remove from the index(es) all of the information pertaining to the nodes at or below the supplied path. Note that
+     * implementors should simply just use the writers to add documents to the index(es), and should never call any of the writer
+     * lifecycle methods (e.g., {@link IndexWriter#commit()}, {@link IndexWriter#rollback()}, etc.).
+     * 
+     * @param path the path identifying the graph content that is to be removed; never null
+     * @return the (approximate) number of nodes that were affected by the changes
+     * @throws IOException if there is a problem indexing or using the writers
+     */
+    int deleteBelow( Path path ) throws IOException;
+
+    /**
+     * Optimize the indexes, if required.
+     * 
+     * @throws IOException if there is a problem optimizing
+     */
+    void optimize() throws IOException;
+
+    /**
+     * Close this session by committing all of the changes. This session is no longer usable after this method is called.
+     * 
+     * @throws IOException if there is a problem committing
+     */
+    void commit() throws IOException;
+
+    /**
+     * Close this session by rolling back all of the changes that have been made. This session is no longer usable after this
+     * method is called.
+     * 
+     * @throws IOException if there is a problem rolling back
+     */
+    void rollback() throws IOException;
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/IndexSession.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Deleted: trunk/dna-search/src/main/java/org/jboss/dna/search/IndexStrategy.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/IndexStrategy.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/IndexStrategy.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -1,133 +0,0 @@
-/*
- * JBoss DNA (http://www.jboss.org/dna)
- * See the COPYRIGHT.txt file distributed with this work for information
- * regarding copyright ownership.  Some portions may be licensed
- * to Red Hat, Inc. under one or more contributor license agreements.
- * See the AUTHORS.txt file in the distribution for a full listing of 
- * individual contributors.
- *
- * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
- * is licensed to you under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- * 
- * JBoss DNA is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.search;
-
-import java.io.IOException;
-import java.util.List;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.queryParser.ParseException;
-import org.jboss.dna.common.text.TextEncoder;
-import org.jboss.dna.graph.Location;
-import org.jboss.dna.graph.Node;
-import org.jboss.dna.graph.property.Path;
-import org.jboss.dna.graph.query.QueryResults;
-import org.jboss.dna.graph.query.model.QueryCommand;
-import org.jboss.dna.graph.query.validate.Schemata;
-import org.jboss.dna.graph.request.ChangeRequest;
-
-/**
- * Interface defining the behaviors associated with indexing graph content.
- */
-interface IndexStrategy {
-
-    /**
-     * Get the number of changes that are allowed before optimization is automatically run.
-     * 
-     * @return a positive number denoting the minimum number of changes between automatic optimization operations, or a
-     *         non-positive number if automatic optimization should never be run
-     */
-    int getChangeCountForAutomaticOptimization();
-
-    /**
-     * Get the {@link TextEncoder} that should be used to encode the namespace URIs.
-     * 
-     * @return the encoder; may not be null
-     */
-    TextEncoder getNamespaceEncoder();
-
-    /**
-     * Index the node given the index writers. Note that implementors should simply just use the writers to add documents to the
-     * index(es), and should never call any of the writer lifecycle methods (e.g., {@link IndexWriter#commit()},
-     * {@link IndexWriter#rollback()}, etc.).
-     * 
-     * @param node the node to be indexed; never null
-     * @param indexes the set of index readers and writers; never null
-     * @throws IOException if there is a problem indexing or using the writers
-     */
-    void index( Node node,
-                IndexContext indexes ) throws IOException;
-
-    /**
-     * Update the indexes to reflect the supplied changes to the graph content. Note that implementors should simply just use the
-     * writers to add documents to the index(es), and should never call any of the writer lifecycle methods (e.g.,
-     * {@link IndexWriter#commit()}, {@link IndexWriter#rollback()}, etc.).
-     * 
-     * @param changes the set of changes to the content
-     * @param indexes the set of index readers and writers; never null
-     * @return the (approximate) number of nodes that were affected by the changes
-     * @throws IOException if there is a problem indexing or using the writers
-     */
-    int apply( Iterable<ChangeRequest> changes,
-               IndexContext indexes ) throws IOException;
-
-    /**
-     * Remove from the index(es) all of the information pertaining to the nodes at or below the supplied path. Note that
-     * implementors should simply just use the writers to add documents to the index(es), and should never call any of the writer
-     * lifecycle methods (e.g., {@link IndexWriter#commit()}, {@link IndexWriter#rollback()}, etc.).
-     * 
-     * @param path the path identifying the graph content that is to be removed; never null
-     * @param indexes the set of index readers and writers; never null
-     * @return the (approximate) number of nodes that were affected by the changes
-     * @throws IOException if there is a problem indexing or using the writers
-     */
-    int deleteBelow( Path path,
-                     IndexContext indexes ) throws IOException;
-
-    /**
-     * Create the analyzer that is used for reading and updating the indexes.
-     * 
-     * @return the analyzer; may not be null
-     */
-    Analyzer createAnalyzer();
-
-    /**
-     * Perform a full-text search given the supplied query.
-     * 
-     * @param fullTextString the full-text query; never null or blank
-     * @param maxResults the maximum number of results that are to be returned; always positive
-     * @param offset the number of initial results to skip, or 0 if the first results are to be returned
-     * @param indexes the set of index readers and writers; never null
-     * @param results the list where the results should be accumulated; never null
-     * @throws IOException if there is a problem indexing or using the writers
-     * @throws ParseException if there is a problem parsing the query
-     */
-    void search( String fullTextString,
-                 int maxResults,
-                 int offset,
-                 IndexContext indexes,
-                 List<Location> results ) throws IOException, ParseException;
-
-    /**
-     * Perform a query of the content. The {@link QueryCommand query} is supplied in the form of the Abstract Query Model, with
-     * the {@link Schemata} that defines the tables and views that are available to the query, and the set of index readers (and
-     * writers) that should be used.
-     * 
-     * @param context the context in which the query should be executed; never null
-     * @param query the query; never null
-     * @return the results of the query
-     */
-    QueryResults query( SearchContext context,
-                        QueryCommand query );
-}

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexLayout.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexLayout.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexLayout.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,656 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import net.jcip.annotations.ThreadSafe;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.FieldSelector;
+import org.apache.lucene.document.FieldSelectorResult;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Collector;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.PhraseQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.store.Directory;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.graph.DnaLexicon;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.JcrLexicon;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.property.Binary;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.ValueFactory;
+import org.jboss.dna.graph.query.QueryContext;
+import org.jboss.dna.graph.query.QueryResults.Columns;
+import org.jboss.dna.graph.query.model.And;
+import org.jboss.dna.graph.query.model.BindVariableName;
+import org.jboss.dna.graph.query.model.ChildNode;
+import org.jboss.dna.graph.query.model.Comparison;
+import org.jboss.dna.graph.query.model.Constraint;
+import org.jboss.dna.graph.query.model.DescendantNode;
+import org.jboss.dna.graph.query.model.DynamicOperand;
+import org.jboss.dna.graph.query.model.FullTextSearch;
+import org.jboss.dna.graph.query.model.FullTextSearchScore;
+import org.jboss.dna.graph.query.model.Length;
+import org.jboss.dna.graph.query.model.Literal;
+import org.jboss.dna.graph.query.model.LowerCase;
+import org.jboss.dna.graph.query.model.NodeDepth;
+import org.jboss.dna.graph.query.model.NodeLocalName;
+import org.jboss.dna.graph.query.model.NodeName;
+import org.jboss.dna.graph.query.model.NodePath;
+import org.jboss.dna.graph.query.model.Not;
+import org.jboss.dna.graph.query.model.Operator;
+import org.jboss.dna.graph.query.model.Or;
+import org.jboss.dna.graph.query.model.PropertyExistence;
+import org.jboss.dna.graph.query.model.PropertyValue;
+import org.jboss.dna.graph.query.model.QueryCommand;
+import org.jboss.dna.graph.query.model.SameNode;
+import org.jboss.dna.graph.query.model.SelectorName;
+import org.jboss.dna.graph.query.model.SetCriteria;
+import org.jboss.dna.graph.query.model.StaticOperand;
+import org.jboss.dna.graph.query.model.UpperCase;
+import org.jboss.dna.graph.query.model.Visitors;
+import org.jboss.dna.graph.query.model.FullTextSearch.NegationTerm;
+import org.jboss.dna.graph.query.plan.PlanNode;
+import org.jboss.dna.graph.query.process.AbstractAccessComponent;
+import org.jboss.dna.graph.query.process.ProcessingComponent;
+import org.jboss.dna.graph.query.process.SelectComponent;
+import org.jboss.dna.graph.query.process.SelectComponent.Analyzer;
+
+/**
+ * An {@link IndexLayout} implementation that stores all content within a set of two indexes: one for the node content and a
+ * second one for paths and UUIDs.
+ */
+ at ThreadSafe
+public class KitchenSinkIndexLayout extends DualIndexLayout {
+
+    /**
+     * The default set of {@link IndexRules} used by {@link KitchenSinkIndexLayout} instances when no rules are provided.
+     */
+    public static final IndexRules DEFAULT_RULES;
+
+    static {
+        IndexRules.Builder builder = IndexRules.createBuilder();
+        // Configure the default behavior ...
+        builder.defaultTo(IndexRules.INDEX | IndexRules.ANALYZE);
+        // Configure the UUID properties to be just indexed (not stored, not analyzed, not included in full-text) ...
+        builder.index(JcrLexicon.UUID, DnaLexicon.UUID);
+        // Configure the properties that we'll treat as dates ...
+        builder.treatAsDates(JcrLexicon.CREATED, JcrLexicon.LAST_MODIFIED);
+        DEFAULT_RULES = builder.build();
+    }
+
+    private final IndexRules rules;
+    private final DirectoryConfiguration directoryConfiguration;
+
+    public KitchenSinkIndexLayout( DirectoryConfiguration directoryConfiguration ) {
+        this.rules = DEFAULT_RULES;
+        this.directoryConfiguration = directoryConfiguration;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.search.IndexLayout#createSession(org.jboss.dna.graph.ExecutionContext, java.lang.String,
+     *      java.lang.String, boolean, boolean)
+     */
+    public IndexSession createSession( ExecutionContext context,
+                                       String sourceName,
+                                       String workspaceName,
+                                       boolean overwrite,
+                                       boolean readOnly ) {
+        Directory pathIndexDirectory = directoryConfiguration.getDirectory(workspaceName, PATHS_INDEX_NAME);
+        Directory contentIndexDirectory = directoryConfiguration.getDirectory(workspaceName, CONTENT_INDEX_NAME);
+        assert pathIndexDirectory != null;
+        assert contentIndexDirectory != null;
+        return new Session(context, sourceName, workspaceName, rules, pathIndexDirectory, contentIndexDirectory, overwrite,
+                           readOnly);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.search.IndexLayout#destroyIndexes(org.jboss.dna.graph.ExecutionContext, java.lang.String,
+     *      java.lang.String)
+     */
+    public boolean destroyIndexes( ExecutionContext context,
+                                   String sourceName,
+                                   String workspaceName ) {
+        directoryConfiguration.destroyDirectory(workspaceName, PATHS_INDEX_NAME);
+        directoryConfiguration.destroyDirectory(workspaceName, CONTENT_INDEX_NAME);
+        return true;
+    }
+
+    protected class Session extends LuceneSession {
+
+        protected Session( ExecutionContext context,
+                           String sourceName,
+                           String workspaceName,
+                           IndexRules rules,
+                           Directory pathsIndexDirectory,
+                           Directory contentIndexDirectory,
+                           boolean overwrite,
+                           boolean readOnly ) {
+            super(context, sourceName, workspaceName, rules, pathsIndexDirectory, contentIndexDirectory, overwrite, readOnly);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.search.DualIndexLayout.LuceneSession#createAccessComponent(org.jboss.dna.graph.query.model.QueryCommand,
+         *      org.jboss.dna.graph.query.QueryContext, org.jboss.dna.graph.query.plan.PlanNode,
+         *      org.jboss.dna.graph.query.QueryResults.Columns, org.jboss.dna.graph.query.process.SelectComponent.Analyzer)
+         */
+        @Override
+        protected ProcessingComponent createAccessComponent( QueryCommand originalQuery,
+                                                             QueryContext context,
+                                                             PlanNode accessNode,
+                                                             Columns resultColumns,
+                                                             Analyzer analyzer ) throws IOException {
+            // Create a processing component for this access query ...
+            return new LuceneQueryComponent(this, originalQuery, context, resultColumns, accessNode, analyzer, sourceName,
+                                            workspaceName);
+        }
+
+    }
+
+    /**
+     * The {@link ProcessingComponent} implementation that executes a single atomic access query against the Lucene indexes.
+     */
+    protected static class LuceneQueryComponent extends AbstractAccessComponent {
+        private final QueryCommand originalQuery;
+        private final Session session;
+        private final IndexReader pathIndexReader;
+        private final Analyzer analyzer;
+        private final String sourceName;
+        private final String workspaceName;
+
+        protected LuceneQueryComponent( Session session,
+                                        QueryCommand originalQuery,
+                                        QueryContext context,
+                                        Columns columns,
+                                        PlanNode accessNode,
+                                        Analyzer analyzer,
+                                        String sourceName,
+                                        String workspaceName ) throws IOException {
+            super(context, columns, accessNode);
+            this.originalQuery = originalQuery;
+            this.analyzer = analyzer;
+            this.session = session;
+            this.pathIndexReader = session.getPathsReader();
+            this.sourceName = sourceName;
+            this.workspaceName = workspaceName;
+        }
+
+        protected String fieldNameFor( Name name ) {
+            return session.stringFactory.create(name);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.query.process.ProcessingComponent#execute()
+         */
+        @Override
+        public List<Object[]> execute() {
+
+            // Some kinds of constraints are not easily pushed down to Lucene as are of a Lucene Query, and
+            // instead are applied by filtering the results. For example, a FullTextSearchScore applies
+            // to the score of the tuple, which cannot be (easily?) applied as a Query.
+            //
+            // Therefore, each of the AND-ed constraints of the query are evaluated separately. After all,
+            // each of the tuples returned by the planned query must satisfy all of the AND-ed constraints.
+            // Or, to put it another way, if a tuple does not satisfy one of the AND-ed constraints, the
+            // tuple should not be included in the query results.
+            //
+            // Logically, any AND-ed criteria that cannot be pushed down to Lucene can of course be applied
+            // as a filter on the results. Thus, each AND-ed constraint is processed to first determine if
+            // it can be represented as a Lucene query; all other AND-ed constraints must be handled as
+            // a results filter. Since most queries will likely use one or more simple constraints AND-ed
+            // together, this approach will likely work very well.
+            //
+            // The only hairy case is when any AND-ed constraint is actually an OR-ed combination of multiple
+            // constraints of which at least one cannot be pushed down to Lucene. In this case, the entire
+            // AND-ed constraint must be treated as a results filter (even if many of those constraints that
+            // make up the OR-ed constraint can be pushed down). Hopefully, this will not be a common case
+            // in actual queries.
+
+            // For each of the AND-ed constraints ...
+            Query pushDownQuery = null;
+            Constraint postProcessConstraint = null;
+            try {
+                for (Constraint andedConstraint : this.andedConstraints) {
+                    // Determine if it can be represented as a Lucene query ...
+                    Query constraintQuery = createQuery(andedConstraint);
+                    if (constraintQuery != null) {
+                        // The AND-ed constraint _can_ be represented as a push-down Lucene query ...
+                        if (pushDownQuery == null) {
+                            // This must be the first query ...
+                            pushDownQuery = constraintQuery;
+                        } else if (pushDownQuery instanceof BooleanQuery) {
+                            // We have to add the constraint query to the existing boolean ...
+                            BooleanQuery booleanQuery = (BooleanQuery)pushDownQuery;
+                            booleanQuery.add(constraintQuery, Occur.MUST);
+                        } else {
+                            // This is the second push-down query, so create a BooleanQuery ...
+                            BooleanQuery booleanQuery = new BooleanQuery();
+                            booleanQuery.add(pushDownQuery, Occur.MUST);
+                            booleanQuery.add(constraintQuery, Occur.MUST);
+                            pushDownQuery = booleanQuery;
+                        }
+                    } else {
+                        // The AND-ed constraint _cannot_ be represented as a push-down Lucene query ...
+                        if (postProcessConstraint == null) {
+                            postProcessConstraint = andedConstraint;
+                        } else {
+                            postProcessConstraint = new And(postProcessConstraint, andedConstraint);
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                // There was a error working with the constraints (such as a ValueFormatException) ...
+                QueryContext context = getContext();
+                I18n msg = SearchI18n.errorWhilePerformingQuery;
+                String origQueryString = Visitors.readable(originalQuery, context.getExecutionContext());
+                context.getProblems().addError(e, msg, origQueryString, workspaceName, sourceName, e.getMessage());
+                return emptyTuples();
+            } catch (RuntimeException e) {
+                // There was a error working with the constraints (such as a ValueFormatException) ...
+                QueryContext context = getContext();
+                I18n msg = SearchI18n.errorWhilePerformingQuery;
+                String origQueryString = Visitors.readable(originalQuery, context.getExecutionContext());
+                context.getProblems().addError(e, msg, origQueryString, workspaceName, sourceName, e.getMessage());
+                return emptyTuples();
+            }
+
+            if (pushDownQuery == null) {
+                // There are no constraints that can be pushed down, so return _all_ the nodes ...
+                pushDownQuery = new MatchAllDocsQuery();
+            }
+
+            // Get the results from Lucene ...
+            List<Object[]> tuples = null;
+            final Columns columns = getColumns();
+            final QueryContext context = getContext();
+            final ExecutionContext execContext = context.getExecutionContext();
+            try {
+                // Execute the query against the content indexes ...
+                IndexSearcher searcher = session.getContentSearcher();
+                TupleCollector collector = new TupleCollector(columns, execContext.getValueFactories().getUuidFactory());
+                searcher.search(pushDownQuery, collector);
+                tuples = collector.getTuples();
+            } catch (IOException e) {
+                // There was a problem executing the Lucene query ...
+                I18n msg = SearchI18n.errorWhilePerformingLuceneQuery;
+                String origQueryString = Visitors.readable(originalQuery, execContext);
+                context.getProblems().addError(e, msg, pushDownQuery, origQueryString, workspaceName, sourceName, e.getMessage());
+                return emptyTuples();
+            }
+
+            if (postProcessConstraint != null && !tuples.isEmpty()) {
+                // Create a delegate processing component that will return the tuples we've already found ...
+                final List<Object[]> allTuples = tuples;
+                ProcessingComponent tuplesProcessor = new ProcessingComponent(context, columns) {
+                    @Override
+                    public List<Object[]> execute() {
+                        return allTuples;
+                    }
+                };
+                // Create a processing component that will apply these constraints to the tuples we already found ...
+                return new SelectComponent(tuplesProcessor, postProcessConstraint, context.getVariables()).execute();
+            }
+            return tuples;
+        }
+
+        protected Query createQuery( Constraint constraint ) throws IOException {
+            if (constraint instanceof And) {
+                And and = (And)constraint;
+                Query leftQuery = createQuery(and.getLeft());
+                Query rightQuery = createQuery(and.getRight());
+                if (leftQuery == null || rightQuery == null) return null;
+                BooleanQuery booleanQuery = new BooleanQuery();
+                booleanQuery.add(createQuery(and.getLeft()), Occur.MUST);
+                booleanQuery.add(createQuery(and.getRight()), Occur.MUST);
+                return booleanQuery;
+            }
+            if (constraint instanceof Or) {
+                Or or = (Or)constraint;
+                Query leftQuery = createQuery(or.getLeft());
+                Query rightQuery = createQuery(or.getRight());
+                if (leftQuery == null) {
+                    return rightQuery != null ? rightQuery : null;
+                } else if (rightQuery == null) {
+                    return leftQuery;
+                }
+                BooleanQuery booleanQuery = new BooleanQuery();
+                booleanQuery.add(createQuery(or.getLeft()), Occur.SHOULD);
+                booleanQuery.add(createQuery(or.getRight()), Occur.SHOULD);
+                return booleanQuery;
+            }
+            if (constraint instanceof Not) {
+                Not not = (Not)constraint;
+                Query notted = createQuery(not.getConstraint());
+                if (notted == null) return new MatchAllDocsQuery();
+            }
+            if (constraint instanceof SetCriteria) {
+                SetCriteria setCriteria = (SetCriteria)constraint;
+                DynamicOperand left = setCriteria.getLeftOperand();
+                int numRightOperands = setCriteria.getRightOperands().size();
+                assert numRightOperands > 0;
+                if (numRightOperands == 1) {
+                    return createQuery(left, Operator.EQUAL_TO, setCriteria.getRightOperands().iterator().next());
+                }
+                BooleanQuery setQuery = new BooleanQuery();
+                for (StaticOperand right : setCriteria.getRightOperands()) {
+                    Query rightQuery = createQuery(left, Operator.EQUAL_TO, right);
+                    if (rightQuery == null) return null;
+                    setQuery.add(rightQuery, Occur.SHOULD);
+                }
+                return setQuery;
+            }
+            if (constraint instanceof PropertyExistence) {
+                PropertyExistence existence = (PropertyExistence)constraint;
+                return createQuery(existence.getSelectorName(), existence.getPropertyName());
+            }
+            if (constraint instanceof Comparison) {
+                Comparison comparison = (Comparison)constraint;
+                return createQuery(comparison.getOperand1(), comparison.getOperator(), comparison.getOperand2());
+            }
+            if (constraint instanceof FullTextSearch) {
+                FullTextSearch search = (FullTextSearch)constraint;
+                String fieldName = ContentIndex.FULL_TEXT;
+                Name propertyName = search.getPropertyName();
+                if (propertyName != null) {
+                    fieldName = session.fullTextFieldName(fieldNameFor(propertyName));
+                }
+                return createQuery(fieldName, search.getTerm());
+            }
+            try {
+                if (constraint instanceof SameNode) {
+                    SameNode sameNode = (SameNode)constraint;
+                    return session.findNodeAt(sameNode.getPath());
+                }
+                if (constraint instanceof ChildNode) {
+                    ChildNode childNode = (ChildNode)constraint;
+                    return session.findChildNodes(childNode.getParentPath());
+                }
+                if (constraint instanceof DescendantNode) {
+                    DescendantNode descendantNode = (DescendantNode)constraint;
+                    return session.findAllNodesBelow(descendantNode.getAncestorPath());
+                }
+            } catch (IOException e) {
+                I18n msg = SearchI18n.errorWhilePerformingQuery;
+                getContext().getProblems().addError(e,
+                                                    msg,
+                                                    Visitors.readable(originalQuery),
+                                                    workspaceName,
+                                                    sourceName,
+                                                    e.getMessage());
+                return null;
+            }
+            // Should not get here ...
+            assert false;
+            return null;
+        }
+
+        protected Query createQuery( DynamicOperand left,
+                                     Operator operator,
+                                     StaticOperand right ) throws IOException {
+            return createQuery(left, operator, right, true);
+        }
+
+        protected Query createQuery( DynamicOperand left,
+                                     Operator operator,
+                                     StaticOperand right,
+                                     boolean caseSensitive ) throws IOException {
+            // Handle the static operand ...
+            Object value = null;
+            if (right instanceof Literal) {
+                Literal literal = (Literal)right;
+                value = literal.getValue();
+                if (!caseSensitive) value = lowerCase(value);
+            } else if (right instanceof BindVariableName) {
+                BindVariableName variable = (BindVariableName)right;
+                String variableName = variable.getVariableName();
+                value = getContext().getVariables().get(variableName);
+                if (!caseSensitive) value = lowerCase(value);
+            } else {
+                assert false;
+                return null;
+            }
+
+            // Address the dynamic operand ...
+            if (left instanceof FullTextSearchScore) {
+                // This can only be represented as a filter ...
+                return null;
+            } else if (left instanceof PropertyValue) {
+                return session.findNodesWith((PropertyValue)left, operator, value, caseSensitive);
+            } else if (left instanceof Length) {
+                Length length = (Length)left;
+                PropertyValue nested = length.getPropertyValue();
+
+                return null;
+            } else if (left instanceof LowerCase) {
+                LowerCase lowercase = (LowerCase)left;
+                return createQuery(lowercase.getOperand(), operator, right, false);
+            } else if (left instanceof UpperCase) {
+                UpperCase lowercase = (UpperCase)left;
+                return createQuery(lowercase.getOperand(), operator, right, false);
+            } else if (left instanceof NodeDepth) {
+                assert operator != Operator.LIKE;
+                // Could be represented as a result filter, but let's do this now ...
+                return session.findNodesWith((NodeDepth)left, operator, value);
+            } else if (left instanceof NodePath) {
+                return session.findNodesWith((NodePath)left, operator, value, caseSensitive);
+            } else if (left instanceof NodeName) {
+                return session.findNodesWith((NodeName)left, operator, value, caseSensitive);
+            } else if (left instanceof NodeLocalName) {
+                return session.findNodesWith((NodeLocalName)left, operator, value, caseSensitive);
+            } else {
+                assert false;
+                return null;
+            }
+        }
+
+        protected Object lowerCase( Object value ) {
+            if (value instanceof String) {
+                return ((String)value).toLowerCase();
+            }
+            assert !(value instanceof Binary);
+            ValueFactory<String> stringFactory = getContext().getExecutionContext().getValueFactories().getStringFactory();
+            ValueFactory<?> valueFactory = getContext().getExecutionContext().getValueFactories().getValueFactory(value);
+            return valueFactory.create(stringFactory.create(value).toLowerCase());
+        }
+
+        protected Query createQuery( SelectorName selectorName,
+                                     Name propertyName ) {
+            Term term = new Term(fieldNameFor(propertyName));
+            return new TermQuery(term);
+        }
+
+        protected Query createQuery( String fieldName,
+                                     FullTextSearch.Term term ) {
+            if (term instanceof FullTextSearch.Conjunction) {
+                FullTextSearch.Conjunction conjunction = (FullTextSearch.Conjunction)term;
+                BooleanQuery query = new BooleanQuery();
+                for (FullTextSearch.Term nested : conjunction) {
+                    if (nested instanceof NegationTerm) {
+                        query.add(createQuery(fieldName, ((NegationTerm)nested).getNegatedTerm()), Occur.MUST_NOT);
+                    } else {
+                        query.add(createQuery(fieldName, nested), Occur.MUST);
+                    }
+                }
+                return query;
+            }
+            if (term instanceof FullTextSearch.Disjunction) {
+                FullTextSearch.Disjunction disjunction = (FullTextSearch.Disjunction)term;
+                BooleanQuery query = new BooleanQuery();
+                for (FullTextSearch.Term nested : disjunction) {
+                    if (nested instanceof NegationTerm) {
+                        query.add(createQuery(fieldName, ((NegationTerm)nested).getNegatedTerm()), Occur.MUST_NOT);
+                    } else {
+                        query.add(createQuery(fieldName, nested), Occur.SHOULD);
+                    }
+                }
+                return query;
+            }
+            if (term instanceof FullTextSearch.SimpleTerm) {
+                FullTextSearch.SimpleTerm simple = (FullTextSearch.SimpleTerm)term;
+                if (simple.isQuotingRequired()) {
+                    PhraseQuery query = new PhraseQuery();
+                    query.setSlop(0); // terms must be adjacent
+                    for (String value : simple.getValues()) {
+                        query.add(new Term(fieldName, value));
+                    }
+                    return query;
+                }
+                return new TermQuery(new Term(fieldName, simple.getValue()));
+            }
+            // Should not get here ...
+            assert false;
+            return null;
+        }
+    }
+
+    /**
+     * This collector is responsible for loading the value for each of the columns into each tuple array.
+     */
+    protected static class TupleCollector extends Collector {
+        private final LinkedList<Object[]> tuples = new LinkedList<Object[]>();
+        private final Columns columns;
+        private final int numValues;
+        private final boolean recordScore;
+        private final int scoreIndex;
+        private final FieldSelector fieldSelector;
+        private final int locationIndex;
+        private final ValueFactory<UUID> uuidFactory;
+        private Scorer scorer;
+        private IndexReader currentReader;
+        private int docOffset;
+
+        protected TupleCollector( Columns columns,
+                                  ValueFactory<UUID> uuidFactory ) {
+            this.columns = columns;
+            this.uuidFactory = uuidFactory;
+            assert this.columns != null;
+            assert this.uuidFactory != null;
+            this.numValues = this.columns.getTupleSize();
+            assert this.numValues >= 0;
+            assert this.columns.getSelectorNames().size() == 1;
+            final String selectorName = this.columns.getSelectorNames().get(0);
+            this.locationIndex = this.columns.getLocationIndex(selectorName);
+            this.recordScore = this.columns.hasFullTextSearchScores();
+            this.scoreIndex = this.recordScore ? this.columns.getFullTextSearchScoreIndexFor(selectorName) : -1;
+            final Set<String> columnNames = new HashSet<String>(this.columns.getColumnNames());
+            columnNames.add(ContentIndex.UUID); // add the UUID, which we'll put into the Location ...
+            this.fieldSelector = new FieldSelector() {
+                private static final long serialVersionUID = 1L;
+
+                public FieldSelectorResult accept( String fieldName ) {
+                    return columnNames.contains(fieldName) ? FieldSelectorResult.LOAD : FieldSelectorResult.NO_LOAD;
+                }
+            };
+        }
+
+        /**
+         * @return tuples
+         */
+        public LinkedList<Object[]> getTuples() {
+            return tuples;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#acceptsDocsOutOfOrder()
+         */
+        @Override
+        public boolean acceptsDocsOutOfOrder() {
+            return true;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#setNextReader(org.apache.lucene.index.IndexReader, int)
+         */
+        @Override
+        public void setNextReader( IndexReader reader,
+                                   int docBase ) {
+            this.currentReader = reader;
+            this.docOffset = docBase;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#setScorer(org.apache.lucene.search.Scorer)
+         */
+        @Override
+        public void setScorer( Scorer scorer ) {
+            this.scorer = scorer;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Collector#collect(int)
+         */
+        @Override
+        public void collect( int doc ) throws IOException {
+            int docId = doc + docOffset;
+            Object[] tuple = new Object[numValues];
+            Document document = currentReader.document(docId, fieldSelector);
+            for (String columnName : columns.getColumnNames()) {
+                int index = columns.getColumnIndexForName(columnName);
+                // We just need to retrieve the first value if there is more than one ...
+                tuple[index] = document.get(columnName);
+            }
+
+            // Set the score column if required ...
+            if (recordScore) {
+                assert scorer != null;
+                tuple[scoreIndex] = scorer.score();
+            }
+
+            // Load the UUID into a Location object ...
+            UUID uuid = uuidFactory.create(document.get(ContentIndex.UUID));
+            tuple[locationIndex] = Location.create(uuid);
+            tuples.add(tuple);
+        }
+    }
+
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexLayout.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Deleted: trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexStrategy.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexStrategy.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/KitchenSinkIndexStrategy.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -1,115 +0,0 @@
-/*
- * JBoss DNA (http://www.jboss.org/dna)
- * See the COPYRIGHT.txt file distributed with this work for information
- * regarding copyright ownership.  Some portions may be licensed
- * to Red Hat, Inc. under one or more contributor license agreements.
- * See the AUTHORS.txt file in the distribution for a full listing of 
- * individual contributors.
- *
- * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
- * is licensed to you under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- * 
- * JBoss DNA is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.search;
-
-import java.util.List;
-import net.jcip.annotations.ThreadSafe;
-import org.jboss.dna.graph.DnaLexicon;
-import org.jboss.dna.graph.JcrLexicon;
-import org.jboss.dna.graph.query.QueryResults.Columns;
-import org.jboss.dna.graph.query.plan.PlanNode;
-import org.jboss.dna.graph.query.process.AbstractAccessComponent;
-import org.jboss.dna.graph.query.process.ProcessingComponent;
-import org.jboss.dna.graph.query.process.SelectComponent.Analyzer;
-import org.jboss.dna.graph.request.ChangeRequest;
-
-/**
- * An {@link IndexStrategy} implementation that stores all content within a set of two indexes: one for the node content and a
- * second one for paths and UUIDs.
- */
- at ThreadSafe
-class KitchenSinkIndexStrategy extends DualIndexStrategy {
-
-    /**
-     * The default set of {@link IndexRules} used by {@link KitchenSinkIndexStrategy} instances when no rules are provided.
-     */
-    public static final IndexRules DEFAULT_RULES;
-
-    static {
-        IndexRules.Builder builder = IndexRules.createBuilder();
-        // Configure the default behavior ...
-        builder.defaultTo(IndexRules.INDEX | IndexRules.ANALYZE);
-        // Configure the UUID properties to be just indexed (not stored, not analyzed, not included in full-text) ...
-        builder.index(JcrLexicon.UUID, DnaLexicon.UUID);
-        // Configure the properties that we'll treat as dates ...
-        builder.treatAsDates(JcrLexicon.CREATED, JcrLexicon.LAST_MODIFIED);
-        DEFAULT_RULES = builder.build();
-    }
-
-    /**
-     * Create a new indexing strategy instance uses the {@link #DEFAULT_RULES default indexing rules}.
-     */
-    public KitchenSinkIndexStrategy() {
-        this(null);
-    }
-
-    /**
-     * Create a new indexing strategy instance.
-     * 
-     * @param rules the indexing rules that govern how properties are to be index, or null if the {@link #DEFAULT_RULES default
-     *        rules} are to be used
-     */
-    public KitchenSinkIndexStrategy( IndexRules rules ) {
-        super(rules != null ? rules : DEFAULT_RULES);
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.DualIndexStrategy#createAccessComponent(org.jboss.dna.search.SearchContext,
-     *      org.jboss.dna.graph.query.plan.PlanNode, org.jboss.dna.graph.query.QueryResults.Columns,
-     *      org.jboss.dna.graph.query.process.SelectComponent.Analyzer)
-     */
-    @Override
-    protected ProcessingComponent createAccessComponent( final SearchContext context,
-                                                         PlanNode accessNode,
-                                                         Columns resultColumns,
-                                                         Analyzer analyzer ) {
-        // Create a processing component for this access query ...
-        return new AbstractAccessComponent(context, resultColumns, accessNode) {
-            /**
-             * {@inheritDoc}
-             * 
-             * @see org.jboss.dna.graph.query.process.ProcessingComponent#execute()
-             */
-            @Override
-            public List<Object[]> execute() {
-                return null;
-            }
-        };
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.search.IndexStrategy#apply(Iterable, IndexContext)
-     */
-    public int apply( Iterable<ChangeRequest> changes,
-                      IndexContext indexes ) /*throws IOException*/{
-        for (ChangeRequest change : changes) {
-            if (change != null) continue;
-        }
-        return 0;
-    }
-}

Deleted: trunk/dna-search/src/main/java/org/jboss/dna/search/SearchContext.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/SearchContext.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/SearchContext.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -1,161 +0,0 @@
-/*
- * JBoss DNA (http://www.jboss.org/dna)
- * See the COPYRIGHT.txt file distributed with this work for information
- * regarding copyright ownership.  Some portions may be licensed
- * to Red Hat, Inc. under one or more contributor license agreements.
- * See the AUTHORS.txt file in the distribution for a full listing of 
- * individual contributors.
- *
- * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
- * is licensed to you under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- * 
- * JBoss DNA is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.search;
-
-import java.util.Map;
-import org.jboss.dna.common.collection.Problems;
-import org.jboss.dna.common.util.CheckArg;
-import org.jboss.dna.graph.ExecutionContext;
-import org.jboss.dna.graph.query.QueryContext;
-import org.jboss.dna.graph.query.plan.PlanHints;
-import org.jboss.dna.graph.query.validate.Schemata;
-
-/**
- * 
- */
-class SearchContext extends QueryContext {
-
-    private final IndexContext indexes;
-
-    /**
-     * Create a new context for searching and querying.
-     * 
-     * @param indexes the indexes that should be used
-     * @param schemata the definition of the tables available to this query
-     */
-    public SearchContext( IndexContext indexes,
-                          Schemata schemata ) {
-        super(indexes.context(), schemata);
-        this.indexes = indexes;
-        assert this.indexes != null;
-    }
-
-    /**
-     * Create a new context for searching and querying.
-     * 
-     * @param queryContext
-     * @param indexes
-     */
-    public SearchContext( QueryContext queryContext,
-                          IndexContext indexes ) {
-        super(queryContext.getExecutionContext(), queryContext.getSchemata(), queryContext.getHints(),
-              queryContext.getProblems(), queryContext.getVariables());
-        this.indexes = indexes;
-        assert this.indexes != null;
-    }
-
-    /**
-     * Create a new context for searching and querying.
-     * 
-     * @param context the execution context
-     * @param schemata the schemata
-     * @param hints the hints, or null if there are no hints
-     * @param problems the problems container, or null if a new problems container should be created
-     * @param variables the mapping of variables and values, or null if there are no such variables
-     * @throws IllegalArgumentException if the context or schmata are null
-     */
-    public SearchContext( IndexContext context,
-                          Schemata schemata,
-                          PlanHints hints,
-                          Problems problems,
-                          Map<String, Object> variables ) {
-        super(context.context(), schemata, hints, problems, variables);
-        this.indexes = context;
-        assert this.indexes != null;
-    }
-
-    /**
-     * Get the {@link IndexContext} for this query context.
-     * 
-     * @return the index context; never null
-     */
-    public IndexContext getIndexes() {
-        return indexes;
-    }
-
-    /**
-     * Obtain a copy of this context, except that the copy uses the supplied index context.
-     * 
-     * @param context the index context that should be used in the new query context
-     * @return the new context; never null
-     * @throws IllegalArgumentException if the index context reference is null
-     */
-    public SearchContext with( IndexContext context ) {
-        CheckArg.isNotNull(context, "context");
-        return new SearchContext(context, getSchemata(), getHints(), getProblems(), getVariables());
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.graph.query.QueryContext#with(org.jboss.dna.graph.ExecutionContext)
-     */
-    @Override
-    public SearchContext with( ExecutionContext context ) {
-        CheckArg.isNotNull(context, "context");
-        return new SearchContext(indexes.with(context), getSchemata(), getHints(), getProblems(), getVariables());
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.graph.query.QueryContext#with(org.jboss.dna.graph.query.validate.Schemata)
-     */
-    @Override
-    public SearchContext with( Schemata schemata ) {
-        CheckArg.isNotNull(schemata, "schemata");
-        return new SearchContext(indexes, schemata, getHints(), getProblems(), getVariables());
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.graph.query.QueryContext#with(org.jboss.dna.graph.query.plan.PlanHints)
-     */
-    @Override
-    public SearchContext with( PlanHints hints ) {
-        CheckArg.isNotNull(hints, "hints");
-        return new SearchContext(indexes, getSchemata(), hints, getProblems(), getVariables());
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.graph.query.QueryContext#with(org.jboss.dna.common.collection.Problems)
-     */
-    @Override
-    public SearchContext with( Problems problems ) {
-        return new SearchContext(indexes, getSchemata(), getHints(), problems, getVariables());
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * @see org.jboss.dna.graph.query.QueryContext#with(java.util.Map)
-     */
-    @Override
-    public SearchContext with( Map<String, Object> variables ) {
-        return new SearchContext(indexes, getSchemata(), getHints(), getProblems(), variables);
-    }
-}

Modified: trunk/dna-search/src/main/java/org/jboss/dna/search/SearchEngine.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/SearchEngine.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/SearchEngine.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -23,26 +23,33 @@
  */
 package org.jboss.dna.search;
 
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import net.jcip.annotations.GuardedBy;
 import net.jcip.annotations.ThreadSafe;
-import org.apache.lucene.store.Directory;
+import org.apache.lucene.queryParser.ParseException;
+import org.jboss.dna.common.i18n.I18n;
 import org.jboss.dna.common.util.CheckArg;
 import org.jboss.dna.graph.ExecutionContext;
 import org.jboss.dna.graph.Graph;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.Subgraph;
+import org.jboss.dna.graph.SubgraphNode;
 import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
 import org.jboss.dna.graph.connector.RepositorySource;
 import org.jboss.dna.graph.connector.RepositorySourceException;
 import org.jboss.dna.graph.property.Path;
-import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.query.QueryContext;
 import org.jboss.dna.graph.query.QueryResults;
 import org.jboss.dna.graph.query.model.QueryCommand;
 import org.jboss.dna.graph.query.validate.Schemata;
@@ -57,45 +64,68 @@
 @ThreadSafe
 public class SearchEngine {
 
-    private final ExecutionContext context;
+    /**
+     * The default maximum number of changes that can be made to an index before the indexes are automatically optimized is * * *
+     * * {@value}
+     */
+    public static final int DEFAULT_MAX_CHANGES_BEFORE_AUTOMATIC_OPTIMIZATION = 0;
+
+    protected final ExecutionContext context;
     private final String sourceName;
     private final RepositoryConnectionFactory connectionFactory;
-    private final DirectoryConfiguration directoryFactory;
-    private final IndexStrategy indexStrategy;
-    private final PathFactory pathFactory;
-    @GuardedBy( "workspaceEngineLock" )
-    private final Map<String, WorkspaceSearchEngine> workspaceEnginesByName;
-    private final ReadWriteLock workspaceEngineLock = new ReentrantReadWriteLock();
+    protected final IndexLayout indexLayout;
+    private final int maxChangesBeforeAutomaticOptimization;
+    @GuardedBy( "workspacesLock" )
+    private final Map<String, Workspace> workspacesByName = new HashMap<String, Workspace>();
+    private final ReadWriteLock workspacesLock = new ReentrantReadWriteLock();
 
     /**
      * Create a search engine instance given the supplied {@link ExecutionContext execution context}, name of the
      * {@link RepositorySource}, the {@link RepositoryConnectionFactory factory for RepositorySource connections}, and the
      * {@link DirectoryConfiguration directory factory} that defines where each workspace's indexes should be placed.
      * 
-     * @param context the execution context in which all indexing operations should be performed
+     * @param context the execution context for indexing and optimization operations
      * @param sourceName the name of the {@link RepositorySource}
      * @param connectionFactory the connection factory
-     * @param directoryFactory the factory for Lucene {@link Directory directories}
+     * @param indexLayout the specification of the Lucene index layout
+     * @param maxChangesBeforeAutomaticOptimization the number of changes that can be made to the index before the indexes are
+     *        automatically optimized; may be 0 or a negative number if no automatic optimization should be done
      * @throws IllegalArgumentException if any of the parameters (other than indexing strategy) are null
      */
     public SearchEngine( ExecutionContext context,
                          String sourceName,
                          RepositoryConnectionFactory connectionFactory,
-                         DirectoryConfiguration directoryFactory ) {
+                         IndexLayout indexLayout,
+                         int maxChangesBeforeAutomaticOptimization ) {
         CheckArg.isNotNull(context, "context");
         CheckArg.isNotNull(sourceName, "sourceName");
         CheckArg.isNotNull(connectionFactory, "connectionFactory");
-        CheckArg.isNotNull(directoryFactory, "directoryFactory");
         this.sourceName = sourceName;
         this.connectionFactory = connectionFactory;
-        this.directoryFactory = directoryFactory;
+        this.indexLayout = indexLayout;
         this.context = context;
-        this.pathFactory = context.getValueFactories().getPathFactory();
-        this.workspaceEnginesByName = new HashMap<String, WorkspaceSearchEngine>();
-        this.indexStrategy = new KitchenSinkIndexStrategy();
+        this.maxChangesBeforeAutomaticOptimization = maxChangesBeforeAutomaticOptimization < 0 ? 0 : maxChangesBeforeAutomaticOptimization;
     }
 
     /**
+     * Create a search engine instance given the supplied {@link ExecutionContext execution context}, name of the
+     * {@link RepositorySource}, the {@link RepositoryConnectionFactory factory for RepositorySource connections}, and the
+     * {@link DirectoryConfiguration directory factory} that defines where each workspace's indexes should be placed.
+     * 
+     * @param context the execution context for indexing and optimization operations
+     * @param sourceName the name of the {@link RepositorySource}
+     * @param connectionFactory the connection factory
+     * @param indexLayout the specification of the Lucene index layout
+     * @throws IllegalArgumentException if any of the parameters (other than indexing strategy) are null
+     */
+    public SearchEngine( ExecutionContext context,
+                         String sourceName,
+                         RepositoryConnectionFactory connectionFactory,
+                         IndexLayout indexLayout ) {
+        this(context, sourceName, connectionFactory, indexLayout, DEFAULT_MAX_CHANGES_BEFORE_AUTOMATIC_OPTIMIZATION);
+    }
+
+    /**
      * Get the name of the RepositorySource that this engine is to use.
      * 
      * @return the source name; never null
@@ -114,6 +144,13 @@
     }
 
     /**
+     * @return maxChangesBeforeAutomaticOptimization
+     */
+    public int getMaxChangesBeforeAutomaticOptimization() {
+        return maxChangesBeforeAutomaticOptimization;
+    }
+
+    /**
      * Utility to create a Graph for the source.
      * 
      * @return the graph instance; never null
@@ -123,46 +160,58 @@
     }
 
     /**
-     * Get the search engine for the workspace with the supplied name.
+     * Utility to obtain the root path.
      * 
+     * @return the root path; never null
+     */
+    final Path rootPath() {
+        return context.getValueFactories().getPathFactory().createRootPath();
+    }
+
+    final String readable( Path path ) {
+        return context.getValueFactories().getStringFactory().create(path);
+    }
+
+    /**
+     * Index all of the content at or below the supplied path in the named workspace within the {@link #getSourceName() source}.
+     * If the starting point is the root node, then this method will drop the existing index(es) and rebuild from the content in
+     * the workspace and source.
+     * <p>
+     * This method operates synchronously and returns when the requested indexing is completed.
+     * </p>
+     * 
      * @param workspaceName the name of the workspace
-     * @return the workspace's search engine
+     * @param startingPoint the location that represents the content to be indexed; must have a path
+     * @param depthPerRead the depth of each subgraph read operation
+     * @throws IllegalArgumentException if the workspace name or location are null
+     * @throws RepositorySourceException if there is a problem accessing the content
+     * @throws SearchEngineException if there is a problem updating the indexes
      * @throws InvalidWorkspaceException if the workspace does not exist
      */
-    protected WorkspaceSearchEngine getWorkspaceEngine( String workspaceName ) {
-        WorkspaceSearchEngine engine = null;
-        try {
-            workspaceEngineLock.readLock().lock();
-            engine = workspaceEnginesByName.get(workspaceName);
-        } finally {
-            workspaceEngineLock.readLock().unlock();
-        }
+    public void index( String workspaceName,
+                       Location startingPoint,
+                       int depthPerRead ) throws RepositorySourceException, SearchEngineException {
+        CheckArg.isNotNull(workspaceName, "workspaceName");
+        CheckArg.isNotNull(startingPoint, "startingPoint");
+        assert startingPoint.hasPath();
 
-        if (engine == null) {
-            // Verify the workspace does exist ...
-            if (!graph().getWorkspaces().contains(workspaceName)) {
-                String msg = GraphI18n.workspaceDoesNotExistInRepository.text(workspaceName, getSourceName());
-                throw new InvalidWorkspaceException(msg);
-            }
-            try {
-                workspaceEngineLock.writeLock().lock();
-                // Check whether another thread got in and created the engine while we waited ...
-                engine = workspaceEnginesByName.get(workspaceName);
-                if (engine == null) {
-                    // Create the engine and register it ...
-                    engine = new WorkspaceSearchEngine(context, directoryFactory, indexStrategy, sourceName, workspaceName,
-                                                       connectionFactory);
-                    workspaceEnginesByName.put(workspaceName, engine);
-                }
-            } finally {
-                workspaceEngineLock.writeLock().unlock();
-            }
+        Workspace workspace = getWorkspace(workspaceName);
+        if (startingPoint.getPath().isRoot()) {
+            // More efficient to just start over with a new index ...
+            workspace.execute(true, addContent(startingPoint, depthPerRead));
+        } else {
+            // Have to first remove the content below the starting point, then add it again ...
+            workspace.execute(false, removeContent(startingPoint), addContent(startingPoint, depthPerRead));
         }
-        return engine;
     }
 
     /**
      * Index all of the content at or below the supplied path in the named workspace within the {@link #getSourceName() source}.
+     * If the starting point is the root node, then this method will drop the existing index(es) and rebuild from the content in
+     * the workspace and source.
+     * <p>
+     * This method operates synchronously and returns when the requested indexing is completed.
+     * </p>
      * 
      * @param workspaceName the name of the workspace
      * @param startingPoint the path that represents the content to be indexed
@@ -172,16 +221,17 @@
      * @throws SearchEngineException if there is a problem updating the indexes
      * @throws InvalidWorkspaceException if the workspace does not exist
      */
-    public void indexContent( String workspaceName,
-                              Path startingPoint,
-                              int depthPerRead ) throws RepositorySourceException, SearchEngineException {
+    public void index( String workspaceName,
+                       Path startingPoint,
+                       int depthPerRead ) throws RepositorySourceException, SearchEngineException {
         CheckArg.isNotNull(workspaceName, "workspaceName");
         CheckArg.isNotNull(startingPoint, "startingPoint");
-        getWorkspaceEngine(workspaceName).indexContent(startingPoint, depthPerRead);
+        index(workspaceName, Location.create(startingPoint), depthPerRead);
     }
 
     /**
-     * Index all of the content in the named workspace within the {@link #getSourceName() source}.
+     * Index all of the content in the named workspace within the {@link #getSourceName() source}. This method operates
+     * synchronously and returns when the requested indexing is completed.
      * 
      * @param workspaceName the name of the workspace
      * @param depthPerRead the depth of each subgraph read operation
@@ -190,23 +240,24 @@
      * @throws SearchEngineException if there is a problem updating the indexes
      * @throws InvalidWorkspaceException if the workspace does not exist
      */
-    public void indexContent( String workspaceName,
-                              int depthPerRead ) throws RepositorySourceException, SearchEngineException {
+    public void index( String workspaceName,
+                       int depthPerRead ) throws RepositorySourceException, SearchEngineException {
         CheckArg.isNotNull(workspaceName, "workspaceName");
-        indexContent(workspaceName, pathFactory.createRootPath(), depthPerRead);
+        index(workspaceName, rootPath(), depthPerRead);
     }
 
     /**
-     * Index (or re-index) all of the content in all of the workspaces within the source.
+     * Index (or re-index) all of the content in all of the workspaces within the source. This method operates synchronously and
+     * returns when the requested indexing is completed.
      * 
      * @param depthPerRead the depth of each subgraph read operation
      * @throws RepositorySourceException if there is a problem accessing the content
      * @throws SearchEngineException if there is a problem updating the indexes
      */
-    public void indexContent( int depthPerRead ) throws RepositorySourceException, SearchEngineException {
-        Path rootPath = pathFactory.createRootPath();
+    public void index( int depthPerRead ) throws RepositorySourceException, SearchEngineException {
+        Path rootPath = rootPath();
         for (String workspaceName : graph().getWorkspaces()) {
-            getWorkspaceEngine(workspaceName).indexContent(rootPath, depthPerRead);
+            index(workspaceName, rootPath, depthPerRead);
         }
     }
 
@@ -218,7 +269,7 @@
      * @throws RepositorySourceException if there is a problem accessing the content
      * @throws SearchEngineException if there is a problem updating the indexes
      */
-    public void indexChanges( final Iterable<ChangeRequest> changes ) throws SearchEngineException {
+    public void index( final Iterable<ChangeRequest> changes ) throws SearchEngineException {
         // First break up all the changes into different collections, one collection per workspace ...
         Map<String, Collection<ChangeRequest>> changesByWorkspace = new HashMap<String, Collection<ChangeRequest>>();
         for (ChangeRequest request : changes) {
@@ -235,7 +286,7 @@
         for (Map.Entry<String, Collection<ChangeRequest>> entry : changesByWorkspace.entrySet()) {
             String workspaceName = entry.getKey();
             Collection<ChangeRequest> changesForWorkspace = entry.getValue();
-            getWorkspaceEngine(workspaceName).indexChanges(changesForWorkspace);
+            getWorkspace(workspaceName).execute(false, updateContent(changesForWorkspace));
         }
     }
 
@@ -247,7 +298,7 @@
      */
     public void optimize() throws SearchEngineException {
         for (String workspaceName : graph().getWorkspaces()) {
-            getWorkspaceEngine(workspaceName).optimize();
+            getWorkspace(workspaceName).execute(false, optimizeContent());
         }
     }
 
@@ -262,46 +313,498 @@
      */
     public void optimize( String workspaceName ) throws SearchEngineException {
         CheckArg.isNotNull(workspaceName, "workspaceName");
-        getWorkspaceEngine(workspaceName).optimize();
+        getWorkspace(workspaceName).execute(false, optimizeContent());
     }
 
     /**
      * Perform a full-text search of the content in the named workspace, given the maximum number of results and the offset
      * defining the first result the caller is interested in.
      * 
+     * @param context the execution context in which the search is to take place; may not be null
      * @param workspaceName the name of the workspace
      * @param fullTextSearch the full-text search to be performed; may not be null
      * @param maxResults the maximum number of results that are to be returned; always positive
      * @param offset the number of initial results to skip, or 0 if the first results are to be returned
      * @return the activity that will perform the work
-     * @throws IllegalArgumentException if the workspace name is null
+     * @throws IllegalArgumentException if the execution context or workspace name are null
      * @throws SearchEngineException if there is a problem during optimization
      * @throws InvalidWorkspaceException if the workspace does not exist
      */
-    public List<Location> fullTextSearch( String workspaceName,
+    public List<Location> fullTextSearch( ExecutionContext context,
+                                          String workspaceName,
                                           String fullTextSearch,
                                           int maxResults,
                                           int offset ) {
+        CheckArg.isNotNull(context, "context");
         CheckArg.isNotNull(workspaceName, "workspaceName");
-        return getWorkspaceEngine(workspaceName).fullTextSearch(fullTextSearch, maxResults, offset);
+        Search searchActivity = searchContent(context, fullTextSearch, maxResults, offset);
+        getWorkspace(workspaceName).execute(false, searchActivity);
+        return searchActivity.getResults();
     }
 
     /**
      * Perform a query of the content in the named workspace, given the Abstract Query Model representation of the query.
      * 
+     * @param context the execution context in which the search is to take place; may not be null
      * @param workspaceName the name of the workspace
      * @param query the query that is to be executed, in the form of the Abstract Query Model
      * @param schemata the definition of the tables and views that can be used in the query; may not be null
      * @return the query results; never null
      * @throws IllegalArgumentException if the context, query, or schemata references are null
      */
-    public QueryResults execute( String workspaceName,
-                                 QueryCommand query,
-                                 Schemata schemata ) {
+    public QueryResults query( ExecutionContext context,
+                               String workspaceName,
+                               QueryCommand query,
+                               Schemata schemata ) {
+        CheckArg.isNotNull(context, "context");
         CheckArg.isNotNull(workspaceName, "workspaceName");
         CheckArg.isNotNull(query, "query");
         CheckArg.isNotNull(schemata, "schemata");
-        return getWorkspaceEngine(workspaceName).execute(query, schemata);
+        QueryContext queryContext = new QueryContext(context, schemata);
+        Query queryActivity = queryContent(queryContext, query);
+        getWorkspace(workspaceName).execute(false, queryActivity);
+        return queryActivity.getResults();
     }
 
+    /**
+     * Remove the supplied index from the search engine. This is typically done when the workspace has been deleted from the
+     * source, or when
+     * 
+     * @param workspaceName the name of the workspace
+     * @throws IllegalArgumentException if the workspace name is null
+     * @throws SearchEngineException if there is a problem removing the workspace
+     */
+    public void removeWorkspace( String workspaceName ) throws SearchEngineException {
+        CheckArg.isNotNull(workspaceName, "workspaceName");
+        try {
+            workspacesLock.writeLock().lock();
+            // Check whether another thread got in and created the engine while we waited ...
+            Workspace workspace = workspacesByName.remove(workspaceName);
+            if (workspace != null) {
+                indexLayout.destroyIndexes(context, getSourceName(), workspaceName);
+            }
+        } catch (IOException e) {
+            String message = SearchI18n.errorWhileRemovingIndexesForWorkspace.text(sourceName, workspaceName, e.getMessage());
+            throw new SearchEngineException(message, e);
+        } finally {
+            workspacesLock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Remove from the search engine all workspace-related indexes, thereby cleaning up any resources used by this search engine.
+     * 
+     * @throws SearchEngineException if there is a problem removing any of the workspace
+     */
+    public void removeWorkspaces() throws SearchEngineException {
+        try {
+            workspacesLock.writeLock().lock();
+            for (String workspaceName : new HashSet<String>(workspacesByName.keySet())) {
+                removeWorkspace(workspaceName);
+            }
+        } finally {
+            workspacesLock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Get the search engine for the workspace with the supplied name.
+     * 
+     * @param workspaceName the name of the workspace
+     * @return the workspace's search engine
+     * @throws InvalidWorkspaceException if the workspace does not exist
+     */
+    protected Workspace getWorkspace( String workspaceName ) {
+        Workspace workspace = null;
+        try {
+            workspacesLock.readLock().lock();
+            workspace = workspacesByName.get(workspaceName);
+        } finally {
+            workspacesLock.readLock().unlock();
+        }
+
+        if (workspace == null) {
+            // Verify the workspace does exist ...
+            if (!graph().getWorkspaces().contains(workspaceName)) {
+                String msg = GraphI18n.workspaceDoesNotExistInRepository.text(workspaceName, getSourceName());
+                throw new InvalidWorkspaceException(msg);
+            }
+            try {
+                workspacesLock.writeLock().lock();
+                // Check whether another thread got in and created the engine while we waited ...
+                workspace = workspacesByName.get(workspaceName);
+                if (workspace == null) {
+                    // Create the engine and register it ...
+                    workspace = new Workspace(workspaceName);
+                    workspacesByName.put(workspaceName, workspace);
+                }
+            } finally {
+                workspacesLock.writeLock().unlock();
+            }
+        }
+        return workspace;
+    }
+
+    protected class Workspace {
+        private final String sourceName;
+        private final String workspaceName;
+        protected final AtomicInteger modifiedNodesSinceLastOptimize = new AtomicInteger(0);
+
+        protected Workspace( String workspaceName ) {
+            this.workspaceName = workspaceName;
+            this.sourceName = getSourceName();
+        }
+
+        /**
+         * Get the workspace name.
+         * 
+         * @return the workspace name; never null
+         */
+        public String getWorkspaceName() {
+            return workspaceName;
+        }
+
+        /**
+         * Execute the supplied activities against the indexes.
+         * 
+         * @param overwrite true if the existing indexes should be overwritten, or false if they should be used
+         * @param activities the activities to execute
+         * @throws SearchEngineException if there is a problem performing the activities
+         */
+        protected final void execute( boolean overwrite,
+                                      Activity... activities ) throws SearchEngineException {
+            // Determine if the activities are readonly ...
+            boolean readOnly = true;
+            for (Activity activity : activities) {
+                if (!(activity instanceof ReadOnlyActivity)) {
+                    readOnly = false;
+                    break;
+                }
+            }
+
+            // Create a session ...
+            IndexSession session = indexLayout.createSession(context, sourceName, workspaceName, overwrite, readOnly);
+            assert session != null;
+
+            // Execute the various activities ...
+            Throwable error = null;
+            try {
+                int numChanges = 0;
+                for (Activity activity : activities) {
+                    try {
+                        numChanges += activity.execute(session);
+                    } catch (IOException e) {
+                        error = e;
+                        throw new SearchEngineException(activity.messageFor(e, sourceName, workspaceName), e);
+                    } catch (ParseException e) {
+                        error = e;
+                        throw new SearchEngineException(activity.messageFor(e, sourceName, workspaceName), e);
+                    } catch (RuntimeException e) {
+                        error = e;
+                        throw e;
+                    }
+                }
+                if (numChanges > 0) {
+                    numChanges = this.modifiedNodesSinceLastOptimize.addAndGet(numChanges);
+                    // Determine if there have been enough changes made to run the optimizer ...
+                    int maxChanges = getMaxChangesBeforeAutomaticOptimization();
+                    if (maxChanges > 0 && numChanges >= maxChanges) {
+                        Activity optimizer = optimizeContent();
+                        try {
+                            optimizer.execute(session);
+                        } catch (ParseException e) {
+                            error = e;
+                            throw new SearchEngineException(optimizer.messageFor(e, sourceName, workspaceName), e);
+                        } catch (IOException e) {
+                            error = e;
+                            throw new SearchEngineException(optimizer.messageFor(e, sourceName, workspaceName), e);
+                        } catch (RuntimeException e) {
+                            error = e;
+                            throw e;
+                        }
+                    }
+                }
+            } finally {
+                try {
+                    if (error == null) {
+                        session.commit();
+                    } else {
+                        session.rollback();
+                    }
+                } catch (IOException e2) {
+                    // We don't want to lose the existing error, if there is one ...
+                    if (error == null) {
+                        I18n msg = SearchI18n.errorWhileCommittingIndexChanges;
+                        throw new SearchEngineException(msg.text(workspaceName, sourceName, e2.getMessage()), e2);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Create an activity that will optimize the indexes.
+     * 
+     * @return the activity that will perform the work
+     */
+    protected Activity optimizeContent() {
+        return new Activity() {
+            /**
+             * {@inheritDoc}
+             * 
+             * @see org.jboss.dna.search.SearchEngine.Activity#execute(org.jboss.dna.search.IndexSession)
+             */
+            public int execute( IndexSession indexSession ) throws IOException {
+                indexSession.optimize();
+                return 0; // no lines changed
+            }
+
+            public String messageFor( Throwable error,
+                                      String sourceName,
+                                      String workspaceName ) {
+                return SearchI18n.errorWhileOptimizingIndexes.text(sourceName, workspaceName, error.getMessage());
+            }
+        };
+    }
+
+    /**
+     * Create an activity that will read from the source the content at the supplied location and add the content to the search
+     * index.
+     * 
+     * @param location the location of the content to read; may not be null
+     * @param depthPerRead the depth of each read operation; always positive
+     * @return the activity that will perform the work
+     */
+    protected Activity addContent( final Location location,
+                                   final int depthPerRead ) {
+        return new Activity() {
+            public int execute( IndexSession indexSession ) throws IOException {
+
+                // Create a queue that we'll use to walk the content ...
+                LinkedList<Location> locationsToRead = new LinkedList<Location>();
+                locationsToRead.add(location);
+                int count = 0;
+
+                // Now read and index the content ...
+                Graph graph = graph();
+                graph.useWorkspace(indexSession.getWorkspaceName());
+                while (!locationsToRead.isEmpty()) {
+                    Location location = locationsToRead.poll();
+                    if (location == null) continue;
+                    Subgraph subgraph = graph.getSubgraphOfDepth(depthPerRead).at(location);
+                    // Index all of the nodes within this subgraph ...
+                    for (SubgraphNode node : subgraph) {
+                        // Index the node ...
+                        indexSession.index(node);
+                        ++count;
+
+                        // Process the children ...
+                        for (Location child : node.getChildren()) {
+                            if (!subgraph.includes(child)) {
+                                // Record this location as needing to be read ...
+                                locationsToRead.add(child);
+                            }
+                        }
+                    }
+                }
+                return count;
+            }
+
+            public String messageFor( Throwable error,
+                                      String sourceName,
+                                      String workspaceName ) {
+                String path = readable(location.getPath());
+                return SearchI18n.errorWhileIndexingContentAtPath.text(path, workspaceName, sourceName, error.getMessage());
+            }
+        };
+    }
+
+    /**
+     * Create an activity that will remove from the indexes all documents that represent content at or below the specified
+     * location.
+     * 
+     * @param location the location of the content to removed; may not be null
+     * @return the activity that will perform the work
+     */
+    protected Activity removeContent( final Location location ) {
+        return new Activity() {
+
+            public int execute( IndexSession indexSession ) throws IOException {
+                // Delete the content at/below the path ...
+                return indexSession.deleteBelow(location.getPath());
+            }
+
+            public String messageFor( Throwable error,
+                                      String sourceName,
+                                      String workspaceName ) {
+                String path = readable(location.getPath());
+                return SearchI18n.errorWhileRemovingContentAtPath.text(path, workspaceName, sourceName, error.getMessage());
+            }
+        };
+    }
+
+    /**
+     * Create an activity that will update the indexes with changes that were already made to the content.
+     * 
+     * @param changes the changes that have been made to the content; may not be null
+     * @return the activity that will perform the work
+     */
+    protected Activity updateContent( final Iterable<ChangeRequest> changes ) {
+        return new Activity() {
+
+            public int execute( IndexSession indexSession ) throws IOException {
+                return indexSession.apply(changes);
+            }
+
+            public String messageFor( Throwable error,
+                                      String sourceName,
+                                      String workspaceName ) {
+                return SearchI18n.errorWhileUpdatingContent.text(workspaceName, sourceName, error.getMessage());
+            }
+        };
+    }
+
+    /**
+     * Create an activity that will perform a full-text search given the supplied query.
+     * 
+     * @param context the context in which the search is to be performed; may not be null
+     * @param fullTextSearch the full-text search to be performed; may not be null
+     * @param maxResults the maximum number of results that are to be returned; always positive
+     * @param offset the number of initial results to skip, or 0 if the first results are to be returned
+     * @return the activity that will perform the work; never null
+     */
+    protected Search searchContent( final ExecutionContext context,
+                                    final String fullTextSearch,
+                                    final int maxResults,
+                                    final int offset ) {
+        final List<Location> results = new ArrayList<Location>(maxResults);
+        return new Search() {
+            /**
+             * {@inheritDoc}
+             * 
+             * @see org.jboss.dna.search.SearchEngine.Activity#execute(org.jboss.dna.search.IndexSession)
+             */
+            public int execute( IndexSession session ) throws IOException, ParseException {
+                session.search(context, fullTextSearch, maxResults, offset, results);
+                return 0;
+            }
+
+            /**
+             * {@inheritDoc}
+             * 
+             * @see org.jboss.dna.search.SearchEngine.Activity#messageFor(java.lang.Throwable, java.lang.String,
+             *      java.lang.String)
+             */
+            public String messageFor( Throwable error,
+                                      String sourceName,
+                                      String workspaceName ) {
+                return SearchI18n.errorWhilePerformingSearch.text(fullTextSearch, workspaceName, sourceName, error.getMessage());
+            }
+
+            public List<Location> getResults() {
+                return results;
+            }
+        };
+    }
+
+    /**
+     * Create an activity that will perform a query against the index.
+     * 
+     * @param context the context in which the search is to be performed; may not be null
+     * @param query the query to be performed; may not be null
+     * @return the activity that will perform the query; never null
+     */
+    protected Query queryContent( final QueryContext context,
+                                  final QueryCommand query ) {
+        return new Query() {
+            private QueryResults results = null;
+
+            /**
+             * {@inheritDoc}
+             * 
+             * @see org.jboss.dna.search.SearchEngine.Activity#execute(org.jboss.dna.search.IndexSession)
+             */
+            public int execute( IndexSession session ) throws IOException, ParseException {
+                results = session.query(context, query);
+                return 0;
+            }
+
+            /**
+             * {@inheritDoc}
+             * 
+             * @see org.jboss.dna.search.SearchEngine.Activity#messageFor(java.lang.Throwable, java.lang.String,
+             *      java.lang.String)
+             */
+            public String messageFor( Throwable error,
+                                      String sourceName,
+                                      String workspaceName ) {
+                return SearchI18n.errorWhilePerformingQuery.text(query, workspaceName, sourceName, error.getMessage());
+            }
+
+            public QueryResults getResults() {
+                return results;
+            }
+        };
+    }
+
+    /**
+     * Interface for activities that will be executed against a workspace. These activities don't have to commit or roll back the
+     * writer, nor do they have to translate the exceptions, since this is done by the
+     * {@link Workspace#execute(boolean, Activity...)} method.
+     */
+    protected interface Activity {
+
+        /**
+         * Perform the activity by using the index writer.
+         * 
+         * @param indexSession the index session that should be used by the activity; never null
+         * @return the number of changes that were made by this activity
+         * @throws IOException if there is an error using the writer
+         * @throws ParseException if there is an error due to parsing
+         */
+        int execute( IndexSession indexSession ) throws IOException, ParseException;
+
+        /**
+         * Translate an exception obtained during {@link #execute(IndexSession) execution} into a single message.
+         * 
+         * @param t the exception
+         * @param sourceName the name of the source
+         * @param workspaceName the name of the workspace
+         * @return the error message
+         */
+        String messageFor( Throwable t,
+                           String sourceName,
+                           String workspaceName );
+    }
+
+    /**
+     * A read-only activity.
+     */
+    protected interface ReadOnlyActivity extends Activity {
+    }
+
+    /**
+     * A search activity.
+     */
+    protected interface Search extends ReadOnlyActivity {
+        /**
+         * Get the results of the search.
+         * 
+         * @return the list of {@link Location} objects for each node satisfying the results; never null
+         */
+        List<Location> getResults();
+    }
+
+    /**
+     * A query activity.
+     */
+    protected interface Query extends ReadOnlyActivity {
+        /**
+         * Get the results of the query.
+         * 
+         * @return the results of a query; never null
+         */
+        QueryResults getResults();
+    }
+
 }

Modified: trunk/dna-search/src/main/java/org/jboss/dna/search/SearchI18n.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/SearchI18n.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/SearchI18n.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -40,11 +40,14 @@
     public static I18n errorWhileRemovingContentAtPath;
     public static I18n errorWhileUpdatingContent;
     public static I18n errorWhileCommittingIndexChanges;
+    public static I18n errorWhileRollingBackIndexChanges;
     public static I18n errorCreatingIndexWriter;
     public static I18n errorWhileOptimizingIndexes;
     public static I18n errorWhilePerformingSearch;
     public static I18n errorWhilePerformingQuery;
     public static I18n errorWhileInitializingSearchEngine;
+    public static I18n errorWhileRemovingIndexesForWorkspace;
+    public static I18n errorWhilePerformingLuceneQuery;
 
     static {
         try {

Deleted: trunk/dna-search/src/main/java/org/jboss/dna/search/WorkspaceSearchEngine.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/WorkspaceSearchEngine.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/WorkspaceSearchEngine.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -1,569 +0,0 @@
-/*
- * JBoss DNA (http://www.jboss.org/dna)
- * See the COPYRIGHT.txt file distributed with this work for information
- * regarding copyright ownership.  Some portions may be licensed
- * to Red Hat, Inc. under one or more contributor license agreements.
- * See the AUTHORS.txt file in the distribution for a full listing of 
- * individual contributors.
- *
- * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
- * is licensed to you under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- * 
- * JBoss DNA is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.search;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-import net.jcip.annotations.ThreadSafe;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriter.MaxFieldLength;
-import org.apache.lucene.queryParser.ParseException;
-import org.apache.lucene.store.Directory;
-import org.jboss.dna.common.i18n.I18n;
-import org.jboss.dna.common.util.CheckArg;
-import org.jboss.dna.graph.ExecutionContext;
-import org.jboss.dna.graph.Graph;
-import org.jboss.dna.graph.Location;
-import org.jboss.dna.graph.Subgraph;
-import org.jboss.dna.graph.SubgraphNode;
-import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
-import org.jboss.dna.graph.connector.RepositorySource;
-import org.jboss.dna.graph.connector.RepositorySourceException;
-import org.jboss.dna.graph.property.Path;
-import org.jboss.dna.graph.query.QueryResults;
-import org.jboss.dna.graph.query.model.QueryCommand;
-import org.jboss.dna.graph.query.validate.Schemata;
-import org.jboss.dna.graph.request.ChangeRequest;
-
-/**
- * A search engine dedicated to a single workspace.
- */
- at ThreadSafe
-class WorkspaceSearchEngine {
-
-    protected static final String PATHS_INDEX_NAME = "paths";
-    protected static final String CONTENT_INDEX_NAME = "content";
-
-    private final Directory pathsDirectory;
-    private final Directory contentDirectory;
-    private final ExecutionContext context;
-    private final ExecutionContext encodedContext;
-    private final String sourceName;
-    private final String workspaceName;
-    private final RepositoryConnectionFactory connectionFactory;
-    private final IndexStrategy indexingStrategy;
-    protected final AtomicInteger modifiedNodesSinceLastOptimize = new AtomicInteger(0);
-
-    /**
-     * Create a search engine instance given the supplied {@link ExecutionContext execution context}, name of the
-     * {@link RepositorySource}, the {@link RepositoryConnectionFactory factory for RepositorySource connections}, and the
-     * {@link DirectoryConfiguration directory factory} that defines where each workspace's indexes should be placed.
-     * 
-     * @param context the execution context in which all indexing operations should be performed
-     * @param directoryFactory the factory from which can be obtained the Lucene directory where the indexes should be persisted
-     * @param indexingStrategy the indexing strategy that governs how properties are to be indexed; may not be null
-     * @param sourceName the name of the {@link RepositorySource}
-     * @param workspaceName the name of the workspace
-     * @param connectionFactory the connection factory
-     * @throws IllegalArgumentException if any of the parameters are null
-     * @throws SearchEngineException if there is a problem initializing this engine
-     */
-    protected WorkspaceSearchEngine( ExecutionContext context,
-                                     DirectoryConfiguration directoryFactory,
-                                     IndexStrategy indexingStrategy,
-                                     String sourceName,
-                                     String workspaceName,
-                                     RepositoryConnectionFactory connectionFactory ) throws SearchEngineException {
-        CheckArg.isNotNull(context, "context");
-        CheckArg.isNotNull(directoryFactory, "directoryFactory");
-        CheckArg.isNotNull(sourceName, "sourceName");
-        CheckArg.isNotNull(workspaceName, "workspaceName");
-        CheckArg.isNotNull(connectionFactory, "connectionFactory");
-        CheckArg.isNotNull(indexingStrategy, "indexingStrategy");
-        this.sourceName = sourceName;
-        this.workspaceName = workspaceName;
-        this.connectionFactory = connectionFactory;
-        this.context = context;
-        this.indexingStrategy = indexingStrategy;
-        this.encodedContext = context.with(new EncodingNamespaceRegistry(context.getNamespaceRegistry(),
-                                                                         this.indexingStrategy.getNamespaceEncoder()));
-        this.pathsDirectory = directoryFactory.getDirectory(workspaceName, PATHS_INDEX_NAME);
-        this.contentDirectory = directoryFactory.getDirectory(workspaceName, CONTENT_INDEX_NAME);
-
-        initialize();
-    }
-
-    protected void initialize() throws SearchEngineException {
-        // Always create the index if not there ...
-        try {
-            Analyzer analyzer = this.indexingStrategy.createAnalyzer();
-            ensureIndexesExist(this.pathsDirectory, analyzer);
-            ensureIndexesExist(this.contentDirectory, analyzer);
-        } catch (IOException e) {
-            String msg = SearchI18n.errorWhileInitializingSearchEngine.text(workspaceName, sourceName, e.getMessage());
-            throw new SearchEngineException(msg, e);
-        }
-    }
-
-    private static void ensureIndexesExist( Directory directory,
-                                            Analyzer analyzer ) throws IOException {
-        IndexWriter writer = null;
-        Throwable error = null;
-        try {
-            writer = new IndexWriter(directory, analyzer, false, MaxFieldLength.UNLIMITED);
-        } catch (FileNotFoundException e) {
-            // The index files don't yet exist, so we need to create them ...
-            try {
-                writer = new IndexWriter(directory, analyzer, true, MaxFieldLength.UNLIMITED);
-            } catch (Throwable t) {
-                error = t;
-            }
-        } catch (Throwable t) {
-            error = t;
-        } finally {
-            if (writer != null) {
-                // Either way, make sure we close the writer that we created ...
-                try {
-                    writer.close();
-                } catch (IOException e) {
-                    if (error == null) throw e;
-                } catch (RuntimeException e) {
-                    if (error == null) throw e;
-                }
-            }
-        }
-    }
-
-    final Graph graph() {
-        Graph graph = Graph.create(sourceName, connectionFactory, context);
-        graph.useWorkspace(workspaceName);
-        return graph;
-    }
-
-    final String workspaceName() {
-        return workspaceName;
-    }
-
-    final String sourceName() {
-        return sourceName;
-    }
-
-    final String readable( Path path ) {
-        return context.getValueFactories().getStringFactory().create(path);
-    }
-
-    final IndexStrategy strategy() {
-        return indexingStrategy;
-    }
-
-    /**
-     * Index all of the content at or below the supplied path.
-     * 
-     * @param startingPoint the path that represents the content to be indexed
-     * @param depthPerBatch the depth of each subgraph read operation
-     * @throws IllegalArgumentException if the path is null or the depth is not positive
-     * @throws RepositorySourceException if there is a problem accessing the content
-     * @throws SearchEngineException if there is a problem updating the indexes
-     */
-    public void indexContent( Path startingPoint,
-                              int depthPerBatch ) throws RepositorySourceException, SearchEngineException {
-        CheckArg.isNotNull(startingPoint, "startingPoint");
-        indexContent(Location.create(startingPoint), depthPerBatch);
-    }
-
-    /**
-     * Index all of the content at or below the supplied location.
-     * 
-     * @param startingPoint the location that represents the content to be indexed
-     * @param depthPerRead the depth of each subgraph read operation
-     * @throws IllegalArgumentException if the location is null or the depth is not positive
-     * @throws RepositorySourceException if there is a problem accessing the content
-     * @throws SearchEngineException if there is a problem updating the indexes
-     */
-    public void indexContent( Location startingPoint,
-                              int depthPerRead ) throws RepositorySourceException, SearchEngineException {
-        CheckArg.isNotNull(startingPoint, "startingPoint");
-        CheckArg.isPositive(depthPerRead, "depthPerBatch");
-        assert startingPoint.hasPath();
-
-        if (startingPoint.getPath().isRoot()) {
-            // More efficient to just start over with a new index ...
-            execute(true, addContent(startingPoint, depthPerRead));
-        } else {
-            // Have to first remove the content below the starting point, then add it again ...
-            execute(false, removeContent(startingPoint), addContent(startingPoint, depthPerRead));
-        }
-    }
-
-    /**
-     * Update the indexes with the supplied set of changes to the content.
-     * 
-     * @param changes the set of changes to the content
-     * @throws IllegalArgumentException if the path is null
-     * @throws RepositorySourceException if there is a problem accessing the content
-     * @throws SearchEngineException if there is a problem updating the indexes
-     */
-    public void indexChanges( final Iterable<ChangeRequest> changes ) throws SearchEngineException {
-        CheckArg.isNotNull(changes, "changes");
-        execute(false, updateContent(changes));
-    }
-
-    /**
-     * Invoke the engine's garbage collection on all indexes used by this workspace. This method reclaims space and optimizes the
-     * index. This should be done on a periodic basis after changes are made to the engine's indexes.
-     * 
-     * @throws SearchEngineException if there is a problem during optimization
-     */
-    public void optimize() throws SearchEngineException {
-        execute(false, optimizeContent());
-    }
-
-    /**
-     * Create an activity that will perform a full-text search given the supplied query.
-     * 
-     * @param fullTextSearch the full-text search to be performed; may not be null
-     * @param maxResults the maximum number of results that are to be returned; always positive
-     * @param offset the number of initial results to skip, or 0 if the first results are to be returned
-     * @return the activity that will perform the work
-     */
-    public List<Location> fullTextSearch( final String fullTextSearch,
-                                          final int maxResults,
-                                          final int offset ) {
-        return execute(false, searchContent(fullTextSearch, maxResults, offset)).getResults();
-    }
-
-    /**
-     * Create an activity that will perform a query of the content in this workspace, given the Abstract Query Model
-     * representation of the query.
-     * 
-     * @param query the query that is to be executed, in the form of the Abstract Query Model
-     * @param schemata the definition of the tables available for the query; may not be null
-     * @return the query results; never null
-     * @throws IllegalArgumentException if the context or query references are null
-     */
-    public QueryResults execute( QueryCommand query,
-                                 Schemata schemata ) {
-        return execute(false, queryContent(query, schemata)).getResults();
-    }
-
-    /**
-     * Execute the supplied activities against the indexes.
-     * 
-     * @param <ActivityType> the type of activity
-     * @param overwrite true if the existing indexes should be overwritten, or false if they should be used
-     * @param activity the activity to execute
-     * @return the same activity that was supplied as a parameter, returned as a convenience
-     * @throws SearchEngineException if there is a problem performing the activities
-     */
-    protected final <ActivityType extends Activity> ActivityType execute( boolean overwrite,
-                                                                          ActivityType activity ) throws SearchEngineException {
-        execute(overwrite, new Activity[] {activity});
-        return activity;
-    }
-
-    /**
-     * Execute the supplied activities against the indexes.
-     * 
-     * @param overwrite true if the existing indexes should be overwritten, or false if they should be used
-     * @param activities the activities to execute
-     * @throws SearchEngineException if there is a problem performing the activities
-     */
-    protected final void execute( boolean overwrite,
-                                  Activity... activities ) throws SearchEngineException {
-        // Determine if the activities are readonly ...
-        boolean readOnly = true;
-        for (Activity activity : activities) {
-            if (!(activity instanceof ReadOnlyActivity)) {
-                readOnly = false;
-                break;
-            }
-        }
-
-        Analyzer analyzer = this.indexingStrategy.createAnalyzer();
-        IndexContext indexes = new IndexContext(encodedContext, pathsDirectory, contentDirectory, analyzer, overwrite, readOnly);
-
-        // Execute the various activities ...
-        Throwable error = null;
-        try {
-            for (Activity activity : activities) {
-                try {
-                    activity.execute(indexes);
-                } catch (IOException e) {
-                    error = e;
-                    throw new SearchEngineException(activity.messageFor(e), e);
-                } catch (ParseException e) {
-                    error = e;
-                    throw new SearchEngineException(activity.messageFor(e), e);
-                } catch (RuntimeException e) {
-                    error = e;
-                    throw e;
-                }
-            }
-            if (indexes.hasWriters()) {
-                // Determine if there have been enough changes made to run the optimizer ...
-                int maxChanges = this.indexingStrategy.getChangeCountForAutomaticOptimization();
-                if (maxChanges > 0 && this.modifiedNodesSinceLastOptimize.get() >= maxChanges) {
-                    Activity optimizer = optimizeContent();
-                    try {
-                        optimizer.execute(indexes);
-                    } catch (ParseException e) {
-                        error = e;
-                        throw new SearchEngineException(optimizer.messageFor(e), e);
-                    } catch (IOException e) {
-                        error = e;
-                        throw new SearchEngineException(optimizer.messageFor(e), e);
-                    } catch (RuntimeException e) {
-                        error = e;
-                        throw e;
-                    }
-                }
-            }
-        } finally {
-            try {
-                if (error == null) {
-                    indexes.commit();
-                } else {
-                    indexes.rollback();
-                }
-            } catch (IOException e2) {
-                // We don't want to lose the existing error, if there is one ...
-                if (error == null) {
-                    I18n msg = SearchI18n.errorWhileCommittingIndexChanges;
-                    throw new SearchEngineException(msg.text(workspaceName(), sourceName(), e2.getMessage()), e2);
-                }
-            }
-        }
-    }
-
-    /**
-     * Interface for activities that will be executed against the set of indexes. These activities don't have to commit or roll
-     * back the writer, nor do they have to translate the exceptions, since this is done by the
-     * {@link WorkspaceSearchEngine#execute(boolean, Activity...)} method.
-     * 
-     * @see ReadOnlyActivity
-     */
-    protected interface Activity {
-
-        /**
-         * Perform the activity by using the index writer.
-         * 
-         * @param indexes the set of indexes to use; never null
-         * @throws IOException if there is an error using the writer
-         * @throws ParseException if there is an error due to parsing
-         */
-        void execute( IndexContext indexes ) throws IOException, ParseException;
-
-        /**
-         * Translate an exception obtained during {@link #execute(IndexContext) execution} into a single message.
-         * 
-         * @param t the exception
-         * @return the error message
-         */
-        String messageFor( Throwable t );
-    }
-
-    protected interface ReadOnlyActivity extends Activity {
-    }
-
-    protected interface Search extends ReadOnlyActivity {
-        /**
-         * Get the results of the search.
-         * 
-         * @return the list of {@link Location} objects for each node satisfying the results; never null
-         */
-        List<Location> getResults();
-    }
-
-    protected interface Query extends ReadOnlyActivity {
-        /**
-         * Get the results of the query.
-         * 
-         * @return the results of a query; never null
-         */
-        QueryResults getResults();
-    }
-
-    /**
-     * Create an activity that will read from the source the content at the supplied location and add the content to the search
-     * index.
-     * 
-     * @param location the location of the content to read; may not be null
-     * @param depthPerRead the depth of each read operation; always positive
-     * @return the activity that will perform the work
-     */
-    protected Activity addContent( final Location location,
-                                   final int depthPerRead ) {
-        return new Activity() {
-            public void execute( IndexContext indexes ) throws IOException {
-
-                // Create a queue that we'll use to walk the content ...
-                LinkedList<Location> locationsToRead = new LinkedList<Location>();
-                locationsToRead.add(location);
-                int count = 0;
-
-                // Now read and index the content ...
-                Graph graph = graph();
-                while (!locationsToRead.isEmpty()) {
-                    Location location = locationsToRead.poll();
-                    if (location == null) continue;
-                    Subgraph subgraph = graph.getSubgraphOfDepth(depthPerRead).at(location);
-                    // Index all of the nodes within this subgraph ...
-                    for (SubgraphNode node : subgraph) {
-                        // Index the node ...
-                        strategy().index(node, indexes);
-                        ++count;
-
-                        // Process the children ...
-                        for (Location child : node.getChildren()) {
-                            if (!subgraph.includes(child)) {
-                                // Record this location as needing to be read ...
-                                locationsToRead.add(child);
-                            }
-                        }
-                    }
-                }
-                modifiedNodesSinceLastOptimize.addAndGet(count);
-            }
-
-            public String messageFor( Throwable error ) {
-                String path = readable(location.getPath());
-                return SearchI18n.errorWhileIndexingContentAtPath.text(path, workspaceName(), sourceName(), error.getMessage());
-            }
-        };
-    }
-
-    /**
-     * Create an activity that will remove from the indexes all documents that represent content at or below the specified
-     * location.
-     * 
-     * @param location the location of the content to removed; may not be null
-     * @return the activity that will perform the work
-     */
-    protected Activity removeContent( final Location location ) {
-        return new Activity() {
-
-            public void execute( IndexContext indexes ) throws IOException {
-                // Delete the content at/below the path ...
-                modifiedNodesSinceLastOptimize.addAndGet(strategy().deleteBelow(location.getPath(), indexes));
-            }
-
-            public String messageFor( Throwable error ) {
-                String path = readable(location.getPath());
-                return SearchI18n.errorWhileRemovingContentAtPath.text(path, workspaceName(), sourceName(), error.getMessage());
-            }
-        };
-    }
-
-    /**
-     * Create an activity that will optimize the indexes.
-     * 
-     * @return the activity that will perform the work
-     */
-    protected Activity optimizeContent() {
-        return new Activity() {
-            public void execute( IndexContext indexes ) throws IOException {
-                // Don't block ...
-                indexes.getContentWriter().optimize();
-                indexes.getPathsWriter().optimize();
-            }
-
-            public String messageFor( Throwable error ) {
-                return SearchI18n.errorWhileOptimizingIndexes.text(workspaceName(), sourceName(), error.getMessage());
-            }
-        };
-    }
-
-    /**
-     * Create an activity that will update the indexes with changes that were already made to the content.
-     * 
-     * @param changes the changes that have been made to the content; may not be null
-     * @return the activity that will perform the work
-     */
-    protected Activity updateContent( final Iterable<ChangeRequest> changes ) {
-        return new Activity() {
-
-            public void execute( IndexContext indexes ) throws IOException {
-                // Iterate over the changes ...
-                modifiedNodesSinceLastOptimize.addAndGet(strategy().apply(changes, indexes));
-            }
-
-            public String messageFor( Throwable error ) {
-                return SearchI18n.errorWhileUpdatingContent.text(workspaceName(), sourceName(), error.getMessage());
-            }
-        };
-    }
-
-    /**
-     * Create an activity that will perform a full-text search given the supplied query.
-     * 
-     * @param fullTextSearch the full-text search to be performed; may not be null
-     * @param maxResults the maximum number of results that are to be returned; always positive
-     * @param offset the number of initial results to skip, or 0 if the first results are to be returned
-     * @return the activity that will perform the work
-     */
-    protected Search searchContent( final String fullTextSearch,
-                                    final int maxResults,
-                                    final int offset ) {
-        final List<Location> results = new ArrayList<Location>(maxResults);
-        return new Search() {
-            public void execute( IndexContext indexes ) throws IOException, ParseException {
-                strategy().search(fullTextSearch, maxResults, offset, indexes, results);
-            }
-
-            public String messageFor( Throwable error ) {
-                return SearchI18n.errorWhilePerformingSearch.text(fullTextSearch,
-                                                                  workspaceName(),
-                                                                  sourceName(),
-                                                                  error.getMessage());
-            }
-
-            public List<Location> getResults() {
-                return results;
-            }
-        };
-    }
-
-    /**
-     * Create an activity that will perform a query against the index.
-     * 
-     * @param query the query to be performed; may not be null
-     * @param schemata the definition of the tables being used in the query; may not be null
-     * @return the activity that will perform the work
-     */
-    protected Query queryContent( final QueryCommand query,
-                                  final Schemata schemata ) {
-        return new Query() {
-            private QueryResults results = null;
-            private SearchContext context = null;
-
-            public void execute( IndexContext indexes ) {
-                context = new SearchContext(indexes, schemata);
-                results = strategy().query(context, query);
-            }
-
-            public String messageFor( Throwable error ) {
-                return SearchI18n.errorWhilePerformingQuery.text(query, workspaceName(), sourceName(), error.getMessage());
-            }
-
-            public QueryResults getResults() {
-                return results;
-            }
-        };
-    }
-}

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/filters/ResultFilter.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/filters/ResultFilter.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/filters/ResultFilter.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,36 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.filters;
+
+import org.jboss.dna.graph.query.QueryResults.Columns;
+
+/**
+ * 
+ */
+public interface ResultFilter {
+
+    boolean allows( Object[] tuple,
+                    Columns columns );
+
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/filters/ResultFilter.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareNameQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareNameQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareNameQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,258 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import java.io.IOException;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.FieldSelector;
+import org.apache.lucene.document.FieldSelectorResult;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Weight;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.property.ValueComparators;
+import org.jboss.dna.graph.property.ValueFactories;
+import org.jboss.dna.graph.property.ValueFactory;
+import org.jboss.dna.graph.query.model.Comparison;
+
+/**
+ * A Lucene {@link Query} implementation that is used to apply a {@link Comparison} constraint against the name of nodes. This
+ * query implementation works by using the {@link Query#weight(Searcher) weight} and
+ * {@link Weight#scorer(IndexReader, boolean, boolean) scorer} of the wrapped query to score (and return) only those documents
+ * that correspond to nodes with Names that satisfy the constraint.
+ */
+public class CompareNameQuery extends CompareQuery<Path.Segment> {
+
+    private static final long serialVersionUID = 1L;
+    protected static final Evaluator<Path.Segment> IS_LESS_THAN = new Evaluator<Path.Segment>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path.Segment nodeValue,
+                                            Path.Segment constraintValue ) {
+            return ValueComparators.PATH_SEGMENT_COMPARATOR.compare(nodeValue, constraintValue) < 0;
+        }
+
+        @Override
+        public String toString() {
+            return " < ";
+        }
+    };
+    protected static final Evaluator<Path.Segment> IS_LESS_THAN_OR_EQUAL_TO = new Evaluator<Path.Segment>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path.Segment nodeValue,
+                                            Path.Segment constraintValue ) {
+            return ValueComparators.PATH_SEGMENT_COMPARATOR.compare(nodeValue, constraintValue) <= 0;
+        }
+
+        @Override
+        public String toString() {
+            return " <= ";
+        }
+    };
+    protected static final Evaluator<Path.Segment> IS_GREATER_THAN = new Evaluator<Path.Segment>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path.Segment nodeValue,
+                                            Path.Segment constraintValue ) {
+            return ValueComparators.PATH_SEGMENT_COMPARATOR.compare(nodeValue, constraintValue) > 0;
+        }
+
+        @Override
+        public String toString() {
+            return " > ";
+        }
+    };
+    protected static final Evaluator<Path.Segment> IS_GREATER_THAN_OR_EQUAL_TO = new Evaluator<Path.Segment>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path.Segment nodeValue,
+                                            Path.Segment constraintValue ) {
+            return ValueComparators.PATH_SEGMENT_COMPARATOR.compare(nodeValue, constraintValue) >= 0;
+        }
+
+        @Override
+        public String toString() {
+            return " >= ";
+        }
+    };
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a name
+     * that is greater than the supplied constraint name.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param localNameField the name of the document field containing the local name value; may not be null
+     * @param snsIndexFieldName the name of the document field containing the same-name-sibling index; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareNameQuery createQueryForNodesWithNameGreaterThan( Path.Segment constraintValue,
+                                                                           String localNameField,
+                                                                           String snsIndexFieldName,
+                                                                           ValueFactories factories,
+                                                                           boolean caseSensitive ) {
+        return new CompareNameQuery(localNameField, snsIndexFieldName, constraintValue, factories.getPathFactory(),
+                                    factories.getStringFactory(), factories.getLongFactory(), IS_GREATER_THAN, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a name
+     * that is greater than or equal to the supplied constraint name.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param localNameField the name of the document field containing the local name value; may not be null
+     * @param snsIndexFieldName the name of the document field containing the same-name-sibling index; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareNameQuery createQueryForNodesWithNameGreaterThanOrEqualTo( Path.Segment constraintValue,
+                                                                                    String localNameField,
+                                                                                    String snsIndexFieldName,
+                                                                                    ValueFactories factories,
+                                                                                    boolean caseSensitive ) {
+        return new CompareNameQuery(localNameField, snsIndexFieldName, constraintValue, factories.getPathFactory(),
+                                    factories.getStringFactory(), factories.getLongFactory(), IS_GREATER_THAN_OR_EQUAL_TO,
+                                    caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a name
+     * that is less than the supplied constraint name.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param localNameField the name of the document field containing the local name value; may not be null
+     * @param snsIndexFieldName the name of the document field containing the same-name-sibling index; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareNameQuery createQueryForNodesWithNameLessThan( Path.Segment constraintValue,
+                                                                        String localNameField,
+                                                                        String snsIndexFieldName,
+                                                                        ValueFactories factories,
+                                                                        boolean caseSensitive ) {
+        return new CompareNameQuery(localNameField, snsIndexFieldName, constraintValue, factories.getPathFactory(),
+                                    factories.getStringFactory(), factories.getLongFactory(), IS_LESS_THAN, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a name
+     * that is less than or equal to the supplied constraint name.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param localNameField the name of the document field containing the local name value; may not be null
+     * @param snsIndexFieldName the name of the document field containing the same-name-sibling index; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareNameQuery createQueryForNodesWithNameLessThanOrEqualTo( Path.Segment constraintValue,
+                                                                                 String localNameField,
+                                                                                 String snsIndexFieldName,
+                                                                                 ValueFactories factories,
+                                                                                 boolean caseSensitive ) {
+        return new CompareNameQuery(localNameField, snsIndexFieldName, constraintValue, factories.getPathFactory(),
+                                    factories.getStringFactory(), factories.getLongFactory(), IS_LESS_THAN_OR_EQUAL_TO,
+                                    caseSensitive);
+    }
+
+    private final String snsIndexFieldName;
+    private final ValueFactory<Long> longFactory;
+    private final PathFactory pathFactory;
+    private final boolean caseSensitive;
+
+    /**
+     * Construct a {@link Query} implementation that scores nodes according to the supplied comparator.
+     * 
+     * @param localNameField the name of the document field containing the local name value; may not be null
+     * @param snsIndexFieldName the name of the document field containing the same-name-sibling index; may not be null
+     * @param constraintValue the constraint path; may not be null
+     * @param pathFactory the path factory that can be used during the scoring; may not be null
+     * @param stringFactory the string factory that can be used during the scoring; may not be null
+     * @param longFactory the long factory that can be used during the scoring; may not be null
+     * @param evaluator the {@link CompareQuery.Evaluator} implementation that returns whether the node path satisfies the
+     *        constraint; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     */
+    protected CompareNameQuery( final String localNameField,
+                                final String snsIndexFieldName,
+                                Path.Segment constraintValue,
+                                PathFactory pathFactory,
+                                ValueFactory<String> stringFactory,
+                                ValueFactory<Long> longFactory,
+                                Evaluator<Path.Segment> evaluator,
+                                boolean caseSensitive ) {
+        super(localNameField, constraintValue, null, stringFactory, evaluator, new FieldSelector() {
+            private static final long serialVersionUID = 1L;
+
+            public FieldSelectorResult accept( String fieldName ) {
+                if (fieldName.equals(localNameField)) return FieldSelectorResult.LOAD;
+                if (fieldName.equals(snsIndexFieldName)) return FieldSelectorResult.LOAD;
+                return FieldSelectorResult.NO_LOAD;
+            }
+        });
+        this.snsIndexFieldName = snsIndexFieldName;
+        this.longFactory = longFactory;
+        this.pathFactory = pathFactory;
+        this.caseSensitive = caseSensitive;
+        assert this.snsIndexFieldName != null;
+        assert this.longFactory != null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.search.query.CompareQuery#readFromDocument(org.apache.lucene.index.IndexReader, int)
+     */
+    @Override
+    protected Path.Segment readFromDocument( IndexReader reader,
+                                             int docId ) throws IOException {
+        Document doc = reader.document(docId, fieldSelector);
+        String localName = doc.get(fieldName);
+        if (!caseSensitive) localName = localName.toLowerCase();
+        int sns = longFactory.create(doc.get(snsIndexFieldName)).intValue();
+        return pathFactory.createSegment(localName, sns);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#clone()
+     */
+    @Override
+    public Object clone() {
+        return new CompareNameQuery(fieldName, snsIndexFieldName, constraintValue, pathFactory, stringFactory, longFactory,
+                                    evaluator, caseSensitive);
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareNameQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/ComparePathQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/ComparePathQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/ComparePathQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,223 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import java.io.IOException;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Weight;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.ValueComparators;
+import org.jboss.dna.graph.property.ValueFactories;
+import org.jboss.dna.graph.property.ValueFactory;
+import org.jboss.dna.graph.query.model.Comparison;
+
+/**
+ * A Lucene {@link Query} implementation that is used to apply a {@link Comparison} constraint against the Path of nodes. This
+ * query implementation works by using the {@link Query#weight(Searcher) weight} and
+ * {@link Weight#scorer(IndexReader, boolean, boolean) scorer} of the wrapped query to score (and return) only those documents
+ * that correspond to nodes with Paths that satisfy the constraint.
+ */
+public class ComparePathQuery extends CompareQuery<Path> {
+
+    private static final long serialVersionUID = 1L;
+    protected static final Evaluator<Path> PATH_IS_LESS_THAN = new Evaluator<Path>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path nodePath,
+                                            Path constraintPath ) {
+            return ValueComparators.PATH_COMPARATOR.compare(nodePath, constraintPath) < 0;
+        }
+
+        @Override
+        public String toString() {
+            return " < ";
+        }
+    };
+    protected static final Evaluator<Path> PATH_IS_LESS_THAN_OR_EQUAL_TO = new Evaluator<Path>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path nodePath,
+                                            Path constraintPath ) {
+            return ValueComparators.PATH_COMPARATOR.compare(nodePath, constraintPath) <= 0;
+        }
+
+        @Override
+        public String toString() {
+            return " <= ";
+        }
+    };
+    protected static final Evaluator<Path> PATH_IS_GREATER_THAN = new Evaluator<Path>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path nodePath,
+                                            Path constraintPath ) {
+            return ValueComparators.PATH_COMPARATOR.compare(nodePath, constraintPath) > 0;
+        }
+
+        @Override
+        public String toString() {
+            return " > ";
+        }
+    };
+    protected static final Evaluator<Path> PATH_IS_GREATER_THAN_OR_EQUAL_TO = new Evaluator<Path>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( Path nodePath,
+                                            Path constraintPath ) {
+            return ValueComparators.PATH_COMPARATOR.compare(nodePath, constraintPath) >= 0;
+        }
+
+        @Override
+        public String toString() {
+            return " >= ";
+        }
+    };
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a path
+     * that is greater than the supplied constraint path.
+     * 
+     * @param constraintPath the constraint path; may not be null
+     * @param fieldName the name of the document field containing the path value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the path query; never null
+     */
+    public static ComparePathQuery createQueryForNodesWithPathGreaterThan( Path constraintPath,
+                                                                           String fieldName,
+                                                                           ValueFactories factories,
+                                                                           boolean caseSensitive ) {
+        return new ComparePathQuery(fieldName, constraintPath, factories.getPathFactory(), factories.getStringFactory(),
+                                    PATH_IS_GREATER_THAN, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a path
+     * that is greater than or equal to the supplied constraint path.
+     * 
+     * @param constraintPath the constraint path; may not be null
+     * @param fieldName the name of the document field containing the path value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the path query; never null
+     */
+    public static ComparePathQuery createQueryForNodesWithPathGreaterThanOrEqualTo( Path constraintPath,
+                                                                                    String fieldName,
+                                                                                    ValueFactories factories,
+                                                                                    boolean caseSensitive ) {
+        return new ComparePathQuery(fieldName, constraintPath, factories.getPathFactory(), factories.getStringFactory(),
+                                    PATH_IS_GREATER_THAN_OR_EQUAL_TO, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a path
+     * that is less than the supplied constraint path.
+     * 
+     * @param constraintPath the constraint path; may not be null
+     * @param fieldName the name of the document field containing the path value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the path query; never null
+     */
+    public static ComparePathQuery createQueryForNodesWithPathLessThan( Path constraintPath,
+                                                                        String fieldName,
+                                                                        ValueFactories factories,
+                                                                        boolean caseSensitive ) {
+        return new ComparePathQuery(fieldName, constraintPath, factories.getPathFactory(), factories.getStringFactory(),
+                                    PATH_IS_LESS_THAN, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents such that the node represented by the document has a path
+     * that is less than or equal to the supplied constraint path.
+     * 
+     * @param constraintPath the constraint path; may not be null
+     * @param fieldName the name of the document field containing the path value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the path query; never null
+     */
+    public static ComparePathQuery createQueryForNodesWithPathLessThanOrEqualTo( Path constraintPath,
+                                                                                 String fieldName,
+                                                                                 ValueFactories factories,
+                                                                                 boolean caseSensitive ) {
+        return new ComparePathQuery(fieldName, constraintPath, factories.getPathFactory(), factories.getStringFactory(),
+                                    PATH_IS_LESS_THAN_OR_EQUAL_TO, caseSensitive);
+    }
+
+    private final boolean caseSensitive;
+
+    /**
+     * Construct a {@link Query} implementation that scores nodes according to the supplied comparator.
+     * 
+     * @param fieldName the name of the document field containing the path value; may not be null
+     * @param constraintPath the constraint path; may not be null
+     * @param pathFactory the value factory that can be used during the scoring; may not be null
+     * @param stringFactory the string factory that can be used during the scoring; may not be null
+     * @param evaluator the {@link CompareQuery.Evaluator} implementation that returns whether the node path satisfies the
+     *        constraint; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     */
+    protected ComparePathQuery( String fieldName,
+                                Path constraintPath,
+                                ValueFactory<Path> pathFactory,
+                                ValueFactory<String> stringFactory,
+                                Evaluator<Path> evaluator,
+                                boolean caseSensitive ) {
+        super(fieldName, constraintPath, pathFactory, stringFactory, evaluator);
+        this.caseSensitive = caseSensitive;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.search.query.CompareQuery#readFromDocument(org.apache.lucene.index.IndexReader, int)
+     */
+    @Override
+    protected Path readFromDocument( IndexReader reader,
+                                     int docId ) throws IOException {
+        Document doc = reader.document(docId, fieldSelector);
+        String valueString = doc.get(fieldName);
+        if (!caseSensitive) valueString = valueString.toLowerCase();
+        return valueTypeFactory.create(valueString);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#clone()
+     */
+    @Override
+    public Object clone() {
+        return new ComparePathQuery(fieldName, constraintValue, valueTypeFactory, stringFactory, evaluator, caseSensitive);
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/ComparePathQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,308 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import java.io.IOException;
+import java.io.Serializable;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.FieldSelector;
+import org.apache.lucene.document.FieldSelectorResult;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Similarity;
+import org.apache.lucene.search.Weight;
+import org.jboss.dna.graph.property.ValueFactory;
+import org.jboss.dna.graph.query.model.Comparison;
+
+/**
+ * A Lucene {@link Query} implementation that is used to apply a {@link Comparison} constraint against the Path of nodes. This
+ * query implementation works by using the {@link Query#weight(Searcher) weight} and
+ * {@link Weight#scorer(IndexReader, boolean, boolean) scorer} of the wrapped query to score (and return) only those documents
+ * that correspond to nodes with Paths that satisfy the constraint.
+ * 
+ * @param <ValueType>
+ */
+public abstract class CompareQuery<ValueType> extends Query {
+
+    private static final long serialVersionUID = 1L;
+
+    protected static interface Evaluator<ValueType> extends Serializable {
+        boolean satisfiesConstraint( ValueType nodeValue,
+                                     ValueType constraintValue );
+    }
+
+    /**
+     * The operand that is being negated by this query.
+     */
+    protected final String fieldName;
+    protected final FieldSelector fieldSelector;
+    protected final ValueType constraintValue;
+    protected final Evaluator<ValueType> evaluator;
+    protected final ValueFactory<ValueType> valueTypeFactory;
+    protected final ValueFactory<String> stringFactory;
+
+    /**
+     * Construct a {@link Query} implementation that scores nodes according to the supplied comparator.
+     * 
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param constraintValue the constraint value; may not be null
+     * @param valueTypeFactory the value factory that can be used during the scoring; may not be null
+     * @param stringFactory the string factory that can be used during the scoring; may not be null
+     * @param evaluator the {@link Evaluator} implementation that returns whether the node value satisfies the constraint; may not
+     *        be null
+     */
+    protected CompareQuery( String fieldName,
+                            ValueType constraintValue,
+                            ValueFactory<ValueType> valueTypeFactory,
+                            ValueFactory<String> stringFactory,
+                            Evaluator<ValueType> evaluator ) {
+        this(fieldName, constraintValue, valueTypeFactory, stringFactory, evaluator, null);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores nodes according to the supplied comparator.
+     * 
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param constraintValue the constraint value; may not be null
+     * @param valueTypeFactory the value factory that can be used during the scoring; may not be null unless
+     *        {@link #readFromDocument(IndexReader, int)} is overloaded to not use it
+     * @param stringFactory the string factory that can be used during the scoring; may not be null
+     * @param evaluator the {@link Evaluator} implementation that returns whether the node value satisfies the constraint; may not
+     *        be null
+     * @param fieldSelector the field selector that should load the fields needed to recover the value; may be null if the field
+     *        selector should be generated automatically
+     */
+    protected CompareQuery( String fieldName,
+                            ValueType constraintValue,
+                            ValueFactory<ValueType> valueTypeFactory,
+                            ValueFactory<String> stringFactory,
+                            Evaluator<ValueType> evaluator,
+                            FieldSelector fieldSelector ) {
+        this.fieldName = fieldName;
+        this.constraintValue = constraintValue;
+        this.valueTypeFactory = valueTypeFactory;
+        this.stringFactory = stringFactory;
+        this.evaluator = evaluator;
+        assert this.fieldName != null;
+        assert this.constraintValue != null;
+        assert this.evaluator != null;
+        this.fieldSelector = fieldSelector != null ? fieldSelector : new FieldSelector() {
+            private static final long serialVersionUID = 1L;
+
+            public FieldSelectorResult accept( String fieldName ) {
+                return fieldName.equals(fieldName) ? FieldSelectorResult.LOAD_AND_BREAK : FieldSelectorResult.NO_LOAD;
+            }
+        };
+    }
+
+    protected ValueType readFromDocument( IndexReader reader,
+                                          int docId ) throws IOException {
+        Document doc = reader.document(docId, fieldSelector);
+        String valueString = doc.get(fieldName);
+        return valueTypeFactory.create(valueString);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#createWeight(org.apache.lucene.search.Searcher)
+     */
+    @Override
+    public Weight createWeight( Searcher searcher ) {
+        return new NotWeight(searcher);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#toString(java.lang.String)
+     */
+    @Override
+    public String toString( String field ) {
+        return fieldName + " " + evaluator.toString() + " " + stringFactory != null ? stringFactory.create(constraintValue) : constraintValue.toString();
+    }
+
+    /**
+     * Calculates query weights and builds query scores for our NOT queries.
+     */
+    protected class NotWeight extends Weight {
+        private static final long serialVersionUID = 1L;
+        private final Searcher searcher;
+
+        protected NotWeight( Searcher searcher ) {
+            this.searcher = searcher;
+            assert this.searcher != null;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#getQuery()
+         */
+        @Override
+        public Query getQuery() {
+            return CompareQuery.this;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a weight factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#getValue()
+         */
+        @Override
+        public float getValue() {
+            return 1.0f; // weight factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a normalization factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#sumOfSquaredWeights()
+         */
+        @Override
+        public float sumOfSquaredWeights() {
+            return 1.0f; // normalization factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always does nothing, as there is nothing to normalize.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#normalize(float)
+         */
+        @Override
+        public void normalize( float norm ) {
+            // No need to do anything here
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#scorer(org.apache.lucene.index.IndexReader, boolean, boolean)
+         */
+        @Override
+        public Scorer scorer( IndexReader reader,
+                              boolean scoreDocsInOrder,
+                              boolean topScorer ) {
+            // Return a custom scorer ...
+            return new NotScorer(reader);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#explain(org.apache.lucene.index.IndexReader, int)
+         */
+        @Override
+        public Explanation explain( IndexReader reader,
+                                    int doc ) {
+            return new Explanation(getValue(), getQuery().toString());
+        }
+    }
+
+    /**
+     * A scorer for the Path query.
+     */
+    protected class NotScorer extends Scorer {
+        private int docId = -1;
+        private final int maxDocId;
+        private final IndexReader reader;
+
+        protected NotScorer( IndexReader reader ) {
+            // We don't care which Similarity we have, because we don't use it. So get the default.
+            super(Similarity.getDefault());
+            this.reader = reader;
+            assert this.reader != null;
+            this.maxDocId = this.reader.maxDoc();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#docID()
+         */
+        @Override
+        public int docID() {
+            return docId;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#nextDoc()
+         */
+        @Override
+        public int nextDoc() throws IOException {
+            do {
+                ++docId;
+                if (reader.isDeleted(docId)) {
+                    // We should skip this document ...
+                    continue;
+                }
+                ValueType value = readFromDocument(reader, docId);
+                if (evaluator.satisfiesConstraint(value, constraintValue)) return docId;
+            } while (docId < maxDocId);
+            return Scorer.NO_MORE_DOCS;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#advance(int)
+         */
+        @Override
+        public int advance( int target ) throws IOException {
+            if (target == Scorer.NO_MORE_DOCS) return target;
+            while (true) {
+                int doc = nextDoc();
+                if (doc >= target) return doc;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This method always returns a score of 1.0 for the current document, since only those documents that satisfy the NOT are
+         * scored by this scorer.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Scorer#score()
+         */
+        @Override
+        public float score() {
+            return 1.0f;
+        }
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareStringQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareStringQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareStringQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,219 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import java.io.IOException;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Weight;
+import org.jboss.dna.graph.property.ValueComparators;
+import org.jboss.dna.graph.property.ValueFactories;
+import org.jboss.dna.graph.property.ValueFactory;
+import org.jboss.dna.graph.query.model.Comparison;
+
+/**
+ * A Lucene {@link Query} implementation that is used to apply a {@link Comparison} constraint against a string field. This query
+ * implementation works by using the {@link Query#weight(Searcher) weight} and
+ * {@link Weight#scorer(IndexReader, boolean, boolean) scorer} of the wrapped query to score (and return) only those documents
+ * with string fields that satisfy the constraint.
+ */
+public class CompareStringQuery extends CompareQuery<String> {
+
+    private static final long serialVersionUID = 1L;
+    protected static final Evaluator<String> IS_LESS_THAN = new Evaluator<String>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( String nodeValue,
+                                            String constraintValue ) {
+            return ValueComparators.STRING_COMPARATOR.compare(nodeValue, constraintValue) < 0;
+        }
+
+        @Override
+        public String toString() {
+            return " < ";
+        }
+    };
+    protected static final Evaluator<String> IS_LESS_THAN_OR_EQUAL_TO = new Evaluator<String>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( String nodeValue,
+                                            String constraintValue ) {
+            return ValueComparators.STRING_COMPARATOR.compare(nodeValue, constraintValue) <= 0;
+        }
+
+        @Override
+        public String toString() {
+            return " <= ";
+        }
+    };
+    protected static final Evaluator<String> IS_GREATER_THAN = new Evaluator<String>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( String nodeValue,
+                                            String constraintValue ) {
+            return ValueComparators.STRING_COMPARATOR.compare(nodeValue, constraintValue) > 0;
+        }
+
+        @Override
+        public String toString() {
+            return " > ";
+        }
+    };
+    protected static final Evaluator<String> IS_GREATER_THAN_OR_EQUAL_TO = new Evaluator<String>() {
+        private static final long serialVersionUID = 1L;
+
+        public boolean satisfiesConstraint( String nodeValue,
+                                            String constraintValue ) {
+            return ValueComparators.STRING_COMPARATOR.compare(nodeValue, constraintValue) >= 0;
+        }
+
+        @Override
+        public String toString() {
+            return " >= ";
+        }
+    };
+
+    /**
+     * Construct a {@link Query} implementation that scores documents with a string field value that is greater than the supplied
+     * constraint value.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareStringQuery createQueryForNodesWithFieldGreaterThan( String constraintValue,
+                                                                              String fieldName,
+                                                                              ValueFactories factories,
+                                                                              boolean caseSensitive ) {
+        return new CompareStringQuery(fieldName, constraintValue, factories.getStringFactory(), factories.getStringFactory(),
+                                      IS_GREATER_THAN, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents with a string field value that is greater than or equal to
+     * the supplied constraint value.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareStringQuery createQueryForNodesWithFieldGreaterThanOrEqualTo( String constraintValue,
+                                                                                       String fieldName,
+                                                                                       ValueFactories factories,
+                                                                                       boolean caseSensitive ) {
+        return new CompareStringQuery(fieldName, constraintValue, factories.getStringFactory(), factories.getStringFactory(),
+                                      IS_GREATER_THAN_OR_EQUAL_TO, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents with a string field value that is less than the supplied
+     * constraint value.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareStringQuery createQueryForNodesWithFieldLessThan( String constraintValue,
+                                                                           String fieldName,
+                                                                           ValueFactories factories,
+                                                                           boolean caseSensitive ) {
+        return new CompareStringQuery(fieldName, constraintValue, factories.getStringFactory(), factories.getStringFactory(),
+                                      IS_LESS_THAN, caseSensitive);
+    }
+
+    /**
+     * Construct a {@link Query} implementation that scores documents with a string field value that is less than or equal to the
+     * supplied constraint value.
+     * 
+     * @param constraintValue the constraint value; may not be null
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param factories the value factories that can be used during the scoring; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     * @return the query; never null
+     */
+    public static CompareStringQuery createQueryForNodesWithFieldLessThanOrEqualTo( String constraintValue,
+                                                                                    String fieldName,
+                                                                                    ValueFactories factories,
+                                                                                    boolean caseSensitive ) {
+        return new CompareStringQuery(fieldName, constraintValue, factories.getStringFactory(), factories.getStringFactory(),
+                                      IS_LESS_THAN_OR_EQUAL_TO, caseSensitive);
+    }
+
+    private final boolean caseSensitive;
+
+    /**
+     * Construct a {@link Query} implementation that scores nodes according to the supplied comparator.
+     * 
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param constraintValue the constraint value; may not be null
+     * @param valueFactory the value factory that can be used during the scoring; may not be null
+     * @param stringFactory the string factory that can be used during the scoring; may not be null
+     * @param evaluator the {@link CompareQuery.Evaluator} implementation that returns whether the node path satisfies the
+     *        constraint; may not be null
+     * @param caseSensitive true if the comparison should be done in a case-sensitive manner, or false if it is to be
+     *        case-insensitive
+     */
+    protected CompareStringQuery( String fieldName,
+                                  String constraintValue,
+                                  ValueFactory<String> valueFactory,
+                                  ValueFactory<String> stringFactory,
+                                  Evaluator<String> evaluator,
+                                  boolean caseSensitive ) {
+        super(fieldName, caseSensitive ? constraintValue : constraintValue.toLowerCase(), valueFactory, stringFactory, evaluator);
+        this.caseSensitive = caseSensitive;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.search.query.CompareQuery#readFromDocument(org.apache.lucene.index.IndexReader, int)
+     */
+    @Override
+    protected String readFromDocument( IndexReader reader,
+                                       int docId ) throws IOException {
+        String result = super.readFromDocument(reader, docId);
+        return caseSensitive ? result : result.toLowerCase();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#clone()
+     */
+    @Override
+    public Object clone() {
+        return new CompareStringQuery(fieldName, constraintValue, valueTypeFactory, stringFactory, evaluator, caseSensitive);
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/CompareStringQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/MatchNoneQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/MatchNoneQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/MatchNoneQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,216 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Similarity;
+import org.apache.lucene.search.Weight;
+
+/**
+ * A Lucene {@link Query} implementation that always matches no documents.
+ */
+public class MatchNoneQuery extends Query {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Construct a query that always returns no documents.
+     */
+    public MatchNoneQuery() {
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#createWeight(org.apache.lucene.search.Searcher)
+     */
+    @Override
+    public Weight createWeight( Searcher searcher ) {
+        return new NoneWeight();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#clone()
+     */
+    @Override
+    public Object clone() {
+        return new MatchNoneQuery();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#toString(java.lang.String)
+     */
+    @Override
+    public String toString( String field ) {
+        return "NO DOCS";
+    }
+
+    /**
+     * Calculates query weights and builds query scores for our NOT queries.
+     */
+    protected class NoneWeight extends Weight {
+        private static final long serialVersionUID = 1L;
+
+        protected NoneWeight() {
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#getQuery()
+         */
+        @Override
+        public Query getQuery() {
+            return MatchNoneQuery.this;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a weight factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#getValue()
+         */
+        @Override
+        public float getValue() {
+            return 1.0f; // weight factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a normalization factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#sumOfSquaredWeights()
+         */
+        @Override
+        public float sumOfSquaredWeights() {
+            return 1.0f; // normalization factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always does nothing, as there is nothing to normalize.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#normalize(float)
+         */
+        @Override
+        public void normalize( float norm ) {
+            // No need to do anything here
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#scorer(org.apache.lucene.index.IndexReader, boolean, boolean)
+         */
+        @Override
+        public Scorer scorer( IndexReader reader,
+                              boolean scoreDocsInOrder,
+                              boolean topScorer ) {
+            return new NoneScorer();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#explain(org.apache.lucene.index.IndexReader, int)
+         */
+        @Override
+        public Explanation explain( IndexReader reader,
+                                    int doc ) {
+            return new Explanation(getValue(), "NO VALUES");
+        }
+    }
+
+    /**
+     * A scorer for the NOT query that iterates over documents (in increasing docId order), using the given scorer implementation
+     * for the operand of the NOT.
+     */
+    protected static class NoneScorer extends Scorer {
+        private int docId = -1;
+
+        protected NoneScorer() {
+            // We don't care which Similarity we have, because we don't use it. So get the default.
+            super(Similarity.getDefault());
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#docID()
+         */
+        @Override
+        public int docID() {
+            return docId;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#nextDoc()
+         */
+        @Override
+        public int nextDoc() {
+            docId = Scorer.NO_MORE_DOCS;
+            return docId;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#advance(int)
+         */
+        @Override
+        public int advance( int target ) {
+            return Scorer.NO_MORE_DOCS;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This method always returns a score of 1.0 for the current document, since only those documents that satisfy the NOT are
+         * scored by this scorer.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Scorer#score()
+         */
+        @Override
+        public float score() {
+            return 1.0f;
+        }
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/MatchNoneQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/NotQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/NotQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/NotQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,274 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import java.io.IOException;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Similarity;
+import org.apache.lucene.search.Weight;
+
+/**
+ * A Lucene {@link Query} implementation that is used to represent a NOT expression of another wrapped Query object. This query
+ * implementation works by using the {@link Query#weight(Searcher) weight} and
+ * {@link Weight#scorer(IndexReader, boolean, boolean) scorer} of the wrapped query to score (and return) only those documents
+ * that were <i>not</i> scored by the wrapped query. In other words, if the wrapped query ended up scoring any document, that
+ * document is <i>not</i> scored (i.e., skipped) by this query.
+ */
+public class NotQuery extends Query {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * The operand that is being negated by this query.
+     */
+    protected final Query operand;
+
+    /**
+     * Construct a NOT(x) constraint where the 'x' operand is supplied.
+     * 
+     * @param operand the operand being negated
+     */
+    public NotQuery( Query operand ) {
+        this.operand = operand;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#createWeight(org.apache.lucene.search.Searcher)
+     */
+    @Override
+    public Weight createWeight( Searcher searcher ) {
+        return new NotWeight(searcher);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#clone()
+     */
+    @Override
+    public Object clone() {
+        return new NotQuery(operand);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#toString(java.lang.String)
+     */
+    @Override
+    public String toString( String field ) {
+        return "NOT(" + operand.toString(field) + ")";
+    }
+
+    /**
+     * Calculates query weights and builds query scores for our NOT queries.
+     */
+    protected class NotWeight extends Weight {
+        private static final long serialVersionUID = 1L;
+        private final Searcher searcher;
+
+        protected NotWeight( Searcher searcher ) {
+            this.searcher = searcher;
+            assert this.searcher != null;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#getQuery()
+         */
+        @Override
+        public Query getQuery() {
+            return NotQuery.this;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a weight factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#getValue()
+         */
+        @Override
+        public float getValue() {
+            return 1.0f; // weight factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a normalization factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#sumOfSquaredWeights()
+         */
+        @Override
+        public float sumOfSquaredWeights() {
+            return 1.0f; // normalization factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always does nothing, as there is nothing to normalize.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#normalize(float)
+         */
+        @Override
+        public void normalize( float norm ) {
+            // No need to do anything here
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#scorer(org.apache.lucene.index.IndexReader, boolean, boolean)
+         */
+        @Override
+        public Scorer scorer( IndexReader reader,
+                              boolean scoreDocsInOrder,
+                              boolean topScorer ) throws IOException {
+            // Get the operand's score, and set this on the NOT query
+            Scorer operandScorer = operand.weight(searcher).scorer(reader, scoreDocsInOrder, topScorer);
+            // Return a custom scorer ...
+            return new NotScorer(operandScorer, reader);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#explain(org.apache.lucene.index.IndexReader, int)
+         */
+        @Override
+        public Explanation explain( IndexReader reader,
+                                    int doc ) throws IOException {
+            Explanation operandExplanation = operand.weight(searcher).explain(reader, doc);
+            String desc = operandExplanation.getDescription();
+            return new Explanation(getValue(), "NOT(" + desc + ")");
+        }
+    }
+
+    /**
+     * A scorer for the NOT query that iterates over documents (in increasing docId order), using the given scorer implementation
+     * for the operand of the NOT.
+     */
+    protected static class NotScorer extends Scorer {
+        private int docId = -1;
+        private int nextScoredDocId = -1;
+        private final Scorer operandScorer;
+        private final IndexReader reader;
+        private final int pastMaxDocId;
+
+        /**
+         * @param operandScorer the scorer that is used to score the documents based upon the operand of the NOT; may not be null
+         * @param reader the reader that has access to all the docs ...
+         */
+        protected NotScorer( Scorer operandScorer,
+                             IndexReader reader ) {
+            // We don't care which Similarity we have, because we don't use it. So get the default.
+            super(Similarity.getDefault());
+            this.operandScorer = operandScorer;
+            this.reader = reader;
+            assert this.operandScorer != null;
+            assert this.reader != null;
+            this.pastMaxDocId = this.reader.maxDoc();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#docID()
+         */
+        @Override
+        public int docID() {
+            return docId;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#nextDoc()
+         */
+        @Override
+        public int nextDoc() throws IOException {
+            if (nextScoredDocId == -1) {
+                // Find the first document that is scored by the operand's scorer ...
+                nextScoredDocId = operandScorer.nextDoc();
+            }
+            do {
+                ++docId;
+                if (docId == pastMaxDocId) {
+                    // We're aleady to the end of the documents in the index, so return no more docs
+                    return Scorer.NO_MORE_DOCS;
+                }
+                if (docId == nextScoredDocId) {
+                    // Find the next document that is scored by the operand's scorer ...
+                    nextScoredDocId = operandScorer.nextDoc();
+                    continue;
+                }
+                if (reader.isDeleted(docId)) {
+                    // We should skip this document ...
+                    continue;
+                }
+                return docId;
+            } while (true);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#advance(int)
+         */
+        @Override
+        public int advance( int target ) throws IOException {
+            if (target == Scorer.NO_MORE_DOCS) return target;
+            while (true) {
+                int doc = nextDoc();
+                if (doc >= target) return doc;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This method always returns a score of 1.0 for the current document, since only those documents that satisfy the NOT are
+         * scored by this scorer.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Scorer#score()
+         */
+        @Override
+        public float score() {
+            return 1.0f;
+        }
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/NotQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/ScoreQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/ScoreQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/ScoreQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,275 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import java.io.IOException;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Similarity;
+import org.apache.lucene.search.Weight;
+import org.jboss.dna.graph.query.model.FullTextSearchScore;
+
+/**
+ * A Lucene {@link Query} implementation that is used to apply a {@link FullTextSearchScore} criteria a NOT expression of another
+ * wrapped Query object. This query implementation works by using the {@link Query#weight(Searcher) weight} and
+ * {@link Weight#scorer(IndexReader, boolean, boolean) scorer} of the wrapped query to score (and return) only those documents
+ * that were <i>not</i> scored by the wrapped query. In other words, if the wrapped query ended up scoring any document, that
+ * document is <i>not</i> scored (i.e., skipped) by this query.
+ */
+public class ScoreQuery extends Query {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * The operand that is being negated by this query.
+     */
+    protected final Query operand;
+
+    /**
+     * Construct a NOT(x) constraint where the 'x' operand is supplied.
+     * 
+     * @param operand the operand being negated
+     */
+    public ScoreQuery( Query operand ) {
+        this.operand = operand;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#createWeight(org.apache.lucene.search.Searcher)
+     */
+    @Override
+    public Weight createWeight( Searcher searcher ) {
+        return new NotWeight(searcher);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#clone()
+     */
+    @Override
+    public Object clone() {
+        return new ScoreQuery(operand);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#toString(java.lang.String)
+     */
+    @Override
+    public String toString( String field ) {
+        return "NOT(" + operand.toString(field) + ")";
+    }
+
+    /**
+     * Calculates query weights and builds query scores for our NOT queries.
+     */
+    protected class NotWeight extends Weight {
+        private static final long serialVersionUID = 1L;
+        private final Searcher searcher;
+
+        protected NotWeight( Searcher searcher ) {
+            this.searcher = searcher;
+            assert this.searcher != null;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#getQuery()
+         */
+        @Override
+        public Query getQuery() {
+            return ScoreQuery.this;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a weight factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#getValue()
+         */
+        @Override
+        public float getValue() {
+            return 1.0f; // weight factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a normalization factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#sumOfSquaredWeights()
+         */
+        @Override
+        public float sumOfSquaredWeights() {
+            return 1.0f; // normalization factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always does nothing, as there is nothing to normalize.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#normalize(float)
+         */
+        @Override
+        public void normalize( float norm ) {
+            // No need to do anything here
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#scorer(org.apache.lucene.index.IndexReader, boolean, boolean)
+         */
+        @Override
+        public Scorer scorer( IndexReader reader,
+                              boolean scoreDocsInOrder,
+                              boolean topScorer ) throws IOException {
+            // Get the operand's score, and set this on the NOT query
+            Scorer operandScorer = operand.weight(searcher).scorer(reader, scoreDocsInOrder, topScorer);
+            // Return a custom scorer ...
+            return new NotScorer(operandScorer, reader);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#explain(org.apache.lucene.index.IndexReader, int)
+         */
+        @Override
+        public Explanation explain( IndexReader reader,
+                                    int doc ) throws IOException {
+            Explanation operandExplanation = operand.weight(searcher).explain(reader, doc);
+            String desc = operandExplanation.getDescription();
+            return new Explanation(getValue(), "NOT(" + desc + ")");
+        }
+    }
+
+    /**
+     * A scorer for the NOT query that iterates over documents (in increasing docId order), using the given scorer implementation
+     * for the operand of the NOT.
+     */
+    protected static class NotScorer extends Scorer {
+        private int docId = -1;
+        private int nextScoredDocId = -1;
+        private final Scorer operandScorer;
+        private final IndexReader reader;
+        private final int pastMaxDocId;
+
+        /**
+         * @param operandScorer the scorer that is used to score the documents based upon the operand of the NOT; may not be null
+         * @param reader the reader that has access to all the docs ...
+         */
+        protected NotScorer( Scorer operandScorer,
+                             IndexReader reader ) {
+            // We don't care which Similarity we have, because we don't use it. So get the default.
+            super(Similarity.getDefault());
+            this.operandScorer = operandScorer;
+            this.reader = reader;
+            assert this.operandScorer != null;
+            assert this.reader != null;
+            this.pastMaxDocId = this.reader.maxDoc();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#docID()
+         */
+        @Override
+        public int docID() {
+            return docId;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#nextDoc()
+         */
+        @Override
+        public int nextDoc() throws IOException {
+            if (nextScoredDocId == -1) {
+                // Find the first document that is scored by the operand's scorer ...
+                nextScoredDocId = operandScorer.nextDoc();
+            }
+            do {
+                ++docId;
+                if (docId == pastMaxDocId) {
+                    // We're aleady to the end of the documents in the index, so return no more docs
+                    return Scorer.NO_MORE_DOCS;
+                }
+                if (docId == nextScoredDocId) {
+                    // Find the next document that is scored by the operand's scorer ...
+                    nextScoredDocId = operandScorer.nextDoc();
+                    continue;
+                }
+                if (reader.isDeleted(docId)) {
+                    // We should skip this document ...
+                    continue;
+                }
+                return docId;
+            } while (true);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#advance(int)
+         */
+        @Override
+        public int advance( int target ) throws IOException {
+            if (target == Scorer.NO_MORE_DOCS) return target;
+            while (true) {
+                int doc = nextDoc();
+                if (doc >= target) return doc;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This method always returns a score of 1.0 for the current document, since only those documents that satisfy the NOT are
+         * scored by this scorer.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Scorer#score()
+         */
+        @Override
+        public float score() {
+            return 1.0f;
+        }
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/ScoreQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-search/src/main/java/org/jboss/dna/search/query/UuidsQuery.java
===================================================================
--- trunk/dna-search/src/main/java/org/jboss/dna/search/query/UuidsQuery.java	                        (rev 0)
+++ trunk/dna-search/src/main/java/org/jboss/dna/search/query/UuidsQuery.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,268 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.FieldSelector;
+import org.apache.lucene.document.FieldSelectorResult;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Similarity;
+import org.apache.lucene.search.Weight;
+import org.jboss.dna.graph.property.ValueFactory;
+
+/**
+ * A Lucene {@link Query} implementation that is used to score positively those documents that have a UUID in the supplied set.
+ * This works for large sets of UUIDs; in smaller numbers, it may be more efficient to create a boolean query that checks for each
+ * fo the UUIDs.
+ */
+public class UuidsQuery extends Query {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * The operand that is being negated by this query.
+     */
+    protected final Set<UUID> uuids;
+    protected final FieldSelector fieldSelector;
+    protected final String fieldName;
+    protected final ValueFactory<UUID> uuidFactory;
+
+    /**
+     * Construct a {@link Query} implementation that scores nodes according to the supplied comparator.
+     * 
+     * @param fieldName the name of the document field containing the value; may not be null
+     * @param uuids the set of UUID values; may not be null
+     * @param uuidFactory the factory to create UUID values; may not be null
+     */
+    public UuidsQuery( String fieldName,
+                       Set<UUID> uuids,
+                       ValueFactory<UUID> uuidFactory ) {
+        this.fieldName = fieldName;
+        this.uuids = uuids;
+        this.uuidFactory = uuidFactory;
+        assert this.fieldName != null;
+        assert this.uuids != null;
+        assert this.uuidFactory != null;
+        this.fieldSelector = new FieldSelector() {
+            private static final long serialVersionUID = 1L;
+
+            public FieldSelectorResult accept( String fieldName ) {
+                return fieldName.equals(fieldName) ? FieldSelectorResult.LOAD_AND_BREAK : FieldSelectorResult.NO_LOAD;
+            }
+        };
+    }
+
+    protected boolean includeDocument( IndexReader reader,
+                                       int docId ) throws IOException {
+        Document doc = reader.document(docId, fieldSelector);
+        String valueString = doc.get(fieldName);
+        return uuids.contains(uuidFactory.create(valueString));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#createWeight(org.apache.lucene.search.Searcher)
+     */
+    @Override
+    public Weight createWeight( Searcher searcher ) {
+        return new UuidSetWeight(searcher);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.apache.lucene.search.Query#toString(java.lang.String)
+     */
+    @Override
+    public String toString( String field ) {
+        return fieldName + " IN UUIDs";
+    }
+
+    /**
+     * Calculates query weights and builds query scores for our NOT queries.
+     */
+    protected class UuidSetWeight extends Weight {
+        private static final long serialVersionUID = 1L;
+        private final Searcher searcher;
+
+        protected UuidSetWeight( Searcher searcher ) {
+            this.searcher = searcher;
+            assert this.searcher != null;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#getQuery()
+         */
+        @Override
+        public Query getQuery() {
+            return UuidsQuery.this;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a weight factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#getValue()
+         */
+        @Override
+        public float getValue() {
+            return 1.0f; // weight factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always returns a normalization factor of 1.0.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#sumOfSquaredWeights()
+         */
+        @Override
+        public float sumOfSquaredWeights() {
+            return 1.0f; // normalization factor of 1.0
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This implementation always does nothing, as there is nothing to normalize.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Weight#normalize(float)
+         */
+        @Override
+        public void normalize( float norm ) {
+            // No need to do anything here
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#scorer(org.apache.lucene.index.IndexReader, boolean, boolean)
+         */
+        @Override
+        public Scorer scorer( IndexReader reader,
+                              boolean scoreDocsInOrder,
+                              boolean topScorer ) {
+            // Return a custom scorer ...
+            return new UuidScorer(reader);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Weight#explain(org.apache.lucene.index.IndexReader, int)
+         */
+        @Override
+        public Explanation explain( IndexReader reader,
+                                    int doc ) {
+            return new Explanation(getValue(), getQuery().toString());
+        }
+    }
+
+    /**
+     * A scorer for the Path query.
+     */
+    protected class UuidScorer extends Scorer {
+        private int docId = -1;
+        private final int maxDocId;
+        private final IndexReader reader;
+
+        protected UuidScorer( IndexReader reader ) {
+            // We don't care which Similarity we have, because we don't use it. So get the default.
+            super(Similarity.getDefault());
+            this.reader = reader;
+            assert this.reader != null;
+            this.maxDocId = this.reader.maxDoc();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#docID()
+         */
+        @Override
+        public int docID() {
+            return docId;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#nextDoc()
+         */
+        @Override
+        public int nextDoc() throws IOException {
+            do {
+                ++docId;
+                if (reader.isDeleted(docId)) {
+                    // We should skip this document ...
+                    continue;
+                }
+                if (includeDocument(reader, docId)) return docId;
+            } while (docId < maxDocId);
+            return Scorer.NO_MORE_DOCS;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#advance(int)
+         */
+        @Override
+        public int advance( int target ) throws IOException {
+            if (target == Scorer.NO_MORE_DOCS) return target;
+            while (true) {
+                int doc = nextDoc();
+                if (doc >= target) return doc;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This method always returns a score of 1.0 for the current document, since only those documents that satisfy the NOT are
+         * scored by this scorer.
+         * </p>
+         * 
+         * @see org.apache.lucene.search.Scorer#score()
+         */
+        @Override
+        public float score() {
+            return 1.0f;
+        }
+    }
+}


Property changes on: trunk/dna-search/src/main/java/org/jboss/dna/search/query/UuidsQuery.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Modified: trunk/dna-search/src/main/resources/org/jboss/dna/search/SearchI18n.properties
===================================================================
--- trunk/dna-search/src/main/resources/org/jboss/dna/search/SearchI18n.properties	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/main/resources/org/jboss/dna/search/SearchI18n.properties	2009-11-16 23:25:36 UTC (rev 1320)
@@ -30,8 +30,11 @@
 errorWhileRemovingContentAtPath = Error while removing the content at/below "{0}" in the "{1}" workspace of the "{2}" source: {3}
 errorWhileUpdatingContent = Error while updating content in the "{0}" workspace of the "{1}" source: {2}
 errorWhileCommittingIndexChanges = Error while committing changes to the indexes for the "{0}" workspace of the "{1}" source: {2}
+errorWhileRollingBackIndexChanges = Error while rolling back changes to the indexes for the "{0}" workspace of the "{1}" source: {2}
 errorCreatingIndexWriter = Error attempting to create an index writer for the "{0}" index for the "{1}" workspace of the "{2}" source: {3}
 errorWhileOptimizingIndexes = Error while optimizing the indexes for the "{0}" workspace of the "{1}" source: {2}
 errorWhilePerformingSearch = Error while searching for "{0}" in the "{1}" workspace of the "{2}" source: {3}
 errorWhilePerformingQuery = Error while performing the query "{0}" against the content in the "{1}" workspace of the "{2}" source: {3}
-errorWhileInitializingSearchEngine = Error while initializing the search engine for the "{0}" workspace of the "{1}" source: {2}
\ No newline at end of file
+errorWhileInitializingSearchEngine = Error while initializing the search engine for the "{0}" workspace of the "{1}" source: {2}
+errorWhileRemovingIndexesForWorkspace = Error while removing the indexes for the "{0}" workspace of the "{1}" source: {2}
+errorWhilePerformingLuceneQuery = Error while performing the Lucene query "{0}" as part of the "{1}" query against the "{2}" workspace of the "{3}" source: {4}
\ No newline at end of file

Modified: trunk/dna-search/src/test/java/org/jboss/dna/search/SearchEngineTest.java
===================================================================
--- trunk/dna-search/src/test/java/org/jboss/dna/search/SearchEngineTest.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/test/java/org/jboss/dna/search/SearchEngineTest.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -25,16 +25,32 @@
 
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsNull.notNullValue;
-import static org.hamcrest.core.IsSame.sameInstance;
 import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import java.util.List;
 import org.jboss.dna.graph.ExecutionContext;
 import org.jboss.dna.graph.Graph;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.Node;
 import org.jboss.dna.graph.connector.RepositoryConnection;
 import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
 import org.jboss.dna.graph.connector.RepositorySourceException;
 import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
 import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.query.QueryContext;
+import org.jboss.dna.graph.query.model.Query;
+import org.jboss.dna.graph.query.validate.Schemata;
 import org.jboss.dna.graph.request.InvalidWorkspaceException;
+import org.jboss.dna.search.IndexLayout;
+import org.jboss.dna.search.IndexSession;
+import org.jboss.dna.search.SearchEngine;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -47,7 +63,10 @@
     private String workspaceName2;
     private InMemoryRepositorySource source;
     private RepositoryConnectionFactory connectionFactory;
-    private DirectoryConfiguration directoryFactory;
+    private IndexLayout layout;
+    private IndexSession sessionWs1;
+    private IndexSession sessionWs2;
+    private IndexSession sessionDefault;
     private Graph content;
 
     @Before
@@ -66,12 +85,6 @@
         content.createWorkspace().named(workspaceName1);
         content.createWorkspace().named(workspaceName2);
 
-        // Load some content ...
-        content.useWorkspace(workspaceName1);
-        content.importXmlFrom(getClass().getClassLoader().getResourceAsStream("cars.xml")).into("/");
-        content.useWorkspace(workspaceName2);
-        content.importXmlFrom(getClass().getClassLoader().getResourceAsStream("aircraft.xml")).into("/");
-
         // Set up the connection factory ...
         connectionFactory = new RepositoryConnectionFactory() {
             @SuppressWarnings( "synthetic-access" )
@@ -80,67 +93,261 @@
             }
         };
 
+        // Set up the index layout ...
+        layout = mock(IndexLayout.class);
+        sessionWs1 = mockSession(layout, workspaceName1);
+        sessionWs2 = mockSession(layout, workspaceName2);
+        sessionDefault = mockSession(layout, "");
+
         // Now set up the search engine ...
-        directoryFactory = DirectoryConfigurations.inMemory();
-        engine = new SearchEngine(context, sourceName, connectionFactory, directoryFactory);
+        engine = new SearchEngine(context, sourceName, connectionFactory, layout);
     }
 
-    protected Path path( String string ) {
-        return context.getValueFactories().getPathFactory().create(string);
+    protected IndexSession mockSession( IndexLayout mockLayout,
+                                        String workspaceName ) {
+        IndexSession session = mock(IndexSession.class);
+        stub(layout.createSession(context, sourceName, workspaceName, false, false)).toReturn(session);
+        stub(layout.createSession(context, sourceName, workspaceName, false, true)).toReturn(session);
+        stub(layout.createSession(context, sourceName, workspaceName, true, false)).toReturn(session);
+        stub(layout.createSession(context, sourceName, workspaceName, true, true)).toReturn(session);
+        stub(session.getWorkspaceName()).toReturn(workspaceName);
+        stub(session.getSourceName()).toReturn(sourceName);
+        return session;
     }
 
-    @Test
-    public void shouldHaveLoadedTestContentIntoRepositorySource() {
+    protected Path path( String path ) {
+        return context.getValueFactories().getPathFactory().create(path);
+    }
+
+    protected void loadContent() throws Exception {
+        // Load some content ...
         content.useWorkspace(workspaceName1);
-        assertThat(content.getNodeAt("/Cars/Hybrid/Toyota Prius").getProperty("msrp").getFirstValue(), is((Object)"$21,500"));
+        content.importXmlFrom(getClass().getClassLoader().getResourceAsStream("cars.xml")).into("/");
         content.useWorkspace(workspaceName2);
-        assertThat(content.getNodeAt("/Aircraft/Commercial/Boeing 787").getProperty("range").getFirstValue(),
-                   is((Object)"3050nm"));
+        content.importXmlFrom(getClass().getClassLoader().getResourceAsStream("aircraft.xml")).into("/");
     }
 
     @Test
-    public void shouldHaveExecutionContext() {
-        assertThat(engine.getContext(), is(sameInstance(context)));
+    public void shouldReturnSearchWorkspaceForExistingWorkspaceInSource() {
+        SearchEngine.Workspace workspace = engine.getWorkspace(workspaceName1);
+        assertThat(workspace, is(notNullValue()));
+        assertThat(workspace.modifiedNodesSinceLastOptimize.get(), is(0));
+        assertThat(workspace.getWorkspaceName(), is(workspaceName1));
     }
 
+    @Test( expected = InvalidWorkspaceException.class )
+    public void shouldFailToReturnSearchWorkspaceForNonExistantWorkspaceInSource() {
+        engine.getWorkspace(workspaceName1 + "foobar");
+    }
+
     @Test
-    public void shouldHaveSourceName() {
-        assertThat(engine.getSourceName(), is(sourceName));
+    public void shouldDoNothingDuringRemoveWorkspaceIfWorkspaceHasNotBeenLoaded() throws Exception {
+        engine.removeWorkspace(workspaceName1);
+        verifyZeroInteractions(layout);
     }
 
     @Test
-    public void shouldFindExistingWorkspaces() {
-        assertThat(engine.getWorkspaceEngine(workspaceName1), is(notNullValue()));
-        assertThat(engine.getWorkspaceEngine(workspaceName2), is(notNullValue()));
+    public void shouldForwardRemoveWorkspaceToIndexLayout() throws Exception {
+        engine.getWorkspace(workspaceName1);
+        engine.removeWorkspace(workspaceName1);
+        verify(layout).destroyIndexes(context, sourceName, workspaceName1);
+        verifyNoMoreInteractions(layout);
     }
 
-    @Test( expected = InvalidWorkspaceException.class )
-    public void shouldNotFindNonExistingWorkspaces() {
-        engine.getWorkspaceEngine("Non-existant workspace");
+    @Test
+    public void shouldForwardRemoveWorkspaceToIndexLayoutForEachWorkspaceThatWasLoaded() throws Exception {
+        engine.getWorkspace(workspaceName1);
+        engine.removeWorkspaces();
+        verify(layout).destroyIndexes(context, sourceName, workspaceName1);
+        verifyZeroInteractions(layout);
     }
 
     @Test
-    public void shouldIndexAllContentInRepositorySource() {
-        engine.indexContent(3);
+    public void shouldForwardRemoveWorkspaceToIndexLayoutForAllWorkspacesThatWereLoaded() throws Exception {
+        engine.getWorkspace(workspaceName1);
+        engine.getWorkspace(workspaceName2);
+        engine.removeWorkspaces();
+        verify(layout).destroyIndexes(context, sourceName, workspaceName1);
+        verify(layout).destroyIndexes(context, sourceName, workspaceName2);
+        verifyNoMoreInteractions(layout);
     }
 
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailIfNullWorkspaceNamePassedToRemoveWorkspace() throws Exception {
+        engine.removeWorkspace(null);
+    }
+
     @Test
-    public void shouldIndexAllContentInWorkspace() {
-        engine.indexContent(workspaceName1, 3);
-        engine.indexContent(workspaceName2, 5);
+    public void shouldForwardOptimizeOfWorkspaceToIndexSession() throws Exception {
+        engine.optimize(workspaceName1);
+        verify(sessionWs1).optimize();
+        verify(sessionWs1).commit();
+        verifyNoMoreInteractions(sessionWs1);
     }
 
     @Test
-    public void shouldIndexAllContentInWorkspaceBelowPath() {
-        engine.indexContent(workspaceName1, path("/Cars/Hybrid"), 3);
-        engine.indexContent(workspaceName2, path("/Aircraft/Commercial"), 5);
+    public void shouldForwardOptimizeOfAllWorkspacesToEachIndexSession() throws Exception {
+        engine.optimize(); // will find all three workspaces
+        verify(sessionWs1).optimize();
+        verify(sessionWs1).commit();
+        verifyNoMoreInteractions(sessionWs1);
+        verify(sessionWs2).optimize();
+        verify(sessionWs2).commit();
+        verifyNoMoreInteractions(sessionWs2);
+        verify(sessionDefault).optimize();
+        verify(sessionDefault).commit();
+        verifyNoMoreInteractions(sessionDefault);
     }
 
     @Test
-    public void shouldReIndexAllContentInWorkspaceBelowPath() {
-        for (int i = 0; i != 0; i++) {
-            engine.indexContent(workspaceName1, path("/Cars/Hybrid"), 3);
-            engine.indexContent(workspaceName2, path("/Aircraft/Commercial"), 5);
-        }
+    public void shouldForwardIndexOfWorkspaceToIndexSession() throws Exception {
+        loadContent();
+        engine.index(workspaceName1, 3);
+        verify(sessionWs1, times(18)).index((Node)anyObject());
+        verify(sessionWs1).commit();
     }
+
+    @Test
+    public void shouldForwardIndexOfSubgraphInWorkspaceToIndexSession() throws Exception {
+        loadContent();
+        engine.index(workspaceName1, path("/Cars"), 3);
+        verify(sessionWs1).deleteBelow(path("/Cars"));
+        verify(sessionWs1, times(17)).index((Node)anyObject());
+        verify(sessionWs1).commit();
+    }
+
+    @Test
+    public void shouldForwardIndexEntireWorkspaceToIndexSession() throws Exception {
+        loadContent();
+        engine.index(workspaceName1, path("/"), 3);
+        verify(sessionWs1, times(18)).index((Node)anyObject());
+        verify(sessionWs1).commit();
+    }
+
+    @Test
+    public void shouldForwardIndexOfAllWorkspacesToEachIndexSession() throws Exception {
+        loadContent();
+        engine.index(3); // will find all three workspaces
+        verify(sessionWs1, times(18)).index((Node)anyObject());
+        verify(sessionWs1).commit();
+        verify(sessionWs2, times(24)).index((Node)anyObject());
+        verify(sessionWs2).commit();
+        verify(sessionDefault, times(1)).index((Node)anyObject());
+        verify(sessionDefault).commit();
+    }
+
+    @SuppressWarnings( "unchecked" )
+    @Test
+    public void shouldForwardSearchToIndexSession() throws Exception {
+        String query = "term1 term2";
+        engine.fullTextSearch(context, workspaceName1, query, 3, 0);
+        verify(sessionWs1).search(eq(context), eq(query), eq(3), eq(0), (List<Location>)anyObject());
+        verify(sessionWs1).commit();
+    }
+
+    @Test
+    public void shouldForwardQueryToIndexSession() throws Exception {
+        Query query = mock(Query.class);
+        Schemata schemata = mock(Schemata.class);
+        engine.query(context, workspaceName1, query, schemata);
+        verify(sessionWs1).query(eq(new QueryContext(context, schemata)), eq(query));
+        verify(sessionWs1).commit();
+    }
+
+    // These tests expect there to be some real IndexLayout ...
+
+    // @Test
+    // public void shouldIndexAllContentInRepositorySource() throws Exception {
+    // loadContent();
+    // engine.index(3);
+    // }
+    //
+    // @Test
+    // public void shouldIndexAllContentInWorkspace() throws Exception {
+    // loadContent();
+    // engine.index(workspaceName1, 3);
+    // engine.index(workspaceName2, 5);
+    // }
+    //
+    // @Test
+    // public void shouldIndexAllContentInWorkspaceBelowPath() throws Exception {
+    // loadContent();
+    // engine.index(workspaceName1, path("/Cars/Hybrid"), 3);
+    // engine.index(workspaceName2, path("/Aircraft/Commercial"), 5);
+    // }
+    //
+    // @Test
+    // public void shouldReIndexAllContentInWorkspaceBelowPath() throws Exception {
+    // loadContent();
+    // for (int i = 0; i != 0; i++) {
+    // engine.index(workspaceName1, path("/Cars/Hybrid"), 3);
+    // engine.index(workspaceName2, path("/Aircraft/Commercial"), 5);
+    // }
+    // }
+    //
+    // @Test
+    // public void shouldHaveLoadedTestContentIntoRepositorySource() {
+    // assertThat(content.getNodeAt("/Cars/Hybrid/Toyota Prius").getProperty("msrp").getFirstValue(), is((Object)"$21,500"));
+    // }
+    //
+    // @Test
+    // public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfOne() {
+    // engine.index(workspaceName1, path("/"), 1);
+    // }
+    //
+    // @Test
+    // public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfTwo() {
+    // engine.index(workspaceName1, path("/"), 2);
+    // }
+    //
+    // @Test
+    // public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfThree() {
+    // engine.index(workspaceName1, path("/"), 3);
+    // }
+    //
+    // @Test
+    // public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfFour() {
+    // engine.index(workspaceName1, path("/"), 4);
+    // }
+    //
+    // @Test
+    // public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfTen() {
+    // engine.index(workspaceName1, path("/"), 10);
+    // }
+    //
+    // @Test
+    // public void shouldIndexRepositoryContentStartingAtNonRootNode() {
+    // engine.index(workspaceName1, path("/Cars"), 10);
+    // }
+    //
+    // @Test
+    // public void shouldReIndexRepositoryContentStartingAtNonRootNode() {
+    // for (int i = 0; i != 3; ++i) {
+    // engine.index(workspaceName1, path("/Cars"), 10);
+    // }
+    // }
+    //
+    // @Test
+    // public void shouldFindNodesByFullTextSearch() {
+    // engine.index(workspaceName1, path("/"), 100);
+    // List<Location> results = engine.fullTextSearch(context, workspaceName1, "Toyota Prius", 10, 0);
+    // assertThat(results, is(notNullValue()));
+    // assertThat(results.size(), is(2));
+    // assertThat(results.get(0).getPath(), is(path("/Cars/Hybrid/Toyota Prius")));
+    // assertThat(results.get(1).getPath(), is(path("/Cars/Hybrid/Toyota Highlander")));
+    // }
+    //
+    // @Test
+    // public void shouldFindNodesByFullTextSearchWithOffset() {
+    // engine.index(workspaceName1, path("/"), 100);
+    // List<Location> results = engine.fullTextSearch(context, workspaceName1, "toyota prius", 1, 0);
+    // assertThat(results, is(notNullValue()));
+    // assertThat(results.size(), is(1));
+    // assertThat(results.get(0).getPath(), is(path("/Cars/Hybrid/Toyota Prius")));
+    //
+    // results = engine.fullTextSearch(context, workspaceName1, "+Toyota", 1, 1);
+    // assertThat(results, is(notNullValue()));
+    // assertThat(results.size(), is(1));
+    // assertThat(results.get(0).getPath(), is(path("/Cars/Hybrid/Toyota Highlander")));
+    // }
 }

Deleted: trunk/dna-search/src/test/java/org/jboss/dna/search/WorkspaceSearchEngineTest.java
===================================================================
--- trunk/dna-search/src/test/java/org/jboss/dna/search/WorkspaceSearchEngineTest.java	2009-11-16 23:24:06 UTC (rev 1319)
+++ trunk/dna-search/src/test/java/org/jboss/dna/search/WorkspaceSearchEngineTest.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -1,171 +0,0 @@
-/*
- * JBoss DNA (http://www.jboss.org/dna)
- * See the COPYRIGHT.txt file distributed with this work for information
- * regarding copyright ownership.  Some portions may be licensed
- * to Red Hat, Inc. under one or more contributor license agreements.
- * See the AUTHORS.txt file in the distribution for a full listing of 
- * individual contributors.
- *
- * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
- * is licensed to you under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- * 
- * JBoss DNA is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.search;
-
-import static org.hamcrest.core.Is.is;
-import static org.hamcrest.core.IsNull.notNullValue;
-import static org.junit.Assert.assertThat;
-import java.util.List;
-import org.jboss.dna.graph.ExecutionContext;
-import org.jboss.dna.graph.Graph;
-import org.jboss.dna.graph.Location;
-import org.jboss.dna.graph.connector.RepositoryConnection;
-import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
-import org.jboss.dna.graph.connector.RepositorySourceException;
-import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
-import org.jboss.dna.graph.property.Path;
-import org.junit.Before;
-import org.junit.Test;
-
-public class WorkspaceSearchEngineTest {
-
-    private WorkspaceSearchEngine engine;
-    private ExecutionContext context;
-    private String sourceName;
-    private String workspaceName;
-    private InMemoryRepositorySource source;
-    private RepositoryConnectionFactory connectionFactory;
-    private DirectoryConfiguration directoryFactory;
-    private IndexStrategy indexingStrategy;
-    private Graph content;
-
-    @Before
-    public void beforeEach() throws Exception {
-        context = new ExecutionContext();
-        sourceName = "sourceA";
-        workspaceName = "workspace1";
-
-        // Set up the source and graph instance ...
-        source = new InMemoryRepositorySource();
-        source.setName(sourceName);
-        source.setDefaultWorkspaceName(workspaceName);
-        content = Graph.create(source, context);
-
-        // Load some content ...
-        content.importXmlFrom(getClass().getClassLoader().getResourceAsStream("cars.xml")).into("/");
-
-        // Set up the connection factory ...
-        connectionFactory = new RepositoryConnectionFactory() {
-            @SuppressWarnings( "synthetic-access" )
-            public RepositoryConnection createConnection( String sourceName ) throws RepositorySourceException {
-                return source.getConnection();
-            }
-        };
-
-        // Set up the indexing strategy ...
-        IndexRules rules = IndexRules.createBuilder(KitchenSinkIndexStrategy.DEFAULT_RULES)
-                                           .defaultTo(IndexRules.INDEX | IndexRules.ANALYZE | IndexRules.FULL_TEXT)
-                                           .build();
-        indexingStrategy = new KitchenSinkIndexStrategy(rules);
-
-        // Now set up the search engine ...
-        directoryFactory = DirectoryConfigurations.inMemory();
-        engine = new WorkspaceSearchEngine(context, directoryFactory, indexingStrategy, sourceName, workspaceName,
-                                           connectionFactory);
-    }
-
-    protected Path path( String string ) {
-        return context.getValueFactories().getPathFactory().create(string);
-    }
-
-    protected void assertSearchResults( String fullTextSearch,
-                                        Path... expectedPaths ) {
-        int numExpected = expectedPaths.length;
-        List<Location> results = engine.fullTextSearch(fullTextSearch, numExpected, 0);
-        int numFound = results.size();
-        assertThat("Different number of results were found", numExpected, is(numFound));
-        Path[] actualPaths = new Path[numFound];
-        int i = 0;
-        for (Location actual : results) {
-            actualPaths[i++] = actual.getPath();
-        }
-        assertThat(expectedPaths, is(actualPaths));
-    }
-
-    @Test
-    public void shouldHaveLoadedTestContentIntoRepositorySource() {
-        assertThat(content.getNodeAt("/Cars/Hybrid/Toyota Prius").getProperty("msrp").getFirstValue(), is((Object)"$21,500"));
-    }
-
-    @Test
-    public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfOne() {
-        engine.indexContent(path("/"), 1);
-    }
-
-    @Test
-    public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfTwo() {
-        engine.indexContent(path("/"), 2);
-    }
-
-    @Test
-    public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfThree() {
-        engine.indexContent(path("/"), 3);
-    }
-
-    @Test
-    public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfFour() {
-        engine.indexContent(path("/"), 4);
-    }
-
-    @Test
-    public void shouldIndexRepositoryContentStartingAtRootAndUsingDepthOfTen() {
-        engine.indexContent(path("/"), 10);
-    }
-
-    @Test
-    public void shouldIndexRepositoryContentStartingAtNonRootNode() {
-        engine.indexContent(path("/Cars"), 10);
-    }
-
-    @Test
-    public void shouldReIndexRepositoryContentStartingAtNonRootNode() {
-        for (int i = 0; i != 3; ++i) {
-            engine.indexContent(path("/Cars"), 10);
-        }
-    }
-
-    @Test
-    public void shouldFindNodesByFullTextSearch() {
-        engine.indexContent(path("/"), 100);
-        List<Location> results = engine.fullTextSearch("Toyota Prius", 10, 0);
-        assertThat(results, is(notNullValue()));
-        assertThat(results.size(), is(2));
-        assertThat(results.get(0).getPath(), is(path("/Cars/Hybrid/Toyota Prius")));
-        assertThat(results.get(1).getPath(), is(path("/Cars/Hybrid/Toyota Highlander")));
-    }
-
-    @Test
-    public void shouldFindNodesByFullTextSearchWithOffset() {
-        engine.indexContent(path("/"), 100);
-        List<Location> results = engine.fullTextSearch("toyota prius", 1, 0);
-        assertThat(results, is(notNullValue()));
-        assertThat(results.size(), is(1));
-        assertThat(results.get(0).getPath(), is(path("/Cars/Hybrid/Toyota Prius")));
-
-        results = engine.fullTextSearch("+Toyota", 1, 1);
-        assertThat(results, is(notNullValue()));
-        assertThat(results.size(), is(1));
-        assertThat(results.get(0).getPath(), is(path("/Cars/Hybrid/Toyota Highlander")));
-    }
-}

Added: trunk/dna-search/src/test/java/org/jboss/dna/search/query/LuceneNotQueryTest.java
===================================================================
--- trunk/dna-search/src/test/java/org/jboss/dna/search/query/LuceneNotQueryTest.java	                        (rev 0)
+++ trunk/dna-search/src/test/java/org/jboss/dna/search/query/LuceneNotQueryTest.java	2009-11-16 23:25:36 UTC (rev 1320)
@@ -0,0 +1,126 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ * 
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.search.query;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Similarity;
+import org.jboss.dna.search.query.NotQuery;
+import org.junit.Test;
+
+public class LuceneNotQueryTest {
+
+    @Test
+    public void scorerShouldSkipAdjacentDocsIfScoredByOperandScorer() throws IOException {
+        IndexReader reader = mock(IndexReader.class);
+        stub(reader.isDeleted(anyInt())).toReturn(false);
+        stub(reader.maxDoc()).toReturn(10);
+        Scorer operandScorer = new MockScorer(0, 1, 2, 3, 4);
+        Scorer notScorer = new NotQuery.NotScorer(operandScorer, reader);
+        assertScores(notScorer, 5, 6, 7, 8, 9);
+    }
+
+    @Test
+    public void scorerShouldSkipDocsAtEndIfScoredByOperandScorer() throws IOException {
+        IndexReader reader = mock(IndexReader.class);
+        stub(reader.isDeleted(anyInt())).toReturn(false);
+        stub(reader.maxDoc()).toReturn(10);
+        Scorer operandScorer = new MockScorer(8, 9);
+        Scorer notScorer = new NotQuery.NotScorer(operandScorer, reader);
+        assertScores(notScorer, 0, 1, 2, 3, 4, 5, 6, 7);
+    }
+
+    @Test
+    public void scorerShouldScoreFirstDocsIfNotScoredByOperandScorer() throws IOException {
+        IndexReader reader = mock(IndexReader.class);
+        stub(reader.isDeleted(anyInt())).toReturn(false);
+        stub(reader.maxDoc()).toReturn(10);
+        Scorer operandScorer = new MockScorer(2, 3, 4);
+        Scorer notScorer = new NotQuery.NotScorer(operandScorer, reader);
+        assertScores(notScorer, 0, 1, 5, 6, 7, 8, 9);
+    }
+
+    @Test
+    public void scorerShouldScoreNonAdjacentDocsNotScoredByOperandScorer() throws IOException {
+        IndexReader reader = mock(IndexReader.class);
+        stub(reader.isDeleted(anyInt())).toReturn(false);
+        stub(reader.maxDoc()).toReturn(10);
+        Scorer operandScorer = new MockScorer(2, 4, 8);
+        Scorer notScorer = new NotQuery.NotScorer(operandScorer, reader);
+        assertScores(notScorer, 0, 1, 3, 5, 6, 7, 9);
+    }
+
+    protected void assertScores( Scorer scorer,
+                                 int... docIds ) throws IOException {
+        for (int docId : docIds) {
+            assertThat(scorer.nextDoc(), is(docId));
+            assertThat(scorer.score(), is(1.0f));
+        }
+        assertThat(scorer.nextDoc(), is(Scorer.NO_MORE_DOCS));
+    }
+
+    protected static class MockScorer extends Scorer {
+        private final Iterator<Integer> docIds;
+
+        protected MockScorer( int... docIds ) {
+            super(Similarity.getDefault());
+            List<Integer> ids = new ArrayList<Integer>();
+            for (int docId : docIds) {
+                ids.add(new Integer(docId));
+            }
+            this.docIds = ids.iterator();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.DocIdSetIterator#nextDoc()
+         */
+        @Override
+        public int nextDoc() {
+            if (docIds.hasNext()) return docIds.next();
+            return Scorer.NO_MORE_DOCS;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.apache.lucene.search.Scorer#score()
+         */
+        @Override
+        public float score() {
+            throw new UnsupportedOperationException("Should not be called");
+        }
+    }
+
+}


Property changes on: trunk/dna-search/src/test/java/org/jboss/dna/search/query/LuceneNotQueryTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF



More information about the dna-commits mailing list