Skip to content

Commit 476ab8e

Browse files
committed
HTM-1852: Fix GeoTools blocking (WFS) DescribeFeatureType requests
* Initialise GeoTools on application startup and set our preferred entity resolver * Add an entity resolver that explicity allows DescribeFeatureType (dynamic schema) requests to work around https://osgeo-org.atlassian.net/browse/GEOT-7916
1 parent 249ff57 commit 476ab8e

4 files changed

Lines changed: 149 additions & 2 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (C) 2026 B3Partners B.V.
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
package org.tailormap.api.configuration;
7+
8+
import jakarta.annotation.PostConstruct;
9+
import java.lang.invoke.MethodHandles;
10+
import org.geotools.util.factory.GeoTools;
11+
import org.geotools.util.factory.Hints;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
import org.springframework.context.annotation.Configuration;
15+
import org.tailormap.api.geotools.TMPreventLocalEntityResolver;
16+
17+
@Configuration
18+
public class GeoToolsConfiguration {
19+
private static final Logger logger =
20+
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
21+
22+
@PostConstruct
23+
public void init() {
24+
GeoTools.init(new Hints(Hints.ENTITY_RESOLVER, TMPreventLocalEntityResolver.INSTANCE));
25+
if (logger.isTraceEnabled()) {
26+
logger.trace("GeoTools initialised: {}", GeoTools.getAboutInfo());
27+
logger.trace("GeoTools default hints: {}", GeoTools.getDefaultHints());
28+
}
29+
}
30+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2026 B3Partners B.V.
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
package org.tailormap.api.geotools;
7+
8+
import java.io.IOException;
9+
import org.geotools.util.PreventLocalEntityResolver;
10+
import org.xml.sax.InputSource;
11+
import org.xml.sax.SAXException;
12+
13+
/**
14+
* An entity resolver that extends the {@link PreventLocalEntityResolver} to allow certain DescribeFeatureType requests
15+
* commonly produced by GeoServer.
16+
*
17+
* @see <a href="https://osgeo-org.atlassian.net/browse/GEOT-7916">GEOT-7916</a>
18+
*/
19+
public class TMPreventLocalEntityResolver extends PreventLocalEntityResolver {
20+
public static final TMPreventLocalEntityResolver INSTANCE = new TMPreventLocalEntityResolver();
21+
22+
private static final java.util.regex.Pattern DESCRIBE_FEATURE_TYPE_URL = java.util.regex.Pattern.compile(
23+
"^https?://[^?#;]*\\?(?:[^#;]*[&;])?request=DescribeFeatureType(?:[&;]|$).*",
24+
java.util.regex.Pattern.CASE_INSENSITIVE);
25+
26+
/**
27+
* Next to what the base class allows, explicitly allow (dynamic) {@code DescribeFeatureType} requests such as
28+
* {@code https://service.pdok.nl/kadaster/brk-bestuurlijke-gebieden/wfs/v1_0?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAME=bestuurlijkegebieden:Provinciegebied&OUTPUTFORMAT=application%2Fgml%2Bxml%3B%20version%3D3.2}
29+
* commonly produced by GeoServer.
30+
*/
31+
@Override
32+
public InputSource resolveEntity(String name, String publicId, String baseURI, String systemId)
33+
throws SAXException, IOException {
34+
35+
if (systemId != null && DESCRIBE_FEATURE_TYPE_URL.matcher(systemId).matches()) {
36+
return null;
37+
}
38+
return super.resolveEntity(name, publicId, baseURI, systemId);
39+
}
40+
}

src/main/java/org/tailormap/api/geotools/featuresources/WFSFeatureSourceHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import org.geotools.api.data.SimpleFeatureSource;
1515
import org.geotools.data.wfs.WFSDataStoreFactory;
1616
import org.geotools.data.wfs.internal.FeatureTypeInfo;
17-
import org.geotools.util.PreventLocalEntityResolver;
1817
import org.springframework.util.LinkedCaseInsensitiveMap;
1918
import org.springframework.web.util.UriComponentsBuilder;
19+
import org.tailormap.api.geotools.TMPreventLocalEntityResolver;
2020
import org.tailormap.api.geotools.wfs.SimpleWFSHelper;
2121
import org.tailormap.api.persistence.TMFeatureSource;
2222
import org.tailormap.api.persistence.TMFeatureType;
@@ -28,7 +28,7 @@ public class WFSFeatureSourceHelper extends FeatureSourceHelper {
2828
@Override
2929
public DataStore createDataStore(TMFeatureSource tmfs, Integer timeout) throws IOException {
3030
Map<String, Object> params = new HashMap<>();
31-
params.put(WFSDataStoreFactory.ENTITY_RESOLVER.key, PreventLocalEntityResolver.INSTANCE);
31+
params.put(WFSDataStoreFactory.ENTITY_RESOLVER.key, TMPreventLocalEntityResolver.INSTANCE);
3232

3333
if (timeout != null) {
3434
params.put(WFSDataStoreFactory.TIMEOUT.key, timeout);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (C) 2026 B3Partners B.V.
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
package org.tailormap.api.geotools;
7+
8+
import static org.junit.jupiter.api.Assertions.assertNull;
9+
import static org.junit.jupiter.api.Assertions.assertThrows;
10+
11+
import org.junit.jupiter.api.Test;
12+
import org.xml.sax.InputSource;
13+
import org.xml.sax.SAXException;
14+
15+
class TMPreventLocalEntityResolverTest {
16+
@Test
17+
void should_allow_http_describeFeatureType_requests() throws Exception {
18+
String systemId =
19+
"https://service.example.org/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAME=ns:Type";
20+
InputSource result = TMPreventLocalEntityResolver.INSTANCE.resolveEntity(
21+
"name", "publicId", "https://service.example.org/wfs", systemId);
22+
assertNull(result, "DescribeFeatureType HTTP requests should be allowed (resolver returns null)");
23+
}
24+
25+
@Test
26+
void should_allow_describeFeatureType_request_case_insensitive_and_with_other_query_params() throws Exception {
27+
String systemId = "http://host/service?foo=bar&request=DescribeFeatureTYPE&other=1";
28+
InputSource result =
29+
TMPreventLocalEntityResolver.INSTANCE.resolveEntity(null, null, "http://host/service", systemId);
30+
assertNull(result);
31+
}
32+
33+
@Test
34+
void should_allow_absolute_http_xsd_uris() throws Exception {
35+
String systemId = "http://schemas.example.org/some/schema.xsd";
36+
InputSource result = TMPreventLocalEntityResolver.INSTANCE.resolveEntity(null, null, null, systemId);
37+
assertNull(result, "Absolute http(s) .xsd URIs must be allowed by the resolver");
38+
}
39+
40+
@Test
41+
void should_allow_relative_xsd_resolved_against_baseUri() throws Exception {
42+
String baseURI = "http://schemas.example.org/path/";
43+
String systemId = "schema.xsd";
44+
InputSource result = TMPreventLocalEntityResolver.INSTANCE.resolveEntity(null, null, baseURI, systemId);
45+
assertNull(result, "Relative .xsd should be resolved against baseURI and allowed");
46+
}
47+
48+
@Test
49+
void should_reject_file_scheme_systemId() {
50+
String systemId = "file:///etc/passwd";
51+
assertThrows(
52+
SAXException.class,
53+
() -> TMPreventLocalEntityResolver.INSTANCE.resolveEntity(null, null, null, systemId));
54+
}
55+
56+
@Test
57+
void should_allow_absolute_https_scheme_systemId() throws Exception {
58+
String systemId = "https://example.org/schema.xsd";
59+
InputSource result = TMPreventLocalEntityResolver.INSTANCE.resolveEntity(null, null, null, systemId);
60+
assertNull(result, "Absolute https .xsd URIs must be allowed by the resolver");
61+
}
62+
63+
@Test
64+
void should_throw_when_systemId_is_null() {
65+
assertThrows(
66+
SAXException.class, () -> TMPreventLocalEntityResolver.INSTANCE.resolveEntity(null, null, null, null));
67+
}
68+
69+
@Test
70+
void should_throw_when_protocol_is_not_supported() {
71+
String systemId =
72+
"file://service.example.org/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAME=ns:Type";
73+
assertThrows(
74+
SAXException.class,
75+
() -> TMPreventLocalEntityResolver.INSTANCE.resolveEntity(null, null, null, systemId));
76+
}
77+
}

0 commit comments

Comments
 (0)