-
Notifications
You must be signed in to change notification settings - Fork 49
Add geojson support for XYPoint #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ | |
|
|
||
| import java.io.IOException; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| import org.opensearch.OpenSearchParseException; | ||
|
|
@@ -15,13 +17,18 @@ | |
| import org.opensearch.common.xcontent.XContentParser; | ||
| import org.opensearch.common.xcontent.XContentSubParser; | ||
| import org.opensearch.common.xcontent.support.MapXContentParser; | ||
| import org.opensearch.geometry.ShapeType; | ||
|
|
||
| /** | ||
| * Parse the value and set XYPoint represented as a String, Object, WKT, array. | ||
| */ | ||
| public class XYPointParser { | ||
| private static final String ERR_MSG_INVALID_TOKEN = "token [{}] not allowed"; | ||
| private static final String ERR_MSG_INVALID_FIELDS = "field must be either [x|y], or [type|coordinates]"; | ||
| private static final String X_PARAMETER = "x"; | ||
| private static final String Y_PARAMETER = "y"; | ||
| public static final String GEOJSON_TYPE = "type"; | ||
| public static final String GEOJSON_COORDS = "coordinates"; | ||
| private static final String NULL_VALUE_PARAMETER = "null_value"; | ||
| private static final Boolean TRUE = true; | ||
|
|
||
|
|
@@ -33,7 +40,7 @@ public class XYPointParser { | |
| * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse | ||
| * @throws OpenSearchParseException | ||
| */ | ||
| public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) throws OpenSearchParseException { | ||
| public static XYPoint parseXYPoint(final Object value, final boolean ignoreZValue) throws OpenSearchParseException { | ||
| Objects.requireNonNull(value, "input value which needs to be parsed should not be null"); | ||
|
|
||
| try ( | ||
|
|
@@ -54,13 +61,13 @@ public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) thr | |
| } | ||
|
|
||
| /** | ||
| * Parse the values to set the XYPoint which was represented as a String, Object, WKT or an array. | ||
| * | ||
| * Parse the values to set the XYPoint which was represented as a String, Object, WKT, Array, or GeoJson. | ||
| * <ul> | ||
| * <li> String: "100.35, -200.54" </li> | ||
| * <li> Object: {"x" : 100.35, "y" : -200.54} </li> | ||
| * <li> WKT: "POINT (-200.54 100.35)"</li> | ||
| * <li> Array: [ -200.54, 100.35 ]</li> | ||
| * <li>Object: <pre>{@code {"x": <x>, "y": <y}}</pre></li> | ||
| * <li>String: <pre>{@code "<x>,<y>"}</pre></li> | ||
| * <li>WKT: <pre>{@code "POINT (<x> <y>)"}</pre></li> | ||
| * <li>Array: <pre>{@code [<x>, <y>]}</pre></li> | ||
| * <li>GeoJson: <pre>{@code {"type": "Point", "coordinates": [<x>, <y>]}}</pre><li> | ||
| * </ul> | ||
| * | ||
| * @param parser {@link XContentParser} to parse the value from | ||
|
|
@@ -69,103 +76,207 @@ public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) thr | |
| * @throws IOException | ||
| * @throws OpenSearchParseException | ||
| */ | ||
| public static XYPoint parseXYPoint(XContentParser parser, final boolean ignoreZValue) throws IOException, OpenSearchParseException { | ||
| public static XYPoint parseXYPoint(final XContentParser parser, final boolean ignoreZValue) throws IOException, | ||
| OpenSearchParseException { | ||
| Objects.requireNonNull(parser, "parser should not be null"); | ||
|
|
||
| XYPoint point = new XYPoint(); | ||
| double x = Double.NaN; | ||
| double y = Double.NaN; | ||
|
|
||
| if (parser.currentToken() == XContentParser.Token.START_OBJECT) { | ||
| try (XContentSubParser subParser = new XContentSubParser(parser)) { | ||
| while (subParser.nextToken() != XContentParser.Token.END_OBJECT) { | ||
| if (subParser.currentToken() != XContentParser.Token.FIELD_NAME) { | ||
| throw new OpenSearchParseException("token [{}] not allowed", subParser.currentToken()); | ||
| } | ||
| String field = subParser.currentName(); | ||
| if (!(X_PARAMETER.equals(field) || Y_PARAMETER.equals(field))) { | ||
| throw new OpenSearchParseException("field must be either [{}] or [{}]", X_PARAMETER, Y_PARAMETER); | ||
| } | ||
| if (X_PARAMETER.equals(field)) { | ||
| subParser.nextToken(); | ||
| switch (subParser.currentToken()) { | ||
| case VALUE_NUMBER: | ||
| case VALUE_STRING: | ||
| try { | ||
| x = subParser.doubleValue(TRUE); | ||
| } catch (NumberFormatException numberFormatException) { | ||
| throw new OpenSearchParseException("[x] must be valid double value", numberFormatException); | ||
| } | ||
| break; | ||
| default: | ||
| throw new OpenSearchParseException("[x] must be a number"); | ||
| } | ||
| } | ||
| if (Y_PARAMETER.equals(field)) { | ||
| subParser.nextToken(); | ||
| switch (subParser.currentToken()) { | ||
| case VALUE_NUMBER: | ||
| case VALUE_STRING: | ||
| try { | ||
| y = subParser.doubleValue(TRUE); | ||
| } catch (NumberFormatException numberFormatException) { | ||
| throw new OpenSearchParseException("[y] must be valid double value", numberFormatException); | ||
| } | ||
| break; | ||
| default: | ||
| throw new OpenSearchParseException("[y] must be a number"); | ||
| } | ||
| } | ||
| } | ||
| switch (parser.currentToken()) { | ||
| case START_OBJECT: | ||
| parseXYPointObject(parser, point, ignoreZValue); | ||
| break; | ||
| case START_ARRAY: | ||
| parseXYPointArray(parser, point, ignoreZValue); | ||
| break; | ||
| case VALUE_STRING: | ||
| String val = parser.text(); | ||
| point.resetFromString(val, ignoreZValue); | ||
| break; | ||
| default: | ||
| throw new OpenSearchParseException("expecting xy_point as an array, a string, or an object format"); | ||
| } | ||
| return point; | ||
| } | ||
|
|
||
| /** | ||
| * Parse point in either basic object format or GeoJson format | ||
| * | ||
| * Parser is expected to be pointing the start of the object. | ||
| * ex) Parser is pointing { in {"x": 12.3, "y": 45.6} | ||
| * | ||
| * @param parser {@link XContentParser} to parse the value from | ||
| * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse | ||
| * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse | ||
| * @throws IOException | ||
| */ | ||
| private static XYPoint parseXYPointObject(final XContentParser parser, final XYPoint point, final boolean ignoreZValue) | ||
| throws IOException { | ||
| try (XContentSubParser subParser = new XContentSubParser(parser)) { | ||
| if (subParser.nextToken() != XContentParser.Token.FIELD_NAME) { | ||
| throw new OpenSearchParseException(ERR_MSG_INVALID_TOKEN, subParser.currentToken()); | ||
| } | ||
| if (Double.isNaN(x)) { | ||
| throw new OpenSearchParseException("field [{}] missing", X_PARAMETER); | ||
|
|
||
| String field = subParser.currentName(); | ||
| if (X_PARAMETER.equals(field) || Y_PARAMETER.equals(field)) { | ||
| parseXYPointObjectBasicFields(subParser, point); | ||
| } else if (GEOJSON_TYPE.equals(field) || GEOJSON_COORDS.equals(field)) { | ||
| parseGeoJsonFields(subParser, point, ignoreZValue); | ||
| } else { | ||
| throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS); | ||
| } | ||
| if (Double.isNaN(y)) { | ||
| throw new OpenSearchParseException("field [{}] missing", Y_PARAMETER); | ||
|
|
||
| if (subParser.nextToken() != XContentParser.Token.END_OBJECT) { | ||
| throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS); | ||
| } | ||
| return point.reset(x, y); | ||
|
|
||
| return point; | ||
| } | ||
| } | ||
|
|
||
| if (parser.currentToken() == XContentParser.Token.START_ARRAY) { | ||
| return parseXYPointArray(parser, ignoreZValue, x, y); | ||
| /** | ||
| * Parse point in basic object format | ||
| * | ||
| * Parser is expected to be pointing the first field of the object. | ||
| * ex) Parser is pointing x in {"x": 12.3, "y": 45.6} | ||
| * | ||
| * @param parser {@link XContentParser} to parse the value from | ||
| * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse | ||
| * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse | ||
| * @throws IOException | ||
| */ | ||
| private static XYPoint parseXYPointObjectBasicFields(final XContentParser parser, final XYPoint point) throws IOException { | ||
| final int numberOfFields = 2; | ||
| Map<String, Double> data = new HashMap<>(); | ||
| for (int i = 0; i < numberOfFields; i++) { | ||
| if (i != 0) { | ||
| parser.nextToken(); | ||
| } | ||
|
|
||
| if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { | ||
| break; | ||
| } | ||
|
|
||
| String field = parser.currentName(); | ||
| if (X_PARAMETER.equals(field) == false && Y_PARAMETER.equals(field) == false) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume syntax with "==" is for better readability, correct?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. OpenSearch's standard convention is avoiding negation like |
||
| throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS); | ||
| } | ||
| switch (parser.nextToken()) { | ||
| case VALUE_NUMBER: | ||
| case VALUE_STRING: | ||
| try { | ||
| data.put(field, parser.doubleValue(true)); | ||
| } catch (NumberFormatException e) { | ||
| throw new OpenSearchParseException("[{}] and [{}] must be valid double values", e, X_PARAMETER, Y_PARAMETER); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be consistent to create anew constant for error message here?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The message is used only in this method. I don't think it is worth to make is as a constant?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm leaving it up to you, but I would make it a constant anyway. From my experience it helps to avoid creating multiple identical messages with slightly different wording. If all messages are in one place chance is higher that next person will reuse one from the list. |
||
| } | ||
| break; | ||
| default: | ||
| throw new OpenSearchParseException("{} must be a number", field); | ||
| } | ||
| } | ||
|
|
||
| if (parser.currentToken() == XContentParser.Token.VALUE_STRING) { | ||
| String val = parser.text(); | ||
| return point.resetFromString(val, ignoreZValue); | ||
| if (data.get(X_PARAMETER) == null) { | ||
| throw new OpenSearchParseException("field [{}] missing", X_PARAMETER); | ||
| } | ||
| if (data.get(Y_PARAMETER) == null) { | ||
| throw new OpenSearchParseException("field [{}] missing", Y_PARAMETER); | ||
| } | ||
| throw new OpenSearchParseException("Expected xy_point. But, the provided mapping is not of type xy_point"); | ||
|
|
||
| return point.reset(data.get(X_PARAMETER), data.get(Y_PARAMETER)); | ||
| } | ||
|
|
||
| /** | ||
| * Parse the values to set the XYPoint which was represented as an array. | ||
| * Parse point in GeoJson format | ||
| * | ||
| * Parser is expected to be pointing the first field of the object. | ||
| * ex) Parser is pointing type in {"type": "Point", "coordinates": [12.3, 45.6]} | ||
| * | ||
| * @param subParser {@link XContentParser} to parse the values from an array | ||
| * @param parser {@link XContentParser} to parse the value from | ||
| * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse | ||
| * @param ignoreZValue boolean parameter which decides if third coordinate needs to be ignored or not | ||
| * @param x x coordinate that will be set by parsing the value from array | ||
| * @param y y coordinate that will be set by parsing the value from array | ||
| * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse | ||
| * @throws IOException | ||
| */ | ||
| private static XYPoint parseXYPointArray(XContentParser subParser, final boolean ignoreZValue, double x, double y) throws IOException { | ||
| XYPoint point = new XYPoint(); | ||
| int element = 0; | ||
| while (subParser.nextToken() != XContentParser.Token.END_ARRAY) { | ||
| if (subParser.currentToken() != XContentParser.Token.VALUE_NUMBER) { | ||
| throw new OpenSearchParseException("numeric value expected"); | ||
| private static XYPoint parseGeoJsonFields(final XContentParser parser, final XYPoint point, final boolean ignoreZValue) | ||
| throws IOException { | ||
| final int numberOfFields = 2; | ||
| boolean hasTypePoint = false; | ||
| boolean hasCoordinates = false; | ||
| for (int i = 0; i < numberOfFields; i++) { | ||
| if (i != 0) { | ||
| parser.nextToken(); | ||
| } | ||
|
|
||
| if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { | ||
| if (hasTypePoint == false) { | ||
| throw new OpenSearchParseException("field [{}] missing", GEOJSON_TYPE); | ||
| } | ||
| if (hasCoordinates == false) { | ||
| throw new OpenSearchParseException("field [{}] missing", GEOJSON_COORDS); | ||
| } | ||
| } | ||
| element++; | ||
| if (element == 1) { | ||
| x = subParser.doubleValue(); | ||
| } else if (element == 2) { | ||
| y = subParser.doubleValue(); | ||
| } else if (element == 3) { | ||
| XYPoint.assertZValue(ignoreZValue, subParser.doubleValue()); | ||
|
|
||
| if (GEOJSON_TYPE.equals(parser.currentName())) { | ||
| if (parser.nextToken() != XContentParser.Token.VALUE_STRING) { | ||
| throw new OpenSearchParseException("{} must be a string", GEOJSON_TYPE); | ||
| } | ||
|
|
||
| // To be consistent with geo_shape parsing, ignore case here as well. | ||
| if (ShapeType.POINT.name().equalsIgnoreCase(parser.text()) == false) { | ||
| throw new OpenSearchParseException("{} must be Point", GEOJSON_TYPE); | ||
| } | ||
| hasTypePoint = true; | ||
| } else if (GEOJSON_COORDS.equals(parser.currentName())) { | ||
| if (parser.nextToken() != XContentParser.Token.START_ARRAY) { | ||
| throw new OpenSearchParseException("{} must be an array", GEOJSON_COORDS); | ||
| } | ||
| parseXYPointArray(parser, point, ignoreZValue); | ||
| hasCoordinates = true; | ||
| } else { | ||
| throw new OpenSearchParseException("[xy_point] field type does not accept more than 3 dimensions"); | ||
| throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS); | ||
| } | ||
| } | ||
| return point.reset(x, y); | ||
|
|
||
| return point; | ||
| } | ||
|
|
||
| /** | ||
| * Parse point in an array format | ||
| * | ||
| * Parser is expected to be pointing the start of the array. | ||
| * ex) Parser is pointing [ in [12.3, 45.6] | ||
| * | ||
| * @param parser {@link XContentParser} to parse the value from | ||
| * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse | ||
| * @param ignoreZValue boolean parameter which decides if third coordinate needs to be ignored or not | ||
| * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse | ||
| * @throws IOException | ||
| */ | ||
| private static XYPoint parseXYPointArray(final XContentParser parser, final XYPoint point, final boolean ignoreZValue) | ||
heemin32 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| throws IOException { | ||
| try (XContentSubParser subParser = new XContentSubParser(parser)) { | ||
| double x = Double.NaN; | ||
| double y = Double.NaN; | ||
|
|
||
| int element = 0; | ||
| while (subParser.nextToken() != XContentParser.Token.END_ARRAY) { | ||
heemin32 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (parser.currentToken() != XContentParser.Token.VALUE_NUMBER) { | ||
| throw new OpenSearchParseException("numeric value expected"); | ||
| } | ||
| element++; | ||
| if (element == 1) { | ||
| x = parser.doubleValue(); | ||
| } else if (element == 2) { | ||
| y = parser.doubleValue(); | ||
| } else if (element == 3) { | ||
| XYPoint.assertZValue(ignoreZValue, parser.doubleValue()); | ||
| } else { | ||
| throw new OpenSearchParseException("[xy_point] field type does not accept more than 3 values"); | ||
| } | ||
| } | ||
|
|
||
| if (element < 2) { | ||
| throw new OpenSearchParseException("[xy_point] field type should have at least two dimensions"); | ||
| } | ||
| return point.reset(x, y); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's not safe to return user input as part of the output, learned from the last pentest. I suggest minimize error message to just "token not allowed" and add logger with all required details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you share what security risk this might contain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, in my report they have call it "User Input Reflected on Error" and marked it as "Medium" severity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @martin-gaievski . subParser.currentToken() is not random user input. It is enum defined in XContentParser.Token. So, I think there is no security risk returning it.