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