package fr.openwide.core.jpa.more.business.search.query; import java.util.Collection; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.queryparser.simple.SimpleQueryParser; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.Query; import org.bindgen.binding.AbstractBinding; import org.hibernate.search.jpa.FullTextEntityManager; import org.hibernate.search.jpa.Search; import org.hibernate.search.query.dsl.BooleanJunction; import org.hibernate.search.query.dsl.QueryBuilder; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import fr.openwide.core.jpa.search.bridge.GenericEntityIdFieldBridge; import fr.openwide.core.jpa.search.bridge.NullEncodingGenericEntityIdFieldBridge; import fr.openwide.core.spring.util.StringUtils; import fr.openwide.core.spring.util.lucene.search.LuceneUtils; public class HibernateSearchLuceneQueryFactoryImpl implements IHibernateSearchLuceneQueryFactory { private static final Function<AbstractBinding<?, String>, String> BINDING_TO_PATH_FUNCTION = new Function<AbstractBinding<?, String>, String>() { @Override public String apply(AbstractBinding<?, String> input) { if (input == null) { throw new IllegalStateException("Path may not be null."); } return input.getPath(); } }; @PersistenceContext private EntityManager entityManager; private FullTextEntityManager fullTextEntityManager; private Map<Class<?>, QueryBuilder> queryBuilderCache = new HashMap<Class<?>, QueryBuilder>(); private Map<Class<?>, Analyzer> analyzerCache = Maps.newHashMap(); private Class<?> defaultClass; @PostConstruct private void init() { this.fullTextEntityManager = Search.getFullTextEntityManager(entityManager); } protected FullTextEntityManager getFullTextEntityManager() { return fullTextEntityManager; } @Override public Class<?> getDefaultClass() { return defaultClass; } @Override public void setDefaultClass(Class<?> defaultClass) { this.defaultClass = defaultClass; } @Override public QueryBuilder getDefaultQueryBuilder() { return getQueryBuilder(defaultClass); } @Override public QueryBuilder getQueryBuilder(Class<?> clazz) { QueryBuilder queryBuilder = queryBuilderCache.get(clazz); if (queryBuilder == null) { queryBuilder = createQueryBuilder(fullTextEntityManager, clazz); queryBuilderCache.put(clazz, queryBuilder); } return queryBuilder; } protected QueryBuilder createQueryBuilder(FullTextEntityManager fullTextEntityManager, Class<?> clazz) { return fullTextEntityManager.getSearchFactory().buildQueryBuilder() .forEntity(clazz).get(); } @Override public Analyzer getDefaultAnalyzer() { return getAnalyzer(defaultClass); } @Override public Analyzer getAnalyzer(Class<?> clazz) { Analyzer analyzer = analyzerCache.get(clazz); if (analyzer == null) { analyzer = createAnalyzer(fullTextEntityManager, clazz); analyzerCache.put(clazz, analyzer); } return analyzer; } protected Analyzer createAnalyzer(FullTextEntityManager fullTextEntityManager, Class<?> clazz) { return fullTextEntityManager.getSearchFactory().getAnalyzer(clazz); } // Any/all @Override public Query all(Query ... queries) { return all(getDefaultQueryBuilder(), queries); } @Override public Query all(QueryBuilder builder, Query ... queries) { BooleanJunction<?> junction = null; for (Query query : queries) { if (query != null) { junction = junction != null ? junction : builder.bool(); junction.must(query); } } return junction != null ? junction.createQuery() : null; } @Override public Query any(Query ... queries) { return any(getDefaultQueryBuilder(), queries); } @Override public Query any(QueryBuilder builder, Query ... queries) { BooleanJunction<?> junction = null; for (Query query : queries) { if (query != null) { junction = junction != null ? junction : builder.bool(); junction.should(query); } } return junction != null ? junction.createQuery() : null; } // > Match null /** * <strong>Be careful</strong>: using this method needs null values to be indexed. * You can use {@link NullEncodingGenericEntityIdFieldBridge} instead of the classical {@link GenericEntityIdFieldBridge} for example. */ @Override public final Query matchNull(AbstractBinding<?, ?> binding) { return matchNull(getDefaultQueryBuilder(), binding); } @Override public final Query matchNull(QueryBuilder builder, AbstractBinding<?, ?> binding) { return matchNull(builder, binding.getPath()); } @Override public final Query matchNull(String fieldPath) { return matchNull(getDefaultQueryBuilder(), fieldPath); } @Override public Query matchNull(QueryBuilder builder, String fieldPath) { return builder.keyword() .onField(fieldPath) .matching(null) .createQuery(); } // > Match if given @Override public final <P> Query matchIfGiven(AbstractBinding<?, P> binding, P value) { return matchIfGiven(getDefaultQueryBuilder(), binding, value); } @Override public final <P> Query matchIfGiven(QueryBuilder builder, AbstractBinding<?, P> binding, P value) { return matchIfGiven(builder, binding.getPath(), value); } @Override public final <P> Query matchIfGiven(String fieldPath, P value) { return matchIfGiven(getDefaultQueryBuilder(), fieldPath, value); } @Override public <P> Query matchIfGiven(QueryBuilder builder, String fieldPath, P value) { if (value == null) { return null; } return builder.keyword() .onField(fieldPath) .matching(value) .createQuery(); } // > Match one term if given @Override public final Query matchOneTermIfGiven(AbstractBinding<?, String> binding, String terms) { return matchOneTermIfGiven(getDefaultQueryBuilder(), binding.getPath(), terms); } @Override public final Query matchOneTermIfGiven(QueryBuilder builder, AbstractBinding<?, String> binding, String terms) { return matchOneTermIfGiven(builder, binding.getPath(), terms); } @Override public final Query matchOneTermIfGiven(String fieldPath, String terms) { return matchOneTermIfGiven(getDefaultQueryBuilder(), fieldPath, terms); } @Override public Query matchOneTermIfGiven(QueryBuilder builder, String fieldPath, String terms) { if (!StringUtils.hasText(terms)) { return null; } return builder.keyword() .onField(fieldPath) .matching(terms) .createQuery(); } // > Match all terms if given @Override @SafeVarargs public final Query matchAllTermsIfGiven(Analyzer analyzer, String terms, AbstractBinding<?, String> binding, AbstractBinding<?, String> ... otherBindings) { return matchAllTermsIfGiven(analyzer, terms, Lists.transform(Lists.asList(binding, otherBindings), BINDING_TO_PATH_FUNCTION)); } @Override @SafeVarargs public final Query matchAllTermsIfGiven(String terms, AbstractBinding<?, String> binding, AbstractBinding<?, String> ... otherBindings) { return matchAllTermsIfGiven(getDefaultAnalyzer(), terms, Lists.transform(Lists.asList(binding, otherBindings), BINDING_TO_PATH_FUNCTION)); } @Override public final Query matchAllTermsIfGiven(String terms, Iterable<String> fieldPaths) { return matchAllTermsIfGiven(getDefaultAnalyzer(), terms, fieldPaths); } @Override public Query matchAllTermsIfGiven(Analyzer analyzer, String terms, Iterable<String> fieldPaths) { if (!StringUtils.hasText(terms)) { return null; } Map<String, Float> fields = Maps.newHashMap(); for (String fieldPath : fieldPaths) { fields.put(fieldPath, 1.0f); } SimpleQueryParser parser = new SimpleQueryParser(analyzer, fields); parser.setDefaultOperator(BooleanClause.Occur.MUST); return parser.parse(terms); } // > Match autocomplete @Override @SafeVarargs public final Query matchAutocompleteIfGiven(Analyzer analyzer, String terms, AbstractBinding<?, String> binding, AbstractBinding<?, String> ... otherBindings) { return matchAutocompleteIfGiven(analyzer, terms, Lists.transform(Lists.asList(binding, otherBindings), BINDING_TO_PATH_FUNCTION)); } @Override @SafeVarargs public final Query matchAutocompleteIfGiven(String terms, AbstractBinding<?, String> binding, AbstractBinding<?, String> ... otherBindings) { return matchAutocompleteIfGiven(getDefaultAnalyzer(), terms, Lists.transform(Lists.asList(binding, otherBindings), BINDING_TO_PATH_FUNCTION)); } @Override public final Query matchAutocompleteIfGiven(String terms, Iterable<String> fieldPaths) { return matchAutocompleteIfGiven(getDefaultAnalyzer(), terms, fieldPaths); } @Override public Query matchAutocompleteIfGiven(Analyzer analyzer, String terms, Iterable<String> fieldPaths) { if (!StringUtils.hasText(terms)) { return null; } return LuceneUtils.getAutocompleteQuery(fieldPaths, analyzer, terms); } // > Match fuzzy @Override @SafeVarargs public final Query matchFuzzyIfGiven(Analyzer analyzer, String terms, Integer maxEditDistance, AbstractBinding<?, String> binding, AbstractBinding<?, String> ... otherBindings) { return matchFuzzyIfGiven(analyzer, terms, maxEditDistance, Lists.transform(Lists.asList(binding, otherBindings), BINDING_TO_PATH_FUNCTION)); } @Override @SafeVarargs public final Query matchFuzzyIfGiven(String terms, Integer maxEditDistance, AbstractBinding<?, String> binding, AbstractBinding<?, String> ... otherBindings) { return matchFuzzyIfGiven(getDefaultAnalyzer(), terms, maxEditDistance, Lists.transform(Lists.asList(binding, otherBindings), BINDING_TO_PATH_FUNCTION)); } @Override public final Query matchFuzzyIfGiven(String terms, Integer maxEditDistance, Iterable<String> fieldPaths) { return matchFuzzyIfGiven(getDefaultAnalyzer(), terms, maxEditDistance, fieldPaths); } @Override public Query matchFuzzyIfGiven(Analyzer analyzer, String terms, Integer maxEditDistance, Iterable<String> fieldPaths) { if (!StringUtils.hasText(terms)) { return null; } return LuceneUtils.getSimilarityQuery(fieldPaths, analyzer, terms, maxEditDistance); } // > Be included if given @Override public final <P> Query beIncludedIfGiven(AbstractBinding<?, ? extends Collection<P>> binding, P value) { return beIncludedIfGiven(getDefaultQueryBuilder(), binding, value); } @Override public final <P> Query beIncludedIfGiven(QueryBuilder builder, AbstractBinding<?, ? extends Collection<P>> binding, P value) { return beIncludedIfGiven(builder, binding.getPath(), value); } @Override public final <P> Query beIncludedIfGiven(String fieldPath, P value) { return beIncludedIfGiven(getDefaultQueryBuilder(), fieldPath, value); } @Override public <P> Query beIncludedIfGiven(QueryBuilder builder, String fieldPath, P value) { if (value == null) { return null; } return builder.keyword() .onField(fieldPath) .matching(value) .createQuery(); } // > Match one if given @Override public final <P> Query matchOneIfGiven(AbstractBinding<?, P> binding, Collection<? extends P> possibleValues) { return matchOneIfGiven(getDefaultQueryBuilder(), binding, possibleValues); } @Override public final <P> Query matchOneIfGiven(QueryBuilder builder, AbstractBinding<?, P> binding, Collection<? extends P> possibleValues) { return matchOneIfGiven(builder, binding.getPath(), possibleValues); } @Override public final <P> Query matchOneIfGiven(String fieldPath, Collection<? extends P> possibleValues) { return matchOneIfGiven(getDefaultQueryBuilder(), fieldPath, possibleValues); } @Override public <P> Query matchOneIfGiven(QueryBuilder builder, String fieldPath, Collection<? extends P> possibleValues) { if (possibleValues == null || possibleValues.isEmpty()) { return null; } BooleanJunction<?> subJunction = builder.bool(); for (P possibleValue : possibleValues) { subJunction.should(builder.keyword() .onField(fieldPath) .matching(possibleValue) .createQuery()); } return subJunction.createQuery(); } // > Match all if given @Override public final <P> Query matchAllIfGiven(AbstractBinding<?, ? extends Collection<P>> binding, Collection<? extends P> possibleValues) { return matchAllIfGiven(getDefaultQueryBuilder(), binding, possibleValues); } @Override public final <P> Query matchAllIfGiven(QueryBuilder builder, AbstractBinding<?, ? extends Collection<P>> binding, Collection<? extends P> possibleValues) { return matchAllIfGiven(builder, binding.getPath(), possibleValues); } @Override public final <P> Query matchAllIfGiven(String fieldPath, Collection<? extends P> possibleValues) { return matchAllIfGiven(getDefaultQueryBuilder(), fieldPath, possibleValues); } @Override public <P> Query matchAllIfGiven(QueryBuilder builder, String fieldPath, Collection<? extends P> values) { if (values == null || values.isEmpty()) { return null; } BooleanJunction<?> subJunction = builder.bool(); for (P possibleValue : values) { subJunction.must(builder.keyword() .onField(fieldPath) .matching(possibleValue) .createQuery()); } return subJunction.createQuery(); } // > Match if true @Override public final Query matchIfTrue(AbstractBinding<?, Boolean> binding, boolean value, Boolean mustMatch) { return matchIfTrue(getDefaultQueryBuilder(), binding, value, mustMatch); } @Override public final Query matchIfTrue(QueryBuilder builder, AbstractBinding<?, Boolean> binding, boolean value, Boolean mustMatch) { return matchIfTrue(builder, binding.getPath(), value, mustMatch); } @Override public final <P> Query matchIfTrue(String fieldPath, boolean value, Boolean mustMatch) { return matchIfTrue(getDefaultQueryBuilder(), fieldPath, value, mustMatch); } @Override public Query matchIfTrue(QueryBuilder builder, String fieldPath, boolean value, Boolean mustMatch) { if (mustMatch == null || !mustMatch) { return null; } return builder.keyword() .onField(fieldPath) .matching(value) .createQuery(); } // > Match range (min, max, both) @Override public final <P> Query matchRangeMin(AbstractBinding<?, P> binding, P min) { return matchRangeMin(getDefaultQueryBuilder(), binding, min); } @Override public final <P> Query matchRangeMin(QueryBuilder builder, AbstractBinding<?, P> binding, P min) { return matchRangeMin(builder, binding.getPath(), min); } @Override public final <P> Query matchRangeMin(String fieldPath, P min) { return matchRangeMin(getDefaultQueryBuilder(), fieldPath, min); } @Override public <P> Query matchRangeMin(QueryBuilder builder, String fieldPath, P min) { if (min == null) { return null; } return builder.range() .onField(fieldPath) .above(min) .createQuery(); } @Override public final <P> Query matchRangeMax(AbstractBinding<?, P> binding, P max) { return matchRangeMax(getDefaultQueryBuilder(), binding, max); } @Override public final <P> Query matchRangeMax(QueryBuilder builder, AbstractBinding<?, P> binding, P max) { return matchRangeMax(builder, binding.getPath(), max); } @Override public final <P> Query matchRangeMax(String fieldPath, P max) { return matchRangeMax(getDefaultQueryBuilder(), fieldPath, max); } @Override public <P> Query matchRangeMax(QueryBuilder builder, String fieldPath, P max) { if (max == null) { return null; } return builder.range() .onField(fieldPath) .below(max) .createQuery(); } @Override public final <P> Query matchRange(AbstractBinding<?, P> binding, P min, P max) { return matchRange(getDefaultQueryBuilder(), binding.getPath(), min, max); } @Override public final <P> Query matchRange(QueryBuilder builder, AbstractBinding<?, P> binding, P min, P max) { return matchRange(builder, binding.getPath(), min, max); } @Override public final <P> Query matchRange(String fieldPath, P min, P max) { return matchRange(getDefaultQueryBuilder(), fieldPath, min, max); } @Override public <P> Query matchRange(QueryBuilder builder, String fieldPath, P min, P max) { if (max != null && min != null) { return builder.range() .onField(fieldPath) .from(min).to(max) .createQuery(); } else if (min != null) { return matchRangeMin(builder, fieldPath, min); } else if (max != null) { return matchRangeMax(builder, fieldPath, max); } else { return null; } } }