/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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.elasticsearch.index.query; import com.vividsolutions.jts.geom.Coordinate; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.test.geo.RandomShapeGenerator; import org.elasticsearch.test.geo.RandomShapeGenerator.ShapeType; import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static org.elasticsearch.test.StreamsUtils.copyToStringFromClasspath; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.notNullValue; public class GeoPolygonQueryBuilderTests extends AbstractQueryTestCase<GeoPolygonQueryBuilder> { @Override protected GeoPolygonQueryBuilder doCreateTestQueryBuilder() { List<GeoPoint> polygon = randomPolygon(); GeoPolygonQueryBuilder builder = new GeoPolygonQueryBuilder(GEO_POINT_FIELD_NAME, polygon); if (randomBoolean()) { builder.setValidationMethod(randomFrom(GeoValidationMethod.values())); } if (randomBoolean()) { builder.ignoreUnmapped(randomBoolean()); } return builder; } @Override protected void doAssertLuceneQuery(GeoPolygonQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException { // todo LatLonPointInPolygon is package private } /** * Overridden here to ensure the test is only run if at least one type is * present in the mappings. Geo queries do not execute if the field is not * explicitly mapped */ @Override public void testToQuery() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); super.testToQuery(); } private static List<GeoPoint> randomPolygon() { ShapeBuilder shapeBuilder = null; // This is a temporary fix because sometimes the RandomShapeGenerator // returns null. This is if there is an error generating the polygon. So // in this case keep trying until we successfully generate one while (shapeBuilder == null) { shapeBuilder = RandomShapeGenerator.createShapeWithin(random(), null, ShapeType.POLYGON); } JtsGeometry shape = (JtsGeometry) shapeBuilder.build(); Coordinate[] coordinates = shape.getGeom().getCoordinates(); ArrayList<GeoPoint> polygonPoints = new ArrayList<>(); for (Coordinate coord : coordinates) { polygonPoints.add(new GeoPoint(coord.y, coord.x)); } return polygonPoints; } public void testNullFieldName() { IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new GeoPolygonQueryBuilder(null, randomPolygon())); assertEquals("fieldName must not be null", e.getMessage()); } public void testEmptyPolygon() { IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new GeoPolygonQueryBuilder(GEO_POINT_FIELD_NAME, Collections.emptyList())); assertEquals("polygon must not be null or empty", e.getMessage()); e = expectThrows(IllegalArgumentException.class, () -> new GeoPolygonQueryBuilder(GEO_POINT_FIELD_NAME, null)); assertEquals("polygon must not be null or empty", e.getMessage()); } public void testInvalidClosedPolygon() { List<GeoPoint> points = new ArrayList<>(); points.add(new GeoPoint(0, 90)); points.add(new GeoPoint(90, 90)); points.add(new GeoPoint(0, 90)); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new GeoPolygonQueryBuilder(GEO_POINT_FIELD_NAME, points)); assertEquals("too few points defined for geo_polygon query", e.getMessage()); } public void testInvalidOpenPolygon() { List<GeoPoint> points = new ArrayList<>(); points.add(new GeoPoint(0, 90)); points.add(new GeoPoint(90, 90)); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new GeoPolygonQueryBuilder(GEO_POINT_FIELD_NAME, points)); assertEquals("too few points defined for geo_polygon query", e.getMessage()); } public void testParsingAndToQueryParsingExceptions() throws IOException { String[] brokenFiles = new String[]{ "/org/elasticsearch/index/query/geo_polygon_exception_1.json", "/org/elasticsearch/index/query/geo_polygon_exception_2.json", "/org/elasticsearch/index/query/geo_polygon_exception_3.json", "/org/elasticsearch/index/query/geo_polygon_exception_4.json", "/org/elasticsearch/index/query/geo_polygon_exception_5.json" }; for (String brokenFile : brokenFiles) { String query = copyToStringFromClasspath(brokenFile); expectThrows(ParsingException.class, () -> parseQuery(query)); } } public void testParsingAndToQuery1() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); String query = "{\n" + " \"geo_polygon\":{\n" + " \"" + GEO_POINT_FIELD_NAME + "\":{\n" + " \"points\":[\n" + " [-70, 40],\n" + " [-80, 30],\n" + " [-90, 20]\n" + " ]\n" + " }\n" + " }\n" + "}\n"; assertGeoPolygonQuery(query); } public void testParsingAndToQuery2() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); String query = "{\n" + " \"geo_polygon\":{\n" + " \"" + GEO_POINT_FIELD_NAME + "\":{\n" + " \"points\":[\n" + " {\n" + " \"lat\":40,\n" + " \"lon\":-70\n" + " },\n" + " {\n" + " \"lat\":30,\n" + " \"lon\":-80\n" + " },\n" + " {\n" + " \"lat\":20,\n" + " \"lon\":-90\n" + " }\n" + " ]\n" + " }\n" + " }\n" + "}\n"; assertGeoPolygonQuery(query); } public void testParsingAndToQuery3() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); String query = "{\n" + " \"geo_polygon\":{\n" + " \"" + GEO_POINT_FIELD_NAME + "\":{\n" + " \"points\":[\n" + " \"40, -70\",\n" + " \"30, -80\",\n" + " \"20, -90\"\n" + " ]\n" + " }\n" + " }\n" + "}\n"; assertGeoPolygonQuery(query); } public void testParsingAndToQuery4() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); String query = "{\n" + " \"geo_polygon\":{\n" + " \"" + GEO_POINT_FIELD_NAME + "\":{\n" + " \"points\":[\n" + " \"drn5x1g8cu2y\",\n" + " \"30, -80\",\n" + " \"20, -90\"\n" + " ]\n" + " }\n" + " }\n" + "}\n"; assertGeoPolygonQuery(query); } private void assertGeoPolygonQuery(String query) throws IOException { QueryShardContext context = createShardContext(); Query parsedQuery = parseQuery(query).toQuery(context); // todo LatLonPointInPolygon is package private, need a closeTo check on the query // since some points can be computed from the geohash } public void testFromJson() throws IOException { String json = "{\n" + " \"geo_polygon\" : {\n" + " \"person.location\" : {\n" + " \"points\" : [ [ -70.0, 40.0 ], [ -80.0, 30.0 ], [ -90.0, 20.0 ], [ -70.0, 40.0 ] ]\n" + " },\n" + " \"validation_method\" : \"STRICT\",\n" + " \"ignore_unmapped\" : false,\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; GeoPolygonQueryBuilder parsed = (GeoPolygonQueryBuilder) parseQuery(json); checkGeneratedJson(json, parsed); assertEquals(json, 4, parsed.points().size()); } @Override public void testMustRewrite() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); super.testMustRewrite(); } public void testIgnoreUnmapped() throws IOException { List<GeoPoint> polygon = randomPolygon(); final GeoPolygonQueryBuilder queryBuilder = new GeoPolygonQueryBuilder("unmapped", polygon); queryBuilder.ignoreUnmapped(true); Query query = queryBuilder.toQuery(createShardContext()); assertThat(query, notNullValue()); assertThat(query, instanceOf(MatchNoDocsQuery.class)); final GeoPolygonQueryBuilder failingQueryBuilder = new GeoPolygonQueryBuilder("unmapped", polygon); failingQueryBuilder.ignoreUnmapped(false); QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext())); assertThat(e.getMessage(), containsString("failed to find geo_point field [unmapped]")); } }