blob: cd4311e7bd235ce8123d33d4dfce2d7fed7b3b21 [file] [log] [blame]
/**
*
*/
package org.eclipse.smila.solr.query;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.Factory;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.params.GroupParams;
import org.eclipse.smila.datamodel.Any;
import org.eclipse.smila.datamodel.AnyMap;
import org.eclipse.smila.datamodel.AnySeq;
import org.eclipse.smila.datamodel.DataFactory;
import org.eclipse.smila.datamodel.InvalidValueTypeException;
import org.eclipse.smila.datamodel.Value;
import org.eclipse.smila.search.api.QueryConstants;
import org.eclipse.smila.search.api.QueryConstants.SortOrder;
import org.eclipse.smila.solr.SolrConfig;
import org.eclipse.smila.solr.SolrConstants;
import org.eclipse.smila.solr.SolrConstants.FacetSort;
import org.eclipse.smila.solr.SolrUtils;
import org.eclipse.smila.solr.SolrUtils.LocalParamsMode;
import org.eclipse.smila.solr.params.QueryParams;
/**
* @author pwissel
*
*/
public class QueryTransformer {
private final Log _log = LogFactory.getLog(getClass());
private final QueryParams _params;
private final SolrConfig _config;
private final String _index;
private final Map<String, FilterGroup> _groupedFilters;
private final StringBuilder _buffer = new StringBuilder(0xfff);
@SuppressWarnings("unchecked")
public QueryTransformer(final QueryParams params, final SolrConfig config, final String index) {
_params = params;
_config = config;
_index = index;
_groupedFilters = LazyMap.decorate(new HashMap<String, FilterGroup>(), new Factory() {
@Override
public Object create() {
return new FilterGroup();
}
});
}
public SolrQuery toSolrQuery() {
return toSolrQuery(new SolrQuery());
}
public SolrQuery toSolrQuery(final SolrQuery solrQuery) {
addCommonQueryParameters(solrQuery);
addQuery(solrQuery);
addHighlighting(solrQuery);
addSorting(solrQuery);
addFaceting(solrQuery);
addGrouping(solrQuery);
addFilters(solrQuery);
addNativeParams(solrQuery);
return solrQuery;
}
SolrQuery addCommonQueryParameters(final SolrQuery solrQuery) {
// start
final int start = _params.getStart();
solrQuery.setStart(start);
// rows
final int rows = _params.getRows();
solrQuery.setRows(rows);
// fields
final String[] fields = _params.getFields();
if (!ArrayUtils.isEmpty(fields)) {
solrQuery.setFields(fields);
}
// if highlighting is enabled the id field must be part of resultAttributes
if (_params.hasHighlightConfig()) {
final String idField = _config.getIdField(_index);
if (!ArrayUtils.contains(fields, idField)) {
solrQuery.addField(idField);
}
}
return solrQuery;
}
SolrQuery addQuery(final SolrQuery solrQuery) {
final Any query = _params.getQueryObject();
if (query != null) {
if (query.isValue()) {
final String queryString = query.asValue().asString();
solrQuery.setQuery(queryString);
} else if (query.isSeq()) {
final String queryString = StringUtils.join(query.asSeq().asStrings(), QueryStringConstants.WHITESPACE);
solrQuery.setQuery(queryString);
} else if (query.isMap()) {
final AnyMap queryObj = query.asMap();
final StringBuilder queryString = new StringBuilder();
// append localParams
final Any localParams = queryObj.remove(SolrConstants.LOCAL_PARAMS);
if (localParams != null && localParams.isMap()) {
SolrUtils.writeLocalParams(queryString, localParams.asMap());
}
// append query parts
for (Entry<String, Any> queryPart : queryObj.entrySet()) {
addFieldedQueryPart(queryString, queryPart.getKey(), queryPart.getValue());
}
// set final query string
solrQuery.setQuery(queryString.toString());
}
}
return solrQuery;
}
private void addFieldedQueryPart(final StringBuilder queryString, final String field, final Any values) {
if (queryString.length() > 0) {
queryString.append(QueryStringConstants.WHITESPACE);
}
queryString.append(field).append(QueryStringConstants.COLON);
if (values.isValue()) {
queryString.append(values.asValue().asString());
} else if (values.isSeq() && values.asSeq().size() > 1) {
final StringBuilder multiValue = new StringBuilder();
multiValue.append(QueryStringConstants.BRACKET_OPEN);
for (final String value : values.asSeq().asStrings()) {
if (multiValue.length() > 1) {
multiValue.append(QueryStringConstants.WHITESPACE);
}
multiValue.append(value);
}
multiValue.append(QueryStringConstants.BRACKET_CLOSE);
queryString.append(multiValue);
}
}
SolrQuery addNativeParams(final SolrQuery solrQuery) {
final AnyMap nativeParams = _params.getNativeParams();
if (nativeParams != null && !nativeParams.isEmpty()) {
for (final String name : nativeParams.keySet()) {
final Any value = nativeParams.get(name);
if (value != null) {
// FIXME: use add or set (as previously)?
if (value.isValue()) {
solrQuery.add(name, value.asValue().asString());
} else if (value.isSeq()) {
final List<String> strings = value.asSeq().asStrings();
final String[] values = strings.toArray(new String[strings.size()]);
solrQuery.add(name, values);
}
}
}
}
return solrQuery;
}
SolrQuery addHighlighting(final SolrQuery solrQuery) {
final AnySeq highlight = _params.getHighlightConfig();
if (highlight != null && !highlight.isEmpty()) {
for (final Any any : highlight) {
if (any.isValue()) {
solrQuery.addHighlightField(any.asValue().asString());
} else if (any.isMap()) {
solrQuery.addHighlightField(any.asMap().getStringValue(QueryConstants.ATTRIBUTE));
}
}
}
return solrQuery;
}
SolrQuery addSorting(final SolrQuery solrQuery) {
final List<AnyMap> sortByConfig = _params.getSortByConfig();
if (sortByConfig != null && !sortByConfig.isEmpty()) {
for (final AnyMap sortBy : sortByConfig) {
final String field = sortBy.getStringValue(QueryConstants.ATTRIBUTE);
final String sortOrderValue = sortBy.getStringValue(QueryConstants.ORDER);
final SortOrder sortOrder = SortOrder.valueOf(sortOrderValue.toUpperCase());
switch (sortOrder) {
case ASCENDING:
solrQuery.addSort(field, ORDER.asc);
break;
case DESCENDING:
solrQuery.addSort(field, ORDER.desc);
break;
default:
final String message = "Unsupported SortOrder: " + sortOrderValue;
handleError(message);
}
}
}
return solrQuery;
}
SolrQuery addFaceting(final SolrQuery solrQuery) {
final List<AnyMap> facetByConfig = _params.getFacetByConfig();
if (facetByConfig != null && !facetByConfig.isEmpty()) {
for (final AnyMap facet : facetByConfig) {
String facetField = null;
// facet.field
if (facet.containsKey(QueryConstants.ATTRIBUTE)) {
facetField = facet.getStringValue(QueryConstants.ATTRIBUTE);
addFacetSettings(solrQuery, facet, facetField);
addFacetFilters(facet, facetField);
final String field = addFacetLocalParams(facet, facetField);
solrQuery.addFacetField(field);
// facet.query
} else if (facet.containsKey(SolrConstants.QUERY)) {
facetField = facet.getStringValue(QueryConstants.QUERY);
addFacetFilters(facet, facetField);
addFacetQueries(solrQuery, facet);
// facet.range
} else if (facet.containsKey(SolrConstants.RANGE)) {
facetField = facet.getStringValue(SolrConstants.RANGE);
addFacetSettings(solrQuery, facet, facetField);
addFacetFilters(facet, facetField);
final String range = addFacetLocalParams(facet, facetField);
final Value start = facet.getValue(SolrConstants.START);
if (start == null) {
final String message = SolrConstants.START + " must not be null.";
handleError(message);
}
final Value end = facet.getValue(SolrConstants.END);
if (end == null) {
final String message = SolrConstants.END + " must not be null.";
handleError(message);
}
final Value gap = facet.getValue(SolrConstants.GAP);
if (gap == null) {
final String message = SolrConstants.GAP + " must not be null.";
handleError(message);
}
if (start.isNumber() && end.isNumber() && gap.isNumber()) {
solrQuery.addNumericRangeFacet(range, start.asDouble(), end.asDouble(), gap.asDouble());
} else if (start.isDateTime() && end.isDateTime() && gap.isString()) {
solrQuery.addDateRangeFacet(range, start.asDate(), end.asDate(), gap.asString());
} else {
final String message = "Invalid facet range arguments";
handleError(message);
}
// facet.pivot
} else if (facet.containsKey(SolrConstants.PIVOT)) {
final Any any = facet.get(SolrConstants.PIVOT);
if (any.isSeq()) {
final AnySeq pivot = any.asSeq();
if (!pivot.isEmpty()) {
final String[] fields = new String[pivot.size()];
int pivotCount = 0;
for (String val : pivot.asSeq().asStrings()) {
fields[pivotCount] = addFacetLocalParams(facet, val);
pivotCount++;
}
solrQuery.addFacetField(fields);
}
}
}
// facet.inverval
else if (facet.containsKey(SolrConstants.INTERVAL)) {
facetField = facet.getStringValue(SolrConstants.INTERVAL);
addFacetFilters(facet, facetField);
final String field = addFacetLocalParams(facet, facetField);
final AnySeq set = facet.getSeq(SolrConstants.SET);
String[] intervals = ArrayUtils.EMPTY_STRING_ARRAY;
if (set != null) {
intervals = set.asStrings().toArray(new String[set.size()]);
}
solrQuery.addIntervalFacets(field, intervals);
} else {
final String message = "Unsupported facet type";
handleError(message);
}
}
}
return solrQuery;
}
private void addFacetSettings(final SolrQuery solrQuery, final AnyMap facet, final String field) {
// set maxcount as per-field value
final Long maxcount = facet.getLongValue(QueryConstants.MAXCOUNT);
if (maxcount != null) {
final String limitParam = SolrUtils.getPerFieldParameter(field, FacetParams.FACET_LIMIT);
solrQuery.set(limitParam, String.valueOf(maxcount));
}
// set sortby as per-field value
if (facet.containsKey(QueryConstants.SORTBY)) {
final String sortString =
facet.getMap(QueryConstants.SORTBY).getStringValue(QueryConstants.FACETBY_SORTCRITERION);
if (sortString != null) {
final FacetSort sort = FacetSort.get(sortString);
final String sortParam = SolrUtils.getPerFieldParameter(field, FacetParams.FACET_SORT);
solrQuery.set(sortParam, sort.toString());
}
}
}
private void addFacetFilters(final AnyMap facet, final String defaultLabel) {
final AnySeq filters = facet.getSeq(QueryConstants.FILTER);
if (filters != null) {
for (final Any any : filters) {
if (any.isMap()) {
final AnyMap filter = any.asMap();
final String filterString = parseFilterStrings(filter, defaultLabel);
final String label = StringUtils.defaultIfEmpty(filter.getStringValue(SolrConstants.GROUP), defaultLabel);
// tag and exclude filter if multiselect is enabled
final boolean multiselect = BooleanUtils.toBoolean(facet.getBooleanValue(SolrConstants.MULTISELECT));
AnyMap localParams = null;
if (multiselect) {
// tag
final String tag =
new StringBuffer(CommonParams.FQ).append(QueryStringConstants.UNDERSCORE).append(label).toString();
localParams = SolrUtils.putLocalParam(filter, QueryStringConstants.TAG, tag, multiselect,
LocalParamsMode.OVERWRITE);
// ex
SolrUtils.putLocalParam(facet, QueryStringConstants.EX, tag, true, LocalParamsMode.ADD);
}
_groupedFilters.get(label).add(filterString, localParams);
}
}
}
}
private String addFacetLocalParams(final AnyMap facet, final String param) {
// add name (only if key is not available)
final String name = facet.getStringValue(SolrConstants.NAME);
if (!StringUtils.isBlank(name)) {
SolrUtils.putLocalParam(facet, QueryStringConstants.KEY, name, true);
}
// add localParams to param
final AnyMap localParams = facet.getMap(SolrConstants.LOCAL_PARAMS);
if (!MapUtils.isEmpty(localParams)) {
return SolrUtils.addLocalParams(param, localParams).toString();
}
return param;
}
private void addFacetQueries(final SolrQuery solrQuery, final AnyMap facet) {
final String attribute = facet.getStringValue(QueryConstants.QUERY);
final AnySeq expression = facet.getSeq(SolrConstants.QUERIES);
final AnyMap localParams = facet.getMap(SolrConstants.LOCAL_PARAMS, true);
// combine attribute and expression as query
final List<String> queries = new ArrayList<String>();
for (final Any any : expression) {
if (any.isValue()) {
final StringBuilder sb = new StringBuilder(attribute);
sb.append(QueryStringConstants.COLON);
sb.append(any.asValue().asString());
queries.add(sb.toString());
}
}
// store original key from localParams
final String originalKey =
StringUtils.defaultIfEmpty(localParams.getStringValue(QueryStringConstants.KEY), attribute);
final ListIterator<String> query = queries.listIterator();
while (query.hasNext()) {
// add index to localParam key
final int index = query.nextIndex();
final String key = originalKey + QueryStringConstants.UNDERSCORE + index;
SolrUtils.putLocalParam(facet, QueryStringConstants.KEY, key, false, LocalParamsMode.OVERWRITE);
// append localParams to current facetQuery
final String facetQuery = SolrUtils.addLocalParams(query.next(), localParams).toString();
solrQuery.addFacetQuery(facetQuery);
}
// set localParams key to originalKey
SolrUtils.putLocalParam(facet, QueryStringConstants.KEY, originalKey, false, LocalParamsMode.OVERWRITE);
}
SolrQuery addGrouping(final SolrQuery solrQuery) {
final List<AnyMap> groupByConfig = _params.getGroupByConfig();
if (groupByConfig != null && !groupByConfig.isEmpty()) {
solrQuery.add(GroupParams.GROUP, Boolean.toString(true));
for (final AnyMap groupBy : groupByConfig) {
if (groupBy.containsKey(QueryConstants.ATTRIBUTE)) {
final String field = groupBy.getStringValue(QueryConstants.ATTRIBUTE);
solrQuery.add(GroupParams.GROUP_FIELD, field);
} else if (groupBy.containsKey(SolrConstants.FUNC)) {
final String func = groupBy.getStringValue(SolrConstants.FUNC);
solrQuery.add(GroupParams.GROUP_FUNC, func);
} else if (groupBy.containsKey(SolrConstants.QUERY)) {
final String query = groupBy.getStringValue(SolrConstants.QUERY);
solrQuery.add(GroupParams.GROUP_QUERY, query);
} else {
final String message = "Invalid group type";
handleError(message);
}
}
}
return solrQuery;
}
SolrQuery addFilters(final SolrQuery solrQuery) {
final List<AnyMap> filterConfig = _params.getFilterConfig();
if (filterConfig != null && !filterConfig.isEmpty()) {
// prepare filter groups
for (final AnyMap filter : filterConfig) {
// read attribute
final String attribute = filter.getStringValue(QueryConstants.ATTRIBUTE);
if (StringUtils.isBlank(attribute)) {
throw new IllegalArgumentException("Filter syntax error: Attribute must not be blank");
}
final String group = StringUtils.defaultString(filter.getStringValue(SolrConstants.GROUP), attribute);
final String filterString = parseFilterStrings(filter, attribute);
final AnyMap localParams = filter.getMap(SolrConstants.LOCAL_PARAMS);
_groupedFilters.get(group).add(filterString, localParams);
}
}
// add filter groups
for (final FilterGroup filterGroup : _groupedFilters.values()) {
final StringBuilder fq = resetBuffer();
SolrUtils.writeLocalParams(fq, filterGroup._localParams);
String mergedFq = filterGroup.merge(fq);
solrQuery.addFilterQuery(mergedFq);
}
return solrQuery;
}
private String parseFilterStrings(final AnyMap filterConfig, final String attribute) {
final StringBuilder fq = resetBuffer();
for (final Entry<String, Any> condition : filterConfig.entrySet()) {
final String key = condition.getKey();
final Any value = condition.getValue();
switch (key) {
case QueryConstants.ATTRIBUTE:
case SolrConstants.GROUP:
case SolrConstants.LOCAL_PARAMS:
continue;
case QueryConstants.FILTER_ALLOF:
appendListFilter(fq, attribute, value.asSeq(), QueryStringConstants.AND);
break;
case QueryConstants.FILTER_ATLEAST:
appendBoundFilter(fq, attribute, value.asValue().asString(), QueryStringConstants.WILDCARD, false);
break;
case QueryConstants.FILTER_ATMOST:
appendBoundFilter(fq, attribute, QueryStringConstants.WILDCARD, value.asValue().asString(), false);
break;
case QueryConstants.FILTER_GREATERTHAN:
appendBoundFilter(fq, attribute, value.asValue().asString(), QueryStringConstants.WILDCARD, true);
break;
case QueryConstants.FILTER_LESSTHAN:
appendBoundFilter(fq, attribute, QueryStringConstants.WILDCARD, value.asValue().asString(), true);
break;
case QueryConstants.FILTER_NONEOF:
appendListFilter(fq, attribute, value.asSeq(), QueryStringConstants.NOT);
break;
case QueryConstants.FILTER_ONEOF:
appendListFilter(fq, attribute, value.asSeq(), QueryStringConstants.OR);
break;
default:
// TODO: error
break;
}
}
return fq.toString();
}
private void appendListFilter(final StringBuilder fq, final String attribute, final AnySeq values,
final String operator) {
final List<String> filterValues;
try {
filterValues = values.asStrings();
} catch (InvalidValueTypeException exception) {
throw new InvalidValueTypeException(
"Filter syntax error: Must be of typ AnySeq and contain only Value elements.");
}
// append list filter -> operator(attribute:value)
for (final String val : filterValues) {
// FIXME: escape filters?
final String escapedVal = SolrUtils.escapeWS(val);
fq.append(operator);
fq.append(QueryStringConstants.BRACKET_OPEN);
fq.append(attribute);
fq.append(QueryStringConstants.COLON);
fq.append(val);
fq.append(QueryStringConstants.BRACKET_CLOSE);
}
}
private void appendBoundFilter(final StringBuilder fq, final String attribute, String lower, String upper,
final boolean exclusive) {
if (StringUtils.isBlank(lower)) {
throw new IllegalArgumentException("Filter syntax error: Lower must not be blank.");
}
if (StringUtils.isBlank(upper)) {
throw new IllegalArgumentException("Filter syntax error: Upper must not be blank.");
}
lower = excludeNonWildcardBound(fq, attribute, lower, exclusive);
upper = excludeNonWildcardBound(fq, attribute, upper, exclusive);
// append bound filter +(attribute:[lower TO upper])
fq.append(QueryStringConstants.AND);
fq.append(QueryStringConstants.BRACKET_OPEN);
fq.append(attribute);
fq.append(QueryStringConstants.COLON);
fq.append(QueryStringConstants.BRACKET_SQUARE_OPEN);
fq.append(lower);
fq.append(QueryStringConstants.WHITESPACE);
fq.append(QueryStringConstants.TO);
fq.append(QueryStringConstants.WHITESPACE);
fq.append(upper);
fq.append(QueryStringConstants.BRACKET_SQUARE_CLOSE);
fq.append(QueryStringConstants.BRACKET_CLOSE);
}
private String excludeNonWildcardBound(final StringBuilder fq, final String attribute, String bound,
final boolean exclusive) {
if (!bound.equals(QueryStringConstants.WILDCARD)) {
bound = SolrUtils.escapeWS(bound);
// appent not filter -> -(attribute:bound)
if (exclusive) {
fq.append(QueryStringConstants.NOT);
fq.append(QueryStringConstants.BRACKET_OPEN);
fq.append(attribute);
fq.append(QueryStringConstants.COLON);
fq.append(bound);
fq.append(QueryStringConstants.BRACKET_CLOSE);
}
}
return bound;
}
class FilterGroup {
final List<String> _filterStrings = new ArrayList<String>();
final AnyMap _localParams = DataFactory.DEFAULT.createAnyMap();
void add(final String filterString, final AnyMap localParams) {
_filterStrings.add(filterString);
if (localParams != null) {
_localParams.putAll(localParams);
}
}
String merge(final StringBuilder fq) {
if (_filterStrings.isEmpty()) {
return StringUtils.EMPTY;
} else if (_filterStrings.size() == 1) {
fq.append(_filterStrings.get(0));
return fq.toString();
} else {
for (final String filterString : _filterStrings) {
fq.append(QueryStringConstants.AND);
fq.append(QueryStringConstants.BRACKET_OPEN);
fq.append(filterString);
fq.append(QueryStringConstants.BRACKET_CLOSE);
}
return fq.toString();
}
}
}
private void handleError(final String message) {
// TODO: error handling!
// case THROW:
// throw new IllegalArgumentException(message);
// case LOG:
// _log.warn(message);
// break;
// default:
// break;
// }
}
private StringBuilder resetBuffer() {
_buffer.setLength(0);
return _buffer;
}
}