package fr.openwide.core.jpa.search.dao; import static fr.openwide.core.jpa.property.JpaPropertyIds.HIBERNATE_SEARCH_REINDEX_BATCH_SIZE; import static fr.openwide.core.jpa.property.JpaPropertyIds.HIBERNATE_SEARCH_REINDEX_LOAD_THREADS; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser.Operator; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.hibernate.CacheMode; import org.hibernate.search.MassIndexer; import org.hibernate.search.SearchFactory; import org.hibernate.search.batchindexing.MassIndexerProgressMonitor; import org.hibernate.search.batchindexing.impl.MassIndexerImpl; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.hcore.util.impl.HibernateHelper; import org.hibernate.search.jpa.FullTextEntityManager; import org.hibernate.search.jpa.FullTextQuery; import org.hibernate.search.jpa.Search; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import org.springframework.util.StringUtils; import com.google.common.collect.Iterables; import fr.openwide.core.jpa.business.generic.model.GenericEntity; import fr.openwide.core.jpa.exception.ServiceException; import fr.openwide.core.spring.property.service.IPropertyService; @Repository("hibernateSearchDao") public class HibernateSearchDaoImpl implements IHibernateSearchDao { private static final Logger LOGGER = LoggerFactory.getLogger(HibernateSearchDaoImpl.class); @Autowired private IPropertyService propertyService; @PersistenceContext private EntityManager entityManager; public HibernateSearchDaoImpl() { } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern, String analyzerName, Integer limit, Integer offset, Sort sort) throws ServiceException { List<Class<? extends T>> classes = new ArrayList<Class<? extends T>>(1); classes.add(clazz); return search(classes, fields, searchPattern, analyzerName, offset, limit, sort); } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern, String analyzerName) throws ServiceException { return search(clazz, fields, searchPattern, analyzerName, (Integer) null, (Integer) null, null); } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern, Integer limit, Integer offset, Sort sort) throws ServiceException { List<Class<? extends T>> classes = new ArrayList<Class<? extends T>>(1); classes.add(clazz); return search(classes, fields, searchPattern, Search.getFullTextEntityManager(entityManager).getSearchFactory().getAnalyzer(clazz), null, limit, offset, sort); } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern) throws ServiceException { return search(clazz, fields, searchPattern, (Integer) null, (Integer) null, null); } @Override @Deprecated public <T> List<T> search(Collection<Class<? extends T>> classes, String[] fields, String searchPattern, String analyzerName, Integer limit, Integer offset, Sort sort) throws ServiceException { return search(classes, fields, searchPattern, Search.getFullTextEntityManager(entityManager).getSearchFactory().getAnalyzer(analyzerName), null, limit, offset, sort); } @Override @Deprecated public <T> List<T> search(Collection<Class<? extends T>> classes, String[] fields, String searchPattern, String analyzerName) throws ServiceException { return search(classes, fields, searchPattern, analyzerName, (Integer) null, (Integer) null, null); } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern, String analyzerName, Query additionalLuceneQuery, Integer limit, Integer offset, Sort sort) throws ServiceException { List<Class<? extends T>> classes = new ArrayList<Class<? extends T>>(1); classes.add(clazz); return search(classes, fields, searchPattern, analyzerName, additionalLuceneQuery, limit, offset, sort); } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern, String analyzerName, Query additionalLuceneQuery) throws ServiceException { return search(clazz, fields, searchPattern, analyzerName, additionalLuceneQuery, (Integer) null, (Integer) null, null); } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern, Query additionalLuceneQuery, Integer limit, Integer offset, Sort sort) throws ServiceException { List<Class<? extends T>> classes = new ArrayList<Class<? extends T>>(1); classes.add(clazz); return search(classes, fields, searchPattern, Search.getFullTextEntityManager(entityManager).getSearchFactory().getAnalyzer(clazz), additionalLuceneQuery, limit, offset, sort); } @Override @Deprecated public <T> List<T> search(Class<T> clazz, String[] fields, String searchPattern, Query additionalLuceneQuery) throws ServiceException { return search(clazz, fields, searchPattern, additionalLuceneQuery, (Integer) null, (Integer) null, null); } @Override @Deprecated public <T> List<T> search(Collection<Class<? extends T>> classes, String[] fields, String searchPattern, String analyzerName, Query additionalLuceneQuery, Integer limit, Integer offset, Sort sort) throws ServiceException { return search(classes, fields, searchPattern, Search.getFullTextEntityManager(entityManager).getSearchFactory().getAnalyzer(analyzerName), additionalLuceneQuery, limit, offset, sort); } @Override @Deprecated public <T> List<T> search(Collection<Class<? extends T>> classes, String[] fields, String searchPattern, String analyzerName, Query additionalLuceneQuery) throws ServiceException { return search(classes, fields, searchPattern, analyzerName, additionalLuceneQuery, (Integer) null, (Integer) null, null); } @SuppressWarnings("unchecked") @Deprecated private <T> List<T> search(Collection<Class<? extends T>> classes, String[] fields, String searchPattern, Analyzer analyzer, Query additionalLuceneQuery, Integer limit, Integer offset, Sort sort) throws ServiceException { if (!StringUtils.hasText(searchPattern)) { return Collections.emptyList(); } try { FullTextEntityManager fullTextSession = Search.getFullTextEntityManager(entityManager); MultiFieldQueryParser parser = getMultiFieldQueryParser(fullTextSession, fields, MultiFieldQueryParser.AND_OPERATOR, analyzer); BooleanQuery.Builder bqBuilder = new BooleanQuery.Builder(); bqBuilder.add(parser.parse(searchPattern), BooleanClause.Occur.MUST); if (additionalLuceneQuery != null) { bqBuilder.add(additionalLuceneQuery, BooleanClause.Occur.MUST); } FullTextQuery hibernateQuery = fullTextSession.createFullTextQuery(bqBuilder.build(), Iterables.toArray(classes, Class.class)); if (offset != null) { hibernateQuery.setFirstResult(offset); } if (limit != null) { hibernateQuery.setMaxResults(limit); } if (sort != null) { hibernateQuery.setSort(sort); } else if (offset != null || limit != null) { LOGGER.warn("La requête ne spécifie pas de sort mais spécifie une limite ou un offset."); } return (List<T>) hibernateQuery.getResultList(); } catch(ParseException e) { throw new ServiceException(String.format("Error parsing request: %1$s", searchPattern), e); } catch (RuntimeException e) { throw new ServiceException(String.format("Error executing search: %1$s for classes: %2$s", searchPattern, classes), e); } } @Override public void reindexAll() throws ServiceException { reindexClasses(Object.class); } @Override public void reindexClasses(Class<?>... classes) throws ServiceException { try { FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); reindexClasses(fullTextEntityManager, getIndexedRootEntities(fullTextEntityManager.getSearchFactory(), classes.length > 0 ? classes : new Class<?>[] { Object.class })); } catch (RuntimeException | InterruptedException e) { throw new ServiceException(e); } } protected void reindexClasses(FullTextEntityManager fullTextEntityManager, Set<Class<?>> entityClasses) throws InterruptedException { int batchSize = propertyService.get(HIBERNATE_SEARCH_REINDEX_BATCH_SIZE); int loadThreads = propertyService.get(HIBERNATE_SEARCH_REINDEX_LOAD_THREADS); if (LOGGER.isInfoEnabled()) { LOGGER.info("Targets for indexing job: {}", entityClasses); } for (Class<?> clazz : entityClasses) { LOGGER.info(String.format("Reindexing %1$s.", clazz)); ProgressMonitor progressMonitor = new ProgressMonitor(); Thread t = new Thread(progressMonitor); t.start(); MassIndexer indexer = fullTextEntityManager.createIndexer(clazz); indexer.batchSizeToLoadObjects(batchSize) .threadsToLoadObjects(loadThreads) .cacheMode(CacheMode.NORMAL) .progressMonitor(progressMonitor) .startAndWait(); progressMonitor.stop(); t.interrupt(); LOGGER.info(String.format("Reindexing %1$s done.", clazz)); } } @Override public Set<Class<?>> getIndexedRootEntities(Class<?>... selection) { FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); Set<Class<?>> indexedEntityClasses = new TreeSet<Class<?>>(new Comparator<Class<?>>() { @Override public int compare(Class<?> o1, Class<?> o2) { return GenericEntity.DEFAULT_STRING_COLLATOR.compare(o1.getSimpleName(), o2.getSimpleName()); } }); indexedEntityClasses.addAll(getIndexedRootEntities(fullTextEntityManager.getSearchFactory(), selection)); return indexedEntityClasses; } @Override public <K extends Serializable & Comparable<K>, E extends GenericEntity<K, ?>> void reindexEntity(E entity) { if (entity != null) { FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); fullTextEntityManager.index(HibernateHelper.unproxy(entity)); } } /** * @see MassIndexerImpl#toRootEntities */ protected Set<Class<?>> getIndexedRootEntities(SearchFactory searchFactory, Class<?>... selection) { ExtendedSearchIntegrator searchIntegrator = searchFactory.unwrap(ExtendedSearchIntegrator.class); Set<Class<?>> entities = new HashSet<Class<?>>(); // first build the "entities" set containing all indexed subtypes of "selection". for (Class<?> entityType : selection) { Set<Class<?>> targetedClasses = searchIntegrator.getIndexedTypesPolymorphic(new Class[] { entityType }); if (targetedClasses.isEmpty()) { String msg = entityType.getName() + " is not an indexed entity or a subclass of an indexed entity"; throw new IllegalArgumentException(msg); } entities.addAll(targetedClasses); } Set<Class<?>> cleaned = new HashSet<Class<?>>(); Set<Class<?>> toRemove = new HashSet<Class<?>>(); //now remove all repeated types to avoid duplicate loading by polymorphic query loading for (Class<?> type : entities) { boolean typeIsOk = true; for (Class<?> existing : cleaned) { if (existing.isAssignableFrom(type)) { typeIsOk = false; break; } if (type.isAssignableFrom(existing)) { toRemove.add(existing); } } if (typeIsOk) { cleaned.add(type); } } cleaned.removeAll(toRemove); return cleaned; } @Deprecated private MultiFieldQueryParser getMultiFieldQueryParser(FullTextEntityManager fullTextEntityManager, String[] fields, Operator defaultOperator, Analyzer analyzer) { MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer); parser.setDefaultOperator(defaultOperator); return parser; } protected EntityManager getEntityManager() { return entityManager; } private static final class ProgressMonitor implements MassIndexerProgressMonitor, Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(ProgressMonitor.class); private long documentsAdded; private int documentsBuilt; private int entitiesLoaded; private int totalCount; private boolean indexingCompleted; private boolean stopped; @Override public void documentsAdded(long increment) { this.documentsAdded += increment; } @Override public void documentsBuilt(int number) { this.documentsBuilt += number; } @Override public void entitiesLoaded(int size) { this.entitiesLoaded += size; } @Override public void addToTotalCount(long count) { this.totalCount += count; } @Override public void indexingCompleted() { this.indexingCompleted = true; } public void stop() { this.stopped = true; log(); } @Override public void run() { if (LOGGER.isDebugEnabled()) { try { while (true) { log(); Thread.sleep(15000); if (indexingCompleted) { LOGGER.debug("Indexing done"); break; } } } catch (RuntimeException | InterruptedException e) { if (!stopped) { LOGGER.error("Error ; massindexer monitor stopped", e); } LOGGER.debug("Massindexer monitor thread interrupted"); } } } private void log() { LOGGER.debug(String.format("Indexing %1$d / %2$d (entities loaded: %3$d, documents built: %4$d)", documentsAdded, totalCount, entitiesLoaded, documentsBuilt)); } } @Override public void flushToIndexes() { Search.getFullTextEntityManager(entityManager).flushToIndexes(); } }