/*******************************************************************************
* Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019)
*
* contact.vitam@culture.gouv.fr
*
* This software is a computer program whose purpose is to implement a digital archiving back-office system managing
* high volumetry securely and efficiently.
*
* This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free
* software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as
* circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license,
* users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the
* successive licensors have only limited liability.
*
* In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or
* developing or reproducing the software by the user in light of its specific status of free software, that may mean
* that it is complicated to manipulate, and that also therefore means that it is reserved for developers and
* experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the
* software's suitability as regards their requirements in conditions enabling the security of their systems and/or data
* to be ensured and, more generally, to use and operate it in the same conditions as regards security.
*
* The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you
* accept its terms.
*******************************************************************************/
package fr.gouv.vitam.common.database.translators.elasticsearch;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.bson.conversions.Bson;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.SimpleQueryStringFlag;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import fr.gouv.vitam.common.database.builder.query.BooleanQuery;
import fr.gouv.vitam.common.database.builder.query.Query;
import fr.gouv.vitam.common.database.builder.request.configuration.BuilderToken.QUERY;
import fr.gouv.vitam.common.database.builder.request.configuration.BuilderToken.QUERYARGS;
import fr.gouv.vitam.common.database.builder.request.configuration.BuilderToken.RANGEARGS;
import fr.gouv.vitam.common.database.parser.query.ParserTokens;
import fr.gouv.vitam.common.database.parser.request.GlobalDatasParser;
import fr.gouv.vitam.common.database.translators.mongodb.MongoDbHelper;
import fr.gouv.vitam.common.exception.InvalidParseOperationException;
import fr.gouv.vitam.common.json.JsonHandler;
/**
* Elasticsearch Translator
*/
public class QueryToElasticsearch {
private static final String FUZZINESS = "AUTO";
private QueryToElasticsearch() {
// Empty constructor
}
/**
* @param field String
* @param roots Set of String
* @return the filter associated with the roots
* @throws InvalidParseOperationException if field is not in roots
*/
public static QueryBuilder getRoots(final String field, final Set<String> roots)
throws InvalidParseOperationException {
final String[] values = new String[roots.size()];
int i = 0;
for (final String node : roots) {
values[i++] = node;
}
// NB: terms and not term since multiple values
return QueryBuilders.termsQuery(field, values);
}
/**
* Merge a request and a root filter
*
* @param command QueryBuilder
* @param roots QueryBuilder
* @return the complete request
*/
public static QueryBuilder getFullCommand(final QueryBuilder command, final QueryBuilder roots) {
return QueryBuilders.boolQuery()
.must(command)
.filter(roots);
// TODO P1 add TenantId filter
}
/**
* Generate sort list from order by mongo query orders : {field1 : -1, field2 : 1} or [{field1 : -1, field2 :
* 1},{field3 : -1}]
*
* @param orderBy orderBy (one or list)
* @return list of order by as sort objects
* @throws InvalidParseOperationException if the orderBy is not valid
*/
public static List<SortBuilder> getSorts(final Bson orderBy) throws InvalidParseOperationException {
// FIXME P0 : clean code and validate orderBy => list translation
// FIXME P0 : use DSL object or JsonNode in place of a Bson
List<SortBuilder> sorts = new ArrayList<>();
if (orderBy != null) {
JsonNode node = JsonHandler.getFromString(MongoDbHelper.bsonToString(orderBy, false), JsonNode.class);
if (node.isArray()) {
((ArrayNode) node).forEach(item -> sorts.addAll(getSortsForNode((ObjectNode) item)));
} else {
sorts.addAll(getSortsForNode((ObjectNode) node));
}
}
return sorts;
}
/**
* Generate sort list from node with form : {field1 : -1, field2 : 1}
*
* @param node node containing sort list from mongo as json
* @return list of order by as sort objects
*/
private static List<SortBuilder> getSortsForNode(ObjectNode node) {
List<SortBuilder> sorts = new ArrayList<>();
Iterator<String> fieldNames = node.fieldNames();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
if (!node.get(fieldName).isInt()) {
break;
}
int value = node.get(fieldName).asInt();
FieldSortBuilder fieldSort = SortBuilders.fieldSort(fieldName);
if (value < 0) {
fieldSort.order(SortOrder.DESC);
sorts.add(fieldSort);
} else if (value > 0) {
fieldSort.order(SortOrder.ASC);
sorts.add(fieldSort);
}
}
return sorts;
}
/**
*
* @param query Query
* @return the associated QueryBuilder
* @throws InvalidParseOperationException if query could not parse to command
*/
public static QueryBuilder getCommand(final Query query)
throws InvalidParseOperationException {
final QUERY req = query.getQUERY();
final JsonNode content = query.getNode(req.exactToken());
switch (req) {
case AND:
case NOT:
case OR:
return andOrNotCommand(req, query);
case EXISTS:
case MISSING:
return existsMissingCommand(req, content);
case FLT:
case MLT:
return xltCommand(req, content);
case MATCH:
case MATCH_PHRASE:
case MATCH_PHRASE_PREFIX:
case PREFIX:
return matchCommand(req, content);
case SEARCH:
return searchCommand(req, content);
case NIN:
case IN:
return inCommand(req, content);
case RANGE:
return rangeCommand(req, content);
case REGEX:
return regexCommand(req, content);
case TERM:
return termCommand(req, content);
case WILDCARD:
return wildcardCommand(req, content);
case EQ:
case NE:
return eqCommand(req, content);
case GT:
case GTE:
case LT:
case LTE:
return compareCommand(req, content);
case ISNULL:
return isNullCommand(req, content);
case SIZE:
return sizeCommand(req, content);
case GEOMETRY:
case BOX:
case POLYGON:
case CENTER:
case GEOINTERSECTS:
case GEOWITHIN:
case PATH:
case NOP:
default:
}
throw new InvalidParseOperationException("Invalid command: " + req.exactToken());
}
/**
* $size : { name : length }
*
* @param req QUERY
* @param content JsonNode
* @return the size Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder sizeCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> element = JsonHandler.checkUnicity(query.exactToken(), content);
final Script script = new Script("doc['" + element.getKey() + "'].values.length == " + element.getValue());
return QueryBuilders.scriptQuery(script);
}
/**
* $gt : { name : value } $gte : { name : value } $lt : { name : value } $lte : { name : value }
*
* @param req QUERY
* @param content JsonNode
* @return the compare Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder compareCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> element = JsonHandler.checkUnicity(query.exactToken(), content);
String key = element.getKey();
// TODO P0 remove after POC validation all DATE from Vitam code
JsonNode node = element.getValue().findValue(ParserTokens.QUERYARGS.DATE.exactToken());
if (node != null) {
key += "." + ParserTokens.QUERYARGS.DATE.exactToken();
} else {
node = element.getValue();
}
final Object value = GlobalDatasParser.getValue(node);
switch (query) {
case GT:
return QueryBuilders.rangeQuery(key).gt(value);
case GTE:
return QueryBuilders.rangeQuery(key).gte(value);
case LT:
return QueryBuilders.rangeQuery(key).lt(value);
case LTE:
default:
return QueryBuilders.rangeQuery(key).lte(value);
}
}
/**
* $mlt : { $fields : [ name1, name2 ], $like : like_text } $flt : { $fields : [ name1, name2 ], $like : like_text }
*
* @param req QUERY
* @param content JsonNode
* @return the xlt Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder xltCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final ArrayNode fields = (ArrayNode) content.get(QUERYARGS.FIELDS.exactToken());
final JsonNode like = content.get(QUERYARGS.LIKE.exactToken());
if (fields == null || like == null) {
throw new InvalidParseOperationException("Incorrect command: " + query.exactToken() + " : " + query);
}
final String[] names = new String[fields.size()];
int i = 0;
for (final JsonNode name : fields) {
names[i++] = name.toString();
}
switch (query) {
case FLT:
final String slike = like.toString();
if (names.length > 0) {
final BoolQueryBuilder builder = QueryBuilders.boolQuery();
for (final String name : names) {
builder.should(QueryBuilders.matchQuery(name, slike).fuzziness(FUZZINESS));
}
return builder;
} else {
return QueryBuilders.matchQuery(names[0], slike).fuzziness(FUZZINESS);
}
case MLT:
default:
return QueryBuilders.moreLikeThisQuery(names).addLikeText(like.toString());
}
}
/**
* $search : { name : searchParameter }
*
* @param req QUERY
* @param content JsonNode
* @return the search Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder searchCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> element = JsonHandler.checkUnicity(query.exactToken(), content);
return QueryBuilders.simpleQueryStringQuery(element.getValue().toString()).field(element.getKey());
}
/**
* $match : { name : words, $max_expansions : n }
*
* @param req QUERY
* @param content JsonNode
* @return the match Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder matchCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
// TODO P1 add operator (and, or)
final JsonNode max = ((ObjectNode) content).remove(QUERYARGS.MAX_EXPANSIONS.exactToken());
final Entry<String, JsonNode> element = JsonHandler.checkUnicity(query.exactToken(), content);
final String attribute = element.getKey();
if ((query == QUERY.MATCH_PHRASE_PREFIX || query == QUERY.MATCH_PHRASE) && isAttributeNotAnalyzed(attribute)) {
return QueryBuilders.prefixQuery(element.getKey(), element.getValue().toString());
} else {
QUERY query2 = query;
if (query == QUERY.PREFIX) {
query2 = QUERY.MATCH_PHRASE_PREFIX;
}
if (max != null && !max.isMissingNode()) {
switch (query2) {
case MATCH:
return QueryBuilders.matchQuery(element.getKey(), element.getValue().toString())
.maxExpansions(max.asInt());
case MATCH_PHRASE:
return QueryBuilders.matchPhraseQuery(element.getKey(), element.getValue().toString())
.maxExpansions(max.asInt());
case MATCH_PHRASE_PREFIX:
return QueryBuilders.matchPhrasePrefixQuery(element.getKey(), element.getValue().toString())
.maxExpansions(max.asInt());
default:
throw new InvalidParseOperationException("Not correctly parsed: " + query);
}
} else {
switch (query) {
case MATCH:
return QueryBuilders.matchQuery(element.getKey(), element.getValue().toString());
case MATCH_PHRASE:
return QueryBuilders.matchPhraseQuery(element.getKey(), element.getValue().toString());
case MATCH_PHRASE_PREFIX:
return QueryBuilders.matchPhrasePrefixQuery(element.getKey(), element.getValue().toString());
default:
throw new InvalidParseOperationException("Not correctly parsed: " + query);
}
}
}
}
/**
* $in : { name : [ value1, value2, ... ] }
*
* @param req QUERY
* @param content JsonNode
* @return the in Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder inCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> element = JsonHandler.checkUnicity(query.exactToken(), content);
String key = element.getKey();
final List<JsonNode> nodes = element.getValue().findValues(ParserTokens.QUERYARGS.DATE.exactToken());
if (nodes != null && !nodes.isEmpty()) {
key += "." + ParserTokens.QUERYARGS.DATE.exactToken();
}
final Set<Object> set = new HashSet<>();
for (final JsonNode value : nodes) {
set.add(getAsObject(value));
}
final QueryBuilder query2 = QueryBuilders.termsQuery(key, set);
if (query == QUERY.NIN) {
final QueryBuilder query3 = QueryBuilders.boolQuery().mustNot(query2);
return query3;
}
return query2;
}
/**
* $range : { name : { $gte : value, $lte : value } }
*
* @param req QUERY
* @param content JsonNode
* @return the range Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder rangeCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> element = JsonHandler.checkUnicity(query.exactToken(), content);
String key = element.getKey();
JsonNode node = element.getValue().findValue(ParserTokens.QUERYARGS.DATE.exactToken());
if (node != null) {
key += "." + ParserTokens.QUERYARGS.DATE.exactToken();
}
final RangeQueryBuilder range = QueryBuilders.rangeQuery(key);
for (final Iterator<Entry<String, JsonNode>> iterator = element.getValue().fields(); iterator.hasNext();) {
final Entry<String, JsonNode> requestItem = iterator.next();
RANGEARGS arg = null;
try {
final String skey = requestItem.getKey();
if (skey.startsWith("$")) {
arg = RANGEARGS.valueOf(skey.substring(1).toUpperCase());
} else {
throw new InvalidParseOperationException("Invalid Range query command: " + requestItem);
}
} catch (final IllegalArgumentException e) {
throw new InvalidParseOperationException("Invalid Range query command: " + requestItem, e);
}
node = requestItem.getValue().findValue(ParserTokens.QUERYARGS.DATE.exactToken());
if (node == null) {
node = requestItem.getValue();
}
switch (arg) {
case GT:
range.gt(getAsObject(node));
break;
case GTE:
range.gte(getAsObject(node));
break;
case LT:
range.lt(getAsObject(node));
break;
case LTE:
default:
range.lte(getAsObject(node));
}
}
return range;
}
/**
* $regex : { name : regex }
*
* @param req QUERY
* @param content JsonNode
* @return the regex Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder regexCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> entry = JsonHandler.checkUnicity(query.exactToken(), content);
return QueryBuilders.regexpQuery(entry.getKey(), "/" + entry.getValue().asText() + "/");
}
/**
* $term : { name : term, name : term }
*
* @param req QUERY
* @param content JsonNode
* @return the term Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder termCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
boolean multiple = false;
QueryBuilder query2 = null;
if (content.size() > 1) {
multiple = true;
query2 = QueryBuilders.boolQuery();
}
for (final Iterator<Entry<String, JsonNode>> iterator = content.fields(); iterator.hasNext();) {
final Entry<String, JsonNode> requestItem = iterator.next();
String key = requestItem.getKey();
JsonNode node = requestItem.getValue().findValue(ParserTokens.QUERYARGS.DATE.exactToken());
boolean isDate = false;
if (node == null) {
node = requestItem.getValue();
} else {
isDate = true;
key += "." + ParserTokens.QUERYARGS.DATE.exactToken();
}
if (node.isNumber()) {
if (!multiple) {
return QueryBuilders.termQuery(key, getAsObject(node));
}
((BoolQueryBuilder) query2).must(QueryBuilders.termQuery(key, getAsObject(node)));
} else {
final String val = node.asText();
QueryBuilder query3 = null;
if (isAttributeNotAnalyzed(key) || isDate) {
query3 = QueryBuilders.termQuery(key, val);
} else {
query3 = QueryBuilders.simpleQueryStringQuery("\"" + val + "\"").field(key)
.flags(SimpleQueryStringFlag.PHRASE);
}
if (!multiple) {
return query3;
}
((BoolQueryBuilder) query2).must(query3);
}
}
return query2;
}
/**
* $wildcard : { name : term }
*
* @param refCommand
* @param command
*/
private static QueryBuilder wildcardCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> entry = JsonHandler.checkUnicity(query.exactToken(), content);
final String key = entry.getKey();
final JsonNode node = entry.getValue();
final String val = node.asText();
return QueryBuilders.wildcardQuery(key, val);
}
/**
* $eq : { name : value }
*
* @param req QUERY
* @param content JsonNode
* @return the eq Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder eqCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final Entry<String, JsonNode> entry = JsonHandler.checkUnicity(query.exactToken(), content);
String key = entry.getKey();
JsonNode node = entry.getValue().findValue(ParserTokens.QUERYARGS.DATE.exactToken());
boolean isDate = false;
if (node == null) {
node = entry.getValue();
} else {
isDate = true;
key += "." + ParserTokens.QUERYARGS.DATE.exactToken();
}
if (isAttributeNotAnalyzed(key) || isDate) {
final QueryBuilder query2 = QueryBuilders.termQuery(key, getAsObject(node));
if (query == QUERY.NE) {
return QueryBuilders.boolQuery().mustNot(query2);
}
return query2;
} else {
final QueryBuilder query2 = QueryBuilders.simpleQueryStringQuery("\"" + getAsObject(node) + "\"").field(key)
.flags(SimpleQueryStringFlag.PHRASE);
if (query == QUERY.NE) {
return QueryBuilders.boolQuery().mustNot(query2);
}
return query2;
}
}
/**
* $exists : name
*
* @param req QUERY
* @param content JsonNode
* @return the exist Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder existsMissingCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final String fieldname = content.asText();
final QueryBuilder queryBuilder = QueryBuilders.existsQuery(fieldname);
switch (query) {
case MISSING:
return QueryBuilders.boolQuery().mustNot(queryBuilder);
case EXISTS:
default:
return queryBuilder;
}
}
/**
* $isNull : name
*
* @param req QUERY
* @param content JsonNode
* @return the isNull Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder isNullCommand(final QUERY query, final JsonNode content)
throws InvalidParseOperationException {
final String fieldname = content.asText();
return QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(fieldname));
}
/**
* $and : [ expression1, expression2, ... ] $or : [ expression1, expression2, ... ] $not : [ expression1,
* expression2, ... ]
*
* @param req QUERY
* @param content JsonNode
* @return the and Or Not Command
* @throws InvalidParseOperationException if check unicity is in error
*/
private static QueryBuilder andOrNotCommand(final QUERY query, final Query req)
throws InvalidParseOperationException {
final BooleanQuery nthrequest = (BooleanQuery) req;
final List<Query> sub = nthrequest.getQueries();
final BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
for (int i = 0; i < sub.size(); i++) {
switch (query) {
case AND:
boolQueryBuilder.must(getCommand(sub.get(i)));
break;
case NOT:
boolQueryBuilder.mustNot(getCommand(sub.get(i)));
break;
case OR:
default:
boolQueryBuilder.should(getCommand(sub.get(i)));
}
}
return boolQueryBuilder;
}
/**
* @param JsonNode
* @return JsonNode as Object
*/
private static final Object getAsObject(JsonNode value) {
if (value.isBoolean()) {
return value.asBoolean();
} else if (value.canConvertToLong()) {
return value.asLong();
} else if (value.isDouble()) {
return value.asDouble();
} else {
return value.asText();
}
}
/*
* Returns True if this attribute is not analyzed by ElasticSearch, else False
*/
private static boolean isAttributeNotAnalyzed(String attributeName) {
// TODO P1 returns True if this attribute is not analyzed by ElasticSearch, else False
return true;
}
}