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.jcr;
25  
26  import org.apache.lucene.analysis.snowball.SnowballAnalyzer;
27  import org.apache.lucene.util.Version;
28  import org.modeshape.common.collection.Problems;
29  import org.modeshape.common.collection.SimpleProblems;
30  import org.modeshape.common.i18n.I18n;
31  import org.modeshape.common.text.TextEncoder;
32  import org.modeshape.common.text.UrlEncoder;
33  import org.modeshape.common.util.Logger;
34  import org.modeshape.graph.ExecutionContext;
35  import org.modeshape.graph.Graph;
36  import org.modeshape.graph.GraphI18n;
37  import org.modeshape.graph.connector.RepositoryConnectionFactory;
38  import org.modeshape.graph.observe.Changes;
39  import org.modeshape.graph.observe.Observable;
40  import org.modeshape.graph.observe.Observer;
41  import org.modeshape.graph.property.Path;
42  import org.modeshape.graph.query.QueryContext;
43  import org.modeshape.graph.query.QueryEngine;
44  import org.modeshape.graph.query.QueryResults;
45  import org.modeshape.graph.query.QueryResults.Columns;
46  import org.modeshape.graph.query.model.QueryCommand;
47  import org.modeshape.graph.query.model.TypeSystem;
48  import org.modeshape.graph.query.optimize.Optimizer;
49  import org.modeshape.graph.query.optimize.RuleBasedOptimizer;
50  import org.modeshape.graph.query.plan.CanonicalPlanner;
51  import org.modeshape.graph.query.plan.PlanHints;
52  import org.modeshape.graph.query.plan.PlanNode;
53  import org.modeshape.graph.query.plan.Planner;
54  import org.modeshape.graph.query.process.AbstractAccessComponent;
55  import org.modeshape.graph.query.process.ProcessingComponent;
56  import org.modeshape.graph.query.process.Processor;
57  import org.modeshape.graph.query.process.QueryProcessor;
58  import org.modeshape.graph.query.process.SelectComponent.Analyzer;
59  import org.modeshape.graph.query.validate.Schemata;
60  import org.modeshape.graph.request.AccessQueryRequest;
61  import org.modeshape.graph.request.InvalidWorkspaceException;
62  import org.modeshape.graph.request.processor.RequestProcessor;
63  import org.modeshape.graph.search.SearchEngine;
64  import org.modeshape.graph.search.SearchEngineIndexer;
65  import org.modeshape.graph.search.SearchEngineProcessor;
66  import org.modeshape.search.lucene.IndexRules;
67  import org.modeshape.search.lucene.LuceneConfiguration;
68  import org.modeshape.search.lucene.LuceneConfigurations;
69  import org.modeshape.search.lucene.LuceneSearchEngine;
70  
71  import javax.jcr.RepositoryException;
72  import javax.jcr.query.InvalidQueryException;
73  import java.io.File;
74  import java.util.List;
75  import java.util.Map;
76  import java.util.Set;
77  import java.util.concurrent.ExecutorService;
78  import java.util.concurrent.Executors;
79  
80  /**
81   * 
82   */
83  abstract class RepositoryQueryManager {
84  
85      protected final String sourceName;
86  
87      RepositoryQueryManager( String sourceName ) {
88          this.sourceName = sourceName;
89      }
90  
91      public abstract QueryResults query( String workspaceName,
92                                          QueryCommand query,
93                                          Schemata schemata,
94                                          PlanHints hints,
95                                          Map<String, Object> variables ) throws InvalidQueryException;
96  
97      public abstract QueryResults search( String workspaceName,
98                                           String searchExpression,
99                                           int maxRowCount,
100                                          int offset ) throws InvalidQueryException;
101 
102     /**
103      * Crawl and index the content in the named workspace.
104      * 
105      * @throws IllegalArgumentException if the workspace is null
106      * @throws InvalidWorkspaceException if there is no workspace with the supplied name
107      */
108     public void reindexContent() {
109         // do nothing by default
110     }
111 
112     /**
113      * Crawl and index the content in the named workspace.
114      * 
115      * @param workspace the workspace
116      * @throws IllegalArgumentException if the workspace is null
117      * @throws InvalidWorkspaceException if there is no workspace with the supplied name
118      */
119     public void reindexContent( JcrWorkspace workspace ) {
120         // do nothing by default
121     }
122 
123     /**
124      * Crawl and index the content starting at the supplied path in the named workspace, to the designated depth.
125      * 
126      * @param workspace the workspace
127      * @param path the path of the content to be indexed
128      * @param depth the depth of the content to be indexed
129      * @throws IllegalArgumentException if the workspace or path are null, or if the depth is less than 1
130      * @throws InvalidWorkspaceException if there is no workspace with the supplied name
131      */
132     public void reindexContent( JcrWorkspace workspace,
133                                 String path,
134                                 int depth ) {
135         // do nothing by default
136     }
137 
138     static class PushDown extends RepositoryQueryManager {
139         private final ExecutionContext context;
140         private final RepositoryConnectionFactory connectionFactory;
141 
142         PushDown( String sourceName,
143                   ExecutionContext context,
144                   RepositoryConnectionFactory connectionFactory ) {
145             super(sourceName);
146             this.context = context;
147             this.connectionFactory = connectionFactory;
148         }
149 
150         private Graph workspaceGraph( String workspaceName ) {
151             Graph graph = Graph.create(this.sourceName, connectionFactory, context);
152 
153             if (workspaceName != null) {
154                 graph.useWorkspace(workspaceName);
155             }
156 
157             return graph;
158         }
159 
160         @Override
161         public QueryResults query( String workspaceName,
162                                    QueryCommand query,
163                                    Schemata schemata,
164                                    PlanHints hints,
165                                    Map<String, Object> variables ) {
166             Graph.BuildQuery builder = workspaceGraph(workspaceName).query(query, schemata);
167             if (variables != null) builder.using(variables);
168             if (hints != null) builder.using(hints);
169             return builder.execute();
170         }
171 
172         @Override
173         public QueryResults search( String workspaceName,
174                                     String searchExpression,
175                                     int maxRowCount,
176                                     int offset ) {
177             return workspaceGraph(workspaceName).search(searchExpression, maxRowCount, offset);
178         }
179 
180     }
181 
182     static class Disabled extends RepositoryQueryManager {
183 
184         Disabled( String sourceName ) {
185             super(sourceName);
186         }
187 
188         /**
189          * {@inheritDoc}
190          * 
191          * @see org.modeshape.jcr.RepositoryQueryManager#query(java.lang.String, org.modeshape.graph.query.model.QueryCommand,
192          *      org.modeshape.graph.query.validate.Schemata, org.modeshape.graph.query.plan.PlanHints, java.util.Map)
193          */
194         @Override
195         public QueryResults query( String workspaceName,
196                                    QueryCommand query,
197                                    Schemata schemata,
198                                    PlanHints hints,
199                                    Map<String, Object> variables ) throws InvalidQueryException {
200             throw new InvalidQueryException(JcrI18n.queryIsDisabledInRepository.text(this.sourceName));
201         }
202 
203         /**
204          * {@inheritDoc}
205          * 
206          * @see org.modeshape.jcr.RepositoryQueryManager#search(java.lang.String, java.lang.String, int, int)
207          */
208         @Override
209         public QueryResults search( String workspaceName,
210                                     String searchExpression,
211                                     int maxRowCount,
212                                     int offset ) throws InvalidQueryException {
213             throw new InvalidQueryException(JcrI18n.queryIsDisabledInRepository.text(this.sourceName));
214         }
215     }
216 
217     static class SelfContained extends RepositoryQueryManager {
218         private final ExecutionContext context;
219         private final String sourceName;
220         private final LuceneConfiguration configuration;
221         private final SearchEngine searchEngine;
222         private final Observer searchObserver;
223         private final ExecutorService service;
224         private final QueryEngine queryEngine;
225         private final RepositoryConnectionFactory connectionFactory;
226 
227         SelfContained( ExecutionContext context,
228                        String nameOfSourceToBeSearchable,
229                        RepositoryConnectionFactory connectionFactory,
230                        Observable observable,
231                        RepositoryNodeTypeManager nodeTypeManager,
232                        String indexDirectory,
233                        boolean updateIndexesSynchronously ) throws RepositoryException {
234             super(nameOfSourceToBeSearchable);
235 
236             this.context = context;
237             this.sourceName = nameOfSourceToBeSearchable;
238             this.connectionFactory = connectionFactory;
239             // Define the configuration ...
240             TextEncoder encoder = new UrlEncoder();
241             if (indexDirectory != null) {
242                 File indexDir = new File(indexDirectory);
243                 if (indexDir.exists()) {
244                     // The location does exist ...
245                     if (!indexDir.isDirectory()) {
246                         // The path is not a directory ...
247                         I18n msg = JcrI18n.searchIndexDirectoryOptionSpecifiesFileNotDirectory;
248                         throw new RepositoryException(msg.text(indexDirectory, sourceName));
249                     }
250                     if (!indexDir.canWrite()) {
251                         // But we cannot write to it ...
252                         I18n msg = JcrI18n.searchIndexDirectoryOptionSpecifiesDirectoryThatCannotBeWrittenTo;
253                         throw new RepositoryException(msg.text(indexDirectory, sourceName));
254                     }
255                     if (!indexDir.canRead()) {
256                         // But we cannot write to it ...
257                         I18n msg = JcrI18n.searchIndexDirectoryOptionSpecifiesDirectoryThatCannotBeRead;
258                         throw new RepositoryException(msg.text(indexDirectory, sourceName));
259                     }
260                     // The directory is usable
261                 } else {
262                     // The location doesn't exist,so try to make it ...
263                     if (!indexDir.mkdirs()) {
264                         I18n msg = JcrI18n.searchIndexDirectoryOptionSpecifiesDirectoryThatCannotBeCreated;
265                         throw new RepositoryException(msg.text(indexDirectory, sourceName));
266                     }
267                     // We successfully create the dirctory (or directories)
268                 }
269                 configuration = LuceneConfigurations.using(indexDir, encoder, encoder);
270             } else {
271                 // Use in-memory as a fall-back ...
272                 configuration = LuceneConfigurations.inMemory();
273             }
274             assert configuration != null;
275 
276             // Set up the indexing rules ...
277             IndexRules indexRules = nodeTypeManager.getRepositorySchemata().getIndexRules();
278 
279             // Set up the search engine ...
280             org.apache.lucene.analysis.Analyzer analyzer = new SnowballAnalyzer(Version.LUCENE_30, "English");
281             boolean verifyWorkspaces = false;
282             searchEngine = new LuceneSearchEngine(nameOfSourceToBeSearchable, connectionFactory, verifyWorkspaces, configuration,
283                                                   indexRules, analyzer);
284 
285             // Set up an original source observer to keep the index up to date ...
286             if (updateIndexesSynchronously) {
287                 this.service = null;
288                 this.searchObserver = new Observer() {
289                     @SuppressWarnings( "synthetic-access" )
290                     public void notify( Changes changes ) {
291                         if (changes.getSourceName().equals(sourceName)) {
292                             process(changes);
293                         }
294                     }
295                 };
296             } else {
297                 // It's asynchronous, so create a single-threaded executor and an observer that enqueues the results
298                 this.service = Executors.newCachedThreadPool();
299                 this.searchObserver = new Observer() {
300                     @SuppressWarnings( "synthetic-access" )
301                     public void notify( final Changes changes ) {
302                         if (changes.getSourceName().equals(sourceName)) {
303                             service.submit(new Runnable() {
304                                 public void run() {
305                                     process(changes);
306                                 }
307                             });
308                         }
309                     }
310                 };
311             }
312             observable.register(this.searchObserver);
313 
314             // Set up the query engine ...
315             Planner planner = new CanonicalPlanner();
316             Optimizer optimizer = new RuleBasedOptimizer();
317             Processor processor = new QueryProcessor() {
318 
319                 /**
320                  * {@inheritDoc}
321                  * 
322                  * @see org.modeshape.graph.query.process.QueryProcessor#createAccessComponent(org.modeshape.graph.query.model.QueryCommand,
323                  *      org.modeshape.graph.query.QueryContext, org.modeshape.graph.query.plan.PlanNode,
324                  *      org.modeshape.graph.query.QueryResults.Columns,
325                  *      org.modeshape.graph.query.process.SelectComponent.Analyzer)
326                  */
327                 @Override
328                 protected ProcessingComponent createAccessComponent( QueryCommand originalQuery,
329                                                                      QueryContext context,
330                                                                      PlanNode accessNode,
331                                                                      Columns resultColumns,
332                                                                      Analyzer analyzer ) {
333                     return new AccessQueryProcessor((GraphQueryContext)context, resultColumns, accessNode);
334                 }
335             };
336             this.queryEngine = new QueryEngine(planner, optimizer, processor);
337 
338             // Index any existing content ...
339             reindexContent();
340         }
341 
342         protected void process( Changes changes ) {
343             try {
344                 searchEngine.index(context, changes.getChangeRequests());
345             } catch (RuntimeException e) {
346                 Logger.getLogger(getClass()).error(e, JcrI18n.errorUpdatingQueryIndexes, e.getLocalizedMessage());
347             }
348         }
349 
350         @Override
351         public QueryResults query( String workspaceName,
352                                    QueryCommand query,
353                                    Schemata schemata,
354                                    PlanHints hints,
355                                    Map<String, Object> variables ) {
356             TypeSystem typeSystem = context.getValueFactories().getTypeSystem();
357             SearchEngineProcessor processor = searchEngine.createProcessor(context, null, true);
358             try {
359                 QueryContext context = new GraphQueryContext(schemata, typeSystem, hints, new SimpleProblems(), variables,
360                                                              processor, workspaceName);
361                 return queryEngine.execute(context, query);
362             } finally {
363                 processor.close();
364             }
365         }
366 
367         @Override
368         public QueryResults search( String workspaceName,
369                                     String searchExpression,
370                                     int maxRowCount,
371                                     int offset ) {
372             Graph graph = Graph.create(sourceName, connectionFactory, context);
373 
374             if (workspaceName != null) {
375                 graph.useWorkspace(workspaceName);
376             }
377 
378             return graph.search(searchExpression, maxRowCount, offset);
379         }
380 
381         /**
382          * {@inheritDoc}
383          * 
384          * @see org.modeshape.jcr.RepositoryQueryManager#reindexContent()
385          */
386         @Override
387         public void reindexContent() {
388             // Get the workspace names ...
389             Set<String> workspaces = Graph.create(sourceName, connectionFactory, context).getWorkspaces();
390 
391             // Index the existing content (this obtains a connection and possibly locks the source) ...
392             SearchEngineIndexer indexer = new SearchEngineIndexer(context, searchEngine, connectionFactory);
393             try {
394                 for (String workspace : workspaces) {
395                     indexer.index(workspace);
396                 }
397             } finally {
398                 indexer.close();
399             }
400 
401         }
402 
403         /**
404          * {@inheritDoc}
405          * 
406          * @see org.modeshape.jcr.RepositoryQueryManager#reindexContent(org.modeshape.jcr.JcrWorkspace)
407          */
408         @Override
409         public void reindexContent( JcrWorkspace workspace ) {
410             SearchEngineIndexer indexer = new SearchEngineIndexer(context, searchEngine, connectionFactory);
411             try {
412                 indexer.index(workspace.getName());
413             } finally {
414                 indexer.close();
415             }
416         }
417 
418         /**
419          * {@inheritDoc}
420          * 
421          * @see org.modeshape.jcr.RepositoryQueryManager#reindexContent(org.modeshape.jcr.JcrWorkspace, java.lang.String, int)
422          */
423         @Override
424         public void reindexContent( JcrWorkspace workspace,
425                                     String path,
426                                     int depth ) {
427             Path at = workspace.context().getValueFactories().getPathFactory().create(path);
428             SearchEngineIndexer indexer = new SearchEngineIndexer(context, searchEngine, connectionFactory);
429             try {
430                 indexer.index(workspace.getName(), at, depth);
431             } finally {
432                 indexer.close();
433             }
434         }
435 
436         protected class GraphQueryContext extends QueryContext {
437             private final RequestProcessor processor;
438             private final String workspaceName;
439 
440             protected GraphQueryContext( Schemata schemata,
441                                          TypeSystem typeSystem,
442                                          PlanHints hints,
443                                          Problems problems,
444                                          Map<String, Object> variables,
445                                          RequestProcessor processor,
446                                          String workspaceName ) {
447                 super(schemata, typeSystem, hints, problems, variables);
448                 this.processor = processor;
449                 this.workspaceName = workspaceName;
450             }
451 
452             /**
453              * @return processor
454              */
455             public RequestProcessor getProcessor() {
456                 return processor;
457             }
458 
459             /**
460              * @return workspaceName
461              */
462             public String getWorkspaceName() {
463                 return workspaceName;
464             }
465         }
466 
467         protected static class AccessQueryProcessor extends AbstractAccessComponent {
468             private final AccessQueryRequest accessRequest;
469 
470             protected AccessQueryProcessor( GraphQueryContext context,
471                                             Columns columns,
472                                             PlanNode accessNode ) {
473                 super(context, columns, accessNode);
474                 accessRequest = new AccessQueryRequest(context.getWorkspaceName(), sourceName, getColumns(), andedConstraints,
475                                                        limit, context.getSchemata(), context.getVariables());
476                 context.getProcessor().process(accessRequest);
477             }
478 
479             /**
480              * Get the access query request.
481              * 
482              * @return the access query request; never null
483              */
484             public AccessQueryRequest getAccessRequest() {
485                 return accessRequest;
486             }
487 
488             /**
489              * {@inheritDoc}
490              * 
491              * @see org.modeshape.graph.query.process.ProcessingComponent#execute()
492              */
493             @Override
494             public List<Object[]> execute() {
495                 if (accessRequest.getError() != null) {
496                     I18n msg = GraphI18n.errorWhilePerformingQuery;
497                     getContext().getProblems().addError(accessRequest.getError(),
498                                                         msg,
499                                                         accessNode.getString(),
500                                                         accessRequest.workspace(),
501                                                         sourceName,
502                                                         accessRequest.getError().getLocalizedMessage());
503                     return emptyTuples();
504                 }
505                 return accessRequest.getTuples();
506             }
507 
508         }
509 
510     }
511 }