Skip to content

Commit 02b6716

Browse files
committed
Do away with GeoTools WMS and parse the capabilties ourselves
1 parent 8ced838 commit 02b6716

1 file changed

Lines changed: 92 additions & 53 deletions

File tree

src/main/java/org/tailormap/api/geotools/wfs/SimpleWFSHelper.java

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.tailormap.api.util.HttpProxyUtil.setHttpBasicAuthenticationHeader;
99

1010
import java.io.InputStream;
11+
import java.io.StringReader;
1112
import java.lang.invoke.MethodHandles;
1213
import java.net.URI;
1314
import java.net.URLEncoder;
@@ -24,16 +25,10 @@
2425
import javax.xml.XMLConstants;
2526
import javax.xml.parsers.DocumentBuilder;
2627
import javax.xml.parsers.DocumentBuilderFactory;
28+
import javax.xml.parsers.ParserConfigurationException;
2729
import javax.xml.xpath.XPath;
2830
import javax.xml.xpath.XPathConstants;
2931
import javax.xml.xpath.XPathFactory;
30-
import org.geotools.http.HTTPClient;
31-
import org.geotools.http.SimpleHttpClient;
32-
import org.geotools.ows.wms.LayerDescription;
33-
import org.geotools.ows.wms.WMS1_1_1;
34-
import org.geotools.ows.wms.WebMapServer;
35-
import org.geotools.ows.wms.request.DescribeLayerRequest;
36-
import org.geotools.ows.wms.response.DescribeLayerResponse;
3732
import org.slf4j.Logger;
3833
import org.slf4j.LoggerFactory;
3934
import org.springframework.util.LinkedMultiValueMap;
@@ -44,6 +39,8 @@
4439
import org.w3c.dom.Document;
4540
import org.w3c.dom.Element;
4641
import org.w3c.dom.NodeList;
42+
import org.xml.sax.EntityResolver;
43+
import org.xml.sax.InputSource;
4744

