From 689fd15d786428869a7e311ef6c42f0094ae45a9 Mon Sep 17 00:00:00 2001 From: Florian Schilling Date: Wed, 12 Mar 2014 17:29:05 +0100 Subject: [PATCH] Add exceptions to `GeoPointFieldMapper` when parsing `geo_point` object * moved `geo_point` parsing to GeoUtils * cleaned up `gzipped.json` for bulktest * merged `GeoPointFieldMapper` and `GeoPoint` parsing methods Closes #5390 --- .../elasticsearch/common/geo/GeoPoint.java | 104 ---------- .../elasticsearch/common/geo/GeoUtils.java | 119 +++++++++++ .../index/mapper/geo/GeoPointFieldMapper.java | 88 ++------- .../query/GeoBoundingBoxFilterParser.java | 8 +- .../index/query/GeoDistanceFilterParser.java | 2 +- .../query/GeoDistanceRangeFilterParser.java | 4 +- .../index/query/GeoPolygonFilterParser.java | 2 +- .../index/query/GeohashCellFilter.java | 4 +- .../functionscore/DecayFunctionParser.java | 3 +- .../geodistance/GeoDistanceFacetParser.java | 4 +- .../search/sort/GeoDistanceSortParser.java | 4 +- .../context/GeolocationContextMapping.java | 12 +- .../search/geo/GeoPointParsingTests.java | 186 ++++++++++++++++++ .../elasticsearch/search/geo/gzippedmap.json | Bin 7740 -> 7734 bytes .../search/sort/SimpleSortTests.java | 2 - 15 files changed, 348 insertions(+), 194 deletions(-) create mode 100644 src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java diff --git a/src/main/java/org/elasticsearch/common/geo/GeoPoint.java b/src/main/java/org/elasticsearch/common/geo/GeoPoint.java index ba568ab55100d..8278c9ca7347c 100644 --- a/src/main/java/org/elasticsearch/common/geo/GeoPoint.java +++ b/src/main/java/org/elasticsearch/common/geo/GeoPoint.java @@ -19,22 +19,12 @@ package org.elasticsearch.common.geo; -import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentParser.Token; -import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper; - -import java.io.IOException; /** * */ public final class GeoPoint { - public static final String LATITUDE = GeoPointFieldMapper.Names.LAT; - public static final String LONGITUDE = GeoPointFieldMapper.Names.LON; - public static final String GEOHASH = GeoPointFieldMapper.Names.GEOHASH; - private double lat; private double lon; @@ -150,98 +140,4 @@ public static GeoPoint parseFromLatLon(String latLon) { point.resetFromString(latLon); return point; } - - /** - * Parse a {@link GeoPoint} with a {@link XContentParser}: - * - * @param parser {@link XContentParser} to parse the value from - * @return new {@link GeoPoint} parsed from the parse - * - * @throws IOException - * @throws org.elasticsearch.ElasticsearchParseException - */ - public static GeoPoint parse(XContentParser parser) throws IOException, ElasticsearchParseException { - return parse(parser, new GeoPoint()); - } - - /** - * Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms: - * - * - * - * @param parser {@link XContentParser} to parse the value from - * @param point A {@link GeoPoint} that will be reset by the values parsed - * @return new {@link GeoPoint} parsed from the parse - * - * @throws IOException - * @throws org.elasticsearch.ElasticsearchParseException - */ - public static GeoPoint parse(XContentParser parser, GeoPoint point) throws IOException, ElasticsearchParseException { - if(parser.currentToken() == Token.START_OBJECT) { - while(parser.nextToken() != Token.END_OBJECT) { - if(parser.currentToken() == Token.FIELD_NAME) { - String field = parser.text(); - if(LATITUDE.equals(field)) { - if(parser.nextToken() == Token.VALUE_NUMBER) { - point.resetLat(parser.doubleValue()); - } else { - throw new ElasticsearchParseException("latitude must be a number"); - } - } else if (LONGITUDE.equals(field)) { - if(parser.nextToken() == Token.VALUE_NUMBER) { - point.resetLon(parser.doubleValue()); - } else { - throw new ElasticsearchParseException("latitude must be a number"); - } - } else if (GEOHASH.equals(field)) { - if(parser.nextToken() == Token.VALUE_STRING) { - point.resetFromGeoHash(parser.text()); - } else { - throw new ElasticsearchParseException("geohash must be a string"); - } - } else { - throw new ElasticsearchParseException("field must be either '" + LATITUDE + "', '" + LONGITUDE + "' or '" + GEOHASH + "'"); - } - } else { - throw new ElasticsearchParseException("Token '"+parser.currentToken()+"' not allowed"); - } - } - return point; - } else if(parser.currentToken() == Token.START_ARRAY) { - int element = 0; - while(parser.nextToken() != Token.END_ARRAY) { - if(parser.currentToken() == Token.VALUE_NUMBER) { - element++; - if(element == 1) { - point.resetLon(parser.doubleValue()); - } else if(element == 2) { - point.resetLat(parser.doubleValue()); - } else { - throw new ElasticsearchParseException("only two values allowed"); - } - } else { - throw new ElasticsearchParseException("Numeric value expected"); - } - } - return point; - } else if(parser.currentToken() == Token.VALUE_STRING) { - String data = parser.text(); - int comma = data.indexOf(','); - if(comma > 0) { - double lat = Double.parseDouble(data.substring(0, comma).trim()); - double lon = Double.parseDouble(data.substring(comma + 1).trim()); - return point.reset(lat, lon); - } else { - point.resetFromGeoHash(data); - return point; - } - } else { - throw new ElasticsearchParseException("geo_point expected"); - } - } } diff --git a/src/main/java/org/elasticsearch/common/geo/GeoUtils.java b/src/main/java/org/elasticsearch/common/geo/GeoUtils.java index 998504f8164af..3167c19fb1ab0 100644 --- a/src/main/java/org/elasticsearch/common/geo/GeoUtils.java +++ b/src/main/java/org/elasticsearch/common/geo/GeoUtils.java @@ -22,12 +22,22 @@ import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree; import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree; import org.apache.lucene.util.SloppyMath; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; +import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper; + +import java.io.IOException; /** */ public class GeoUtils { + public static final String LATITUDE = GeoPointFieldMapper.Names.LAT; + public static final String LONGITUDE = GeoPointFieldMapper.Names.LON; + public static final String GEOHASH = GeoPointFieldMapper.Names.GEOHASH; + /** Earth ellipsoid major axis defined by WGS 84 in meters */ public static final double EARTH_SEMI_MAJOR_AXIS = 6378137.0; // meters (WGS 84) @@ -293,5 +303,114 @@ private static double centeredModulus(double dividend, double divisor) { } return rtn; } + /** + * Parse a {@link GeoPoint} with a {@link XContentParser}: + * + * @param parser {@link XContentParser} to parse the value from + * @return new {@link GeoPoint} parsed from the parse + * + * @throws IOException + * @throws org.elasticsearch.ElasticsearchParseException + */ + public static GeoPoint parseGeoPoint(XContentParser parser) throws IOException, ElasticsearchParseException { + return parseGeoPoint(parser, new GeoPoint()); + } + + /** + * Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms: + * + * + * + * @param parser {@link XContentParser} to parse the value from + * @param point A {@link GeoPoint} that will be reset by the values parsed + * @return new {@link GeoPoint} parsed from the parse + * + * @throws IOException + * @throws org.elasticsearch.ElasticsearchParseException + */ + public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point) throws IOException, ElasticsearchParseException { + double lat = Double.NaN; + double lon = Double.NaN; + String geohash = null; + + if(parser.currentToken() == Token.START_OBJECT) { + while(parser.nextToken() != Token.END_OBJECT) { + if(parser.currentToken() == Token.FIELD_NAME) { + String field = parser.text(); + if(LATITUDE.equals(field)) { + if(parser.nextToken() == Token.VALUE_NUMBER) { + lat = parser.doubleValue(); + } else { + throw new ElasticsearchParseException("latitude must be a number"); + } + } else if (LONGITUDE.equals(field)) { + if(parser.nextToken() == Token.VALUE_NUMBER) { + lon = parser.doubleValue(); + } else { + throw new ElasticsearchParseException("latitude must be a number"); + } + } else if (GEOHASH.equals(field)) { + if(parser.nextToken() == Token.VALUE_STRING) { + geohash = parser.text(); + } else { + throw new ElasticsearchParseException("geohash must be a string"); + } + } else { + throw new ElasticsearchParseException("field must be either '" + LATITUDE + "', '" + LONGITUDE + "' or '" + GEOHASH + "'"); + } + } else { + throw new ElasticsearchParseException("Token '"+parser.currentToken()+"' not allowed"); + } + } + if (geohash != null) { + if(!Double.isNaN(lat) || !Double.isNaN(lon)) { + throw new ElasticsearchParseException("field must be either lat/lon or geohash"); + } else { + return point.resetFromGeoHash(geohash); + } + } else if (Double.isNaN(lat)) { + throw new ElasticsearchParseException("field [" + LATITUDE + "] missing"); + } else if (Double.isNaN(lon)) { + throw new ElasticsearchParseException("field [" + LONGITUDE + "] missing"); + } else { + return point.reset(lat, lon); + } + + } else if(parser.currentToken() == Token.START_ARRAY) { + int element = 0; + while(parser.nextToken() != Token.END_ARRAY) { + if(parser.currentToken() == Token.VALUE_NUMBER) { + element++; + if(element == 1) { + lon = parser.doubleValue(); + } else if(element == 2) { + lat = parser.doubleValue(); + } else { + throw new ElasticsearchParseException("only two values allowed"); + } + } else { + throw new ElasticsearchParseException("Numeric value expected"); + } + } + return point.reset(lat, lon); + } else if(parser.currentToken() == Token.VALUE_STRING) { + String data = parser.text(); + int comma = data.indexOf(','); + if(comma > 0) { + lat = Double.parseDouble(data.substring(0, comma).trim()); + lon = Double.parseDouble(data.substring(comma + 1).trim()); + return point.reset(lat, lon); + } else { + return point.resetFromGeoHash(data); + } + } else { + throw new ElasticsearchParseException("geo_point expected"); + } + } } diff --git a/src/main/java/org/elasticsearch/index/mapper/geo/GeoPointFieldMapper.java b/src/main/java/org/elasticsearch/index/mapper/geo/GeoPointFieldMapper.java index a5f728eaa53f8..a28c7c80fe854 100644 --- a/src/main/java/org/elasticsearch/index/mapper/geo/GeoPointFieldMapper.java +++ b/src/main/java/org/elasticsearch/index/mapper/geo/GeoPointFieldMapper.java @@ -57,9 +57,7 @@ import java.util.Map; import static org.elasticsearch.index.mapper.MapperBuilders.*; -import static org.elasticsearch.index.mapper.core.TypeParsers.parseField; -import static org.elasticsearch.index.mapper.core.TypeParsers.parseMultiField; -import static org.elasticsearch.index.mapper.core.TypeParsers.parsePathType; +import static org.elasticsearch.index.mapper.core.TypeParsers.*; /** * Parsing: We handle: @@ -488,24 +486,19 @@ public void parse(ParseContext context) throws IOException { context.path().pathType(pathType); context.path().add(name()); - GeoPoint value = context.parseExternalValue(GeoPoint.class); - if (value != null) { - parseLatLon(context, value.lat(), value.lon()); + GeoPoint sparse = context.parseExternalValue(GeoPoint.class); + + if (sparse != null) { + parse(context, sparse, null); } else { + sparse = new GeoPoint(); XContentParser.Token token = context.parser().currentToken(); if (token == XContentParser.Token.START_ARRAY) { token = context.parser().nextToken(); if (token == XContentParser.Token.START_ARRAY) { // its an array of array of lon/lat [ [1.2, 1.3], [1.4, 1.5] ] while (token != XContentParser.Token.END_ARRAY) { - token = context.parser().nextToken(); - double lon = context.parser().doubleValue(); - token = context.parser().nextToken(); - double lat = context.parser().doubleValue(); - while ((token = context.parser().nextToken()) != XContentParser.Token.END_ARRAY) { - - } - parseLatLon(context, lat, lon); + parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse), null); token = context.parser().nextToken(); } } else { @@ -517,22 +510,22 @@ public void parse(ParseContext context) throws IOException { while ((token = context.parser().nextToken()) != XContentParser.Token.END_ARRAY) { } - parseLatLon(context, lat, lon); + parse(context, sparse.reset(lat, lon), null); } else { while (token != XContentParser.Token.END_ARRAY) { - if (token == XContentParser.Token.START_OBJECT) { - parseObjectLatLon(context); - } else if (token == XContentParser.Token.VALUE_STRING) { - parseStringLatLon(context); + if (token == XContentParser.Token.VALUE_STRING) { + parsePointFromString(context, sparse, context.parser().text()); + } else { + parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse), null); } token = context.parser().nextToken(); } } } - } else if (token == XContentParser.Token.START_OBJECT) { - parseObjectLatLon(context); } else if (token == XContentParser.Token.VALUE_STRING) { - parseStringLatLon(context); + parsePointFromString(context, sparse, context.parser().text()); + } else { + parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse), null); } } @@ -540,44 +533,6 @@ public void parse(ParseContext context) throws IOException { context.path().pathType(origPathType); } - private void parseStringLatLon(ParseContext context) throws IOException { - String value = context.parser().text(); - int comma = value.indexOf(','); - if (comma != -1) { - double lat = Double.parseDouble(value.substring(0, comma).trim()); - double lon = Double.parseDouble(value.substring(comma + 1).trim()); - parseLatLon(context, lat, lon); - } else { // geo hash - parseGeohash(context, value); - } - } - - private void parseObjectLatLon(ParseContext context) throws IOException { - XContentParser.Token token; - String currentName = context.parser().currentName(); - Double lat = null; - Double lon = null; - String geohash = null; - while ((token = context.parser().nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentName = context.parser().currentName(); - } else if (token.isValue()) { - if (currentName.equals(Names.LAT)) { - lat = context.parser().doubleValue(); - } else if (currentName.equals(Names.LON)) { - lon = context.parser().doubleValue(); - } else if (currentName.equals(Names.GEOHASH)) { - geohash = context.parser().text(); - } - } - } - if (geohash != null) { - parseGeohash(context, geohash); - } else if (lat != null && lon != null) { - parseLatLon(context, lat, lon); - } - } - private void parseGeohashField(ParseContext context, String geohash) throws IOException { int len = Math.min(geoHashPrecision, geohash.length()); int min = enableGeohashPrefix ? 1 : geohash.length(); @@ -589,13 +544,12 @@ private void parseGeohashField(ParseContext context, String geohash) throws IOEx } } - private void parseLatLon(ParseContext context, double lat, double lon) throws IOException { - parse(context, new GeoPoint(lat, lon), null); - } - - private void parseGeohash(ParseContext context, String geohash) throws IOException { - GeoPoint point = GeoHashUtils.decode(geohash); - parse(context, point, geohash); + private void parsePointFromString(ParseContext context, GeoPoint sparse, String point) throws IOException { + if (point.indexOf(',') < 0) { + parse(context, sparse.resetFromGeoHash(point), point); + } else { + parse(context, sparse.resetFromString(point), null); + } } private void parse(ParseContext context, GeoPoint point, String geohash) throws IOException { diff --git a/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java index 85f7b589a3af1..dd9fe422f10e0 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java @@ -113,19 +113,19 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar right = parser.doubleValue(); } else { if (TOP_LEFT.equals(currentFieldName) || TOPLEFT.equals(currentFieldName)) { - GeoPoint.parse(parser, sparse); + GeoUtils.parseGeoPoint(parser, sparse); top = sparse.getLat(); left = sparse.getLon(); } else if (BOTTOM_RIGHT.equals(currentFieldName) || BOTTOMRIGHT.equals(currentFieldName)) { - GeoPoint.parse(parser, sparse); + GeoUtils.parseGeoPoint(parser, sparse); bottom = sparse.getLat(); right = sparse.getLon(); } else if (TOP_RIGHT.equals(currentFieldName) || TOPRIGHT.equals(currentFieldName)) { - GeoPoint.parse(parser, sparse); + GeoUtils.parseGeoPoint(parser, sparse); top = sparse.getLat(); right = sparse.getLon(); } else if (BOTTOM_LEFT.equals(currentFieldName) || BOTTOMLEFT.equals(currentFieldName)) { - GeoPoint.parse(parser, sparse); + GeoUtils.parseGeoPoint(parser, sparse); bottom = sparse.getLat(); left = sparse.getLon(); } else { diff --git a/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java index 0d7f2bff589aa..2a54a746443f7 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java @@ -83,7 +83,7 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { fieldName = currentFieldName; - GeoPoint.parse(parser, point); + GeoUtils.parseGeoPoint(parser, point); } else if (token == XContentParser.Token.START_OBJECT) { // the json in the format of -> field : { lat : 30, lon : 12 } String currentName = parser.currentName(); diff --git a/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java index e411fb0131153..f709bf200f184 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java @@ -84,12 +84,12 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { - GeoPoint.parse(parser, point); + GeoUtils.parseGeoPoint(parser, point); fieldName = currentFieldName; } else if (token == XContentParser.Token.START_OBJECT) { // the json in the format of -> field : { lat : 30, lon : 12 } fieldName = currentFieldName; - GeoPoint.parse(parser, point); + GeoUtils.parseGeoPoint(parser, point); } else if (token.isValue()) { if (currentFieldName.equals("from")) { if (token == XContentParser.Token.VALUE_NULL) { diff --git a/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java index 71836f8f3ca17..272d4c78c48df 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java @@ -93,7 +93,7 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar } else if (token == XContentParser.Token.START_ARRAY) { if (POINTS.equals(currentFieldName)) { while ((token = parser.nextToken()) != Token.END_ARRAY) { - shell.add(GeoPoint.parse(parser)); + shell.add(GeoUtils.parseGeoPoint(parser)); } } else { throw new QueryParsingException(parseContext.index(), "[geo_polygon] filter does not support [" + currentFieldName + "]"); diff --git a/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java b/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java index 98568b1e8afc2..36c85da1ad86e 100644 --- a/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java +++ b/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java @@ -217,12 +217,12 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar // A string indicates either a gehash or a lat/lon string String location = parser.text(); if(location.indexOf(",")>0) { - geohash = GeoPoint.parse(parser).geohash(); + geohash = GeoUtils.parseGeoPoint(parser).geohash(); } else { geohash = location; } } else { - geohash = GeoPoint.parse(parser).geohash(); + geohash = GeoUtils.parseGeoPoint(parser).geohash(); } } } else { diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java index 6321fe64a8bd3..de6e9067684c6 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java @@ -26,6 +26,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.lucene.search.function.CombineFunction; import org.elasticsearch.common.lucene.search.function.ScoreFunction; import org.elasticsearch.common.unit.DistanceUnit; @@ -206,7 +207,7 @@ private ScoreFunction parseGeoVariable(String fieldName, XContentParser parser, } else if (parameterName.equals(DecayFunctionBuilder.SCALE)) { scaleString = parser.text(); } else if (parameterName.equals(DecayFunctionBuilder.ORIGIN)) { - origin = GeoPoint.parse(parser); + origin = GeoUtils.parseGeoPoint(parser); } else if (parameterName.equals(DecayFunctionBuilder.DECAY)) { decay = parser.doubleValue(); } else if (parameterName.equals(DecayFunctionBuilder.OFFSET)) { diff --git a/src/main/java/org/elasticsearch/search/facet/geodistance/GeoDistanceFacetParser.java b/src/main/java/org/elasticsearch/search/facet/geodistance/GeoDistanceFacetParser.java index 4cbcbbe52bace..4ab90717b0fe7 100644 --- a/src/main/java/org/elasticsearch/search/facet/geodistance/GeoDistanceFacetParser.java +++ b/src/main/java/org/elasticsearch/search/facet/geodistance/GeoDistanceFacetParser.java @@ -109,7 +109,7 @@ public FacetExecutor parse(String facetName, XContentParser parser, SearchContex entries.add(new GeoDistanceFacet.Entry(from, to, 0, 0, 0, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY)); } } else { - GeoPoint.parse(parser, point); + GeoUtils.parseGeoPoint(parser, point); fieldName = currentName; } } else if (token == XContentParser.Token.START_OBJECT) { @@ -118,7 +118,7 @@ public FacetExecutor parse(String facetName, XContentParser parser, SearchContex } else { // the json in the format of -> field : { lat : 30, lon : 12 } fieldName = currentName; - GeoPoint.parse(parser, point); + GeoUtils.parseGeoPoint(parser, point); } } else if (token.isValue()) { if (currentName.equals("unit")) { diff --git a/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java b/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java index 49b1d8af13f21..0f734d7ca4cc7 100644 --- a/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java +++ b/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java @@ -69,7 +69,7 @@ public SortField parse(XContentParser parser, SearchContext context) throws Exce if (token == XContentParser.Token.FIELD_NAME) { currentName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { - GeoPoint.parse(parser, point); + GeoUtils.parseGeoPoint(parser, point); fieldName = currentName; } else if (token == XContentParser.Token.START_OBJECT) { // the json in the format of -> field : { lat : 30, lon : 12 } @@ -78,7 +78,7 @@ public SortField parse(XContentParser parser, SearchContext context) throws Exce nestedFilter = parsedFilter == null ? null : parsedFilter.filter(); } else { fieldName = currentName; - GeoPoint.parse(parser, point); + GeoUtils.parseGeoPoint(parser, point); } } else if (token.isValue()) { if ("reverse".equals(currentName)) { diff --git a/src/main/java/org/elasticsearch/search/suggest/context/GeolocationContextMapping.java b/src/main/java/org/elasticsearch/search/suggest/context/GeolocationContextMapping.java index 38ebb1137c303..a679dc6a6a8ec 100644 --- a/src/main/java/org/elasticsearch/search/suggest/context/GeolocationContextMapping.java +++ b/src/main/java/org/elasticsearch/search/suggest/context/GeolocationContextMapping.java @@ -210,13 +210,13 @@ protected static Collection parseSinglePointOrList(XContentParser parser // otherwise it's a list of locations ArrayList result = Lists.newArrayList(); while (token != Token.END_ARRAY) { - result.add(GeoPoint.parse(parser).geohash()); + result.add(GeoUtils.parseGeoPoint(parser).geohash()); } return result; } } else { // or a single location - return Collections.singleton(GeoPoint.parse(parser).geohash()); + return Collections.singleton(GeoUtils.parseGeoPoint(parser).geohash()); } } @@ -337,8 +337,8 @@ public GeoQuery parseQuery(String name, XContentParser parser) throws IOExceptio precision = new int[] { parsePrecision(parser) }; } } else if (FIELD_VALUE.equals(fieldName)) { - if(lat == Double.NaN && lon == Double.NaN) { - point = GeoPoint.parse(parser); + if(Double.isNaN(lon) && Double.isNaN(lat)) { + point = GeoUtils.parseGeoPoint(parser); } else { throw new ElasticsearchParseException("only lat/lon or [" + FIELD_VALUE + "] is allowed"); } @@ -348,7 +348,7 @@ public GeoQuery parseQuery(String name, XContentParser parser) throws IOExceptio } if (point == null) { - if (lat == Double.NaN || lon == Double.NaN) { + if (Double.isNaN(lat) || Double.isNaN(lon)) { throw new ElasticsearchParseException("location is missing"); } else { point = new GeoPoint(lat, lon); @@ -357,7 +357,7 @@ public GeoQuery parseQuery(String name, XContentParser parser) throws IOExceptio return new GeoQuery(name, point.geohash(), precision); } else { - return new GeoQuery(name, GeoPoint.parse(parser).getGeohash(), precision); + return new GeoQuery(name, GeoUtils.parseGeoPoint(parser).getGeohash(), precision); } } diff --git a/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java b/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java new file mode 100644 index 0000000000000..c423419cb9f02 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java @@ -0,0 +1,186 @@ +/* + * 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.search.geo; + + +import org.elasticsearch.common.geo.GeoHashUtils; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.Matchers.closeTo; + + +public class GeoPointParsingTests extends ElasticsearchTestCase { + + // mind geohash precision and error + private static final double ERROR = 0.00001d; + + @Test + public void testGeoPointReset() throws IOException { + double lat = 1 + randomDouble() * 89; + double lon = 1 + randomDouble() * 179; + + GeoPoint point = new GeoPoint(0, 0); + assertCloseTo(point, 0, 0); + + assertCloseTo(point.reset(lat, lon), lat, lon); + assertCloseTo(point.reset(0, 0), 0, 0); + assertCloseTo(point.resetLat(lat), lat, 0); + assertCloseTo(point.resetLat(0), 0, 0); + assertCloseTo(point.resetLon(lon), 0, lon); + assertCloseTo(point.resetLon(0), 0, 0); + assertCloseTo(point.resetFromGeoHash(GeoHashUtils.encode(lat, lon)), lat, lon); + assertCloseTo(point.reset(0, 0), 0, 0); + assertCloseTo(point.resetFromString(Double.toString(lat) + ", " + Double.toHexString(lon)), lat, lon); + assertCloseTo(point.reset(0, 0), 0, 0); + } + + @Test + public void testGeoPointParsing() throws IOException { + double lat = randomDouble() * 180 - 90; + double lon = randomDouble() * 360 - 180; + + GeoPoint point = GeoUtils.parseGeoPoint(objectLatLon(lat, lon)); + assertCloseTo(point, lat, lon); + + GeoUtils.parseGeoPoint(arrayLatLon(lat, lon), point); + assertCloseTo(point, lat, lon); + + GeoUtils.parseGeoPoint(geohash(lat, lon), point); + assertCloseTo(point, lat, lon); + + GeoUtils.parseGeoPoint(stringLatLon(lat, lon), point); + assertCloseTo(point, lat, lon); + } + + // Based on issue5390 + @Test + public void testInvalidPointEmbeddedObject() throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.startObject(); + content.startObject("location"); + content.field("lat", 0).field("lon", 0); + content.endObject(); + content.endObject(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + + try { + GeoUtils.parseGeoPoint(parser); + assertTrue(false); + } catch (Throwable e) {} + } + + @Test + public void testInvalidPointLatHashMix() throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.startObject(); + content.field("lat", 0).field("geohash", GeoHashUtils.encode(0, 0)); + content.endObject(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + + try { + GeoUtils.parseGeoPoint(parser); + assertTrue(false); + } catch (Throwable e) {} + } + + @Test + public void testInvalidPointLonHashMix() throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.startObject(); + content.field("lon", 0).field("geohash", GeoHashUtils.encode(0, 0)); + content.endObject(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + + try { + GeoUtils.parseGeoPoint(parser); + assertTrue(false); + } catch (Throwable e) {} + } + + @Test + public void testInvalidField() throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.startObject(); + content.field("lon", 0).field("lat", 0).field("test", 0); + content.endObject(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + + try { + GeoUtils.parseGeoPoint(parser); + assertTrue(false); + } catch (Throwable e) {} + } + + private static XContentParser objectLatLon(double lat, double lon) throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.startObject(); + content.field("lat", lat).field("lon", lon); + content.endObject(); + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + return parser; + } + + private static XContentParser arrayLatLon(double lat, double lon) throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.startArray().value(lon).value(lat).endArray(); + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + return parser; + } + + private static XContentParser stringLatLon(double lat, double lon) throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.value(Double.toString(lat) + ", " + Double.toString(lon)); + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + return parser; + } + + private static XContentParser geohash(double lat, double lon) throws IOException { + XContentBuilder content = JsonXContent.contentBuilder(); + content.value(GeoHashUtils.encode(lat, lon)); + XContentParser parser = JsonXContent.jsonXContent.createParser(content.bytes()); + parser.nextToken(); + return parser; + } + + public static void assertCloseTo(GeoPoint point, double lat, double lon) { + assertThat(point.lat(), closeTo(lat, ERROR)); + assertThat(point.lon(), closeTo(lon, ERROR)); + } + +} diff --git a/src/test/java/org/elasticsearch/search/geo/gzippedmap.json b/src/test/java/org/elasticsearch/search/geo/gzippedmap.json index f77bdb8d4a7c367ce8017ac5f46aaa173ea2660b..d903def573b512d50125f216159dc7cc298c8dfa 100644 GIT binary patch literal 7734 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 literal 7740 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