/* * 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 org.apache.lucene.search.Query; import org.apache.lucene.spatial.geopoint.document.GeoPointField; import org.apache.lucene.spatial.geopoint.search.GeoPointInBBoxQuery; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.Version; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.IndexGeoPointFieldData; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper; import org.elasticsearch.index.search.geo.InMemoryGeoBoundingBoxQuery; import org.elasticsearch.index.search.geo.IndexedGeoBoundingBoxQuery; import java.io.IOException; /** * */ public class GeoBoundingBoxQueryParser implements QueryParser { public static final String NAME = "geo_bbox"; public static final String TOP = "top"; public static final String LEFT = "left"; public static final String RIGHT = "right"; public static final String BOTTOM = "bottom"; public static final String TOP_LEFT = TOP + "_" + LEFT; public static final String TOP_RIGHT = TOP + "_" + RIGHT; public static final String BOTTOM_LEFT = BOTTOM + "_" + LEFT; public static final String BOTTOM_RIGHT = BOTTOM + "_" + RIGHT; public static final String TOPLEFT = "topLeft"; public static final String TOPRIGHT = "topRight"; public static final String BOTTOMLEFT = "bottomLeft"; public static final String BOTTOMRIGHT = "bottomRight"; public static final String FIELD = "field"; @Inject public GeoBoundingBoxQueryParser() { } @Override public String[] names() { return new String[]{NAME, "geoBbox", "geo_bounding_box", "geoBoundingBox"}; } @Override public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { XContentParser parser = parseContext.parser(); String fieldName = null; double top = Double.NaN; double bottom = Double.NaN; double left = Double.NaN; double right = Double.NaN; String queryName = null; String currentFieldName = null; XContentParser.Token token; final boolean indexCreatedBeforeV2_0 = parseContext.indexVersionCreated().before(Version.V_2_0_0); boolean coerce = false; boolean ignoreMalformed = false; GeoPoint sparse = new GeoPoint(); String type = "memory"; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.START_OBJECT) { fieldName = currentFieldName; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); token = parser.nextToken(); if (parseContext.isDeprecatedSetting(currentFieldName)) { // skip } else if (FIELD.equals(currentFieldName)) { fieldName = parser.text(); } else if (TOP.equals(currentFieldName)) { top = parser.doubleValue(); } else if (BOTTOM.equals(currentFieldName)) { bottom = parser.doubleValue(); } else if (LEFT.equals(currentFieldName)) { left = parser.doubleValue(); } else if (RIGHT.equals(currentFieldName)) { right = parser.doubleValue(); } else { if (TOP_LEFT.equals(currentFieldName) || TOPLEFT.equals(currentFieldName)) { GeoUtils.parseGeoPoint(parser, sparse); top = sparse.getLat(); left = sparse.getLon(); } else if (BOTTOM_RIGHT.equals(currentFieldName) || BOTTOMRIGHT.equals(currentFieldName)) { GeoUtils.parseGeoPoint(parser, sparse); bottom = sparse.getLat(); right = sparse.getLon(); } else if (TOP_RIGHT.equals(currentFieldName) || TOPRIGHT.equals(currentFieldName)) { GeoUtils.parseGeoPoint(parser, sparse); top = sparse.getLat(); right = sparse.getLon(); } else if (BOTTOM_LEFT.equals(currentFieldName) || BOTTOMLEFT.equals(currentFieldName)) { GeoUtils.parseGeoPoint(parser, sparse); bottom = sparse.getLat(); left = sparse.getLon(); } else { throw new ElasticsearchParseException("failed to parse [{}] query. unexpected field [{}]", NAME, currentFieldName); } } } else { throw new ElasticsearchParseException("failed to parse [{}] query. field name expected but [{}] found", NAME, token); } } } else if (token.isValue()) { if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else if ("coerce".equals(currentFieldName) || (indexCreatedBeforeV2_0 && "normalize".equals(currentFieldName))) { coerce = parser.booleanValue(); if (coerce == true) { ignoreMalformed = true; } } else if ("type".equals(currentFieldName)) { type = parser.text(); } else if ("ignore_malformed".equals(currentFieldName) && coerce == false) { ignoreMalformed = parser.booleanValue(); } else { throw new QueryParsingException(parseContext, "failed to parse [{}] query. unexpected field [{}]", NAME, currentFieldName); } } } final GeoPoint topLeft = sparse.reset(top, left); //just keep the object final GeoPoint bottomRight = new GeoPoint(bottom, right); // validation was not available prior to 2.x, so to support bwc percolation queries we only ignore_malformed on 2.x created indexes if (!indexCreatedBeforeV2_0 && !ignoreMalformed) { if (topLeft.lat() > 90.0 || topLeft.lat() < -90.0) { throw new QueryParsingException(parseContext, "illegal latitude value [{}] for [{}]", topLeft.lat(), NAME); } if (topLeft.lon() > 180.0 || topLeft.lon() < -180) { throw new QueryParsingException(parseContext, "illegal longitude value [{}] for [{}]", topLeft.lon(), NAME); } if (bottomRight.lat() > 90.0 || bottomRight.lat() < -90.0) { throw new QueryParsingException(parseContext, "illegal latitude value [{}] for [{}]", bottomRight.lat(), NAME); } if (bottomRight.lon() > 180.0 || bottomRight.lon() < -180) { throw new QueryParsingException(parseContext, "illegal longitude value [{}] for [{}]", bottomRight.lon(), NAME); } } final Version indexVersionCreated = parseContext.indexVersionCreated(); if (indexVersionCreated.onOrAfter(Version.V_2_2_0) || coerce) { // Special case: if the difference between the left and right is 360 and the right is greater than the left, we are asking for // the complete longitude range so need to set longitude to the complete longditude range boolean completeLonRange = ((right - left) % 360 == 0 && right > left); GeoUtils.normalizePoint(topLeft, true, !completeLonRange); GeoUtils.normalizePoint(bottomRight, true, !completeLonRange); if (completeLonRange) { topLeft.resetLon(-180); bottomRight.resetLon(180); } } // validate top.lat > bottom.lat if (topLeft.lat() < bottomRight.lat()) { throw new QueryParsingException(parseContext, "top is below bottom corner: [{}] vs [{}]", topLeft.lat(), bottomRight.lat()); } else if (topLeft.lat() == bottomRight.lat()) { throw new QueryParsingException(parseContext, "top latitude cannot equal bottom latitude: [{}] vs [{}]", topLeft.lat(), bottomRight.lat()); } MappedFieldType fieldType = parseContext.fieldMapper(fieldName); if (fieldType == null) { throw new QueryParsingException(parseContext, "failed to parse [{}] query. could not find [{}] field [{}]", NAME, GeoPointFieldMapper.CONTENT_TYPE, fieldName); } if (!(fieldType instanceof GeoPointFieldMapper.GeoPointFieldType)) { throw new QueryParsingException(parseContext, "failed to parse [{}] query. field [{}] is expected to be of type [{}], but is of [{}] type instead", NAME, fieldName, GeoPointFieldMapper.CONTENT_TYPE, fieldType.typeName()); } GeoPointFieldMapper.GeoPointFieldType geoFieldType = ((GeoPointFieldMapper.GeoPointFieldType) fieldType); Query query; if (indexVersionCreated.onOrAfter(Version.V_2_2_0)) { // if index created V_2_2 use (soon to be legacy) numeric encoding postings format // if index created V_2_3 > use prefix encoded postings format final GeoPointField.TermEncoding encoding = (indexVersionCreated.before(Version.V_2_3_0)) ? GeoPointField.TermEncoding.NUMERIC : GeoPointField.TermEncoding.PREFIX; query = new GeoPointInBBoxQuery(fieldType.names().fullName(), encoding, topLeft.lon(), bottomRight.lat(), bottomRight.lon(), topLeft.lat()); } else if ("indexed".equals(type)) { query = IndexedGeoBoundingBoxQuery.create(topLeft, bottomRight, geoFieldType); } else if ("memory".equals(type)) { IndexGeoPointFieldData indexFieldData = parseContext.getForField(fieldType); query = new InMemoryGeoBoundingBoxQuery(topLeft, bottomRight, indexFieldData); } else { throw new QueryParsingException(parseContext, "failed to parse [{}] query. geo bounding box type [{}] is not supported. either [indexed] or [memory] are allowed", NAME, type); } if (queryName != null) { parseContext.addNamedQuery(queryName, query); } return query; } }