49
49
import java .io .StreamTokenizer ;
50
50
import java .io .StringReader ;
51
51
import java .text .ParseException ;
52
+ import java .util .ArrayDeque ;
52
53
import java .util .ArrayList ;
53
54
import java .util .Collections ;
55
+ import java .util .Deque ;
54
56
import java .util .List ;
55
57
import java .util .Locale ;
56
58
@@ -67,6 +69,7 @@ public class WellKnownText {
67
69
public static final String RPAREN = ")" ;
68
70
public static final String COMMA = "," ;
69
71
public static final String NAN = "NaN" ;
72
+ public static final int MAX_DEPTH_OF_GEO_COLLECTION = 1000 ;
70
73
71
74
private final String NUMBER = "<NUMBER>" ;
72
75
private final String EOF = "END-OF-STREAM" ;
@@ -278,6 +281,16 @@ public Geometry fromWKT(String wkt) throws IOException, ParseException {
278
281
*/
279
282
private Geometry parseGeometry (StreamTokenizer stream ) throws IOException , ParseException {
280
283
final String type = nextWord (stream ).toLowerCase (Locale .ROOT );
284
+ switch (type ) {
285
+ case "geometrycollection" :
286
+ return parseGeometryCollection (stream );
287
+ default :
288
+ return parseSimpleGeometry (stream , type );
289
+ }
290
+ }
291
+
292
+ private Geometry parseSimpleGeometry (StreamTokenizer stream , String type ) throws IOException , ParseException {
293
+ assert "geometrycollection" .equals (type ) == false ;
281
294
switch (type ) {
282
295
case "point" :
283
296
return parsePoint (stream );
@@ -294,23 +307,72 @@ private Geometry parseGeometry(StreamTokenizer stream) throws IOException, Parse
294
307
case "bbox" :
295
308
return parseBBox (stream );
296
309
case "geometrycollection" :
297
- return parseGeometryCollection ( stream );
310
+ throw new IllegalStateException ( "Unexpected type: geometrycollection" );
298
311
case "circle" : // Not part of the standard, but we need it for internal serialization
299
312
return parseCircle (stream );
300
313
}
301
314
throw new IllegalArgumentException ("Unknown geometry type: " + type );
302
315
}
303
316
317
+ /**
318
+ * Parse geometry collection iteratively
319
+ *
320
+ * Parsing geometry collection recursively can lead to StackOverflowError when there is a deeply nested structure of GeometryCollection
321
+ */
304
322
private GeometryCollection <Geometry > parseGeometryCollection (StreamTokenizer stream ) throws IOException , ParseException {
305
323
if (nextEmptyOrOpen (stream ).equals (EMPTY )) {
306
324
return GeometryCollection .EMPTY ;
307
325
}
308
- List <Geometry > shapes = new ArrayList <>();
309
- shapes .add (parseGeometry (stream ));
310
- while (nextCloserOrComma (stream ).equals (COMMA )) {
311
- shapes .add (parseGeometry (stream ));
326
+
327
+ List <Geometry > topLevelShapes = new ArrayList <>();
328
+ Deque <List <Geometry >> deque = new ArrayDeque <>();
329
+ deque .push (topLevelShapes );
330
+ boolean isFirstIteration = true ;
331
+ List <Geometry > currentLevelShapes = null ;
332
+ while (!deque .isEmpty ()) {
333
+ List <Geometry > previousShapes = deque .pop ();
334
+ if (currentLevelShapes != null ) {
335
+ previousShapes .add (new GeometryCollection <>(currentLevelShapes ));
336
+ }
337
+ currentLevelShapes = previousShapes ;
338
+
339
+ if (isFirstIteration == true ) {
340
+ isFirstIteration = false ;
341
+ } else {
342
+ if (nextCloserOrComma (stream ).equals (COMMA ) == false ) {
343
+ // Done with current level, continue with parent level
344
+ continue ;
345
+ }
346
+ }
347
+ while (true ) {
348
+ final String type = nextWord (stream ).toLowerCase (Locale .ROOT );
349
+ if (type .equals ("geometrycollection" )) {
350
+ if (nextEmptyOrOpen (stream ).equals (EMPTY ) == false ) {
351
+ // GEOMETRYCOLLECTION() -> 1 depth, GEOMETRYCOLLECTION(GEOMETRYCOLLECTION()) -> 2 depth
352
+ // When parsing the top level geometry collection, the queue size is zero.
353
+ // When max depth is 1, we don't want to push any sub geometry collection in the queue.
354
+ // Therefore, we subtract 2 from max depth.
355
+ if (deque .size () >= MAX_DEPTH_OF_GEO_COLLECTION - 2 ) {
356
+ throw new IllegalArgumentException (
357
+ "a geometry collection with a depth greater than " + MAX_DEPTH_OF_GEO_COLLECTION + " is not supported"
358
+ );
359
+ }
360
+ deque .push (currentLevelShapes );
361
+ currentLevelShapes = new ArrayList <>();
362
+ continue ;
363
+ }
364
+ currentLevelShapes .add (GeometryCollection .EMPTY );
365
+ } else {
366
+ currentLevelShapes .add (parseSimpleGeometry (stream , type ));
367
+ }
368
+
369
+ if (nextCloserOrComma (stream ).equals (COMMA ) == false ) {
370
+ break ;
371
+ }
372
+ }
312
373
}
313
- return new GeometryCollection <>(shapes );
374
+
375
+ return new GeometryCollection <>(topLevelShapes );
314
376
}
315
377
316
378
private Point parsePoint (StreamTokenizer stream ) throws IOException , ParseException {
0 commit comments