From eda8d2abd031160a3be7e2c54b8ebb301c8fc6bf Mon Sep 17 00:00:00 2001 From: f-necas <39771412+f-necas@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:14:10 +0100 Subject: [PATCH] CSW fix (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert #253 * CSW GetRecords doesn't escape query values when creating the Elasticsearch query. Fixes #7527 * CSW GetRecords doesn't escape query values when creating the Elasticsearch query / Escape Elasticsearch special chars in EQUAL / NOT EQUAL literal queries * CSW GetRecords doesn't escape query values when creating the Elasticsearch query / Escape Elasticsearch special chars in IS LIKE literal queries * Reset geotools usage and comments tests --------- Co-authored-by: Jose GarcĂ­a --- .../services/getrecords/es/CswFilter2Es.java | 151 +++++++++--------- .../getrecords/es/CswFilter2EsTest.java | 80 ++++++++++ 2 files changed, 157 insertions(+), 74 deletions(-) diff --git a/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java b/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java index 7cb7034cbe..154be6eac3 100644 --- a/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java +++ b/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2023 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -26,6 +26,7 @@ import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; +import org.apache.commons.text.StringEscapeUtils; import org.fao.geonet.constants.Geonet; import org.fao.geonet.kernel.csw.services.getrecords.IFieldMapper; import org.fao.geonet.utils.Log; @@ -82,67 +83,69 @@ import org.opengis.filter.temporal.TOverlaps; import org.opengis.geometry.BoundingBox; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; import java.util.regex.Pattern; /** * Manages the translation from CSW <Filter> into a ES query. */ public class CswFilter2Es extends AbstractFilterVisitor { - private final String BINARY_OPERATOR_AND = "AND"; - private final String BINARY_OPERATOR_OR = "OR"; + private static final String BINARY_OPERATOR_AND = "AND"; + private static final String BINARY_OPERATOR_OR = "OR"; - static final String SPECIAL_RE = "([" + Pattern.quote("+-&|!(){}[]^\\\"~*?:/") + "])"; - static final String SPECIAL_LIKE_RE = "(? stack = new ArrayDeque(); + // Stack to build the Elasticsearch Query + Deque stack = new ArrayDeque<>(); - private final String templateNot = " {\"bool\": {\n" + + private static final String TEMPLATE_NOT = " {\"bool\": {\n" + " \"must_not\": [\n" + " %s\n" + " ]\n" + " }}"; - - private final String templateAnd = " {\"bool\": {\n" + + private static final String TEMPLATE_AND = " {\"bool\": {\n" + " \"must\": [\n" + " %s\n" + " ]\n" + " }}"; - private final String templateAndWithFilter = " \"bool\": {\n" + + private static final String TEMPLATE_AND_WITH_FILTER = " \"bool\": {\n" + " \"must\": [\n" + " %s\n" + " ]\n" + " ,\"filter\":{\"query_string\":{\"query\":\"%s\"}}}"; //, "minimum_should_match" : 1 - private final String templateOr = " {\"bool\": {\n" + + private static final String TEMPLATE_OR = " {\"bool\": {\n" + " \"should\": [\n" + " %s\n" + " ]\n" + " }}"; - private final String templateOrWithFilter = " \"bool\": {\n" + + private static final String TEMPLATE_OR_WITH_FILTER = " \"bool\": {\n" + " \"should\": [\n" + " %s\n" + " ]\n" + " ,\"filter\":{\"query_string\":{\"query\":\"%s\"}}, \"minimum_should_match\" : 1}"; - private final String templateMatch = "{\"query_string\": {\n" + + private static final String TEMPLATE_MATCH = "{\"query_string\": {\n" + " \"fields\": [\"%s\"],\n" + " \"query\": \"%s\"\n" + " }}"; - private final String templatePropertyIsNot = " {\"bool\": {\n" + - " \"must_not\": " + templateMatch + + private static final String TEMPLATE_PROPERTY_IS_NOT = " {\"bool\": {\n" + + " \"must_not\": " + TEMPLATE_MATCH + " }}"; - private final String templateRange = " {\n" + + private static final String TEMPLATE_RANGE = " {\n" + " \"range\" : {\n" + " \"%s\" : {\n" + " \"%s\" : %s\n" + @@ -150,7 +153,7 @@ public class CswFilter2Es extends AbstractFilterVisitor { " }\n" + " }"; - private final String templateBetween = " {\n" + + private static final String TEMPLATE_BETWEEN = " {\n" + " \"range\" : {\n" + " \"%s\" : {\n" + " \"gte\" : %s,\n" + @@ -159,12 +162,12 @@ public class CswFilter2Es extends AbstractFilterVisitor { " }\n" + " }"; - private final String templateIsLike = "{\"query_string\": {\n" + + private static final String TEMPLATE_IS_LIKE = "{\"query_string\": {\n" + " \"fields\": [\"%s\"],\n" + " \"query\": \"%s\"\n" + " }}"; - private final String templateSpatial = "{ \"geo_shape\": {\"geom\": {\n" + + private static final String TEMPLATE_SPATIAL = "{ \"geo_shape\": {\"geom\": {\n" + " \t\"shape\": {\n" + " \t\"type\": \"%s\",\n" + " \t\"coordinates\" : %s\n" + @@ -201,17 +204,21 @@ protected static String escapeLikeLiteral(String text) { protected static String convertLikePattern(PropertyIsLike filter) { String result = filter.getLiteral(); if (!filter.getWildCard().equals("*")) { - final String wildcardRe = "(?\n" // + + " \n" // + + " OnlineResourceType\n" // + + " OGC:WMS\n" // + + " \n" // + + " " // + + ""; + + // EXPECTED: + final ObjectNode expected = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("OnlineResourceType", "OGC\\:WMS"))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected, input); + } + + @Test + void testPropertyIsLike() throws IOException { + + final String input = + "\n" // + + " \n" // + + " AnyText\n" // + + " s\\_rvice\\%\n" // + + " \n" // + + " " // + + ""; + + // EXPECTED: + final ObjectNode expected = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("AnyText", "s?rvice*"))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected, input); + } + + @Test + void testPropertyIsLikeSpecialChars() throws IOException { + + final String input = + "\n" // + + " \n" // + + " AnyText\n" // + + " \"service\"\n" // + + " \n" // + + " " // + + ""; + + // EXPECTED: + final ObjectNode expected = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("AnyText", "\\\"service\\\""))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected, input); + + + final String input2 = + "\n" // + + " \n" // + + " AnyText\n" // + + " OGC:WMS\\%\n" // + + " \n" // + + " " // + + ""; + + // EXPECTED: + final ObjectNode expected2 = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("AnyText", "OGC\\:WMS*"))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected2, input2); + }*/ + @Test void testLogicalAnd() throws IOException {