/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-2016, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.data.wfs.internal.v1_x;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import net.opengis.wfs.FeatureTypeType;
import net.opengis.wfs.GetFeatureType;
import org.apache.commons.io.IOUtils;
import org.geotools.data.wfs.internal.GetFeatureRequest;
import org.geotools.data.wfs.internal.WFSOperationType;
import org.geotools.data.wfs.internal.WFSRequest;
import org.geotools.xs.bindings.XSDoubleBinding;
import org.geotools.xs.bindings.XSIntegerBinding;
import org.geotools.xs.bindings.XSStringBinding;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* This strategy addresses a bug in most MapServer implementations where a filter is required in
* order for all the features to be returned. So if the Filter is Filter.NONE or Query.ALL then a
* BBox Filter is constructed that is the entire layer.
*/
public class MapServerWFSStrategy extends StrictWFS_1_x_Strategy {
private String mapServerVersion = "";
public MapServerWFSStrategy(Document capabilitiesDoc){
super();
getMapServerVersion(capabilitiesDoc);
}
@Override
public FeatureTypeType translateTypeInfo(FeatureTypeType typeInfo){
if ("wfs".equals(typeInfo.getName().getPrefix()) ||
"http://www.opengis.net/wfs".equals(typeInfo.getName().getNamespaceURI())) {
QName newName = new QName( "http://mapserver.gis.umn.edu/mapserver", typeInfo.getName().getLocalPart(), "ms");
typeInfo.setName(newName);
}
return typeInfo;
}
@Override
public Map<QName, Class<?>> getFieldTypeMappings() {
Map<QName, Class<?>> mappings = new HashMap<QName, Class<?>>();
mappings.put(new QName("http://www.w3.org/2001/XMLSchema", "Character"), XSStringBinding.class);
mappings.put(new QName("http://www.w3.org/2001/XMLSchema", "Integer"), XSIntegerBinding.class);
mappings.put(new QName("http://www.w3.org/2001/XMLSchema", "Real"), XSDoubleBinding.class);
return mappings;
}
@Override
public InputStream getPostContents(WFSRequest request) throws IOException {
InputStream in = super.getPostContents(request);
if (request.getOperation().compareTo(WFSOperationType.GET_FEATURE) == 0 &&
getVersion().compareTo("1.0.0") == 0 &&
mapServerVersion != null) {
try {
// Pre-5.6.7 versions of MapServer do not support the following gml:Box coordinate format:
// <gml:coord><gml:X>-59.0</gml:X><gml:Y>-35.0</gml:Y></gml:coord><gml:coord>< gml:X>-58.0</gml:X><gml:Y>-34.0</gml:Y></gml:coord>
// Rewrite the coordinates in the following format:
// <gml:coordinates cs="," decimal="." ts=" ">-59,-35 -58,-34</gml:coordinates>
String [] tokens = mapServerVersion.split("\\.");
if (tokens.length == 3 && versionCompare(mapServerVersion, "5.6.7") < 0) {
StringWriter writer = new StringWriter();
IOUtils.copy(in, writer, "UTF-8");
String pc = writer.toString();
boolean reformatted = false;
if (pc.contains("gml:Box") && pc.contains("gml:coord") &&
pc.contains("gml:X") && pc.contains("gml:Y")) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new ByteArrayInputStream(pc.getBytes()));
NodeList boxes = doc.getElementsByTagName("gml:Box");
for (int b = 0; b < boxes.getLength(); b++) {
Element box = (Element)boxes.item(b);
NodeList coords = box.getElementsByTagName("gml:coord");
if (coords != null && coords.getLength() == 2) {
Element coord1 = (Element)coords.item(0);
Element coord2 = (Element)coords.item(1);
if (coord1 != null && coord2 != null) {
Element coordX1 = (Element)(coord1.getElementsByTagName("gml:X").item(0));
Element coordY1 = (Element)(coord1.getElementsByTagName("gml:Y").item(0));
Element coordX2 = (Element)(coord2.getElementsByTagName("gml:X").item(0));
Element coordY2 = (Element)(coord2.getElementsByTagName("gml:Y").item(0));
if (coordX1 != null && coordY1 != null && coordX2 != null && coordY2 != null) {
reformatted = true;
String x1 = coordX1.getTextContent();
String y1 = coordY1.getTextContent();
String x2 = coordX2.getTextContent();
String y2 = coordY2.getTextContent();
box.removeChild(coord1);
box.removeChild(coord2);
Element coordinates = doc.createElement("gml:coordinates");
coordinates.setAttribute("cs", ",");
coordinates.setAttribute("decimal", ".");
coordinates.setAttribute("ts", " ");
coordinates.appendChild(doc.createTextNode(x1 + "," + y1 + " " + x2 + "," + y2));
box.appendChild(coordinates);
}
}
}
}
if (reformatted) {
DOMSource domSource = new DOMSource(doc);
StringWriter domsw = new StringWriter();
StreamResult result = new StreamResult(domsw);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(domSource, result);
domsw.flush();
pc = domsw.toString();
}
}
in = new ByteArrayInputStream(pc.getBytes());
}
}
catch(SAXException | ParserConfigurationException | TransformerException | IOException ex) {
LOGGER.log(Level.FINE, "Unexpected exception while rewriting 1.0.0 GETFEATURE request with BBOX filter", ex.getMessage());
}
}
return in;
}
//
// Mapserver returns its version in the WFS 1.0.0 getcapabilities response
//
void getMapServerVersion(Document capabilitiesDoc) {
if (getVersion().compareTo("1.0.0") == 0) {
Element cap = capabilitiesDoc.getDocumentElement();
NodeList childNodes = cap.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Element.COMMENT_NODE) {
String nodeValue = child.getNodeValue();
if (nodeValue != null && nodeValue.contains("MapServer version")) {
String [] tokens = nodeValue.split("\\s");
if (tokens.length >= 4) {
mapServerVersion = tokens[3];
break;
}
}
}
}
}
}
/**
* Compares two version strings.
*
* Use this instead of String.compareTo() for a non-lexicographical
* comparison that works for version strings. e.g. "1.10".compareTo("1.6").
*
* @note It does not work if "1.10" is supposed to be equal to "1.10.0".
*
* @param str1 a string of ordinal numbers separated by decimal points.
* @param str2 a string of ordinal numbers separated by decimal points.
* @return The result is a negative integer if str1 is _numerically_ less than str2.
* The result is a positive integer if str1 is _numerically_ greater than str2.
* The result is zero if the strings are _numerically_ equal.
*/
// Code from Stack Overflow:
// http://stackoverflow.com/questions/6701948/efficient-way-to-compare-version-strings-in-java
//
private Integer versionCompare(String str1, String str2)
{
String[] vals1 = str1.split("\\.");
String[] vals2 = str2.split("\\.");
int i = 0;
// set index to first non-equal ordinal or length of shortest version string
while (i < vals1.length && i < vals2.length && vals1[i].equals(vals2[i])) {
i++;
}
// compare first non-equal ordinal number
if (i < vals1.length && i < vals2.length) {
int diff = Integer.valueOf(vals1[i]).compareTo(Integer.valueOf(vals2[i]));
return Integer.signum(diff);
}
// the strings are equal or one string is a substring of the other
// e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4"
else {
return Integer.signum(vals1.length - vals2.length);
}
}
@Override
protected String encodePropertyName(String propertyName) {
return "(" + propertyName + ")";
}
}