4845
public class SimpleWFSHelper {
4946
private static final Logger logger =
@@ -82,7 +79,7 @@ public static URI getOGCRequestURL(
8279
// We need to encode the parameters manually because UriComponentsBuilder annoyingly does not
8380
// encode '+' as used in mime types for output formats, see
8481
// https://stackoverflow.com/questions/18138011
85-
parameters.replaceAll((key, values) -> values.stream()
82+
parameters.replaceAll((unusedKey, values) -> values.stream()
8683
.map(s -> URLEncoder.encode(s, StandardCharsets.UTF_8))
8784
.collect(Collectors.toList()));
8885
params.addAll(parameters);
@@ -119,17 +116,7 @@ public static List<String> getOutputFormats(
119116
HttpResponse<InputStream> response =
120117
httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream());
121118

122-
// Parse capabilities in DOM
123-
124-
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
125-
documentBuilderFactory.setNamespaceAware(true);
126-
documentBuilderFactory.setExpandEntityReferences(false);
127-
documentBuilderFactory.setValidating(false);
128-
documentBuilderFactory.setXIncludeAware(false);
129-
documentBuilderFactory.setCoalescing(true);
130-
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
131-
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
132-
Document doc = documentBuilder.parse(response.body());
119+
Document doc = parseSecureXml(response.body());
133120

134121
// WFS 1.1.0 and WFS 2.0.0 use different namespaces, but the same local element names
135122
boolean wfs2 = "2.0.0".equals(doc.getDocumentElement().getAttribute("version"));
@@ -189,30 +176,18 @@ public static SimpleWFSLayerDescription describeWMSLayer(
189176
public static Map<String, SimpleWFSLayerDescription> describeWMSLayers(
190177
String url, String username, String password, List<String> layers) {
191178
try {
192-
HTTPClient client = new SimpleHttpClient();
193-
client.setUser(username);
194-
client.setPassword(password);
195-
client.setConnectTimeout(TIMEOUT);
196-
client.setReadTimeout(TIMEOUT);
197-
WebMapServer wms = new WebMapServer(new URI(url).toURL(), client);
198-
// Directly create WMS 1.1.1 request. Creating it from WebMapServer errors with GeoServer
199-
// about unsupported request in capabilities unless we override WebMapServer to set up
200-
// specifications.
201-
DescribeLayerRequest describeLayerRequest = new WMS1_1_1().createDescribeLayerRequest(new URI(url).toURL());
202-
// XXX Otherwise GeoTools will send VERSION=1.1.0...
203-
describeLayerRequest.setProperty("VERSION", "1.1.1");
204-
describeLayerRequest.setLayers(String.join(",", layers));
205-
// GeoTools will throw a ClassCastException when a WMS ServiceException is returned
206-
DescribeLayerResponse describeLayerResponse = wms.issueRequest(describeLayerRequest);
207-
208-
Map<String, SimpleWFSLayerDescription> descriptions = new HashMap<>();
209-
for (LayerDescription ld : describeLayerResponse.getLayerDescs()) {
210-
String wfsUrl = getWfsUrl(ld, wms);
211-
212-
if (wfsUrl != null && ld.getQueries() != null && ld.getQueries().length != 0) {
213-
descriptions.put(ld.getName(), new SimpleWFSLayerDescription(wfsUrl, List.of(ld.getQueries())));
214-
}
215-
}
179+
URI describeLayerUri = getDescribeLayerRequestUrl(url, layers);
180+
HttpClient httpClient = getDefaultHttpClient();
181+
182+
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(describeLayerUri);
183+
setHttpBasicAuthenticationHeader(requestBuilder, username, password);
184+
185+
HttpResponse<InputStream> response =
186+
httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream());
187+
188+
Document document = parseXmlAllowingDocType(response.body());
189+
190+
Map<String, SimpleWFSLayerDescription> descriptions = parseDescribeLayerResponse(document, url);
216191
return Collections.unmodifiableMap(descriptions);
217192
} catch (Exception e) {
218193
String msg =
@@ -226,17 +201,81 @@ public static Map<String, SimpleWFSLayerDescription> describeWMSLayers(
226201
return Map.of();
227202
}
228203

229-
private static String getWfsUrl(LayerDescription ld, WebMapServer wms) {
230-
String wfsUrl = (ld.getWfs() != null) ? ld.getWfs().toString() : null;
231-
if (wfsUrl == null && "WFS".equalsIgnoreCase(ld.getOwsType())) {
232-
wfsUrl = ld.getOwsURL().toString();
204+
private static URI getDescribeLayerRequestUrl(String url, List<String> layers) {
205+
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
206+
parameters.add("LAYERS", String.join(",", layers));
207+
return getOGCRequestURL(url, "WMS", "1.1.1", "DescribeLayer", parameters);
208+
}
209+
210+
private static Document parseSecureXml(InputStream inputStream) throws Exception {
211+
return getDocumentBuilder().parse(inputStream);
212+
}
213+
214+
private static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
215+
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
216+
documentBuilderFactory.setNamespaceAware(true);
217+
documentBuilderFactory.setExpandEntityReferences(false);
218+
documentBuilderFactory.setValidating(false);
219+
documentBuilderFactory.setXIncludeAware(false);
220+
documentBuilderFactory.setCoalescing(true);
221+
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
222+
return documentBuilderFactory.newDocumentBuilder();
223+
}
224+
225+
private static Document parseXmlAllowingDocType(InputStream inputStream) throws Exception {
226+
DocumentBuilder documentBuilder = getDocumentBuilder();
227+
EntityResolver entityResolver = (publicId, systemId) -> new InputSource(new StringReader(""));
228+
documentBuilder.setEntityResolver(entityResolver);
229+
230+
return documentBuilder.parse(inputStream);
231+
}
232+
233+
private static Map<String, SimpleWFSLayerDescription> parseDescribeLayerResponse(
234+
Document document, String fallbackUrl) {
235+
Element root = document.getDocumentElement();
236+
if (root == null) {
237+
return Map.of();
238+
}
239+
240+
NodeList layerDescriptions = root.getElementsByTagNameNS("*", "LayerDescription");
241+
Map<String, SimpleWFSLayerDescription> descriptions = new HashMap<>();
242+
243+
for (int i = 0; i < layerDescriptions.getLength(); i++) {
244+
Element layerDescription = (Element) layerDescriptions.item(i);
245+
246+
String layerName = layerDescription.getAttribute("name");
247+
String wfsUrl = getDescribeLayerWfsUrl(layerDescription, fallbackUrl);
248+
249+
List<String> typeNames = new ArrayList<>();
250+
NodeList queryNodes = layerDescription.getElementsByTagNameNS("*", "Query");
251+
for (int j = 0; j < queryNodes.getLength(); j++) {
252+
Element query = (Element) queryNodes.item(j);
253+
String typeName = query.getAttribute("typeName");
254+
if (!typeName.isBlank()) {
255+
typeNames.add(typeName);
256+
}
257+
}
258+
259+
if (!layerName.isBlank() && wfsUrl != null && !typeNames.isEmpty()) {
260+
descriptions.put(layerName, new SimpleWFSLayerDescription(wfsUrl, List.copyOf(typeNames)));
261+
}
233262
}
234-
// OGC 02-070 Annex B says the wfs/owsURL attributed are not required but implied. Some
235-
// Deegree instance encountered has all attributes empty, and apparently the meaning is that
236-
// the WFS URL is the same as the WMS URL (not explicitly defined in the spec).
237-
if (wfsUrl == null) {
238-
wfsUrl = wms.getInfo().getSource().toString();
263+
264+
return descriptions;
265+
}
266+
267+
private static String getDescribeLayerWfsUrl(Element layerDescription, String fallbackUrl) {
268+
String wfsUrl = layerDescription.getAttribute("wfs");
269+
if (!wfsUrl.isBlank()) {
270+
return wfsUrl;
239271
}
240-
return wfsUrl;
272+
273+
String owsType = layerDescription.getAttribute("owsType");
274+
String owsUrl = layerDescription.getAttribute("owsURL");
275+
if ("WFS".equalsIgnoreCase(owsType) && !owsUrl.isBlank()) {
276+
return owsUrl;
277+
}
278+
279+
return fallbackUrl;
241280
}
242281
}

0 commit comments

Comments
 (0)