77
88import java .io .IOException ;
99import java .util .Collections ;
10+ import java .util .HashMap ;
11+ import java .util .Map ;
1012import java .util .Objects ;
1113
1214import org .opensearch .OpenSearchParseException ;
1517import org .opensearch .common .xcontent .XContentParser ;
1618import org .opensearch .common .xcontent .XContentSubParser ;
1719import org .opensearch .common .xcontent .support .MapXContentParser ;
20+ import org .opensearch .geometry .ShapeType ;
1821
1922/**
2023 * Parse the value and set XYPoint represented as a String, Object, WKT, array.
2124 */
2225public class XYPointParser {
26+ private static final String ERR_MSG_INVALID_TOKEN = "token [{}] not allowed" ;
27+ private static final String ERR_MSG_INVALID_FIELDS = "field must be either [x|y], or [type|coordinates]" ;
2328 private static final String X_PARAMETER = "x" ;
2429 private static final String Y_PARAMETER = "y" ;
30+ public static final String GEOJSON_TYPE = "type" ;
31+ public static final String GEOJSON_COORDS = "coordinates" ;
2532 private static final String NULL_VALUE_PARAMETER = "null_value" ;
2633 private static final Boolean TRUE = true ;
2734
@@ -33,7 +40,7 @@ public class XYPointParser {
3340 * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
3441 * @throws OpenSearchParseException
3542 */
36- public static XYPoint parseXYPoint (Object value , final boolean ignoreZValue ) throws OpenSearchParseException {
43+ public static XYPoint parseXYPoint (final Object value , final boolean ignoreZValue ) throws OpenSearchParseException {
3744 Objects .requireNonNull (value , "input value which needs to be parsed should not be null" );
3845
3946 try (
@@ -54,13 +61,13 @@ public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) thr
5461 }
5562
5663 /**
57- * Parse the values to set the XYPoint which was represented as a String, Object, WKT or an array.
58- *
64+ * Parse the values to set the XYPoint which was represented as a String, Object, WKT, Array, or GeoJson.
5965 * <ul>
60- * <li> String: "100.35, -200.54" </li>
61- * <li> Object: {"x" : 100.35, "y" : -200.54} </li>
62- * <li> WKT: "POINT (-200.54 100.35)"</li>
63- * <li> Array: [ -200.54, 100.35 ]</li>
66+ * <li>Object: <pre>{@code {"x": <x>, "y": <y}}</pre></li>
67+ * <li>String: <pre>{@code "<x>,<y>"}</pre></li>
68+ * <li>WKT: <pre>{@code "POINT (<x> <y>)"}</pre></li>
69+ * <li>Array: <pre>{@code [<x>, <y>]}</pre></li>
70+ * <li>GeoJson: <pre>{@code {"type": "Point", "coordinates": [<x>, <y>]}}</pre><li>
6471 * </ul>
6572 *
6673 * @param parser {@link XContentParser} to parse the value from
@@ -69,103 +76,207 @@ public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) thr
6976 * @throws IOException
7077 * @throws OpenSearchParseException
7178 */
72- public static XYPoint parseXYPoint (XContentParser parser , final boolean ignoreZValue ) throws IOException , OpenSearchParseException {
79+ public static XYPoint parseXYPoint (final XContentParser parser , final boolean ignoreZValue ) throws IOException ,
80+ OpenSearchParseException {
7381 Objects .requireNonNull (parser , "parser should not be null" );
74-
7582 XYPoint point = new XYPoint ();
76- double x = Double .NaN ;
77- double y = Double .NaN ;
78-
79- if (parser .currentToken () == XContentParser .Token .START_OBJECT ) {
80- try (XContentSubParser subParser = new XContentSubParser (parser )) {
81- while (subParser .nextToken () != XContentParser .Token .END_OBJECT ) {
82- if (subParser .currentToken () != XContentParser .Token .FIELD_NAME ) {
83- throw new OpenSearchParseException ("token [{}] not allowed" , subParser .currentToken ());
84- }
85- String field = subParser .currentName ();
86- if (!(X_PARAMETER .equals (field ) || Y_PARAMETER .equals (field ))) {
87- throw new OpenSearchParseException ("field must be either [{}] or [{}]" , X_PARAMETER , Y_PARAMETER );
88- }
89- if (X_PARAMETER .equals (field )) {
90- subParser .nextToken ();
91- switch (subParser .currentToken ()) {
92- case VALUE_NUMBER :
93- case VALUE_STRING :
94- try {
95- x = subParser .doubleValue (TRUE );
96- } catch (NumberFormatException numberFormatException ) {
97- throw new OpenSearchParseException ("[x] must be valid double value" , numberFormatException );
98- }
99- break ;
100- default :
101- throw new OpenSearchParseException ("[x] must be a number" );
102- }
103- }
104- if (Y_PARAMETER .equals (field )) {
105- subParser .nextToken ();
106- switch (subParser .currentToken ()) {
107- case VALUE_NUMBER :
108- case VALUE_STRING :
109- try {
110- y = subParser .doubleValue (TRUE );
111- } catch (NumberFormatException numberFormatException ) {
112- throw new OpenSearchParseException ("[y] must be valid double value" , numberFormatException );
113- }
114- break ;
115- default :
116- throw new OpenSearchParseException ("[y] must be a number" );
117- }
118- }
119- }
83+ switch (parser .currentToken ()) {
84+ case START_OBJECT :
85+ parseXYPointObject (parser , point , ignoreZValue );
86+ break ;
87+ case START_ARRAY :
88+ parseXYPointArray (parser , point , ignoreZValue );
89+ break ;
90+ case VALUE_STRING :
91+ String val = parser .text ();
92+ point .resetFromString (val , ignoreZValue );
93+ break ;
94+ default :
95+ throw new OpenSearchParseException ("expecting xy_point as an array, a string, or an object format" );
96+ }
97+ return point ;
98+ }
99+
100+ /**
101+ * Parse point in either basic object format or GeoJson format
102+ *
103+ * Parser is expected to be pointing the start of the object.
104+ * ex) Parser is pointing { in {"x": 12.3, "y": 45.6}
105+ *
106+ * @param parser {@link XContentParser} to parse the value from
107+ * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
108+ * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
109+ * @throws IOException
110+ */
111+ private static XYPoint parseXYPointObject (final XContentParser parser , final XYPoint point , final boolean ignoreZValue )
112+ throws IOException {
113+ try (XContentSubParser subParser = new XContentSubParser (parser )) {
114+ if (subParser .nextToken () != XContentParser .Token .FIELD_NAME ) {
115+ throw new OpenSearchParseException (ERR_MSG_INVALID_TOKEN , subParser .currentToken ());
120116 }
121- if (Double .isNaN (x )) {
122- throw new OpenSearchParseException ("field [{}] missing" , X_PARAMETER );
117+
118+ String field = subParser .currentName ();
119+ if (X_PARAMETER .equals (field ) || Y_PARAMETER .equals (field )) {
120+ parseXYPointObjectBasicFields (subParser , point );
121+ } else if (GEOJSON_TYPE .equals (field ) || GEOJSON_COORDS .equals (field )) {
122+ parseGeoJsonFields (subParser , point , ignoreZValue );
123+ } else {
124+ throw new OpenSearchParseException (ERR_MSG_INVALID_FIELDS );
123125 }
124- if (Double .isNaN (y )) {
125- throw new OpenSearchParseException ("field [{}] missing" , Y_PARAMETER );
126+
127+ if (subParser .nextToken () != XContentParser .Token .END_OBJECT ) {
128+ throw new OpenSearchParseException (ERR_MSG_INVALID_FIELDS );
126129 }
127- return point .reset (x , y );
130+
131+ return point ;
128132 }
133+ }
129134
130- if (parser .currentToken () == XContentParser .Token .START_ARRAY ) {
131- return parseXYPointArray (parser , ignoreZValue , x , y );
135+ /**
136+ * Parse point in basic object format
137+ *
138+ * Parser is expected to be pointing the first field of the object.
139+ * ex) Parser is pointing x in {"x": 12.3, "y": 45.6}
140+ *
141+ * @param parser {@link XContentParser} to parse the value from
142+ * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
143+ * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
144+ * @throws IOException
145+ */
146+ private static XYPoint parseXYPointObjectBasicFields (final XContentParser parser , final XYPoint point ) throws IOException {
147+ final int numberOfFields = 2 ;
148+ Map <String , Double > data = new HashMap <>();
149+ for (int i = 0 ; i < numberOfFields ; i ++) {
150+ if (i != 0 ) {
151+ parser .nextToken ();
152+ }
153+
154+ if (parser .currentToken () != XContentParser .Token .FIELD_NAME ) {
155+ break ;
156+ }
157+
158+ String field = parser .currentName ();
159+ if (X_PARAMETER .equals (field ) == false && Y_PARAMETER .equals (field ) == false ) {
160+ throw new OpenSearchParseException (ERR_MSG_INVALID_FIELDS );
161+ }
162+ switch (parser .nextToken ()) {
163+ case VALUE_NUMBER :
164+ case VALUE_STRING :
165+ try {
166+ data .put (field , parser .doubleValue (true ));
167+ } catch (NumberFormatException e ) {
168+ throw new OpenSearchParseException ("[{}] and [{}] must be valid double values" , e , X_PARAMETER , Y_PARAMETER );
169+ }
170+ break ;
171+ default :
172+ throw new OpenSearchParseException ("{} must be a number" , field );
173+ }
132174 }
133175
134- if (parser .currentToken () == XContentParser .Token .VALUE_STRING ) {
135- String val = parser .text ();
136- return point .resetFromString (val , ignoreZValue );
176+ if (data .get (X_PARAMETER ) == null ) {
177+ throw new OpenSearchParseException ("field [{}] missing" , X_PARAMETER );
178+ }
179+ if (data .get (Y_PARAMETER ) == null ) {
180+ throw new OpenSearchParseException ("field [{}] missing" , Y_PARAMETER );
137181 }
138- throw new OpenSearchParseException ("Expected xy_point. But, the provided mapping is not of type xy_point" );
182+
183+ return point .reset (data .get (X_PARAMETER ), data .get (Y_PARAMETER ));
139184 }
140185
141186 /**
142- * Parse the values to set the XYPoint which was represented as an array.
187+ * Parse point in GeoJson format
188+ *
189+ * Parser is expected to be pointing the first field of the object.
190+ * ex) Parser is pointing type in {"type": "Point", "coordinates": [12.3, 45.6]}
143191 *
144- * @param subParser {@link XContentParser} to parse the values from an array
192+ * @param parser {@link XContentParser} to parse the value from
193+ * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
145194 * @param ignoreZValue boolean parameter which decides if third coordinate needs to be ignored or not
146- * @param x x coordinate that will be set by parsing the value from array
147- * @param y y coordinate that will be set by parsing the value from array
148195 * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
149196 * @throws IOException
150197 */
151- private static XYPoint parseXYPointArray (XContentParser subParser , final boolean ignoreZValue , double x , double y ) throws IOException {
152- XYPoint point = new XYPoint ();
153- int element = 0 ;
154- while (subParser .nextToken () != XContentParser .Token .END_ARRAY ) {
155- if (subParser .currentToken () != XContentParser .Token .VALUE_NUMBER ) {
156- throw new OpenSearchParseException ("numeric value expected" );
198+ private static XYPoint parseGeoJsonFields (final XContentParser parser , final XYPoint point , final boolean ignoreZValue )
199+ throws IOException {
200+ final int numberOfFields = 2 ;
201+ boolean hasTypePoint = false ;
202+ boolean hasCoordinates = false ;
203+ for (int i = 0 ; i < numberOfFields ; i ++) {
204+ if (i != 0 ) {
205+ parser .nextToken ();
206+ }
207+
208+ if (parser .currentToken () != XContentParser .Token .FIELD_NAME ) {
209+ if (hasTypePoint == false ) {
210+ throw new OpenSearchParseException ("field [{}] missing" , GEOJSON_TYPE );
211+ }
212+ if (hasCoordinates == false ) {
213+ throw new OpenSearchParseException ("field [{}] missing" , GEOJSON_COORDS );
214+ }
157215 }
158- element ++;
159- if (element == 1 ) {
160- x = subParser .doubleValue ();
161- } else if (element == 2 ) {
162- y = subParser .doubleValue ();
163- } else if (element == 3 ) {
164- XYPoint .assertZValue (ignoreZValue , subParser .doubleValue ());
216+
217+ if (GEOJSON_TYPE .equals (parser .currentName ())) {
218+ if (parser .nextToken () != XContentParser .Token .VALUE_STRING ) {
219+ throw new OpenSearchParseException ("{} must be a string" , GEOJSON_TYPE );
220+ }
221+
222+ // To be consistent with geo_shape parsing, ignore case here as well.
223+ if (ShapeType .POINT .name ().equalsIgnoreCase (parser .text ()) == false ) {
224+ throw new OpenSearchParseException ("{} must be Point" , GEOJSON_TYPE );
225+ }
226+ hasTypePoint = true ;
227+ } else if (GEOJSON_COORDS .equals (parser .currentName ())) {
228+ if (parser .nextToken () != XContentParser .Token .START_ARRAY ) {
229+ throw new OpenSearchParseException ("{} must be an array" , GEOJSON_COORDS );
230+ }
231+ parseXYPointArray (parser , point , ignoreZValue );
232+ hasCoordinates = true ;
165233 } else {
166- throw new OpenSearchParseException ("[xy_point] field type does not accept more than 3 dimensions" );
234+ throw new OpenSearchParseException (ERR_MSG_INVALID_FIELDS );
167235 }
168236 }
169- return point .reset (x , y );
237+
238+ return point ;
239+ }
240+
241+ /**
242+ * Parse point in an array format
243+ *
244+ * Parser is expected to be pointing the start of the array.
245+ * ex) Parser is pointing [ in [12.3, 45.6]
246+ *
247+ * @param parser {@link XContentParser} to parse the value from
248+ * @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
249+ * @param ignoreZValue boolean parameter which decides if third coordinate needs to be ignored or not
250+ * @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
251+ * @throws IOException
252+ */
253+ private static XYPoint parseXYPointArray (final XContentParser parser , final XYPoint point , final boolean ignoreZValue )
254+ throws IOException {
255+ try (XContentSubParser subParser = new XContentSubParser (parser )) {
256+ double x = Double .NaN ;
257+ double y = Double .NaN ;
258+
259+ int element = 0 ;
260+ while (subParser .nextToken () != XContentParser .Token .END_ARRAY ) {
261+ if (parser .currentToken () != XContentParser .Token .VALUE_NUMBER ) {
262+ throw new OpenSearchParseException ("numeric value expected" );
263+ }
264+ element ++;
265+ if (element == 1 ) {
266+ x = parser .doubleValue ();
267+ } else if (element == 2 ) {
268+ y = parser .doubleValue ();
269+ } else if (element == 3 ) {
270+ XYPoint .assertZValue (ignoreZValue , parser .doubleValue ());
271+ } else {
272+ throw new OpenSearchParseException ("[xy_point] field type does not accept more than 3 values" );
273+ }
274+ }
275+
276+ if (element < 2 ) {
277+ throw new OpenSearchParseException ("[xy_point] field type should have at least two dimensions" );
278+ }
279+ return point .reset (x , y );
280+ }
170281 }
171282}
0 commit comments