View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.search.lucene;
25  
26  import java.io.IOException;
27  import java.io.StringReader;
28  import java.math.BigDecimal;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.HashSet;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Set;
36  import java.util.UUID;
37  import java.util.regex.Pattern;
38  import net.jcip.annotations.NotThreadSafe;
39  import org.apache.lucene.analysis.Analyzer;
40  import org.apache.lucene.analysis.TokenStream;
41  import org.apache.lucene.analysis.tokenattributes.TermAttribute;
42  import org.apache.lucene.document.Document;
43  import org.apache.lucene.document.Field;
44  import org.apache.lucene.document.FieldSelector;
45  import org.apache.lucene.document.FieldSelectorResult;
46  import org.apache.lucene.document.NumericField;
47  import org.apache.lucene.document.Field.Index;
48  import org.apache.lucene.document.Field.Store;
49  import org.apache.lucene.index.IndexReader;
50  import org.apache.lucene.index.IndexWriter;
51  import org.apache.lucene.index.Term;
52  import org.apache.lucene.index.IndexWriter.MaxFieldLength;
53  import org.apache.lucene.queryParser.ParseException;
54  import org.apache.lucene.queryParser.QueryParser;
55  import org.apache.lucene.search.BooleanQuery;
56  import org.apache.lucene.search.IndexSearcher;
57  import org.apache.lucene.search.MatchAllDocsQuery;
58  import org.apache.lucene.search.NumericRangeQuery;
59  import org.apache.lucene.search.PrefixQuery;
60  import org.apache.lucene.search.Query;
61  import org.apache.lucene.search.ScoreDoc;
62  import org.apache.lucene.search.Scorer;
63  import org.apache.lucene.search.TermQuery;
64  import org.apache.lucene.search.TopDocs;
65  import org.apache.lucene.search.BooleanClause.Occur;
66  import org.apache.lucene.search.regex.JavaUtilRegexCapabilities;
67  import org.apache.lucene.search.regex.RegexQuery;
68  import org.apache.lucene.store.Directory;
69  import org.apache.lucene.util.Version;
70  import org.modeshape.common.util.Logger;
71  import org.modeshape.graph.JcrLexicon;
72  import org.modeshape.graph.Location;
73  import org.modeshape.graph.ModeShapeIntLexicon;
74  import org.modeshape.graph.ModeShapeLexicon;
75  import org.modeshape.graph.ModeShapeIntLexicon.Namespace;
76  import org.modeshape.graph.property.DateTime;
77  import org.modeshape.graph.property.Name;
78  import org.modeshape.graph.property.Path;
79  import org.modeshape.graph.property.Property;
80  import org.modeshape.graph.property.Reference;
81  import org.modeshape.graph.property.ValueFactories;
82  import org.modeshape.graph.property.ValueFactory;
83  import org.modeshape.graph.property.basic.BasicName;
84  import org.modeshape.graph.query.QueryResults.Columns;
85  import org.modeshape.graph.query.QueryResults.Statistics;
86  import org.modeshape.graph.query.model.Length;
87  import org.modeshape.graph.query.model.NodeDepth;
88  import org.modeshape.graph.query.model.NodeLocalName;
89  import org.modeshape.graph.query.model.NodeName;
90  import org.modeshape.graph.query.model.NodePath;
91  import org.modeshape.graph.query.model.Operator;
92  import org.modeshape.graph.query.model.PropertyValue;
93  import org.modeshape.graph.query.model.ReferenceValue;
94  import org.modeshape.search.lucene.AbstractLuceneSearchEngine.TupleCollector;
95  import org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession;
96  import org.modeshape.search.lucene.IndexRules.FieldType;
97  import org.modeshape.search.lucene.IndexRules.NumericRule;
98  import org.modeshape.search.lucene.IndexRules.Rule;
99  import org.modeshape.search.lucene.LuceneSearchWorkspace.ContentIndex;
100 import org.modeshape.search.lucene.query.CompareLengthQuery;
101 import org.modeshape.search.lucene.query.CompareNameQuery;
102 import org.modeshape.search.lucene.query.ComparePathQuery;
103 import org.modeshape.search.lucene.query.CompareStringQuery;
104 import org.modeshape.search.lucene.query.MatchNoneQuery;
105 import org.modeshape.search.lucene.query.NotQuery;
106 
107 /**
108  * The {@link WorkspaceSession} implementation for the {@link LuceneSearchEngine}.
109  */
110 @NotThreadSafe
111 public class LuceneSearchSession implements WorkspaceSession {
112 
113     protected static final Set<Name> NON_SEARCHABLE_NAMES = Collections.unmodifiableSet(new HashSet<Name>(
114                                                                                                           Arrays.asList(JcrLexicon.UUID,
115                                                                                                                         ModeShapeLexicon.UUID,
116                                                                                                                         JcrLexicon.PRIMARY_TYPE,
117                                                                                                                         JcrLexicon.MIXIN_TYPES,
118                                                                                                                         ModeShapeIntLexicon.NODE_DEFINITON,
119                                                                                                                         new BasicName(
120                                                                                                                                       Namespace.URI,
121                                                                                                                                       "multiValuedProperties"))));
122 
123     /**
124      * An immutable {@link FieldSelector} instance that accesses the UUID field.
125      */
126     protected static final FieldSelector LOCATION_FIELDS_SELECTOR = new FieldSelector() {
127         private static final long serialVersionUID = 1L;
128 
129         public FieldSelectorResult accept( String fieldName ) {
130             if (ContentIndex.PATH.equals(fieldName) || ContentIndex.LOCATION_ID_PROPERTIES.equals(fieldName)) {
131                 return FieldSelectorResult.LOAD;
132             }
133             return FieldSelectorResult.NO_LOAD;
134         }
135     };
136 
137     protected static final int MIN_DEPTH = 0;
138     protected static final int MAX_DEPTH = 100;
139     protected static final int MIN_SNS_INDEX = 1;
140     protected static final int MAX_SNS_INDEX = 1000; // assume there won't be more than 1000 same-name-siblings
141 
142     private final LuceneSearchWorkspace workspace;
143     protected final LuceneSearchProcessor processor;
144     private final Directory contentIndexDirectory;
145     private IndexReader contentReader;
146     private IndexWriter contentWriter;
147     private IndexSearcher contentSearcher;
148     private int numChanges;
149     private final Logger logger = Logger.getLogger(getClass());
150 
151     protected LuceneSearchSession( LuceneSearchWorkspace workspace,
152                                    LuceneSearchProcessor processor ) {
153         assert workspace != null;
154         assert processor != null;
155         this.workspace = workspace;
156         this.contentIndexDirectory = workspace.contentDirectory;
157         this.processor = processor;
158     }
159 
160     /**
161      * {@inheritDoc}
162      * 
163      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#getWorkspaceName()
164      */
165     public String getWorkspaceName() {
166         return workspace.getWorkspaceName();
167     }
168 
169     /**
170      * @return workspace
171      */
172     public LuceneSearchWorkspace getWorkspace() {
173         return workspace;
174     }
175 
176     protected IndexReader getContentReader() throws IOException {
177         if (contentReader == null) {
178             try {
179                 contentReader = IndexReader.open(contentIndexDirectory, processor.readOnly);
180             } catch (IOException e) {
181                 // try creating the workspace ...
182                 IndexWriter writer = new IndexWriter(contentIndexDirectory, workspace.analyzer, MaxFieldLength.UNLIMITED);
183                 writer.close();
184                 // And try reading again ...
185                 contentReader = IndexReader.open(contentIndexDirectory, processor.readOnly);
186             }
187         }
188         return contentReader;
189     }
190 
191     protected IndexWriter getContentWriter() throws IOException {
192         assert !processor.readOnly;
193         if (contentWriter == null) {
194             // Don't overwrite, but create if missing ...
195             contentWriter = new IndexWriter(contentIndexDirectory, workspace.analyzer, MaxFieldLength.UNLIMITED);
196         }
197         return contentWriter;
198     }
199 
200     public IndexSearcher getContentSearcher() throws IOException {
201         if (contentSearcher == null) {
202             contentSearcher = new IndexSearcher(getContentReader());
203         }
204         return contentSearcher;
205     }
206 
207     /**
208      * {@inheritDoc}
209      * 
210      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#getAnalyzer()
211      */
212     public Analyzer getAnalyzer() {
213         return workspace.analyzer;
214     }
215 
216     public boolean hasWriters() {
217         return contentWriter != null;
218     }
219 
220     protected final void recordChange() {
221         ++numChanges;
222     }
223 
224     protected final void recordChanges( int numberOfChanges ) {
225         assert numberOfChanges >= 0;
226         numChanges += numberOfChanges;
227     }
228 
229     /**
230      * {@inheritDoc}
231      * 
232      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#getChangeCount()
233      */
234     public final int getChangeCount() {
235         return numChanges;
236     }
237 
238     /**
239      * {@inheritDoc}
240      * 
241      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#commit()
242      */
243     public void commit() {
244         if (logger.isTraceEnabled() && numChanges > 0) {
245             logger.trace("index for \"{0}\" workspace: COMMIT", workspace.getWorkspaceName());
246         }
247 
248         // Is optimization required ...
249         final boolean optimize = workspace.isOptimizationRequired(numChanges);
250         numChanges = 0;
251 
252         IOException ioError = null;
253         RuntimeException runtimeError = null;
254         if (contentReader != null) {
255             try {
256                 contentReader.close();
257             } catch (IOException e) {
258                 ioError = e;
259             } catch (RuntimeException e) {
260                 runtimeError = e;
261             } finally {
262                 contentReader = null;
263             }
264         }
265         if (contentWriter != null) {
266             try {
267                 if (optimize) contentWriter.optimize();
268             } catch (IOException e) {
269                 if (ioError == null) ioError = e;
270             } catch (RuntimeException e) {
271                 if (runtimeError == null) runtimeError = e;
272             } finally {
273                 try {
274                     contentWriter.close();
275                 } catch (IOException e) {
276                     if (ioError == null) ioError = e;
277                 } catch (RuntimeException e) {
278                     if (runtimeError == null) runtimeError = e;
279                 } finally {
280                     contentWriter = null;
281                 }
282             }
283         }
284         if (ioError != null) {
285             String msg = LuceneI18n.errorWhileCommittingIndexChanges.text(workspace.getWorkspaceName(),
286                                                                           processor.getSourceName(),
287                                                                           ioError.getMessage());
288             throw new LuceneException(msg, ioError);
289         }
290         if (runtimeError != null) throw runtimeError;
291     }
292 
293     /**
294      * {@inheritDoc}
295      * 
296      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#rollback()
297      */
298     public void rollback() {
299         if (logger.isTraceEnabled() && numChanges > 0) {
300             logger.trace("index for \"{0}\" workspace: ROLLBACK", workspace.getWorkspaceName());
301         }
302         numChanges = 0;
303         IOException ioError = null;
304         RuntimeException runtimeError = null;
305         if (contentReader != null) {
306             try {
307                 contentReader.close();
308             } catch (IOException e) {
309                 ioError = e;
310             } catch (RuntimeException e) {
311                 runtimeError = e;
312             } finally {
313                 contentReader = null;
314             }
315         }
316         if (contentWriter != null) {
317             try {
318                 contentWriter.rollback();
319             } catch (IOException e) {
320                 if (ioError == null) ioError = e;
321             } catch (RuntimeException e) {
322                 if (runtimeError == null) runtimeError = e;
323             } finally {
324                 try {
325                     contentWriter.close();
326                 } catch (IOException e) {
327                     ioError = e;
328                 } catch (RuntimeException e) {
329                     runtimeError = e;
330                 } finally {
331                     contentWriter = null;
332                 }
333             }
334         }
335         if (ioError != null) {
336             String msg = LuceneI18n.errorWhileRollingBackIndexChanges.text(workspace.getWorkspaceName(),
337                                                                            processor.getSourceName(),
338                                                                            ioError.getMessage());
339             throw new LuceneException(msg, ioError);
340         }
341         if (runtimeError != null) throw runtimeError;
342     }
343 
344     protected Statistics search( String fullTextSearchExpression,
345                                  List<Object[]> results,
346                                  int maxRows,
347                                  int offset ) throws ParseException, IOException {
348         // Parse the full-text search and search against the 'fts' field ...
349         long planningNanos = System.nanoTime();
350         QueryParser parser = new QueryParser(Version.LUCENE_29, ContentIndex.FULL_TEXT, workspace.analyzer);
351         Query query = parser.parse(fullTextSearchExpression);
352         planningNanos = System.nanoTime() - planningNanos;
353 
354         // Execute the search and place the results into the supplied list ...
355         TopDocs docs = getContentSearcher().search(query, maxRows + offset);
356         IndexReader contentReader = getContentReader();
357         ScoreDoc[] scoreDocs = docs.scoreDocs;
358         int numberOfResults = scoreDocs.length;
359         if (numberOfResults > offset) {
360             // There are enough results to satisfy the offset ...
361             for (int i = offset, num = scoreDocs.length; i != num; ++i) {
362                 ScoreDoc result = scoreDocs[i];
363                 int docId = result.doc;
364                 // Find the UUID of the node (this UUID might be artificial, so we have to find the path) ...
365                 Document doc = contentReader.document(docId, LOCATION_FIELDS_SELECTOR);
366                 Location location = readLocation(doc);
367                 // Now add the location ...
368                 results.add(new Object[] {location, result.score});
369             }
370         }
371         long executionNanos = System.nanoTime() - planningNanos;
372         return new Statistics(planningNanos, 0L, 0L, executionNanos);
373     }
374 
375     protected Location readLocation( Document doc ) {
376         // Read the path ...
377         String pathString = doc.get(ContentIndex.PATH);
378         Path path = processor.pathFactory.create(pathString);
379         // Look for the Location's ID properties ...
380         String[] idProps = doc.getValues(ContentIndex.LOCATION_ID_PROPERTIES);
381         if (idProps.length == 0) {
382             return Location.create(path);
383         }
384         if (idProps.length == 1) {
385             Property idProp = processor.deserializeProperty(idProps[0]);
386             if (idProp == null) return Location.create(path);
387             if (idProp.isSingle() && (idProp.getName().equals(JcrLexicon.UUID) || idProp.getName().equals(ModeShapeLexicon.UUID))) {
388                 return Location.create(path, (UUID)idProp.getFirstValue()); // know that deserialize returns UUID value
389             }
390             return Location.create(path, idProp);
391         }
392         List<Property> properties = new LinkedList<Property>();
393         for (String idProp : idProps) {
394             Property prop = processor.deserializeProperty(idProp);
395             if (prop != null) properties.add(prop);
396         }
397         return properties.isEmpty() ? Location.create(path) : Location.create(path, properties);
398     }
399 
400     protected void setOrReplaceProperties( Location location,
401                                            Iterable<Property> properties ) throws IOException {
402         // Create the document for the content (properties) ...
403         Document doc = new Document();
404 
405         // Add the information every node has ...
406         Path path = location.getPath();
407         String pathStr = processor.pathAsString(path);
408         String nameStr = path.isRoot() ? "" : processor.stringFactory.create(path.getLastSegment().getName());
409         String localNameStr = path.isRoot() ? "" : path.getLastSegment().getName().getLocalName();
410         int sns = path.isRoot() ? 1 : path.getLastSegment().getIndex();
411 
412         // Create a separate document for the path, which makes it easier to handle moves since the path can
413         // be changed without changing any other content fields ...
414         doc.add(new Field(ContentIndex.PATH, pathStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
415         doc.add(new Field(ContentIndex.NODE_NAME, nameStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
416         doc.add(new Field(ContentIndex.LOCAL_NAME, localNameStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
417         doc.add(new NumericField(ContentIndex.SNS_INDEX, Field.Store.YES, true).setIntValue(sns));
418         doc.add(new NumericField(ContentIndex.DEPTH, Field.Store.YES, true).setIntValue(path.size()));
419         if (location.hasIdProperties()) {
420             for (Property idProp : location.getIdProperties()) {
421                 String fieldValue = processor.serializeProperty(idProp);
422                 doc.add(new Field(ContentIndex.LOCATION_ID_PROPERTIES, fieldValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
423             }
424         }
425 
426         // Always include the local name in the full-text search field ...
427         StringBuilder fullTextSearchValue = new StringBuilder();
428         fullTextSearchValue.append(localNameStr);
429 
430         // Index the properties
431         String stringValue = null;
432         for (Property property : properties) {
433             Name name = property.getName();
434             Rule rule = workspace.rules.getRule(name);
435             if (rule.isSkipped()) continue;
436             String nameString = processor.stringFactory.create(name);
437             FieldType type = rule.getType();
438             if (type == FieldType.DATE) {
439                 boolean index = rule.getIndexOption() != Field.Index.NO;
440                 for (Object value : property) {
441                     if (value == null) continue;
442                     // Add a separate field for each property value ...
443                     DateTime dateValue = processor.dateFactory.create(value);
444                     long longValue = dateValue.getMillisecondsInUtc();
445                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setLongValue(longValue));
446                 }
447                 continue;
448             }
449             if (type == FieldType.INT) {
450                 ValueFactory<Long> longFactory = processor.valueFactories.getLongFactory();
451                 boolean index = rule.getIndexOption() != Field.Index.NO;
452                 for (Object value : property) {
453                     if (value == null) continue;
454                     // Add a separate field for each property value ...
455                     int intValue = longFactory.create(value).intValue();
456                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setIntValue(intValue));
457                 }
458                 continue;
459             }
460             if (type == FieldType.DOUBLE) {
461                 ValueFactory<Double> doubleFactory = processor.valueFactories.getDoubleFactory();
462                 boolean index = rule.getIndexOption() != Field.Index.NO;
463                 for (Object value : property) {
464                     if (value == null) continue;
465                     // Add a separate field for each property value ...
466                     double dValue = doubleFactory.create(value);
467                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setDoubleValue(dValue));
468                 }
469                 continue;
470             }
471             if (type == FieldType.FLOAT) {
472                 ValueFactory<Double> doubleFactory = processor.valueFactories.getDoubleFactory();
473                 boolean index = rule.getIndexOption() != Field.Index.NO;
474                 for (Object value : property) {
475                     if (value == null) continue;
476                     // Add a separate field for each property value ...
477                     float fValue = doubleFactory.create(value).floatValue();
478                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setFloatValue(fValue));
479                 }
480                 continue;
481             }
482             if (type == FieldType.DECIMAL) {
483                 ValueFactory<BigDecimal> decimalFactory = processor.valueFactories.getDecimalFactory();
484                 for (Object value : property) {
485                     if (value == null) continue;
486                     BigDecimal decimal = decimalFactory.create(value);
487                     // Convert to a string that is lexicographically sortable ...
488                     value = FieldUtil.decimalToString(decimal);
489                     doc.add(new Field(nameString, stringValue, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
490                 }
491                 continue;
492             }
493             if (type == FieldType.BINARY) {
494                 // TODO : add to full-text search ...
495                 continue;
496             }
497             if (type == FieldType.WEAK_REFERENCE) {
498                 ValueFactory<Path> pathFactory = processor.valueFactories.getPathFactory();
499                 for (Object value : property) {
500                     if (value == null) continue;
501                     // Add a separate field for each property value ...
502                     String valueStr = processor.stringFactory.create(pathFactory.create(value));
503                     doc.add(new Field(nameString, valueStr, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
504                     // Add a value to the common reference value ...
505                     doc.add(new Field(ContentIndex.REFERENCES, stringValue, Field.Store.NO, Field.Index.NOT_ANALYZED));
506                 }
507                 continue;
508             }
509             if (type == FieldType.REFERENCE) {
510                 for (Object value : property) {
511                     if (value == null) continue;
512                     // Obtain the string value of the reference (i.e., should be the string of the UUID) ...
513                     stringValue = processor.stringFactory.create(value);
514                     // Add a separate field for each property value in exact form (not analyzed) ...
515                     doc.add(new Field(nameString, stringValue, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
516                     // Add a value to the common reference value ...
517                     doc.add(new Field(ContentIndex.REFERENCES, stringValue, Field.Store.NO, Field.Index.NOT_ANALYZED));
518                     // Add a value to the strong reference value ...
519                     doc.add(new Field(ContentIndex.STRONG_REFERENCES, stringValue, Field.Store.NO, Field.Index.NOT_ANALYZED));
520                 }
521                 continue;
522             }
523             if (type == FieldType.BOOLEAN) {
524                 ValueFactory<Boolean> booleanFactory = processor.valueFactories.getBooleanFactory();
525                 boolean index = rule.getIndexOption() != Field.Index.NO;
526                 for (Object value : property) {
527                     if (value == null) continue;
528                     // Add a separate field for each property value ...
529                     int intValue = booleanFactory.create(value).booleanValue() ? 1 : 0;
530                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setIntValue(intValue));
531                 }
532                 continue;
533             }
534             assert type == FieldType.STRING;
535             for (Object value : property) {
536                 if (value == null) continue;
537                 stringValue = processor.stringFactory.create(value);
538                 // Add a separate field for each property value in exact form (not analyzed) ...
539                 doc.add(new Field(nameString, stringValue, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
540 
541                 boolean treatedAsReference = false;
542                 if (value instanceof Reference) {
543                     Reference ref = (Reference)value;
544                     doc.add(new Field(ContentIndex.REFERENCES, stringValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
545                     if (!ref.isWeak()) {
546                         doc.add(new Field(ContentIndex.STRONG_REFERENCES, stringValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
547                     }
548                     treatedAsReference = true;
549                 } else if (rule.canBeReference()) {
550                     if (stringValue.length() == 36 && stringValue.charAt(8) == '-') {
551                         // The value looks like a string representation of a UUID ...
552                         try {
553                             UUID.fromString(stringValue);
554                             // Add a value to the common reference value ...
555                             treatedAsReference = true;
556                             doc.add(new Field(ContentIndex.REFERENCES, stringValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
557                         } catch (IllegalArgumentException e) {
558                             // Must not conform to the UUID format
559                         }
560                     }
561                 }
562                 if (!treatedAsReference && rule.getIndexOption() != Field.Index.NO && rule.isFullTextSearchable()
563                     && !NON_SEARCHABLE_NAMES.contains(name)) {
564                     // This field is to be full-text searchable ...
565                     fullTextSearchValue.append(' ').append(stringValue);
566 
567                     // Also create a full-text-searchable field ...
568                     String fullTextNameString = processor.fullTextFieldName(nameString);
569                     doc.add(new Field(fullTextNameString, stringValue, Store.NO, Index.ANALYZED));
570                 }
571             }
572         }
573         // Add the full-text-search field ...
574         if (fullTextSearchValue.length() != 0) {
575             doc.add(new Field(ContentIndex.FULL_TEXT, fullTextSearchValue.toString(), Field.Store.NO, Field.Index.ANALYZED));
576         }
577         if (logger.isTraceEnabled()) {
578             logger.trace("index for \"{0}\" workspace: ADD {1} {2}", workspace.getWorkspaceName(), pathStr, doc);
579             if (fullTextSearchValue.length() != 0) {
580 
581                 // Run the expression through the Lucene analyzer to extract the terms ...
582                 String fullTextContent = fullTextSearchValue.toString();
583                 TokenStream stream = getAnalyzer().tokenStream(ContentIndex.FULL_TEXT, new StringReader(fullTextContent));
584                 TermAttribute term = stream.addAttribute(TermAttribute.class);
585                 // PositionIncrementAttribute positionIncrement = stream.addAttribute(PositionIncrementAttribute.class);
586                 // OffsetAttribute offset = stream.addAttribute(OffsetAttribute.class);
587                 // TypeAttribute type = stream.addAttribute(TypeAttribute.class);
588                 // int position = 0;
589                 StringBuilder output = new StringBuilder();
590                 while (stream.incrementToken()) {
591                     output.append(term.term()).append(' ');
592                     // // The term attribute object has been modified to contain the next term ...
593                     // int incr = positionIncrement.getPositionIncrement();
594                     // if (incr > 0) {
595                     // position = position + incr;
596                     // output.append(' ').append(position).append(':');
597                     // }
598                     // output.append('[')
599                     // .append(term.term())
600                     // .append(':')
601                     // .append(offset.startOffset())
602                     // .append("->")
603                     // .append(offset.endOffset())
604                     // .append(':')
605                     // .append(type.type())
606                     // .append(']');
607                 }
608                 logger.trace("index for \"{0}\" workspace:     {1} fts terms: {2}", workspace.getWorkspaceName(), pathStr, output);
609             }
610         }
611         getContentWriter().updateDocument(new Term(ContentIndex.PATH, pathStr), doc);
612     }
613 
614     /**
615      * {@inheritDoc}
616      * 
617      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#createTupleCollector(org.modeshape.graph.query.QueryResults.Columns)
618      */
619     public TupleCollector createTupleCollector( Columns columns ) {
620         return new DualIndexTupleCollector(this, columns);
621     }
622 
623     public Location getLocationForRoot() throws IOException {
624         // Look for the root node ...
625         Query query = NumericRangeQuery.newIntRange(ContentIndex.DEPTH, 0, 0, true, true);
626 
627         // Execute the search and place the results into the supplied list ...
628         List<Object[]> tuples = new ArrayList<Object[]>(1);
629         FullTextSearchTupleCollector collector = new FullTextSearchTupleCollector(this, tuples);
630         getContentSearcher().search(query, collector);
631 
632         // Extract the location from the results ...
633         return tuples.isEmpty() ? Location.create(processor.pathFactory.createRootPath()) : (Location)tuples.get(0)[0];
634     }
635 
636     public Query findAllNodesBelow( Path parentPath ) {
637         // Find the path of the parent ...
638         String stringifiedPath = processor.pathAsString(parentPath);
639         // Append a '/' to the parent path, and we'll only get decendants ...
640         stringifiedPath = stringifiedPath + '/';
641 
642         // Create a prefix query ...
643         return new PrefixQuery(new Term(ContentIndex.PATH, stringifiedPath));
644     }
645 
646     public Query findAllNodesAtOrBelow( Path parentPath ) {
647         if (parentPath.isRoot()) {
648             return new MatchAllDocsQuery();
649         }
650         // Find the path of the parent ...
651         String stringifiedPath = processor.pathAsString(parentPath);
652 
653         // Create a prefix query ...
654         return new PrefixQuery(new Term(ContentIndex.PATH, stringifiedPath));
655     }
656 
657     /**
658      * Return a query that can be used to find all of the documents that represent nodes that are children of the node at the
659      * supplied path.
660      * 
661      * @param parentPath the path of the parent node.
662      * @return the query; never null
663      */
664     public Query findChildNodes( Path parentPath ) {
665         // Find the path of the parent ...
666         String stringifiedPath = processor.pathAsString(parentPath);
667         // Append a '/' to the parent path, so we'll only get decendants ...
668         stringifiedPath = stringifiedPath + '/';
669 
670         // Create a query to find all the nodes below the parent path ...
671         Query query = new PrefixQuery(new Term(ContentIndex.PATH, stringifiedPath));
672         // Include only the children ...
673         int childrenDepth = parentPath.size() + 1;
674         Query depthQuery = NumericRangeQuery.newIntRange(ContentIndex.DEPTH, childrenDepth, childrenDepth, true, true);
675         // And combine ...
676         BooleanQuery combinedQuery = new BooleanQuery();
677         combinedQuery.add(query, Occur.MUST);
678         combinedQuery.add(depthQuery, Occur.MUST);
679         return combinedQuery;
680     }
681 
682     /**
683      * Create a query that can be used to find the one document (or node) that exists at the exact path supplied.
684      * 
685      * @param path the path of the node
686      * @return the query; never null
687      */
688     public Query findNodeAt( Path path ) {
689         if (path.isRoot()) {
690             // Look for the root node ...
691             return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, 0, 0, true, true);
692         }
693         String stringifiedPath = processor.pathAsString(path);
694         return new TermQuery(new Term(ContentIndex.PATH, stringifiedPath));
695     }
696 
697     public Query findNodesLike( String fieldName,
698                                 String likeExpression,
699                                 boolean caseSensitive ) {
700         ValueFactories factories = processor.valueFactories;
701         return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression, fieldName, factories, caseSensitive);
702     }
703 
704     public Query findNodesWith( Length propertyLength,
705                                 Operator operator,
706                                 Object value ) {
707         assert propertyLength != null;
708         assert value != null;
709         PropertyValue propertyValue = propertyLength.propertyValue();
710         String field = processor.stringFactory.create(propertyValue.propertyName());
711         ValueFactories factories = processor.valueFactories;
712         int length = factories.getLongFactory().create(value).intValue();
713         switch (operator) {
714             case EQUAL_TO:
715                 return CompareLengthQuery.createQueryForNodesWithFieldEqualTo(length, field, factories);
716             case NOT_EQUAL_TO:
717                 return CompareLengthQuery.createQueryForNodesWithFieldNotEqualTo(length, field, factories);
718             case GREATER_THAN:
719                 return CompareLengthQuery.createQueryForNodesWithFieldGreaterThan(length, field, factories);
720             case GREATER_THAN_OR_EQUAL_TO:
721                 return CompareLengthQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(length, field, factories);
722             case LESS_THAN:
723                 return CompareLengthQuery.createQueryForNodesWithFieldLessThan(length, field, factories);
724             case LESS_THAN_OR_EQUAL_TO:
725                 return CompareLengthQuery.createQueryForNodesWithFieldLessThanOrEqualTo(length, field, factories);
726             case LIKE:
727                 // This is not allowed ...
728                 assert false;
729                 break;
730         }
731         return null;
732     }
733 
734     @SuppressWarnings( "unchecked" )
735     public Query findNodesWith( PropertyValue propertyValue,
736                                 Operator operator,
737                                 Object value,
738                                 boolean caseSensitive ) {
739         ValueFactory<String> stringFactory = processor.stringFactory;
740         String field = stringFactory.create(propertyValue.propertyName());
741         Name fieldName = processor.nameFactory.create(field);
742         ValueFactories factories = processor.valueFactories;
743         IndexRules.Rule rule = workspace.rules.getRule(fieldName);
744         if (rule == null || rule.isSkipped()) return new MatchNoneQuery();
745         FieldType type = rule.getType();
746         switch (type) {
747             case REFERENCE:
748             case WEAK_REFERENCE:
749             case DECIMAL: // stored in lexicographically-ordered form
750             case STRING:
751                 String stringValue = stringFactory.create(value);
752                 if (value instanceof Path) {
753                     stringValue = processor.pathAsString((Path)value);
754                 }
755                 if (!caseSensitive) stringValue = stringValue.toLowerCase();
756                 switch (operator) {
757                     case EQUAL_TO:
758                         return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue,
759                                                                                       field,
760                                                                                       factories,
761                                                                                       caseSensitive);
762                     case NOT_EQUAL_TO:
763                         Query query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue,
764                                                                                              field,
765                                                                                              factories,
766                                                                                              caseSensitive);
767                         return new NotQuery(query);
768                     case GREATER_THAN:
769                         return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue,
770                                                                                           field,
771                                                                                           factories,
772                                                                                           caseSensitive);
773                     case GREATER_THAN_OR_EQUAL_TO:
774                         return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue,
775                                                                                                    field,
776                                                                                                    factories,
777                                                                                                    caseSensitive);
778                     case LESS_THAN:
779                         return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue,
780                                                                                        field,
781                                                                                        factories,
782                                                                                        caseSensitive);
783                     case LESS_THAN_OR_EQUAL_TO:
784                         return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue,
785                                                                                                 field,
786                                                                                                 factories,
787                                                                                                 caseSensitive);
788                     case LIKE:
789                         return findNodesLike(field, stringValue, caseSensitive);
790                 }
791                 break;
792             case DATE:
793                 NumericRule<Long> longRule = (NumericRule<Long>)rule;
794                 long date = factories.getLongFactory().create(value);
795                 switch (operator) {
796                     case EQUAL_TO:
797                         if (date < longRule.getMinimum() || date > longRule.getMaximum()) return new MatchNoneQuery();
798                         return NumericRangeQuery.newLongRange(field, date, date, true, true);
799                     case NOT_EQUAL_TO:
800                         if (date < longRule.getMinimum() || date > longRule.getMaximum()) return new MatchAllDocsQuery();
801                         Query query = NumericRangeQuery.newLongRange(field, date, date, true, true);
802                         return new NotQuery(query);
803                     case GREATER_THAN:
804                         if (date > longRule.getMaximum()) return new MatchNoneQuery();
805                         return NumericRangeQuery.newLongRange(field, date, longRule.getMaximum(), false, true);
806                     case GREATER_THAN_OR_EQUAL_TO:
807                         if (date > longRule.getMaximum()) return new MatchNoneQuery();
808                         return NumericRangeQuery.newLongRange(field, date, longRule.getMaximum(), true, true);
809                     case LESS_THAN:
810                         if (date < longRule.getMinimum()) return new MatchNoneQuery();
811                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), date, true, false);
812                     case LESS_THAN_OR_EQUAL_TO:
813                         if (date < longRule.getMinimum()) return new MatchNoneQuery();
814                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), date, true, true);
815                     case LIKE:
816                         // This is not allowed ...
817                         assert false;
818                         return null;
819                 }
820                 break;
821             case LONG:
822                 longRule = (NumericRule<Long>)rule;
823                 long longValue = factories.getLongFactory().create(value);
824                 switch (operator) {
825                     case EQUAL_TO:
826                         if (longValue < longRule.getMinimum() || longValue > longRule.getMaximum()) return new MatchNoneQuery();
827                         return NumericRangeQuery.newLongRange(field, longValue, longValue, true, true);
828                     case NOT_EQUAL_TO:
829                         if (longValue < longRule.getMinimum() || longValue > longRule.getMaximum()) return new MatchAllDocsQuery();
830                         Query query = NumericRangeQuery.newLongRange(field, longValue, longValue, true, true);
831                         return new NotQuery(query);
832                     case GREATER_THAN:
833                         if (longValue > longRule.getMaximum()) return new MatchNoneQuery();
834                         return NumericRangeQuery.newLongRange(field, longValue, longRule.getMaximum(), false, true);
835                     case GREATER_THAN_OR_EQUAL_TO:
836                         if (longValue > longRule.getMaximum()) return new MatchNoneQuery();
837                         return NumericRangeQuery.newLongRange(field, longValue, longRule.getMaximum(), true, true);
838                     case LESS_THAN:
839                         if (longValue < longRule.getMinimum()) return new MatchNoneQuery();
840                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), longValue, true, false);
841                     case LESS_THAN_OR_EQUAL_TO:
842                         if (longValue < longRule.getMinimum()) return new MatchNoneQuery();
843                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), longValue, true, true);
844                     case LIKE:
845                         // This is not allowed ...
846                         assert false;
847                         return null;
848                 }
849                 break;
850             case BOOLEAN:
851                 boolean booleanValue = factories.getBooleanFactory().create(value);
852                 stringValue = stringFactory.create(value);
853                 switch (operator) {
854                     case EQUAL_TO:
855                         return new TermQuery(new Term(field, stringValue));
856                     case NOT_EQUAL_TO:
857                         return new TermQuery(new Term(field, stringFactory.create(!booleanValue)));
858                     case GREATER_THAN:
859                         if (!booleanValue) {
860                             return new TermQuery(new Term(field, stringFactory.create(true)));
861                         }
862                         // Can't be greater than 'true', per JCR spec
863                         return new MatchNoneQuery();
864                     case GREATER_THAN_OR_EQUAL_TO:
865                         return new TermQuery(new Term(field, stringFactory.create(true)));
866                     case LESS_THAN:
867                         if (booleanValue) {
868                             return new TermQuery(new Term(field, stringFactory.create(false)));
869                         }
870                         // Can't be less than 'false', per JCR spec
871                         return new MatchNoneQuery();
872                     case LESS_THAN_OR_EQUAL_TO:
873                         return new TermQuery(new Term(field, stringFactory.create(false)));
874                     case LIKE:
875                         // This is not allowed ...
876                         assert false;
877                         return null;
878                 }
879                 break;
880             case INT:
881                 NumericRule<Integer> intRule = (NumericRule<Integer>)rule;
882                 int intValue = factories.getLongFactory().create(value).intValue();
883                 switch (operator) {
884                     case EQUAL_TO:
885                         if (intValue < intRule.getMinimum() || intValue > intRule.getMaximum()) return new MatchNoneQuery();
886                         return NumericRangeQuery.newIntRange(field, intValue, intValue, true, true);
887                     case NOT_EQUAL_TO:
888                         if (intValue < intRule.getMinimum() || intValue > intRule.getMaximum()) return new MatchAllDocsQuery();
889                         Query query = NumericRangeQuery.newIntRange(field, intValue, intValue, true, true);
890                         return new NotQuery(query);
891                     case GREATER_THAN:
892                         if (intValue > intRule.getMaximum()) return new MatchNoneQuery();
893                         return NumericRangeQuery.newIntRange(field, intValue, intRule.getMaximum(), false, true);
894                     case GREATER_THAN_OR_EQUAL_TO:
895                         if (intValue > intRule.getMaximum()) return new MatchNoneQuery();
896                         return NumericRangeQuery.newIntRange(field, intValue, intRule.getMaximum(), true, true);
897                     case LESS_THAN:
898                         if (intValue < intRule.getMinimum()) return new MatchNoneQuery();
899                         return NumericRangeQuery.newIntRange(field, intRule.getMinimum(), intValue, true, false);
900                     case LESS_THAN_OR_EQUAL_TO:
901                         if (intValue < intRule.getMinimum()) return new MatchNoneQuery();
902                         return NumericRangeQuery.newIntRange(field, intRule.getMinimum(), intValue, true, true);
903                     case LIKE:
904                         // This is not allowed ...
905                         assert false;
906                         return null;
907                 }
908                 break;
909             case DOUBLE:
910                 NumericRule<Double> dRule = (NumericRule<Double>)rule;
911                 double doubleValue = factories.getDoubleFactory().create(value);
912                 switch (operator) {
913                     case EQUAL_TO:
914                         if (doubleValue < dRule.getMinimum() || doubleValue > dRule.getMaximum()) return new MatchNoneQuery();
915                         return NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true);
916                     case NOT_EQUAL_TO:
917                         if (doubleValue < dRule.getMinimum() || doubleValue > dRule.getMaximum()) return new MatchAllDocsQuery();
918                         Query query = NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true);
919                         return new NotQuery(query);
920                     case GREATER_THAN:
921                         if (doubleValue > dRule.getMaximum()) return new MatchNoneQuery();
922                         return NumericRangeQuery.newDoubleRange(field, doubleValue, dRule.getMaximum(), false, true);
923                     case GREATER_THAN_OR_EQUAL_TO:
924                         if (doubleValue > dRule.getMaximum()) return new MatchNoneQuery();
925                         return NumericRangeQuery.newDoubleRange(field, doubleValue, dRule.getMaximum(), true, true);
926                     case LESS_THAN:
927                         if (doubleValue < dRule.getMinimum()) return new MatchNoneQuery();
928                         return NumericRangeQuery.newDoubleRange(field, dRule.getMinimum(), doubleValue, true, false);
929                     case LESS_THAN_OR_EQUAL_TO:
930                         if (doubleValue < dRule.getMinimum()) return new MatchNoneQuery();
931                         return NumericRangeQuery.newDoubleRange(field, dRule.getMinimum(), doubleValue, true, true);
932                     case LIKE:
933                         // This is not allowed ...
934                         assert false;
935                         return null;
936                 }
937                 break;
938             case FLOAT:
939                 NumericRule<Float> fRule = (NumericRule<Float>)rule;
940                 float floatValue = factories.getDoubleFactory().create(value).floatValue();
941                 switch (operator) {
942                     case EQUAL_TO:
943                         if (floatValue < fRule.getMinimum() || floatValue > fRule.getMaximum()) return new MatchNoneQuery();
944                         return NumericRangeQuery.newFloatRange(field, floatValue, floatValue, true, true);
945                     case NOT_EQUAL_TO:
946                         if (floatValue < fRule.getMinimum() || floatValue > fRule.getMaximum()) return new MatchAllDocsQuery();
947                         Query query = NumericRangeQuery.newFloatRange(field, floatValue, floatValue, true, true);
948                         return new NotQuery(query);
949                     case GREATER_THAN:
950                         if (floatValue > fRule.getMaximum()) return new MatchNoneQuery();
951                         return NumericRangeQuery.newFloatRange(field, floatValue, fRule.getMaximum(), false, true);
952                     case GREATER_THAN_OR_EQUAL_TO:
953                         if (floatValue > fRule.getMaximum()) return new MatchNoneQuery();
954                         return NumericRangeQuery.newFloatRange(field, floatValue, fRule.getMaximum(), true, true);
955                     case LESS_THAN:
956                         if (floatValue < fRule.getMinimum()) return new MatchNoneQuery();
957                         return NumericRangeQuery.newFloatRange(field, fRule.getMinimum(), floatValue, true, false);
958                     case LESS_THAN_OR_EQUAL_TO:
959                         if (floatValue < fRule.getMinimum()) return new MatchNoneQuery();
960                         return NumericRangeQuery.newFloatRange(field, fRule.getMinimum(), floatValue, true, true);
961                     case LIKE:
962                         // This is not allowed ...
963                         assert false;
964                         return null;
965                 }
966                 break;
967             case BINARY:
968                 // This is not allowed ...
969                 assert false;
970                 return null;
971         }
972         return null;
973     }
974 
975     /**
976      * {@inheritDoc}
977      * 
978      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#findNodesWith(org.modeshape.graph.query.model.ReferenceValue,
979      *      org.modeshape.graph.query.model.Operator, java.lang.Object)
980      */
981     @Override
982     public Query findNodesWith( ReferenceValue referenceValue,
983                                 Operator operator,
984                                 Object value ) {
985         String field = referenceValue.propertyName();
986         if (field == null) {
987             if (referenceValue.includesWeakReferences()) {
988                 field = LuceneSearchWorkspace.ContentIndex.REFERENCES;
989             } else {
990                 field = LuceneSearchWorkspace.ContentIndex.STRONG_REFERENCES;
991             }
992         }
993         ValueFactories factories = processor.valueFactories;
994         String stringValue = processor.stringFactory.create(value);
995         switch (operator) {
996             case EQUAL_TO:
997                 return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, true);
998             case NOT_EQUAL_TO:
999                 return new NotQuery(CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, true));
1000             case GREATER_THAN:
1001                 return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue, field, factories, true);
1002             case GREATER_THAN_OR_EQUAL_TO:
1003                 return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue, field, factories, true);
1004             case LESS_THAN:
1005                 return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue, field, factories, true);
1006             case LESS_THAN_OR_EQUAL_TO:
1007                 return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue, field, factories, true);
1008             case LIKE:
1009                 return findNodesLike(field, stringValue, false);
1010         }
1011         return null;
1012     }
1013 
1014     public Query findNodesWithNumericRange( PropertyValue propertyValue,
1015                                             Object lowerValue,
1016                                             Object upperValue,
1017                                             boolean includesLower,
1018                                             boolean includesUpper ) {
1019         String field = processor.stringFactory.create(propertyValue.propertyName());
1020         return findNodesWithNumericRange(field, lowerValue, upperValue, includesLower, includesUpper);
1021     }
1022 
1023     public Query findNodesWithNumericRange( NodeDepth depth,
1024                                             Object lowerValue,
1025                                             Object upperValue,
1026                                             boolean includesLower,
1027                                             boolean includesUpper ) {
1028         return findNodesWithNumericRange(ContentIndex.DEPTH, lowerValue, upperValue, includesLower, includesUpper);
1029     }
1030 
1031     protected Query findNodesWithNumericRange( String field,
1032                                                Object lowerValue,
1033                                                Object upperValue,
1034                                                boolean includesLower,
1035                                                boolean includesUpper ) {
1036         Name fieldName = processor.nameFactory.create(field);
1037         IndexRules.Rule rule = workspace.rules.getRule(fieldName);
1038         if (rule == null || rule.isSkipped()) return new MatchNoneQuery();
1039         FieldType type = rule.getType();
1040         ValueFactories factories = processor.valueFactories;
1041         switch (type) {
1042             case DATE:
1043                 long lowerDate = factories.getLongFactory().create(lowerValue);
1044                 long upperDate = factories.getLongFactory().create(upperValue);
1045                 return NumericRangeQuery.newLongRange(field, lowerDate, upperDate, includesLower, includesUpper);
1046             case LONG:
1047                 long lowerLong = factories.getLongFactory().create(lowerValue);
1048                 long upperLong = factories.getLongFactory().create(upperValue);
1049                 return NumericRangeQuery.newLongRange(field, lowerLong, upperLong, includesLower, includesUpper);
1050             case DOUBLE:
1051                 double lowerDouble = factories.getDoubleFactory().create(lowerValue);
1052                 double upperDouble = factories.getDoubleFactory().create(upperValue);
1053                 return NumericRangeQuery.newDoubleRange(field, lowerDouble, upperDouble, includesLower, includesUpper);
1054             case FLOAT:
1055                 float lowerFloat = factories.getDoubleFactory().create(lowerValue).floatValue();
1056                 float upperFloat = factories.getDoubleFactory().create(upperValue).floatValue();
1057                 return NumericRangeQuery.newFloatRange(field, lowerFloat, upperFloat, includesLower, includesUpper);
1058             case INT:
1059                 int lowerInt = factories.getLongFactory().create(lowerValue).intValue();
1060                 int upperInt = factories.getLongFactory().create(upperValue).intValue();
1061                 return NumericRangeQuery.newIntRange(field, lowerInt, upperInt, includesLower, includesUpper);
1062             case BOOLEAN:
1063                 lowerInt = factories.getBooleanFactory().create(lowerValue).booleanValue() ? 1 : 0;
1064                 upperInt = factories.getBooleanFactory().create(upperValue).booleanValue() ? 1 : 0;
1065                 return NumericRangeQuery.newIntRange(field, lowerInt, upperInt, includesLower, includesUpper);
1066             case DECIMAL:
1067                 BigDecimal lowerDecimal = factories.getDecimalFactory().create(lowerValue);
1068                 BigDecimal upperDecimal = factories.getDecimalFactory().create(upperValue);
1069                 String lsv = FieldUtil.decimalToString(lowerDecimal);
1070                 String usv = FieldUtil.decimalToString(upperDecimal);
1071                 Query lower = null;
1072                 if (includesLower) {
1073                     lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(lsv, field, factories, false);
1074                 } else {
1075                     lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(lsv, field, factories, false);
1076                 }
1077                 Query upper = null;
1078                 if (includesUpper) {
1079                     upper = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(usv, field, factories, false);
1080                 } else {
1081                     upper = CompareStringQuery.createQueryForNodesWithFieldLessThan(usv, field, factories, false);
1082                 }
1083                 BooleanQuery query = new BooleanQuery();
1084                 query.add(lower, Occur.MUST);
1085                 query.add(upper, Occur.MUST);
1086                 return query;
1087             case STRING:
1088             case REFERENCE:
1089             case WEAK_REFERENCE:
1090             case BINARY:
1091                 assert false;
1092         }
1093         return new MatchNoneQuery();
1094     }
1095 
1096     protected String likeExpresionForWildcardPath( String path ) {
1097         if (path.equals("/") || path.equals("%")) return path;
1098         StringBuilder sb = new StringBuilder();
1099         path = path.replaceAll("%+", "%");
1100         if (path.startsWith("%/")) {
1101             sb.append("%");
1102             if (path.length() == 2) return sb.toString();
1103             path = path.substring(2);
1104         }
1105         for (String segment : path.split("/")) {
1106             if (segment.length() == 0) continue;
1107             sb.append("/");
1108             sb.append(segment);
1109             if (segment.equals("%") || segment.equals("_")) continue;
1110             if (!segment.endsWith("]") && !segment.endsWith("]%") && !segment.endsWith("]_")) {
1111                 sb.append("[1]");
1112             }
1113         }
1114         if (path.endsWith("/")) sb.append("/");
1115         return sb.toString();
1116     }
1117 
1118     public Query findNodesWith( NodePath nodePath,
1119                                 Operator operator,
1120                                 Object value,
1121                                 boolean caseSensitive ) {
1122         if (!caseSensitive) value = processor.stringFactory.create(value).toLowerCase();
1123         Path pathValue = operator != Operator.LIKE ? processor.pathFactory.create(value) : null;
1124         Query query = null;
1125         switch (operator) {
1126             case EQUAL_TO:
1127                 return findNodeAt(pathValue);
1128             case NOT_EQUAL_TO:
1129                 return new NotQuery(findNodeAt(pathValue));
1130             case LIKE:
1131                 String likeExpression = processor.stringFactory.create(value);
1132                 likeExpression = likeExpresionForWildcardPath(likeExpression);
1133                 if (likeExpression.indexOf("[%]") != -1) {
1134                     // We can't use '[%]' because we only want to match digits,
1135                     // so handle this using a regex ...
1136                     String regex = likeExpression;
1137                     regex = regex.replace("[%]", "[\\d+]");
1138                     regex = regex.replace("[", "\\[");
1139                     regex = regex.replace("*", ".*").replace("?", ".");
1140                     regex = regex.replace("%", ".*").replace("_", ".");
1141                     // Now create a regex query ...
1142                     RegexQuery regexQuery = new RegexQuery(new Term(ContentIndex.PATH, regex));
1143                     int flags = caseSensitive ? 0 : Pattern.CASE_INSENSITIVE;
1144                     regexQuery.setRegexImplementation(new JavaUtilRegexCapabilities(flags));
1145                     query = regexQuery;
1146                 } else {
1147                     query = findNodesLike(ContentIndex.PATH, likeExpression, caseSensitive);
1148                 }
1149                 break;
1150             case GREATER_THAN:
1151                 query = ComparePathQuery.createQueryForNodesWithPathGreaterThan(pathValue,
1152                                                                                 ContentIndex.PATH,
1153                                                                                 processor.valueFactories,
1154                                                                                 caseSensitive);
1155                 break;
1156             case GREATER_THAN_OR_EQUAL_TO:
1157                 query = ComparePathQuery.createQueryForNodesWithPathGreaterThanOrEqualTo(pathValue,
1158                                                                                          ContentIndex.PATH,
1159                                                                                          processor.valueFactories,
1160                                                                                          caseSensitive);
1161                 break;
1162             case LESS_THAN:
1163                 query = ComparePathQuery.createQueryForNodesWithPathLessThan(pathValue,
1164                                                                              ContentIndex.PATH,
1165                                                                              processor.valueFactories,
1166                                                                              caseSensitive);
1167                 break;
1168             case LESS_THAN_OR_EQUAL_TO:
1169                 query = ComparePathQuery.createQueryForNodesWithPathLessThanOrEqualTo(pathValue,
1170                                                                                       ContentIndex.PATH,
1171                                                                                       processor.valueFactories,
1172                                                                                       caseSensitive);
1173                 break;
1174         }
1175         return query;
1176     }
1177 
1178     public Query findNodesWith( NodeName nodeName,
1179                                 Operator operator,
1180                                 Object value,
1181                                 boolean caseSensitive ) {
1182         ValueFactories factories = processor.valueFactories;
1183         String stringValue = processor.stringFactory.create(value);
1184         if (!caseSensitive) stringValue = stringValue.toLowerCase();
1185         Path.Segment segment = operator != Operator.LIKE ? processor.pathFactory.createSegment(stringValue) : null;
1186         // Determine if the string value contained a SNS index ...
1187         boolean includeSns = stringValue.indexOf('[') != -1;
1188         int snsIndex = operator != Operator.LIKE ? segment.getIndex() : 0;
1189         Query query = null;
1190         switch (operator) {
1191             case EQUAL_TO:
1192                 if (!includeSns) {
1193                     return new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue));
1194                 }
1195                 BooleanQuery booleanQuery = new BooleanQuery();
1196                 booleanQuery.add(new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue)), Occur.MUST);
1197                 booleanQuery.add(NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, snsIndex, snsIndex, true, true),
1198                                  Occur.MUST);
1199                 return booleanQuery;
1200             case NOT_EQUAL_TO:
1201                 if (!includeSns) {
1202                     return new NotQuery(new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue)));
1203                 }
1204                 booleanQuery = new BooleanQuery();
1205                 booleanQuery.add(new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue)), Occur.MUST);
1206                 booleanQuery.add(NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, snsIndex, snsIndex, true, true),
1207                                  Occur.MUST);
1208                 return new NotQuery(booleanQuery);
1209             case GREATER_THAN:
1210                 query = CompareNameQuery.createQueryForNodesWithNameGreaterThan(segment,
1211                                                                                 ContentIndex.NODE_NAME,
1212                                                                                 ContentIndex.SNS_INDEX,
1213                                                                                 factories,
1214                                                                                 caseSensitive,
1215                                                                                 includeSns);
1216                 break;
1217             case GREATER_THAN_OR_EQUAL_TO:
1218                 query = CompareNameQuery.createQueryForNodesWithNameGreaterThanOrEqualTo(segment,
1219                                                                                          ContentIndex.NODE_NAME,
1220                                                                                          ContentIndex.SNS_INDEX,
1221                                                                                          factories,
1222                                                                                          caseSensitive,
1223                                                                                          includeSns);
1224                 break;
1225             case LESS_THAN:
1226                 query = CompareNameQuery.createQueryForNodesWithNameLessThan(segment,
1227                                                                              ContentIndex.NODE_NAME,
1228                                                                              ContentIndex.SNS_INDEX,
1229                                                                              factories,
1230                                                                              caseSensitive,
1231                                                                              includeSns);
1232                 break;
1233             case LESS_THAN_OR_EQUAL_TO:
1234                 query = CompareNameQuery.createQueryForNodesWithNameLessThanOrEqualTo(segment,
1235                                                                                       ContentIndex.NODE_NAME,
1236                                                                                       ContentIndex.SNS_INDEX,
1237                                                                                       factories,
1238                                                                                       caseSensitive,
1239                                                                                       includeSns);
1240                 break;
1241             case LIKE:
1242                 // See whether the like expression has brackets ...
1243                 String likeExpression = stringValue;
1244                 int openBracketIndex = likeExpression.indexOf('[');
1245                 if (openBracketIndex != -1) {
1246                     String localNameExpression = likeExpression.substring(0, openBracketIndex);
1247                     String snsIndexExpression = likeExpression.substring(openBracketIndex);
1248                     Query localNameQuery = CompareStringQuery.createQueryForNodesWithFieldLike(localNameExpression,
1249                                                                                                ContentIndex.NODE_NAME,
1250                                                                                                factories,
1251                                                                                                caseSensitive);
1252                     Query snsQuery = createSnsIndexQuery(snsIndexExpression);
1253                     if (localNameQuery == null) {
1254                         if (snsQuery == null) {
1255                             query = new MatchNoneQuery();
1256                         } else {
1257                             // There is just an SNS part ...
1258                             query = snsQuery;
1259                         }
1260                     } else {
1261                         // There is a local name part ...
1262                         if (snsQuery == null) {
1263                             query = localNameQuery;
1264                         } else {
1265                             // There is both a local name part and a SNS part ...
1266                             booleanQuery = new BooleanQuery();
1267                             booleanQuery.add(localNameQuery, Occur.MUST);
1268                             booleanQuery.add(snsQuery, Occur.MUST);
1269                             query = booleanQuery;
1270                         }
1271                     }
1272                 } else {
1273                     // There is no SNS expression ...
1274                     query = CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression,
1275                                                                                 ContentIndex.NODE_NAME,
1276                                                                                 factories,
1277                                                                                 caseSensitive);
1278                 }
1279                 assert query != null;
1280                 break;
1281         }
1282         return query;
1283     }
1284 
1285     public Query findNodesWith( NodeLocalName nodeName,
1286                                 Operator operator,
1287                                 Object value,
1288                                 boolean caseSensitive ) {
1289         String nameValue = processor.stringFactory.create(value);
1290         Query query = null;
1291         switch (operator) {
1292             case LIKE:
1293                 String likeExpression = processor.stringFactory.create(value);
1294                 query = findNodesLike(ContentIndex.LOCAL_NAME, likeExpression, caseSensitive);
1295                 break;
1296             case EQUAL_TO:
1297                 query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(nameValue,
1298                                                                                ContentIndex.LOCAL_NAME,
1299                                                                                processor.valueFactories,
1300                                                                                caseSensitive);
1301                 break;
1302             case NOT_EQUAL_TO:
1303                 query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(nameValue,
1304                                                                                ContentIndex.LOCAL_NAME,
1305                                                                                processor.valueFactories,
1306                                                                                caseSensitive);
1307                 query = new NotQuery(query);
1308                 break;
1309             case GREATER_THAN:
1310                 query = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(nameValue,
1311                                                                                    ContentIndex.LOCAL_NAME,
1312                                                                                    processor.valueFactories,
1313                                                                                    caseSensitive);
1314                 break;
1315             case GREATER_THAN_OR_EQUAL_TO:
1316                 query = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(nameValue,
1317                                                                                             ContentIndex.LOCAL_NAME,
1318                                                                                             processor.valueFactories,
1319                                                                                             caseSensitive);
1320                 break;
1321             case LESS_THAN:
1322                 query = CompareStringQuery.createQueryForNodesWithFieldLessThan(nameValue,
1323                                                                                 ContentIndex.LOCAL_NAME,
1324                                                                                 processor.valueFactories,
1325                                                                                 caseSensitive);
1326                 break;
1327             case LESS_THAN_OR_EQUAL_TO:
1328                 query = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(nameValue,
1329                                                                                          ContentIndex.LOCAL_NAME,
1330                                                                                          processor.valueFactories,
1331                                                                                          caseSensitive);
1332                 break;
1333         }
1334         return query;
1335     }
1336 
1337     public Query findNodesWith( NodeDepth depthConstraint,
1338                                 Operator operator,
1339                                 Object value ) {
1340         int depth = processor.valueFactories.getLongFactory().create(value).intValue();
1341         switch (operator) {
1342             case EQUAL_TO:
1343                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, depth, true, true);
1344             case NOT_EQUAL_TO:
1345                 Query query = NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, depth, true, true);
1346                 return new NotQuery(query);
1347             case GREATER_THAN:
1348                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, MAX_DEPTH, false, true);
1349             case GREATER_THAN_OR_EQUAL_TO:
1350                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, MAX_DEPTH, true, true);
1351             case LESS_THAN:
1352                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, MIN_DEPTH, depth, true, false);
1353             case LESS_THAN_OR_EQUAL_TO:
1354                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, MIN_DEPTH, depth, true, true);
1355             case LIKE:
1356                 // This is not allowed ...
1357                 return null;
1358         }
1359         return null;
1360     }
1361 
1362     protected Query createLocalNameQuery( String likeExpression,
1363                                           boolean caseSensitive ) {
1364         if (likeExpression == null) return null;
1365         return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression,
1366                                                                    ContentIndex.LOCAL_NAME,
1367                                                                    processor.valueFactories,
1368                                                                    caseSensitive);
1369     }
1370 
1371     /**
1372      * Utility method to generate a query against the SNS indexes. This method attempts to generate a query that works most
1373      * efficiently, depending upon the supplied expression. For example, if the supplied expression is just "[3]", then a range
1374      * query is used to find all values matching '3'. However, if "[3_]" is used (where '_' matches any single-character, or digit
1375      * in this case), then a range query is used to find all values between '30' and '39'. Similarly, if "[3%]" is used, then a
1376      * regular expression query is used.
1377      * 
1378      * @param likeExpression the expression that uses the JCR 2.0 LIKE representation, and which includes the leading '[' and
1379      *        trailing ']' characters
1380      * @return the query, or null if the expression cannot be represented as a query
1381      */
1382     protected Query createSnsIndexQuery( String likeExpression ) {
1383         if (likeExpression == null) return null;
1384         likeExpression = likeExpression.trim();
1385         if (likeExpression.length() == 0) return null;
1386 
1387         // Remove the leading '[' ...
1388         assert likeExpression.charAt(0) == '[';
1389         likeExpression = likeExpression.substring(1);
1390 
1391         // Remove the trailing ']' if it exists ...
1392         int closeBracketIndex = likeExpression.indexOf(']');
1393         if (closeBracketIndex != -1) {
1394             likeExpression = likeExpression.substring(0, closeBracketIndex);
1395         }
1396         if (likeExpression.equals("_")) {
1397             // The SNS expression can only be one digit ...
1398             return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, MIN_SNS_INDEX, 9, true, true);
1399         }
1400         if (likeExpression.equals("%")) {
1401             // The SNS expression can be any digits ...
1402             return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, MIN_SNS_INDEX, MAX_SNS_INDEX, true, true);
1403         }
1404         if (likeExpression.indexOf('_') != -1) {
1405             if (likeExpression.indexOf('%') != -1) {
1406                 // Contains both ...
1407                 return findNodesLike(ContentIndex.SNS_INDEX, likeExpression, true);
1408             }
1409             // It presumably contains some numbers and at least one '_' character ...
1410             int firstWildcardChar = likeExpression.indexOf('_');
1411             if (firstWildcardChar + 1 < likeExpression.length()) {
1412                 // There's at least some characters after the first '_' ...
1413                 int secondWildcardChar = likeExpression.indexOf('_', firstWildcardChar + 1);
1414                 if (secondWildcardChar != -1) {
1415                     // There are multiple '_' characters ...
1416                     return findNodesLike(ContentIndex.SNS_INDEX, likeExpression, true);
1417                 }
1418             }
1419             // There's only one '_', so parse the lowermost value and uppermost value ...
1420             String lowerExpression = likeExpression.replace('_', '0');
1421             String upperExpression = likeExpression.replace('_', '9');
1422             try {
1423                 // This SNS is just a number ...
1424                 int lowerSns = Integer.parseInt(lowerExpression);
1425                 int upperSns = Integer.parseInt(upperExpression);
1426                 return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, lowerSns, upperSns, true, true);
1427             } catch (NumberFormatException e) {
1428                 // It's not a number but it's in the SNS field, so there will be no results ...
1429                 return new MatchNoneQuery();
1430             }
1431         }
1432         if (likeExpression.indexOf('%') != -1) {
1433             // It presumably contains some numbers and at least one '%' character ...
1434             return findNodesLike(ContentIndex.SNS_INDEX, likeExpression, true);
1435         }
1436         // This is not a LIKE expression but an exact value specification and should be a number ...
1437         try {
1438             // This SNS is just a number ...
1439             int sns = Integer.parseInt(likeExpression);
1440             return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, sns, sns, true, true);
1441         } catch (NumberFormatException e) {
1442             // It's not a number but it's in the SNS field, so there will be no results ...
1443             return new MatchNoneQuery();
1444         }
1445     }
1446 
1447     /**
1448      * This collector is responsible for loading the value for each of the columns into each tuple array.
1449      */
1450     protected static class DualIndexTupleCollector extends TupleCollector {
1451         private final LuceneSearchSession session;
1452         private final LinkedList<Object[]> tuples = new LinkedList<Object[]>();
1453         private final Columns columns;
1454         private final int numValues;
1455         private final boolean recordScore;
1456         private final int scoreIndex;
1457         private final FieldSelector fieldSelector;
1458         private final int locationIndex;
1459         private Scorer scorer;
1460         private IndexReader currentReader;
1461         private int docOffset;
1462 
1463         protected DualIndexTupleCollector( LuceneSearchSession session,
1464                                            Columns columns ) {
1465             this.session = session;
1466             this.columns = columns;
1467             assert this.columns != null;
1468             this.numValues = this.columns.getTupleSize();
1469             assert this.numValues >= 0;
1470             assert this.columns.getSelectorNames().size() == 1;
1471             final String selectorName = this.columns.getSelectorNames().get(0);
1472             this.locationIndex = this.columns.getLocationIndex(selectorName);
1473             this.recordScore = this.columns.hasFullTextSearchScores();
1474             this.scoreIndex = this.recordScore ? this.columns.getFullTextSearchScoreIndexFor(selectorName) : -1;
1475 
1476             // Create the set of field names that we need to load from the document ...
1477             final Set<String> fieldNames = new HashSet<String>(this.columns.getColumnNames());
1478             fieldNames.add(ContentIndex.LOCATION_ID_PROPERTIES); // add the UUID, which we'll put into the Location ...
1479             fieldNames.add(ContentIndex.PATH); // add the UUID, which we'll put into the Location ...
1480             this.fieldSelector = new FieldSelector() {
1481                 private static final long serialVersionUID = 1L;
1482 
1483                 public FieldSelectorResult accept( String fieldName ) {
1484                     return fieldNames.contains(fieldName) ? FieldSelectorResult.LOAD : FieldSelectorResult.NO_LOAD;
1485                 }
1486             };
1487         }
1488 
1489         /**
1490          * @return tuples
1491          */
1492         @Override
1493         public LinkedList<Object[]> getTuples() {
1494             return tuples;
1495         }
1496 
1497         /**
1498          * {@inheritDoc}
1499          * 
1500          * @see org.apache.lucene.search.Collector#acceptsDocsOutOfOrder()
1501          */
1502         @Override
1503         public boolean acceptsDocsOutOfOrder() {
1504             return true;
1505         }
1506 
1507         /**
1508          * Get the location index.
1509          * 
1510          * @return locationIndex
1511          */
1512         public int getLocationIndex() {
1513             return locationIndex;
1514         }
1515 
1516         /**
1517          * {@inheritDoc}
1518          * 
1519          * @see org.apache.lucene.search.Collector#setNextReader(org.apache.lucene.index.IndexReader, int)
1520          */
1521         @Override
1522         public void setNextReader( IndexReader reader,
1523                                    int docBase ) {
1524             this.currentReader = reader;
1525             this.docOffset = docBase;
1526         }
1527 
1528         /**
1529          * {@inheritDoc}
1530          * 
1531          * @see org.apache.lucene.search.Collector#setScorer(org.apache.lucene.search.Scorer)
1532          */
1533         @Override
1534         public void setScorer( Scorer scorer ) {
1535             this.scorer = scorer;
1536         }
1537 
1538         /**
1539          * {@inheritDoc}
1540          * 
1541          * @see org.apache.lucene.search.Collector#collect(int)
1542          */
1543         @Override
1544         public void collect( int doc ) throws IOException {
1545             int docId = doc + docOffset;
1546             Object[] tuple = new Object[numValues];
1547             Document document = currentReader.document(docId, fieldSelector);
1548             for (String columnName : columns.getColumnNames()) {
1549                 int index = columns.getColumnIndexForName(columnName);
1550                 // We just need to retrieve the first value if there is more than one ...
1551                 tuple[index] = document.get(columnName);
1552             }
1553 
1554             // Set the score column if required ...
1555             if (recordScore) {
1556                 assert scorer != null;
1557                 tuple[scoreIndex] = scorer.score();
1558             }
1559 
1560             // Read the location ...
1561             tuple[locationIndex] = session.readLocation(document);
1562             tuples.add(tuple);
1563         }
1564     }
1565 
1566     /**
1567      * This collector is responsible for loading the value for each of the columns into each tuple array.
1568      */
1569     protected static class FullTextSearchTupleCollector extends TupleCollector {
1570         private final List<Object[]> tuples;
1571         private final FieldSelector fieldSelector;
1572         private final LuceneSearchSession session;
1573         private Scorer scorer;
1574         private IndexReader currentReader;
1575         private int docOffset;
1576 
1577         protected FullTextSearchTupleCollector( LuceneSearchSession session,
1578                                                 List<Object[]> tuples ) {
1579             assert session != null;
1580             assert tuples != null;
1581             this.session = session;
1582             this.tuples = tuples;
1583             this.fieldSelector = LOCATION_FIELDS_SELECTOR;
1584         }
1585 
1586         /**
1587          * @return tuples
1588          */
1589         @Override
1590         public List<Object[]> getTuples() {
1591             return tuples;
1592         }
1593 
1594         /**
1595          * {@inheritDoc}
1596          * 
1597          * @see org.apache.lucene.search.Collector#acceptsDocsOutOfOrder()
1598          */
1599         @Override
1600         public boolean acceptsDocsOutOfOrder() {
1601             return true;
1602         }
1603 
1604         /**
1605          * {@inheritDoc}
1606          * 
1607          * @see org.apache.lucene.search.Collector#setNextReader(org.apache.lucene.index.IndexReader, int)
1608          */
1609         @Override
1610         public void setNextReader( IndexReader reader,
1611                                    int docBase ) {
1612             this.currentReader = reader;
1613             this.docOffset = docBase;
1614         }
1615 
1616         /**
1617          * {@inheritDoc}
1618          * 
1619          * @see org.apache.lucene.search.Collector#setScorer(org.apache.lucene.search.Scorer)
1620          */
1621         @Override
1622         public void setScorer( Scorer scorer ) {
1623             this.scorer = scorer;
1624         }
1625 
1626         /**
1627          * {@inheritDoc}
1628          * 
1629          * @see org.apache.lucene.search.Collector#collect(int)
1630          */
1631         @Override
1632         public void collect( int doc ) throws IOException {
1633             int docId = doc + docOffset;
1634             Object[] tuple = new Object[2];
1635             Document document = currentReader.document(docId, fieldSelector);
1636             // Read the Location ...
1637             tuple[0] = session.readLocation(document);
1638             // And read the score ...
1639             tuple[1] = scorer.score();
1640             // And add the tuple ...
1641             tuples.add(tuple);
1642         }
1643     }
1644 }