blob: d05e91cbacb06c0e090e2ae7b0e90d41adaae347 [file] [log] [blame]
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.elasticsearch;
import com.google.gerrit.elasticsearch.builders.BoolQueryBuilder;
import com.google.gerrit.elasticsearch.builders.QueryBuilder;
import com.google.gerrit.elasticsearch.builders.QueryBuilders;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.query.AndPredicate;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.IntegerRangePredicate;
import com.google.gerrit.index.query.NotPredicate;
import com.google.gerrit.index.query.OrPredicate;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.RegexPredicate;
import com.google.gerrit.index.query.TimestampRangePredicate;
import com.google.gerrit.server.query.change.AfterPredicate;
import java.time.Instant;
public class ElasticQueryBuilder {
<T> QueryBuilder toQueryBuilder(Predicate<T> p) throws QueryParseException {
if (p instanceof AndPredicate) {
return and(p);
} else if (p instanceof OrPredicate) {
return or(p);
} else if (p instanceof NotPredicate) {
return not(p);
} else if (p instanceof IndexPredicate) {
return fieldQuery((IndexPredicate<T>) p);
} else if (p instanceof PostFilterPredicate) {
return QueryBuilders.matchAllQuery();
} else {
throw new QueryParseException("cannot create query for index: " + p);
}
}
private <T> BoolQueryBuilder and(Predicate<T> p) throws QueryParseException {
BoolQueryBuilder b = QueryBuilders.boolQuery();
for (Predicate<T> c : p.getChildren()) {
b.must(toQueryBuilder(c));
}
return b;
}
private <T> BoolQueryBuilder or(Predicate<T> p) throws QueryParseException {
BoolQueryBuilder q = QueryBuilders.boolQuery();
for (Predicate<T> c : p.getChildren()) {
q.should(toQueryBuilder(c));
}
return q;
}
private <T> QueryBuilder not(Predicate<T> p) throws QueryParseException {
Predicate<T> n = p.getChild(0);
if (n instanceof TimestampRangePredicate) {
return notTimestamp((TimestampRangePredicate<T>) n);
}
// Lucene does not support negation, start with all and subtract.
BoolQueryBuilder q = QueryBuilders.boolQuery();
q.must(QueryBuilders.matchAllQuery());
q.mustNot(toQueryBuilder(n));
return q;
}
private <T> QueryBuilder fieldQuery(IndexPredicate<T> p) throws QueryParseException {
FieldType<?> type = p.getType();
FieldDef<?, ?> field = p.getField();
String name = field.getName();
String value = p.getValue();
if (type == FieldType.INTEGER) {
// QueryBuilder encodes integer fields as prefix coded bits,
// which elasticsearch's queryString can't handle.
// Create integer terms with string representations instead.
return QueryBuilders.termQuery(name, value);
} else if (type == FieldType.INTEGER_RANGE) {
return intRangeQuery(p);
} else if (type == FieldType.TIMESTAMP) {
return timestampQuery(p);
} else if (type == FieldType.EXACT) {
return exactQuery(p);
} else if (type == FieldType.PREFIX) {
return QueryBuilders.matchPhrasePrefixQuery(name, value);
} else if (type == FieldType.FULL_TEXT) {
return QueryBuilders.matchPhraseQuery(name, value);
} else {
throw FieldType.badFieldType(p.getType());
}
}
private <T> QueryBuilder intRangeQuery(IndexPredicate<T> p) throws QueryParseException {
if (p instanceof IntegerRangePredicate) {
IntegerRangePredicate<T> r = (IntegerRangePredicate<T>) p;
int minimum = r.getMinimumValue();
int maximum = r.getMaximumValue();
if (minimum == maximum) {
// Just fall back to a standard integer query.
return QueryBuilders.termQuery(p.getField().getName(), minimum);
}
return QueryBuilders.rangeQuery(p.getField().getName()).gte(minimum).lte(maximum);
}
throw new QueryParseException("not an integer range: " + p);
}
private <T> QueryBuilder notTimestamp(TimestampRangePredicate<T> r) throws QueryParseException {
if (r.getMinTimestamp().getTime() == 0) {
return QueryBuilders.rangeQuery(r.getField().getName())
.gt(Instant.ofEpochMilli(r.getMaxTimestamp().getTime()));
}
throw new QueryParseException("cannot negate: " + r);
}
private <T> QueryBuilder timestampQuery(IndexPredicate<T> p) throws QueryParseException {
if (p instanceof TimestampRangePredicate) {
TimestampRangePredicate<T> r = (TimestampRangePredicate<T>) p;
if (p instanceof AfterPredicate) {
return QueryBuilders.rangeQuery(r.getField().getName())
.gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime()));
}
return QueryBuilders.rangeQuery(r.getField().getName())
.gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime()))
.lte(Instant.ofEpochMilli(r.getMaxTimestamp().getTime()));
}
throw new QueryParseException("not a timestamp: " + p);
}
private <T> QueryBuilder exactQuery(IndexPredicate<T> p) {
String name = p.getField().getName();
String value = p.getValue();
if (!p.getField().isRepeatable() && value.isEmpty()) {
return new BoolQueryBuilder().mustNot(QueryBuilders.existsQuery(name));
} else if (p instanceof RegexPredicate) {
if (value.startsWith("^")) {
value = value.substring(1);
}
if (value.endsWith("$") && !value.endsWith("\\$") && !value.endsWith("\\\\$")) {
value = value.substring(0, value.length() - 1);
}
return QueryBuilders.regexpQuery(name + ".key", value);
} else {
return QueryBuilders.termQuery(name + ".key", value);
}
}
}