1 //$Id: SearchFactoryImpl.java 15376 2008-10-23 02:15:50Z epbernard $
2 package org.hibernate.search.impl;
3
4 import java.beans.Introspector;
5 import java.lang.reflect.Method;
6 import java.util.ArrayList;
7 import java.util.Collections;
8 import java.util.HashMap;
9 import java.util.HashSet;
10 import java.util.Iterator;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.Properties;
14 import java.util.Set;
15 import java.util.WeakHashMap;
16 import java.util.concurrent.atomic.AtomicBoolean;
17 import java.util.concurrent.locks.ReentrantLock;
18
19 import org.apache.lucene.analysis.Analyzer;
20 import org.slf4j.Logger;
21
22 import org.hibernate.annotations.common.reflection.ReflectionManager;
23 import org.hibernate.annotations.common.reflection.XClass;
24 import org.hibernate.annotations.common.reflection.java.JavaReflectionManager;
25 import org.hibernate.annotations.common.util.StringHelper;
26 import org.hibernate.search.Environment;
27 import org.hibernate.search.SearchException;
28 import org.hibernate.search.Version;
29 import org.hibernate.search.annotations.Factory;
30 import org.hibernate.search.annotations.FullTextFilterDef;
31 import org.hibernate.search.annotations.FullTextFilterDefs;
32 import org.hibernate.search.annotations.Indexed;
33 import org.hibernate.search.annotations.Key;
34 import org.hibernate.search.backend.BackendQueueProcessorFactory;
35 import org.hibernate.search.backend.LuceneIndexingParameters;
36 import org.hibernate.search.backend.LuceneWork;
37 import org.hibernate.search.backend.OptimizeLuceneWork;
38 import org.hibernate.search.backend.Worker;
39 import org.hibernate.search.backend.WorkerFactory;
40 import org.hibernate.search.backend.configuration.ConfigurationParseHelper;
41 import org.hibernate.search.cfg.SearchConfiguration;
42 import org.hibernate.search.engine.DocumentBuilder;
43 import org.hibernate.search.engine.FilterDef;
44 import org.hibernate.search.engine.SearchFactoryImplementor;
45 import org.hibernate.search.engine.EntityState;
46 import org.hibernate.search.filter.CachingWrapperFilter;
47 import org.hibernate.search.filter.FilterCachingStrategy;
48 import org.hibernate.search.filter.MRUFilterCachingStrategy;
49 import org.hibernate.search.reader.ReaderProvider;
50 import org.hibernate.search.reader.ReaderProviderFactory;
51 import org.hibernate.search.store.DirectoryProvider;
52 import org.hibernate.search.store.DirectoryProviderFactory;
53 import org.hibernate.search.store.optimization.OptimizerStrategy;
54 import org.hibernate.search.util.LoggerFactory;
55
56 /**
57 * @author Emmanuel Bernard
58 */
59 public class SearchFactoryImpl implements SearchFactoryImplementor {
60 private static final ThreadLocal<WeakHashMap<SearchConfiguration, SearchFactoryImpl>> contexts =
61 new ThreadLocal<WeakHashMap<SearchConfiguration, SearchFactoryImpl>>();
62
63 static {
64 Version.touch();
65 }
66
67 private static final Logger log = LoggerFactory.make();
68
69 private final Map<Class<?>, DocumentBuilder<?>> documentBuilders = new HashMap<Class<?>, DocumentBuilder<?>>();
70 private final Map<Class<?>, DocumentBuilder<?>> containedInOnlyBuilders = new HashMap<Class<?>, DocumentBuilder<?>>();
71 //keep track of the index modifiers per DirectoryProvider since multiple entity can use the same directory provider
72 private final Map<DirectoryProvider<?>, DirectoryProviderData> dirProviderData = new HashMap<DirectoryProvider<?>, DirectoryProviderData>();
73 private final Worker worker;
74 private final ReaderProvider readerProvider;
75 private BackendQueueProcessorFactory backendQueueProcessorFactory;
76 private final Map<String, FilterDef> filterDefinitions = new HashMap<String, FilterDef>();
77 private final FilterCachingStrategy filterCachingStrategy;
78 private Map<String, Analyzer> analyzers;
79 private final AtomicBoolean stopped = new AtomicBoolean( false );
80 private final int cacheBitResultsSize;
81 /*
82 * used as a barrier (piggyback usage) between initialization and subsequent usage of searchFactory in different threads
83 * this is due to our use of the initialize pattern is a few areas
84 * subsequent reads on volatiles should be very cheap on most platform especially since we don't write after init
85 *
86 * This volatile is meant to be written after initialization
87 * and read by all subsequent methods accessing the SearchFactory state
88 * read to be as barrier != 0. If barrier == 0 we have a race condition, but is not likely to happen.
89 */
90 private volatile short barrier;
91
92 /**
93 * Each directory provider (index) can have its own performance settings.
94 */
95 private Map<DirectoryProvider, LuceneIndexingParameters> dirProviderIndexingParams =
96 new HashMap<DirectoryProvider, LuceneIndexingParameters>();
97 private final String indexingStrategy;
98
99
100 public BackendQueueProcessorFactory getBackendQueueProcessorFactory() {
101 if (barrier != 0) { } //read barrier
102 return backendQueueProcessorFactory;
103 }
104
105 public void setBackendQueueProcessorFactory(BackendQueueProcessorFactory backendQueueProcessorFactory) {
106 //no need to set a barrier, we init in the same thread as the init one
107 this.backendQueueProcessorFactory = backendQueueProcessorFactory;
108 }
109
110 public SearchFactoryImpl(SearchConfiguration cfg) {
111 ReflectionManager reflectionManager = cfg.getReflectionManager();
112 if ( reflectionManager == null ) {
113 reflectionManager = new JavaReflectionManager();
114 }
115 this.indexingStrategy = defineIndexingStrategy( cfg ); //need to be done before the document builds
116 initDocumentBuilders( cfg, reflectionManager );
117
118 Set<Class<?>> indexedClasses = documentBuilders.keySet();
119 for (DocumentBuilder builder : documentBuilders.values()) {
120 builder.postInitialize( indexedClasses );
121 }
122 //not really necessary today
123 for (DocumentBuilder builder : containedInOnlyBuilders.values()) {
124 builder.postInitialize( indexedClasses );
125 }
126 this.worker = WorkerFactory.createWorker( cfg, this );
127 this.readerProvider = ReaderProviderFactory.createReaderProvider( cfg, this );
128 this.filterCachingStrategy = buildFilterCachingStrategy( cfg.getProperties() );
129 this.cacheBitResultsSize = ConfigurationParseHelper.getIntValue( cfg.getProperties(), Environment.CACHE_BIT_RESULT_SIZE, CachingWrapperFilter.DEFAULT_SIZE );
130 this.barrier = 1; //write barrier
131 }
132
133 private static String defineIndexingStrategy(SearchConfiguration cfg) {
134 String indexingStrategy = cfg.getProperties().getProperty( Environment.INDEXING_STRATEGY, "event" );
135 if ( ! ("event".equals( indexingStrategy ) || "manual".equals( indexingStrategy ) ) ) {
136 throw new SearchException( Environment.INDEXING_STRATEGY + " unknown: " + indexingStrategy );
137 }
138 return indexingStrategy;
139 }
140
141 public String getIndexingStrategy() {
142 if (barrier != 0) { } //read barrier
143 return indexingStrategy;
144 }
145
146 public void close() {
147 if (barrier != 0) { } //read barrier
148 if ( stopped.compareAndSet( false, true) ) {
149 try {
150 worker.close();
151 }
152 catch (Exception e) {
153 log.error( "Worker raises an exception on close()", e );
154 }
155
156 try {
157 readerProvider.destroy();
158 }
159 catch (Exception e) {
160 log.error( "ReaderProvider raises an exception on destroy()", e );
161 }
162
163 //TODO move to DirectoryProviderFactory for cleaner
164 for (DirectoryProvider dp : getDirectoryProviders() ) {
165 try {
166 dp.stop();
167 }
168 catch (Exception e) {
169 log.error( "DirectoryProvider raises an exception on stop() ", e );
170 }
171 }
172 }
173 }
174
175 public void addClassToDirectoryProvider(Class<?> clazz, DirectoryProvider<?> directoryProvider) {
176 //no need to set a read barrier, we only use this class in the init thread
177 DirectoryProviderData data = dirProviderData.get(directoryProvider);
178 if (data == null) {
179 data = new DirectoryProviderData();
180 dirProviderData.put( directoryProvider, data );
181 }
182 data.classes.add(clazz);
183 }
184
185 public Set<Class<?>> getClassesInDirectoryProvider(DirectoryProvider<?> directoryProvider) {
186 if (barrier != 0) { } //read barrier
187 return Collections.unmodifiableSet( dirProviderData.get(directoryProvider).classes );
188 }
189
190 private void bindFilterDefs(XClass mappedXClass) {
191 FullTextFilterDef defAnn = mappedXClass.getAnnotation( FullTextFilterDef.class );
192 if ( defAnn != null ) {
193 bindFilterDef( defAnn, mappedXClass );
194 }
195 FullTextFilterDefs defsAnn = mappedXClass.getAnnotation( FullTextFilterDefs.class );
196 if (defsAnn != null) {
197 for ( FullTextFilterDef def : defsAnn.value() ) {
198 bindFilterDef( def, mappedXClass );
199 }
200 }
201 }
202
203 private void bindFilterDef(FullTextFilterDef defAnn, XClass mappedXClass) {
204 if ( filterDefinitions.containsKey( defAnn.name() ) ) {
205 throw new SearchException("Multiple definition of @FullTextFilterDef.name=" + defAnn.name() + ": "
206 + mappedXClass.getName() );
207 }
208
209 FilterDef filterDef = new FilterDef(defAnn);
210 try {
211 filterDef.getImpl().newInstance();
212 }
213 catch (IllegalAccessException e) {
214 throw new SearchException("Unable to create Filter class: " + filterDef.getImpl().getName(), e);
215 }
216 catch (InstantiationException e) {
217 throw new SearchException("Unable to create Filter class: " + filterDef.getImpl().getName(), e);
218 }
219 for ( Method method : filterDef.getImpl().getMethods() ) {
220 if ( method.isAnnotationPresent( Factory.class ) ) {
221 if ( filterDef.getFactoryMethod() != null ) {
222 throw new SearchException("Multiple @Factory methods found" + defAnn.name() + ": "
223 + filterDef.getImpl().getName() + "." + method.getName() );
224 }
225 if ( !method.isAccessible() ) method.setAccessible( true );
226 filterDef.setFactoryMethod( method );
227 }
228 if ( method.isAnnotationPresent( Key.class ) ) {
229 if ( filterDef.getKeyMethod() != null ) {
230 throw new SearchException("Multiple @Key methods found" + defAnn.name() + ": "
231 + filterDef.getImpl().getName() + "." + method.getName() );
232 }
233 if ( !method.isAccessible() ) method.setAccessible( true );
234 filterDef.setKeyMethod( method );
235 }
236
237 String name = method.getName();
238 if ( name.startsWith( "set" ) && method.getParameterTypes().length == 1 ) {
239 filterDef.addSetter( Introspector.decapitalize( name.substring( 3 ) ), method );
240 }
241 }
242 filterDefinitions.put( defAnn.name(), filterDef );
243 }
244
245
246 public Map<Class<?>, DocumentBuilder<?>> getDocumentBuilders() {
247 if (barrier != 0) { } //read barrier
248 return documentBuilders;
249 }
250
251 @SuppressWarnings( "unckecked" )
252 public <T> DocumentBuilder<T> getDocumentBuilder(Class<T> entityType) {
253 if (barrier != 0) { } //read barrier
254 return ( DocumentBuilder<T> ) documentBuilders.get( entityType );
255 }
256
257 @SuppressWarnings( "unckecked" )
258 public <T> DocumentBuilder<T> getContainedInOnlyBuilder(Class<T> entityType) {
259 if (barrier != 0) { } //read barrier
260 return ( DocumentBuilder<T> ) containedInOnlyBuilders.get( entityType );
261 }
262
263 public Set<DirectoryProvider<?>> getDirectoryProviders() {
264 if (barrier != 0) { } //read barrier
265 return this.dirProviderData.keySet();
266 }
267
268 public Worker getWorker() {
269 if (barrier != 0) { } //read barrier
270 return worker;
271 }
272
273 public void addOptimizerStrategy(DirectoryProvider<?> provider, OptimizerStrategy optimizerStrategy) {
274 //no need to set a read barrier, we run this method on the init thread
275 DirectoryProviderData data = dirProviderData.get(provider);
276 if (data == null) {
277 data = new DirectoryProviderData();
278 dirProviderData.put( provider, data );
279 }
280 data.optimizerStrategy = optimizerStrategy;
281 }
282
283 public void addIndexingParameters(DirectoryProvider<?> provider, LuceneIndexingParameters indexingParams) {
284 //no need to set a read barrier, we run this method on the init thread
285 dirProviderIndexingParams.put( provider, indexingParams );
286 }
287
288 public OptimizerStrategy getOptimizerStrategy(DirectoryProvider<?> provider) {
289 if (barrier != 0) {} //read barrier
290 return dirProviderData.get( provider ).optimizerStrategy;
291 }
292
293 public LuceneIndexingParameters getIndexingParameters(DirectoryProvider<?> provider ) {
294 if (barrier != 0) {} //read barrier
295 return dirProviderIndexingParams.get( provider );
296 }
297
298 public ReaderProvider getReaderProvider() {
299 if (barrier != 0) {} //read barrier
300 return readerProvider;
301 }
302
303 public DirectoryProvider[] getDirectoryProviders(Class<?> entity) {
304 if (barrier != 0) {} //read barrier
305 DocumentBuilder<?> documentBuilder = getDocumentBuilder( entity );
306 return documentBuilder == null ? null : documentBuilder.getDirectoryProviders();
307 }
308
309 public void optimize() {
310 if (barrier != 0) {} //read barrier
311 Set<Class<?>> clazzs = getDocumentBuilders().keySet();
312 for (Class clazz : clazzs) {
313 optimize( clazz );
314 }
315 }
316
317 public void optimize(Class entityType) {
318 if (barrier != 0) {} //read barrier
319 if ( ! getDocumentBuilders().containsKey( entityType ) ) {
320 throw new SearchException("Entity not indexed: " + entityType);
321 }
322 List<LuceneWork> queue = new ArrayList<LuceneWork>(1);
323 queue.add( new OptimizeLuceneWork( entityType ) );
324 getBackendQueueProcessorFactory().getProcessor( queue ).run();
325 }
326
327 public Analyzer getAnalyzer(String name) {
328 if (barrier != 0) {} //read barrier
329 final Analyzer analyzer = analyzers.get( name );
330 if ( analyzer == null) throw new SearchException( "Unknown Analyzer definition: " + name);
331 return analyzer;
332 }
333
334 public Analyzer getAnalyzer(Class clazz) {
335 if ( clazz == null) {
336 throw new IllegalArgumentException( "A class has to be specified for retrieving a scoped analyzer" );
337 }
338
339 DocumentBuilder<?> builder = documentBuilders.get( clazz );
340 if ( builder == null ) {
341 throw new IllegalArgumentException( "Entity for which to retrieve the scoped analyzer is not an @Indexed entity: " + clazz.getName() );
342 }
343
344 return builder.getAnalyzer();
345 }
346
347 private void initDocumentBuilders(SearchConfiguration cfg, ReflectionManager reflectionManager) {
348 InitContext context = new InitContext( cfg );
349 Iterator<Class<?>> iter = cfg.getClassMappings();
350 DirectoryProviderFactory factory = new DirectoryProviderFactory();
351
352 while ( iter.hasNext() ) {
353 Class mappedClass = iter.next();
354 if (mappedClass != null) {
355 XClass mappedXClass = reflectionManager.toXClass(mappedClass);
356 if ( mappedXClass != null) {
357 if ( mappedXClass.isAnnotationPresent( Indexed.class ) ) {
358 DirectoryProviderFactory.DirectoryProviders providers = factory.createDirectoryProviders( mappedXClass, cfg, this, reflectionManager );
359 //FIXME DocumentBuilder needs to be built by a helper method receiving Class<T> to infer T properly
360 //XClass unfortunately is not (yet) genericized: TODO?
361 final DocumentBuilder<?> documentBuilder = new DocumentBuilder(
362 mappedXClass, context, providers.getProviders(), providers.getSelectionStrategy(),
363 reflectionManager
364 );
365
366 documentBuilders.put( mappedClass, documentBuilder );
367 }
368 else {
369 //FIXME DocumentBuilder needs to be built by a helper method receiving Class<T> to infer T properly
370 //XClass unfortunately is not (yet) genericized: TODO?
371 final DocumentBuilder<?> documentBuilder = new DocumentBuilder(
372 mappedXClass, context, reflectionManager
373 );
374 //TODO enhance that, I don't like to expose EntityState
375 if ( documentBuilder.getEntityState() != EntityState.NON_INDEXABLE ) {
376 containedInOnlyBuilders.put( mappedClass, documentBuilder );
377 }
378 }
379 bindFilterDefs(mappedXClass);
380 //TODO should analyzer def for classes at tyher sqme level???
381 }
382 }
383 }
384 analyzers = context.initLazyAnalyzers();
385 factory.startDirectoryProviders();
386 }
387
388 private static FilterCachingStrategy buildFilterCachingStrategy(Properties properties) {
389 FilterCachingStrategy filterCachingStrategy;
390 String impl = properties.getProperty( Environment.FILTER_CACHING_STRATEGY );
391 if ( StringHelper.isEmpty( impl ) || "mru".equalsIgnoreCase( impl ) ) {
392 filterCachingStrategy = new MRUFilterCachingStrategy();
393 }
394 else {
395 try {
396 Class filterCachingStrategyClass = org.hibernate.annotations.common.util.ReflectHelper.classForName( impl, SearchFactoryImpl.class );
397 filterCachingStrategy = (FilterCachingStrategy) filterCachingStrategyClass.newInstance();
398 }
399 catch (ClassNotFoundException e) {
400 throw new SearchException( "Unable to find filterCachingStrategy class: " + impl, e );
401 }
402 catch (IllegalAccessException e) {
403 throw new SearchException( "Unable to instantiate filterCachingStrategy class: " + impl, e );
404 }
405 catch (InstantiationException e) {
406 throw new SearchException( "Unable to instantiate filterCachingStrategy class: " + impl, e );
407 }
408 }
409 filterCachingStrategy.initialize( properties );
410 return filterCachingStrategy;
411 }
412
413 public FilterCachingStrategy getFilterCachingStrategy() {
414 if (barrier != 0) {} //read barrier
415 return filterCachingStrategy;
416 }
417
418 public FilterDef getFilterDefinition(String name) {
419 if (barrier != 0) {} //read barrier
420 return filterDefinitions.get( name );
421 }
422
423 private static class DirectoryProviderData {
424 public final ReentrantLock dirLock = new ReentrantLock();
425 public OptimizerStrategy optimizerStrategy;
426 public Set<Class<?>> classes = new HashSet<Class<?>>(2);
427 }
428
429 public ReentrantLock getDirectoryProviderLock(DirectoryProvider<?> dp) {
430 if (barrier != 0) {} //read barrier
431 return this.dirProviderData.get( dp ).dirLock;
432 }
433
434 public void addDirectoryProvider(DirectoryProvider<?> provider) {
435 //no need to set a barrier we use this method in the init thread
436 this.dirProviderData.put( provider, new DirectoryProviderData() );
437 }
438
439 public int getFilterCacheBitResultsSize() {
440 if (barrier != 0) {} //read barrier
441 return cacheBitResultsSize;
442 }
443 }