/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.cxf.jaxrs.ext.search.lucene; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Stack; import org.apache.cxf.jaxrs.ext.search.ConditionType; import org.apache.cxf.jaxrs.ext.search.PrimitiveStatement; import org.apache.cxf.jaxrs.ext.search.SearchCondition; import org.apache.cxf.jaxrs.ext.search.visitor.AbstractSearchConditionVisitor; import org.apache.cxf.jaxrs.ext.search.visitor.ThreadLocalVisitorState; import org.apache.cxf.jaxrs.ext.search.visitor.VisitorState; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.DateTools; import org.apache.lucene.document.DateTools.Resolution; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.PhraseQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.util.QueryBuilder; import static org.apache.cxf.jaxrs.ext.search.ParamConverterUtils.getString; import static org.apache.cxf.jaxrs.ext.search.ParamConverterUtils.getValue; /** * LuceneQueryVisitor implements SearchConditionVisitor and returns corresponding Lucene query. The * implementations is thread-safe, however if visitor is called multiple times, each call to visit() * method should be preceded by reset() method call (to properly reset the visitor's internal * state). */ public class LuceneQueryVisitor<T> extends AbstractSearchConditionVisitor<T, Query> { private String contentsFieldName; private Map<String, String> contentsFieldMap; private boolean caseInsensitiveMatch; private VisitorState< Stack< List< Query > > > state = new ThreadLocalVisitorState< Stack< List< Query > > >(); private QueryBuilder queryBuilder; public LuceneQueryVisitor() { this(Collections.<String, String>emptyMap()); } public LuceneQueryVisitor(Analyzer analyzer) { this(Collections.<String, String>emptyMap(), null, analyzer); } public LuceneQueryVisitor(String contentsFieldAlias, String contentsFieldName) { this(Collections.singletonMap(contentsFieldAlias, contentsFieldName)); } public LuceneQueryVisitor(String contentsFieldName) { this(Collections.<String, String>emptyMap(), contentsFieldName); } public LuceneQueryVisitor(String contentsFieldName, Analyzer analyzer) { this(Collections.<String, String>emptyMap(), contentsFieldName, analyzer); } public LuceneQueryVisitor(Map<String, String> fieldsMap) { this(fieldsMap, null); } public LuceneQueryVisitor(Map<String, String> fieldsMap, String contentsFieldName) { this(fieldsMap, contentsFieldName, null); } public LuceneQueryVisitor(String contentsFieldAlias, String contentsFieldName, Analyzer analyzer) { this(Collections.singletonMap(contentsFieldAlias, contentsFieldName), null, analyzer); } public LuceneQueryVisitor(Map<String, String> fieldsMap, String contentsFieldName, Analyzer analyzer) { super(fieldsMap); this.contentsFieldName = contentsFieldName; if (analyzer != null) { queryBuilder = new QueryBuilder(analyzer); } } public void setContentsFieldMap(Map<String, String> map) { this.contentsFieldMap = map; } /** * Resets visitor's internal state. If the instance of the visitor is intended to be used many times, * each call to visit() method should be preceded by reset() method call. */ public void reset() { state.set(new Stack<List<Query>>()); state.get().push(new ArrayList<>()); } public void visit(SearchCondition<T> sc) { if (state.get() == null) { reset(); } PrimitiveStatement statement = sc.getStatement(); if (statement != null) { if (statement.getProperty() != null) { state.get().peek().add(buildSimpleQuery(sc.getConditionType(), statement.getProperty(), statement.getValue())); } } else { state.get().push(new ArrayList<>()); for (SearchCondition<T> condition : sc.getSearchConditions()) { condition.accept(this); } boolean orCondition = sc.getConditionType() == ConditionType.OR; List<Query> queries = state.get().pop(); state.get().peek().add(createCompositeQuery(queries, orCondition)); } } public Query getQuery() { List<Query> queries = state.get().peek(); return queries.isEmpty() ? null : queries.get(0); } public void setCaseInsensitiveMatch(boolean caseInsensitiveMatch) { this.caseInsensitiveMatch = caseInsensitiveMatch; } private Query buildSimpleQuery(ConditionType ct, String name, Object value) { name = super.getRealPropertyName(name); validatePropertyValue(name, value); Class<?> clazz = getPrimitiveFieldClass(name, value.getClass()); Query query = null; switch (ct) { case EQUALS: query = createEqualsQuery(clazz, name, value); break; case NOT_EQUALS: BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(createEqualsQuery(clazz, name, value), BooleanClause.Occur.MUST_NOT); query = booleanQuery; break; case GREATER_THAN: query = createRangeQuery(clazz, name, value, ct); break; case GREATER_OR_EQUALS: query = createRangeQuery(clazz, name, value, ct); break; case LESS_THAN: query = createRangeQuery(clazz, name, value, ct); break; case LESS_OR_EQUALS: query = createRangeQuery(clazz, name, value, ct); break; default: break; } return query; } private Query createEqualsQuery(Class<?> cls, String name, Object value) { Query query = null; if (cls == String.class) { String strValue = value.toString(); if (caseInsensitiveMatch) { strValue = strValue.toLowerCase(); } boolean isWildCard = strValue.contains("*") || super.isWildcardStringMatch(); String theContentsFieldName = getContentsFieldName(name); if (theContentsFieldName == null) { if (!isWildCard) { query = newTermQuery(name, strValue); } else { query = new WildcardQuery(new Term(name, strValue)); } } else if (!isWildCard) { query = newPhraseQuery(theContentsFieldName, strValue); } else { query = new WildcardQuery(new Term(theContentsFieldName, strValue)); } } else { query = createRangeQuery(cls, name, value, ConditionType.EQUALS); } return query; } private String getContentsFieldName(String name) { String fieldName = null; if (contentsFieldMap != null) { fieldName = contentsFieldMap.get(name); } if (fieldName == null) { fieldName = contentsFieldName; } return fieldName; } private Query createRangeQuery(Class<?> cls, String name, Object value, ConditionType type) { boolean minInclusive = type == ConditionType.GREATER_OR_EQUALS || type == ConditionType.EQUALS; boolean maxInclusive = type == ConditionType.LESS_OR_EQUALS || type == ConditionType.EQUALS; if (String.class.isAssignableFrom(cls) || Number.class.isAssignableFrom(cls)) { Query query = null; if (Double.class.isAssignableFrom(cls)) { query = createDoubleRangeQuery(name, value, type, minInclusive, maxInclusive); } else if (Float.class.isAssignableFrom(cls)) { query = createFloatRangeQuery(name, value, type, minInclusive, maxInclusive); } else if (Long.class.isAssignableFrom(cls)) { query = createLongRangeQuery(name, value, type, minInclusive, maxInclusive); } else { query = createIntRangeQuery(name, value, type, minInclusive, maxInclusive); } return query; } else if (Date.class.isAssignableFrom(cls)) { final Date date = getValue(Date.class, getFieldTypeConverter(), value.toString()); final String luceneDateValue = getString(Date.class, getFieldTypeConverter(), date); if (type == ConditionType.LESS_THAN || type == ConditionType.LESS_OR_EQUALS) { return TermRangeQuery.newStringRange(name, null, luceneDateValue, minInclusive, maxInclusive); } else { return TermRangeQuery.newStringRange(name, luceneDateValue, DateTools.dateToString(new Date(), Resolution.MILLISECOND), minInclusive, maxInclusive); } } else { return null; } } private Query createIntRangeQuery(final String name, final Object value, final ConditionType type, final boolean minInclusive, final boolean maxInclusive) { final Integer intValue = Integer.valueOf(value.toString()); return NumericRangeQuery.newIntRange(name, getMin(type, intValue), getMax(type, intValue), minInclusive, maxInclusive); } private Query createLongRangeQuery(final String name, final Object value, final ConditionType type, final boolean minInclusive, final boolean maxInclusive) { final Long longValue = Long.valueOf(value.toString()); return NumericRangeQuery.newLongRange(name, getMin(type, longValue), getMax(type, longValue), minInclusive, maxInclusive); } private Query createDoubleRangeQuery(final String name, final Object value, final ConditionType type, final boolean minInclusive, final boolean maxInclusive) { final Double doubleValue = Double.valueOf(value.toString()); return NumericRangeQuery.newDoubleRange(name, getMin(type, doubleValue), getMax(type, doubleValue), minInclusive, maxInclusive); } private Query createFloatRangeQuery(final String name, final Object value, final ConditionType type, final boolean minInclusive, final boolean maxInclusive) { final Float floatValue = Float.valueOf(value.toString()); return NumericRangeQuery.newFloatRange(name, getMin(type, floatValue), getMax(type, floatValue), minInclusive, maxInclusive); } private< N > N getMax(final ConditionType type, final N value) { return type == ConditionType.GREATER_THAN || type == ConditionType.GREATER_OR_EQUALS ? null : value; } private< N > N getMin(final ConditionType type, final N value) { return type == ConditionType.LESS_THAN || type == ConditionType.LESS_OR_EQUALS ? null : value; } private Query createCompositeQuery(List<Query> queries, boolean orCondition) { BooleanClause.Occur clause = orCondition ? BooleanClause.Occur.SHOULD : BooleanClause.Occur.MUST; BooleanQuery booleanQuery = new BooleanQuery(); for (Query query : queries) { booleanQuery.add(query, clause); } return booleanQuery; } private Query newTermQuery(final String field, final String query) { return (queryBuilder != null) ? queryBuilder.createBooleanQuery(field, query) : new TermQuery(new Term(field, query)); } private Query newPhraseQuery(final String field, final String query) { if (queryBuilder != null) { return queryBuilder.createPhraseQuery(field, query); } final PhraseQuery phraseQuery = new PhraseQuery(); phraseQuery.add(new Term(field, query)); return phraseQuery; } }