diff --git a/CHANGELOG.md b/CHANGELOG.md index b6af9b7041db3..4323ecaa64fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Add BWC version 2.3.1 ([#4513](https://github.com/opensearch-project/OpenSearch/pull/4513)) - [Segment Replication] Add snapshot and restore tests for segment replication feature ([#3993](https://github.com/opensearch-project/OpenSearch/pull/3993)) - Added missing javadocs for `:example-plugins` modules ([#4540](https://github.com/opensearch-project/OpenSearch/pull/4540)) +- Add support for GeoJson Point type in GeoPoint field ([#4597](https://github.com/opensearch-project/OpenSearch/pull/4597)) ### Dependencies - Bumps `log4j-core` from 2.18.0 to 2.19.0 diff --git a/server/src/main/java/org/opensearch/common/geo/GeoPoint.java b/server/src/main/java/org/opensearch/common/geo/GeoPoint.java index a2b06dccded8c..f6b049b46789d 100644 --- a/server/src/main/java/org/opensearch/common/geo/GeoPoint.java +++ b/server/src/main/java/org/opensearch/common/geo/GeoPoint.java @@ -119,7 +119,11 @@ public GeoPoint resetFromString(String value, final boolean ignoreZValue, Effect public GeoPoint resetFromCoordinates(String value, final boolean ignoreZValue) { String[] vals = value.split(","); if (vals.length > 3) { - throw new OpenSearchParseException("failed to parse [{}], expected 2 or 3 coordinates " + "but found: [{}]", vals.length); + throw new OpenSearchParseException( + "failed to parse [{}], expected 2 or 3 coordinates " + "but found: [{}]", + vals.length, + vals.length + ); } final double lat; final double lon; diff --git a/server/src/main/java/org/opensearch/common/geo/GeoUtils.java b/server/src/main/java/org/opensearch/common/geo/GeoUtils.java index 5534967d559d6..8c0cf9ca551bd 100644 --- a/server/src/main/java/org/opensearch/common/geo/GeoUtils.java +++ b/server/src/main/java/org/opensearch/common/geo/GeoUtils.java @@ -40,10 +40,9 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.NamedXContentRegistry; import org.opensearch.common.xcontent.XContentParser; -import org.opensearch.common.xcontent.XContentParser.Token; -import org.opensearch.common.xcontent.XContentSubParser; import org.opensearch.common.xcontent.support.MapXContentParser; import org.opensearch.common.xcontent.support.XContentMapValues; +import org.opensearch.geometry.Point; import org.opensearch.index.fielddata.FieldData; import org.opensearch.index.fielddata.GeoPointValues; import org.opensearch.index.fielddata.MultiGeoPointValues; @@ -95,6 +94,12 @@ public class GeoUtils { /** rounding error for quantized latitude and longitude values */ public static final double TOLERANCE = 1E-6; + public static final PointParser POINT_PARSER; + + static { + POINT_PARSER = new PointParser(LONGITUDE, LATITUDE, true); + } + /** Returns true if latitude is actually a valid latitude value.*/ public static boolean isValidLatitude(double latitude) { if (Double.isNaN(latitude) || Double.isInfinite(latitude) || latitude < GeoUtils.MIN_LAT || latitude > GeoUtils.MAX_LAT) { @@ -444,113 +449,41 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina * 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 + * @param ignoreZValue tells to ignore z value or throw exception when there is a z value + * @param effectivePoint tells which point to use for GeoHash form * @return new {@link GeoPoint} parsed from the parse */ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, final boolean ignoreZValue, EffectivePoint effectivePoint) throws IOException, OpenSearchParseException { - double lat = Double.NaN; - double lon = Double.NaN; - String geohash = null; - NumberFormatException numberFormatException = null; - - if (parser.currentToken() == Token.START_OBJECT) { - try (XContentSubParser subParser = new XContentSubParser(parser)) { - while (subParser.nextToken() != Token.END_OBJECT) { - if (subParser.currentToken() == Token.FIELD_NAME) { - String field = subParser.currentName(); - if (LATITUDE.equals(field)) { - subParser.nextToken(); - switch (subParser.currentToken()) { - case VALUE_NUMBER: - case VALUE_STRING: - try { - lat = subParser.doubleValue(true); - } catch (NumberFormatException e) { - numberFormatException = e; - } - break; - default: - throw new OpenSearchParseException("latitude must be a number"); - } - } else if (LONGITUDE.equals(field)) { - subParser.nextToken(); - switch (subParser.currentToken()) { - case VALUE_NUMBER: - case VALUE_STRING: - try { - lon = subParser.doubleValue(true); - } catch (NumberFormatException e) { - numberFormatException = e; - } - break; - default: - throw new OpenSearchParseException("longitude must be a number"); - } - } else if (GEOHASH.equals(field)) { - if (subParser.nextToken() == Token.VALUE_STRING) { - geohash = subParser.text(); - } else { - throw new OpenSearchParseException("geohash must be a string"); - } - } else { - throw new OpenSearchParseException("field must be either [{}], [{}] or [{}]", LATITUDE, LONGITUDE, GEOHASH); - } - } else { - throw new OpenSearchParseException("token [{}] not allowed", subParser.currentToken()); - } - } - } - if (geohash != null) { - if (!Double.isNaN(lat) || !Double.isNaN(lon)) { - throw new OpenSearchParseException("field must be either lat/lon or geohash"); - } else { - return point.parseGeoHash(geohash, effectivePoint); - } - } else if (numberFormatException != null) { - throw new OpenSearchParseException("[{}] and [{}] must be valid double values", numberFormatException, LATITUDE, LONGITUDE); - } else if (Double.isNaN(lat)) { - throw new OpenSearchParseException("field [{}] missing", LATITUDE); - } else if (Double.isNaN(lon)) { - throw new OpenSearchParseException("field [{}] missing", LONGITUDE); - } else { - return point.reset(lat, lon); - } - - } else if (parser.currentToken() == Token.START_ARRAY) { - try (XContentSubParser subParser = new XContentSubParser(parser)) { - int element = 0; - while (subParser.nextToken() != Token.END_ARRAY) { - if (subParser.currentToken() == Token.VALUE_NUMBER) { - element++; - if (element == 1) { - lon = subParser.doubleValue(); - } else if (element == 2) { - lat = subParser.doubleValue(); - } else if (element == 3) { - GeoPoint.assertZValue(ignoreZValue, subParser.doubleValue()); - } else { - throw new OpenSearchParseException("[geo_point] field type does not accept > 3 dimensions"); - } - } else { - throw new OpenSearchParseException("numeric value expected"); - } - } - } - return point.reset(lat, lon); - } else if (parser.currentToken() == Token.VALUE_STRING) { - String val = parser.text(); - return point.resetFromString(val, ignoreZValue, effectivePoint); - } else { - throw new OpenSearchParseException("geo_point expected"); + switch (parser.currentToken()) { + case START_OBJECT: + Point pointInObject = POINT_PARSER.parseObject(parser, ignoreZValue, effectivePoint); + point.reset(pointInObject.getLat(), pointInObject.getLon()); + break; + case START_ARRAY: + case VALUE_NUMBER: + Point pointInArray = POINT_PARSER.parseFromArray(parser, ignoreZValue, "geo_point"); + point.reset(pointInArray.getLat(), pointInArray.getLon()); + break; + case VALUE_STRING: + String val = parser.text(); + point.resetFromString(val, ignoreZValue, effectivePoint); + break; + default: + throw new OpenSearchParseException("geo_point expected"); } + return point; } /** diff --git a/server/src/main/java/org/opensearch/common/geo/PointParser.java b/server/src/main/java/org/opensearch/common/geo/PointParser.java new file mode 100644 index 0000000000000..5e090d8407cb9 --- /dev/null +++ b/server/src/main/java/org/opensearch/common/geo/PointParser.java @@ -0,0 +1,237 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.common.geo; + +import org.opensearch.OpenSearchParseException; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.geometry.Point; +import org.opensearch.index.mapper.ParseContext; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; + +/** + * A utility class with a point parser methods supporting different point representation formats + * + * @opensearch.internal + */ +public class PointParser { + private static final String FN_GEOJSON_TYPE = "type"; + private static final String FV_GEOJSON_TYPE_POINT = "Point"; + private static final String FN_GEOJSON_COORDS = "coordinates"; + + private static final String FN_GEOHASH = "geohash"; + private static final String ERR_MSG_INVALID_TOKEN = "token [{}] not allowed"; + + private String fieldNameX; + private String fieldNameY; + private boolean supportGeoHash; + + private String invalidFieldErrMsg; + + public PointParser(final String fieldNameX, final String fieldNameY, final boolean supportGeoHash) { + this.fieldNameX = fieldNameX; + this.fieldNameY = fieldNameY; + this.supportGeoHash = supportGeoHash; + this.invalidFieldErrMsg = supportGeoHash + ? String.format( + Locale.ROOT, + "field must be either %s/%s, %s/%s, or %s", + fieldNameX, + fieldNameY, + FN_GEOJSON_TYPE, + FN_GEOJSON_COORDS, + FN_GEOHASH + ) + : String.format( + Locale.ROOT, + "field must be either %s/%s, or %s/%s", + fieldNameX, + fieldNameY, + FN_GEOJSON_TYPE, + FN_GEOJSON_COORDS + ); + } + + public Point parseObject(final XContentParser parser, final boolean ignoreZValue, final GeoUtils.EffectivePoint effectivePoint) + throws IOException { + if (parser.currentToken() != XContentParser.Token.START_OBJECT) { + throw new OpenSearchParseException("object is expected"); + } + + parser.nextToken(); + + if (parser.currentToken() == XContentParser.Token.END_OBJECT) { + throw new OpenSearchParseException(invalidFieldErrMsg); + } + + if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { + throw new OpenSearchParseException(ERR_MSG_INVALID_TOKEN, parser.currentToken()); + } + + Point point = null; + String field = parser.currentName(); + if (fieldNameX.equals(field) || fieldNameY.equals(field)) { + point = parseBasicFields(parser); + } else if (supportGeoHash && FN_GEOHASH.equals(field)) { + point = parseGeoHashFields(parser, effectivePoint); + } else if (FN_GEOJSON_TYPE.equals(field) || FN_GEOJSON_COORDS.equals(field)) { + point = parseGeoJsonFields(parser, ignoreZValue); + } else { + throw new OpenSearchParseException(invalidFieldErrMsg); + } + + parser.nextToken(); + if (parser.currentToken() != XContentParser.Token.END_OBJECT) { + throw new OpenSearchParseException(invalidFieldErrMsg); + } + + return point; + } + + private Point parseGeoHashFields(XContentParser parser, GeoUtils.EffectivePoint effectivePoint) throws IOException { + if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { + throw new OpenSearchParseException(ERR_MSG_INVALID_TOKEN, parser.currentToken()); + } + + String field = parser.currentName(); + if (!FN_GEOHASH.equals(field)) { + throw new OpenSearchParseException(invalidFieldErrMsg); + } + + parser.nextToken(); + + if (parser.currentToken() != XContentParser.Token.VALUE_STRING) { + throw new OpenSearchParseException("{} must be a string", FN_GEOHASH); + } + + String geoHash = parser.text(); + GeoPoint geoPoint = new GeoPoint(); + geoPoint = geoPoint.parseGeoHash(geoHash, effectivePoint); + return new Point(geoPoint.lon(), geoPoint.lat()); + } + + private Point parseBasicFields(final XContentParser parser) throws IOException { + HashMap xy = new HashMap<>(); + for (int i = 0; i < 2; i++) { + if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { + throw new OpenSearchParseException(ERR_MSG_INVALID_TOKEN, parser.currentToken()); + } + + String field = parser.currentName(); + if (!fieldNameX.equals(field) && !fieldNameY.equals(field)) { + throw new OpenSearchParseException(invalidFieldErrMsg); + } + parser.nextToken(); + switch (parser.currentToken()) { + case VALUE_NUMBER: + case VALUE_STRING: + try { + xy.put(field, parser.doubleValue(true)); + } catch (NumberFormatException e) { + throw new OpenSearchParseException("[{}] and [{}] must be valid double values", e, fieldNameX, fieldNameY); + } + break; + default: + throw new OpenSearchParseException("{} must be a number", field); + } + parser.nextToken(); + } + + if (xy.get(fieldNameX) == null) { + throw new OpenSearchParseException("field [{}] missing", fieldNameX); + } + if (xy.get(fieldNameY) == null) { + throw new OpenSearchParseException("field [{}] missing", fieldNameY); + } + + return new Point(xy.get(fieldNameX), xy.get(fieldNameY)); + } + + private Point parseGeoJsonFields(XContentParser parser, boolean ignoreZValue) throws IOException { + boolean isTypePoint = false; + Point point = null; + for (int i = 0; i < 2; i++) { + if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { + throw new OpenSearchParseException(ERR_MSG_INVALID_TOKEN, parser.currentToken()); + } + + String field = parser.currentName(); + parser.nextToken(); + if (FN_GEOJSON_TYPE.equals(field)) { + if (parser.currentToken() != XContentParser.Token.VALUE_STRING) { + throw new OpenSearchParseException("{} must be a string", FN_GEOJSON_TYPE); + } + + if (!FV_GEOJSON_TYPE_POINT.equals(parser.text())) { + throw new OpenSearchParseException("{} must be {}", FN_GEOJSON_TYPE, FV_GEOJSON_TYPE_POINT); + } + isTypePoint = true; + } else if (FN_GEOJSON_COORDS.equals(field)) { + if (parser.currentToken() != XContentParser.Token.START_ARRAY) { + throw new OpenSearchParseException("{} must be an array", FN_GEOJSON_COORDS); + } + point = parseFromArray(parser, ignoreZValue, FN_GEOJSON_COORDS); + } else { + throw new OpenSearchParseException(invalidFieldErrMsg); + } + parser.nextToken(); + } + + if (!isTypePoint) { + throw new OpenSearchParseException("field [{}] missing", FN_GEOJSON_TYPE); + } + + if (point == null) { + throw new OpenSearchParseException("field [{}] missing", FN_GEOJSON_COORDS); + } + + return point; + } + + /** + * Parse array of points + * + * Array can take two forms: "[1, 2]", "1, 2]" + * Have a look at {@link org.opensearch.index.mapper.AbstractPointGeometryFieldMapper#parse(ParseContext)} + * + */ + public Point parseFromArray(XContentParser parser, boolean ignoreZValue, String fieldName) throws IOException { + double x = Double.NaN; + double y = Double.NaN; + + if (parser.currentToken() == XContentParser.Token.START_ARRAY) { + parser.nextToken(); + } + + int element = 0; + while (parser.currentToken() != XContentParser.Token.END_ARRAY) { + if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) { + element++; + if (element == 1) { + x = parser.doubleValue(); + } else if (element == 2) { + y = parser.doubleValue(); + } else if (element == 3) { + GeoPoint.assertZValue(ignoreZValue, parser.doubleValue()); + } else { + throw new OpenSearchParseException("[{}] field type does not accept > 3 dimensions", fieldName); + } + } else { + throw new OpenSearchParseException("numeric value is expected"); + } + parser.nextToken(); + } + if (element < 2) { + throw new OpenSearchParseException("[{}] field type should have at least two dimensions", fieldName); + } + return new Point(x, y); + } +} diff --git a/server/src/main/java/org/opensearch/index/mapper/AbstractPointGeometryFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/AbstractPointGeometryFieldMapper.java index b546eeca1ec0a..82919f99cc8ac 100644 --- a/server/src/main/java/org/opensearch/index/mapper/AbstractPointGeometryFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/AbstractPointGeometryFieldMapper.java @@ -32,11 +32,9 @@ package org.opensearch.index.mapper; import org.apache.lucene.document.FieldType; -import org.opensearch.OpenSearchParseException; import org.opensearch.common.CheckedBiFunction; import org.opensearch.common.Explicit; import org.opensearch.common.ParseField; -import org.opensearch.common.geo.GeoPoint; import org.opensearch.common.geo.GeometryFormat; import org.opensearch.common.geo.GeometryParser; import org.opensearch.common.xcontent.LoggingDeprecationHandler; @@ -279,41 +277,34 @@ private P process(P in) { return in; } + /** + * Parse array of points, null value and single point + * + * The {@link #objectParser} should assume a content as an array if current token is either START_ARRAY or VALUE_NUMBER + * For example, if a content is a list of array like [[1, 2], [3, 4]], {@link #objectParser} will see complete + * array format each time: "[1, 2]" and "[3, 4]". If a content is a single array like [1, 2], {@link #objectParser} + * will see partial form of array "1, 2]" without a START_ARRAY token. + * + */ @Override public List

parse(XContentParser parser) throws IOException, ParseException { - - if (parser.currentToken() == XContentParser.Token.START_ARRAY) { - XContentParser.Token token = parser.nextToken(); - P point = pointSupplier.get(); + if (parser.currentToken() == XContentParser.Token.START_ARRAY && parser.nextToken() != XContentParser.Token.VALUE_NUMBER) { + // Array of points ArrayList

points = new ArrayList<>(); - if (token == XContentParser.Token.VALUE_NUMBER) { - double x = parser.doubleValue(); + while (parser.currentToken() != XContentParser.Token.END_ARRAY) { + points.add(process(objectParser.apply(parser, pointSupplier.get()))); parser.nextToken(); - double y = parser.doubleValue(); - token = parser.nextToken(); - if (token == XContentParser.Token.VALUE_NUMBER) { - GeoPoint.assertZValue(ignoreZValue, parser.doubleValue()); - } else if (token != XContentParser.Token.END_ARRAY) { - throw new OpenSearchParseException("field type does not accept > 3 dimensions"); - } - - point.resetCoords(x, y); - points.add(process(point)); - } else { - while (token != XContentParser.Token.END_ARRAY) { - points.add(process(objectParser.apply(parser, point))); - point = pointSupplier.get(); - token = parser.nextToken(); - } } return points; } else if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { + // Null value if (nullValue == null) { return null; } else { return Collections.singletonList(nullValue); } } else { + // Single point return Collections.singletonList(process(objectParser.apply(parser, pointSupplier.get()))); } } diff --git a/server/src/test/java/org/opensearch/common/geo/PointParserTests.java b/server/src/test/java/org/opensearch/common/geo/PointParserTests.java new file mode 100644 index 0000000000000..dd90ed7636612 --- /dev/null +++ b/server/src/test/java/org/opensearch/common/geo/PointParserTests.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.common.geo; + +import org.opensearch.OpenSearchParseException; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.geometry.Point; +import org.opensearch.test.OpenSearchTestCase; + +import java.io.IOException; + +public class PointParserTests extends OpenSearchTestCase { + private static final PointParser PARSER; + private static final PointParser PARSER_WITHOUT_GEOHASH; + private static final String FIELD_NAME_X = "x"; + private static final String FIELD_NAME_Y = "y"; + private static final String FN_GEOHASH = "geohash"; + private static final String FN_GEOJSON_TYPE = "type"; + private static final String FV_GEOJSON_TYPE_POINT = "Point"; + private static final String FN_GEOJSON_COORDS = "coordinates"; + + private static final String FN_IN_ERR_MSG = "test error message"; + + static { + PARSER = new PointParser(FIELD_NAME_X, FIELD_NAME_Y, true); + PARSER_WITHOUT_GEOHASH = new PointParser(FIELD_NAME_X, FIELD_NAME_Y, false); + } + + public void testParseFromObjectOfBasicForm() throws IOException { + double x = 74.00; + double y = 40.71; + XContentBuilder pointInJson = XContentFactory.jsonBuilder().startObject().field(FIELD_NAME_X, x).field(FIELD_NAME_Y, y).endObject(); + try (XContentParser parser = createParser(pointInJson)) { + parser.nextToken(); + Point point = PARSER.parseObject(parser, true, GeoUtils.EffectivePoint.BOTTOM_LEFT); + assertEquals(x, point.getX(), 0.001); + assertEquals(y, point.getY(), 0.001); + } + } + + public void testParseFromObjectOfBasicFormMissingValue() throws IOException { + double x = 74.00; + XContentBuilder pointInJson = XContentFactory.jsonBuilder().startObject().field(FIELD_NAME_X, x).endObject(); + try (XContentParser parser = createParser(pointInJson)) { + parser.nextToken(); + Point point = PARSER.parseObject(parser, true, GeoUtils.EffectivePoint.BOTTOM_LEFT); + assertTrue("Expect an exception", false); + } catch (OpenSearchParseException e) { + // Expected exception + } + } + + public void testParseFromObjectOfBasicFormAdditionalField() throws IOException { + double x = 74.00; + double y = 40.71; + XContentBuilder pointInJson = XContentFactory.jsonBuilder() + .startObject() + .field(FIELD_NAME_X, x) + .field(FIELD_NAME_Y, y) + .field("InvalidField", 10.1) + .endObject(); + try (XContentParser parser = createParser(pointInJson)) { + parser.nextToken(); + Point point = PARSER.parseObject(parser, true, GeoUtils.EffectivePoint.BOTTOM_LEFT); + assertTrue("Expect an exception", false); + } catch (OpenSearchParseException e) { + // Expected exception + } + } + + public void testParseFromObjectOfGeoHash() throws IOException { + double longitude = 74.00; + double latitude = 40.71; + String geohash = "txhxegj0uyp3"; + XContentBuilder pointInJson = XContentFactory.jsonBuilder().startObject().field(FN_GEOHASH, geohash).endObject(); + try (XContentParser parser = createParser(pointInJson)) { + parser.nextToken(); + Point point = PARSER.parseObject(parser, true, GeoUtils.EffectivePoint.BOTTOM_LEFT); + assertEquals(longitude, point.getLon(), 0.001); + assertEquals(latitude, point.getLat(), 0.001); + } + } + + public void testParseFromObjectOfGeoHashWithNoGeoHashSupport() throws IOException { + String geohash = "txhxegj0uyp3"; + XContentBuilder pointInJson = XContentFactory.jsonBuilder().startObject().field(FN_GEOHASH, geohash).endObject(); + try (XContentParser parser = createParser(pointInJson)) { + parser.nextToken(); + PARSER_WITHOUT_GEOHASH.parseObject(parser, true, GeoUtils.EffectivePoint.BOTTOM_LEFT); + assertTrue("Expected an exception", false); + } catch (OpenSearchParseException e) { + // Expected exception + } + } + + public void testParseFromObjectOfGeoJson() throws IOException { + double x = 41.12; + double y = -71.34; + XContentBuilder pointInJson = XContentFactory.jsonBuilder() + .startObject() + .field(FN_GEOJSON_TYPE, FV_GEOJSON_TYPE_POINT) + .array(FN_GEOJSON_COORDS, new double[] { x, y }) + .endObject(); + try (XContentParser parser = createParser(pointInJson)) { + parser.nextToken(); + Point point = PARSER_WITHOUT_GEOHASH.parseObject(parser, true, GeoUtils.EffectivePoint.BOTTOM_LEFT); + assertEquals(x, point.getX(), 0.001); + assertEquals(y, point.getY(), 0.001); + } + } + + public void testParseFromArrayOne() throws IOException { + double x = 41.12; + XContentBuilder pointArray = XContentFactory.jsonBuilder().startArray().value(x).endArray(); + try (XContentParser parser = createParser(pointArray)) { + parser.nextToken(); + // parser -> "[41.12]" + Point point = PARSER.parseFromArray(parser, true, FN_IN_ERR_MSG); + assertTrue("Expected an exception", false); + } catch (OpenSearchParseException e) { + // Expected exception + } + } + + public void testParseFromArrayTwo() throws IOException { + double x = 41.12; + double y = -71.34; + XContentBuilder pointArray = XContentFactory.jsonBuilder().startArray().value(x).value(y).endArray(); + try (XContentParser parser = createParser(pointArray)) { + parser.nextToken(); + // parser -> "[41.12, -71.34]" + Point point = PARSER.parseFromArray(parser, true, FN_IN_ERR_MSG); + assertEquals(x, point.getX(), 0.001); + assertEquals(y, point.getY(), 0.001); + } + } + + public void testParseFromArrayThree() throws IOException { + double x = 41.12; + double y = -71.34; + double z = 12.12; + XContentBuilder pointArray = XContentFactory.jsonBuilder().startArray().value(x).value(y).value(z).endArray(); + try (XContentParser parser = createParser(pointArray)) { + parser.nextToken(); + // parser -> "[41.12, -71.34, 12.12]" + Point point = PARSER.parseFromArray(parser, false, FN_IN_ERR_MSG); + assertTrue("Expected an exception", false); + } catch (OpenSearchParseException e) { + // Expected exception + } + } + + public void testParseFromArrayThreeIgnore() throws IOException { + double x = 41.12; + double y = -71.34; + double z = 12.12; + XContentBuilder pointArray = XContentFactory.jsonBuilder().startArray().value(x).value(y).value(z).endArray(); + try (XContentParser parser = createParser(pointArray)) { + parser.nextToken(); + // parser -> "[41.12, -71.34, 12.12]" + Point point = PARSER.parseFromArray(parser, true, FN_IN_ERR_MSG); + assertEquals(x, point.getX(), 0.001); + assertEquals(y, point.getY(), 0.001); + } + } + + public void testParseFromArrayFourIgnore() throws IOException { + double x = 41.12; + double y = -71.34; + double z = 12.12; + double a = 33.12; + XContentBuilder pointArray = XContentFactory.jsonBuilder().startArray().value(x).value(y).value(z).value(a).endArray(); + try (XContentParser parser = createParser(pointArray)) { + parser.nextToken(); + // parser -> "[41.12, -71.34, 12.12, 33.12]" + Point point = PARSER.parseFromArray(parser, false, FN_IN_ERR_MSG); + assertTrue("Expected an exception", false); + } catch (OpenSearchParseException e) { + // Expected exception + } + } + + // "41.12, -71.34]" + public void testParseFromArrayPartialForm() throws IOException { + double x = 41.12; + double y = -71.34; + XContentBuilder pointArray = XContentFactory.jsonBuilder().startArray().value(x).value(y).endArray(); + try (XContentParser parser = createParser(pointArray)) { + parser.nextToken(); + parser.nextToken(); + // parser -> "41.12, -71.34]" + Point point = PARSER.parseFromArray(parser, true, FN_IN_ERR_MSG); + assertEquals(x, point.getX(), 0.001); + assertEquals(y, point.getY(), 0.001); + } + } +}