5353import java .util .Collections ;
5454import java .util .List ;
5555import java .util .Locale ;
56+ import java .util .Stack ;
5657
5758/**
5859 * Utility class for converting to and from WKT
@@ -278,6 +279,16 @@ public Geometry fromWKT(String wkt) throws IOException, ParseException {
278279 */
279280 private Geometry parseGeometry (StreamTokenizer stream ) throws IOException , ParseException {
280281 final String type = nextWord (stream ).toLowerCase (Locale .ROOT );
282+ switch (type ) {
283+ case "geometrycollection" :
284+ return parseGeometryCollection (stream );
285+ default :
286+ return parseSimpleGeometry (stream , type );
287+ }
288+ }
289+
290+ private Geometry parseSimpleGeometry (StreamTokenizer stream , String type ) throws IOException , ParseException {
291+ assert "geometrycollection" .equals (type ) == false ;
281292 switch (type ) {
282293 case "point" :
283294 return parsePoint (stream );
@@ -294,23 +305,67 @@ private Geometry parseGeometry(StreamTokenizer stream) throws IOException, Parse
294305 case "bbox" :
295306 return parseBBox (stream );
296307 case "geometrycollection" :
297- return parseGeometryCollection ( stream );
308+ throw new IllegalStateException ( "Unexpected type: geometrycollection" );
298309 case "circle" : // Not part of the standard, but we need it for internal serialization
299310 return parseCircle (stream );
300311 }
301312 throw new IllegalArgumentException ("Unknown geometry type: " + type );
302313 }
303314
315+ /**
316+ * Parse geometry collection iteratively
317+ *
318+ * Parsing geometry collection recursively can lead to StackOverflowError when there is a deeply nested structure of GeometryCollection
319+ */
304320 private GeometryCollection <Geometry > parseGeometryCollection (StreamTokenizer stream ) throws IOException , ParseException {
305321 if (nextEmptyOrOpen (stream ).equals (EMPTY )) {
306322 return GeometryCollection .EMPTY ;
307323 }
308- List <Geometry > shapes = new ArrayList <>();
309- shapes .add (parseGeometry (stream ));
310- while (nextCloserOrComma (stream ).equals (COMMA )) {
311- shapes .add (parseGeometry (stream ));
324+
325+ List <Geometry > topLevelShapes = new ArrayList <>();
326+ Stack <List <Geometry >> stack = new Stack <>();
327+ stack .push (topLevelShapes );
328+ boolean isFirstIteration = true ;
329+ List <Geometry > currentLevelShapes = null ;
330+ while (!stack .isEmpty ()) {
331+ List <Geometry > previousShapes = stack .pop ();
332+ if (currentLevelShapes != null ) {
333+ previousShapes .add (new GeometryCollection <>(currentLevelShapes ));
334+ }
335+ currentLevelShapes = previousShapes ;
336+
337+ if (isFirstIteration == true ) {
338+ isFirstIteration = false ;
339+ } else {
340+ if (!nextCloserOrComma (stream ).equals (COMMA )) {
341+ // Done with current level, continue with parent level
342+ continue ;
343+ }
344+ }
345+ while (true ) {
346+ final String type = nextWord (stream ).toLowerCase (Locale .ROOT );
347+ switch (type ) {
348+ case "geometrycollection" :
349+ if (nextEmptyOrOpen (stream ).equals (EMPTY )) {
350+ currentLevelShapes .add (GeometryCollection .EMPTY );
351+ break ;
352+ } else {
353+ stack .push (currentLevelShapes );
354+ currentLevelShapes = new ArrayList <>();
355+ continue ;
356+ }
357+ default :
358+ currentLevelShapes .add (parseSimpleGeometry (stream , type ));
359+ break ;
360+ }
361+
362+ if (!nextCloserOrComma (stream ).equals (COMMA )) {
363+ break ;
364+ }
365+ }
312366 }
313- return new GeometryCollection <>(shapes );
367+
368+ return new GeometryCollection <>(topLevelShapes );
314369 }
315370
316371 private Point parsePoint (StreamTokenizer stream ) throws IOException , ParseException {
0 commit comments