package org.jboss.seam.wiki.core.search; import org.apache.lucene.search.*; import org.apache.lucene.index.Term; import org.hibernate.Hibernate; import org.hibernate.search.FullTextQuery; import org.hibernate.search.FullTextSession; import org.hibernate.search.engine.DocumentBuilder; import org.hibernate.search.bridge.StringBridge; import org.jboss.seam.Component; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.*; import org.jboss.seam.log.Log; import org.jboss.seam.wiki.core.search.annotations.SearchableType; import org.jboss.seam.wiki.core.search.metamodel.SearchRegistry; import org.jboss.seam.wiki.core.search.metamodel.SearchableEntity; import org.jboss.seam.wiki.core.search.metamodel.SearchableProperty; import javax.persistence.EntityManager; import java.io.Serializable; import java.util.*; /** * Core search engine, coordinates the search UI, query building, and hit extraction. * <p> * This controller is the backend for two different UIs: A simple query input field that * is available on all pages, and the complete and complex search mask on the search page. * * @author Christian Bauer */ @Name("wikiSearch") @Scope(ScopeType.CONVERSATION) public class WikiSearch implements Serializable { public static final String FIELD_READACCESSLVL = "readAccessLevel"; @Logger static Log log; @In protected EntityManager restrictedEntityManager; @In private SearchRegistry searchRegistry; // For UI binding to the global search field (and simplified search mask) private String simpleQuery = "Search..."; private Boolean simpleQueryMatchExactPhrase; public String getSimpleQuery() { return simpleQuery; } public void setSimpleQuery(String simpleQuery) { this.simpleQuery = simpleQuery; } public Boolean getSimpleQueryMatchExactPhrase() { return simpleQueryMatchExactPhrase; } public void setSimpleQueryMatchExactPhrase(Boolean simpleQueryMatchExactPhrase) { this.simpleQueryMatchExactPhrase = simpleQueryMatchExactPhrase; } /// For UI binding of the complex search mask (with expanded options) private SearchableEntity selectedSearchableEntity; public SearchableEntity getSelectedSearchableEntity() { return selectedSearchableEntity; } public void setSelectedSearchableEntity(SearchableEntity selectedSearchableEntity) { this.selectedSearchableEntity = selectedSearchableEntity; } private Map<SearchableEntity, List<PropertySearch>> searches = new HashMap<SearchableEntity, List<PropertySearch>>(); public Map<SearchableEntity, List<PropertySearch>> getSearches() { return searches; } public void setSearches(Map<SearchableEntity, List<PropertySearch>> searches) { this.searches = searches; } Set<SearchableEntity> searchEntities; private int totalCount; private int maxPageSize; private int pageSize; private int page; @Create public void create() { // TODO: Minor optimization, do this lazily when search.xhtml is rendered, not when the component is created (on every wiki page render) // Initialize the value holders used for UI binding for (SearchableEntity searchableEntity : searchRegistry.getSearchableEntities()) { log.trace("preparing search value holder for entity: " + searchableEntity.getDescription()); List<PropertySearch> searchesForEntity = new ArrayList<PropertySearch>(); for (SearchableProperty prop : searchableEntity.getProperties()) { log.trace("preparing search value holder for property: " + prop.getDescription()); searchesForEntity.add(new PropertySearch(prop)); } searches.put(searchableEntity, searchesForEntity); } pageSize = 15; maxPageSize = 100; } List<SearchHit> searchResult; public List<SearchHit> getSearchResult() { if (searchResult == null) search(); return searchResult; } public void search() { page = 0; searchEntities = new TreeSet<SearchableEntity>(); if (selectedSearchableEntity == null) { // Nothing selected, do a global search on all entities that support phrases and // use the simpleQuery as "include" search term for these phrases log.debug("global search on all entities with phrase-type properties"); for (Map.Entry<SearchableEntity, List<PropertySearch>> entry : searches.entrySet()) { for (PropertySearch propertySearch : entry.getValue()) { if (SearchableType.PHRASE.equals(propertySearch.getProperty().getType())) { propertySearch.getTerms().put(SearchableProperty.TERM_INCLUDE, getSimpleQuery()); propertySearch.getTerms().put(SearchableProperty.TERM_EXCLUDE, ""); propertySearch.getTerms().put(SearchableProperty.TERM_MATCHEXACTPHRASE, getSimpleQueryMatchExactPhrase()); searchEntities.add(entry.getKey()); } // And also simple string queries get the term if (SearchableType.STRING.equals(propertySearch.getProperty().getType())) { propertySearch.getTerms().put(SearchableProperty.TERM_INCLUDE, getSimpleQuery()); } } } } else { // Form with search details selected and filled out log.debug("searching only indexed entity: " + selectedSearchableEntity); searchEntities.add(selectedSearchableEntity); } executeSearch(searchEntities); } private void executeSearch(Set<SearchableEntity> searchableEntities) { log.debug("searching entities: " + searchableEntities.size()); BooleanQuery mainQuery = new BooleanQuery(); // Get value holders filled out by UI forms and generate a Lucene query for (SearchableEntity searchableEntity : searchableEntities) { log.debug("building query for entity: " + searchableEntity.getClazz()); BooleanQuery entityQuery = new BooleanQuery(); // Add restriction to entity clazz // We use a Hibernate Search internal constant here to limit THIS particular entity query // fragment to a particular indexed persistent entity type. log.debug("adding restriction to entity clazz: " + searchableEntity.getClazz().getName()); entityQuery.add( new TermQuery( new Term(DocumentBuilder.CLASS_FIELDNAME, searchableEntity.getClazz().getName()) ), BooleanClause.Occur.MUST ); // Add sub-queries for all entity properties BooleanQuery allPropertiesQuery = new BooleanQuery(); for (PropertySearch search : searches.get(searchableEntity)) { log.debug("building query for property: " + search.getProperty()); Query propertiesQuery = search.getProperty().getQuery(search); if (propertiesQuery != null) { // Any property can match, except if we are searching only one entity, then all must match allPropertiesQuery.add( propertiesQuery, searchableEntities.size() == 1 ? BooleanClause.Occur.MUST : BooleanClause.Occur.SHOULD ); } } // But SOME of the property searches for this entity must match log.debug("adding query to owning entity for properties: " + allPropertiesQuery.getClauses().length); entityQuery.add(allPropertiesQuery, BooleanClause.Occur.MUST); // Finally, figure out if this entity query needs to be read-restricted, we have indexed the readAccessLevel of it if (searchableEntity.getHandler().isReadAccessChecked()) { Integer currentAccessLevel = (Integer)Component.getInstance("currentAccessLevel"); StringBridge paddingBridge = new PaddedIntegerBridge(); Query accessLimitQuery = new ConstantScoreRangeQuery(FIELD_READACCESSLVL, null, paddingBridge.objectToString(currentAccessLevel), true, true); Filter accessFilter = new QueryWrapperFilter(accessLimitQuery); FilteredQuery accessFilterQuery = new FilteredQuery(entityQuery, accessFilter); log.debug("adding filtered entity query to main query: " + accessFilterQuery); mainQuery.add(accessFilterQuery, BooleanClause.Occur.SHOULD); } else { log.debug("adding unfiltered entity query to main query: " + entityQuery); mainQuery.add(entityQuery, BooleanClause.Occur.SHOULD); } } log.debug(">>>>> search query: " + mainQuery.toString()); try { FullTextQuery ftQuery = getFullTextSession().createFullTextQuery(mainQuery); ftQuery.setFirstResult(page * pageSize).setMaxResults(pageSize); totalCount = ftQuery.getResultSize(); log.debug("total search hits (might be paginated next): " + totalCount); List result = ftQuery.list(); // Extract hits log.debug("search hits passed to handlers: " + result.size()); searchResult = new ArrayList<SearchHit>(); for (Object o : result) { SearchableEntity se = searchRegistry.getSearchableEntitiesByName().get(Hibernate.getClass(o).getName()); if (se != null) { log.debug("extracting hit for indexed class: " + Hibernate.getClass(o).getName()); //noinspection unchecked searchResult.add( se.getHandler().extractHit(mainQuery, o) ); } } log.debug("extracted search hits and final result: " + searchResult.size()); } catch (Exception e) { throw new RuntimeException(e); } } private FullTextSession getFullTextSession() { return (FullTextSession) restrictedEntityManager.getDelegate(); } public void nextPage() { page++; executeSearch(searchEntities); } public void previousPage() { page--; executeSearch(searchEntities); } public void firstPage() { page = 0; executeSearch(searchEntities); } public void lastPage() { page = (totalCount / pageSize); if (totalCount % pageSize == 0) page--; executeSearch(searchEntities); } public boolean isNextPageAvailable() { return totalCount > ((page * pageSize) + pageSize); } public boolean isPreviousPageAvailable() { return page > 0; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize > maxPageSize ? maxPageSize : pageSize; // Prevent tampering } public long getFirstRow() { return page * pageSize + 1; } public long getLastRow() { return (page * pageSize + pageSize) > totalCount ? totalCount : page * pageSize + pageSize; } public int getTotalCount() { return totalCount; } }