/* See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* Esri Inc. licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.esri.gpt.framework.util;
import com.esri.gpt.framework.util.Val;
import com.esri.gpt.framework.xml.DomUtil;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* KML/KMZ utility class.
*/
public class KmlUtil {
/**
* Creates a stream from the URL possibly referencing KML/KMZ. If this is
* a KMZ, it will be interrogated for the enclosed KML, than that KML will be
* reopened.
* @param url KML/KMZ url
* @throws IOException if creating stream failed
*/
public static InputStream openKmlStream(String url)
throws IOException {
try {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();
return openKmlStream(xPath, url);
} catch (ParserConfigurationException ex) {
throw new IOException("Error extracting kml stream.");
} catch (SAXException ex) {
throw new IOException("Error extracting kml stream.");
} catch (XPathExpressionException ex) {
throw new IOException("Error extracting kml stream.");
}
}
/**
* Gets KML input stream from KMZ stream.
* @param kmzInputStream KMZ stream
* @return encoded input stream containing KML data
* @throws IOException extracting KML stream fails
*/
public static InputStream extractKmlStream(InputStream kmzInputStream)
throws IOException {
try {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();
return extractKmlStream(xPath, kmzInputStream);
} catch (ParserConfigurationException ex) {
throw new IOException("Error extracting kml stream.");
} catch (SAXException ex) {
throw new IOException("Error extracting kml stream.");
} catch (XPathExpressionException ex) {
throw new IOException("Error extracting kml stream.");
}
}
/**
* Drills-down KML stream for "Placemark" and follows "NetworkLink". THis method
* ALWAYS closes input stream passed as an argument.
* @param kmlInputStream KML input stream
* @param rootUrl root URL used when KML contains relative URL
* @return first found feature stream.
* @throws IOException if convertiong stream fails
*/
public static InputStream convertToFeaturesStream(InputStream kmlInputStream, String rootUrl)
throws IOException {
rootUrl = Val.chkStr(rootUrl);
try {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();
return convertToFeaturesStream(xPath, kmlInputStream, rootUrl);
} catch (ParserConfigurationException ex) {
throw new IOException("Error extracting kml stream.");
} catch (SAXException ex) {
throw new IOException("Error extracting kml stream.");
} catch (XPathExpressionException ex) {
throw new IOException("Error extracting kml stream.");
}
}
/**
* Fully reads the characters from an input stream.
* @param stream the input stream
* @param charset the encoding of the input stream
* @return the characters read
* @throws IOException if an exception occurs
*/
private static String readCharacters(InputStream stream, String charset)
throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = null;
InputStreamReader ir = null;
try {
if ((charset == null) || (charset.trim().length() == 0)) charset = "UTF-8";
char cbuf[] = new char[2048];
int n = 0;
int nLen = cbuf.length;
ir = new InputStreamReader(stream,charset);
br = new BufferedReader(ir);
while ((n = br.read(cbuf,0,nLen)) > 0) sb.append(cbuf,0,n);
} finally {
try {if (br != null) br.close();} catch (Exception ef) {}
try {if (ir != null) ir.close();} catch (Exception ef) {}
}
return sb.toString();
}
/**
* Drills-down KML stream for "Placemark" and follows "NetworkLink". THis method
* ALWAYS closes input stream passed as an argument.
* @param xPath XPath
* @param kmlInputStream KML input stream
* @param rootUrl root URL used when KML contains relative URL
* @return first found feature stream.
* @throws IOException if reading stream fails
* @throws XPathExpressionException if invalid XPath expression
* @throws SAXException if error parsing document
* @throws ParserConfigurationException if error obtaining parser
*/
private static InputStream convertToFeaturesStream(XPath xPath, InputStream kmlInputStream, String root)
throws IOException, XPathExpressionException, SAXException, ParserConfigurationException {
try {
// read stream
String kmlData = readCharacters(kmlInputStream, "UTF-8");
// create XML document
Document doc = DomUtil.makeDomFromString(Val.removeBOM(kmlData), false);
// find placemarks
NodeList ndPlacemarks = (NodeList) xPath.evaluate("//Placemark", doc, XPathConstants.NODESET);
if (ndPlacemarks.getLength()>0)
return new ByteArrayInputStream(kmlData.getBytes("UTF-8"));
// find network links
NodeList ndNetworkLinks = (NodeList) xPath.evaluate("//NetworkLink/Url/href", doc, XPathConstants.NODESET);
if (ndNetworkLinks.getLength()==0) {
ndNetworkLinks = (NodeList) xPath.evaluate("//NetworkLink/Link/href", doc, XPathConstants.NODESET);
if (ndNetworkLinks.getLength()==0) {
return null;
}
}
for (int i=0; i<ndNetworkLinks.getLength(); i++) {
Node ndNetworkLink = ndNetworkLinks.item(i);
String value = (String) xPath.evaluate(".", ndNetworkLink, XPathConstants.STRING);
if (root.length()>0) {
try {
URI valueUrl = new URI(value);
if (!valueUrl.isAbsolute()) {
URI rootUrl = new URI(root);
value = rootUrl.resolve(valueUrl.normalize()).toString();
}
} catch (URISyntaxException ex) {
continue;
}
}
if (value.length()>0) {
InputStream is = openKmlStream(xPath, value);
if (is!=null) {
is = convertToFeaturesStream(is, value);
if (is!=null)
return is;
}
}
}
} finally {
try {
kmlInputStream.close();
} catch (IOException ex){}
}
return null;
}
/**
* Gets encoded input stream from KMZ stream.
* @param xPath XPath
* @param kmzInputStream KMZ stream
* @return encoded input stream containing KML data
* @throws IOException KML input stream
* @throws SAXException if error parsing document
* @throws ParserConfigurationException if error obtaining parser
* @throws XPathExpressionException if invalid XPath expression
*/
private static InputStream extractKmlStream(XPath xPath, InputStream kmzInputStream)
throws IOException, ParserConfigurationException, SAXException, XPathExpressionException {
ZipInputStream zipStream = new ZipInputStream(kmzInputStream);
for (ZipEntry ze=zipStream.getNextEntry(); ze!=null; ze=zipStream.getNextEntry()) {
if (!ze.isDirectory() && ze.getName().endsWith(".kml")) {
String kml = readCharacters(zipStream, "UTF-8");
Document doc = DomUtil.makeDomFromString(Val.removeBOM(kml), false);
Node kmlNode = (Node) xPath.evaluate("/kml", doc, XPathConstants.NODE);
if (kmlNode!=null) {
return new ByteArrayInputStream(kml.getBytes("UTF-8"));
}
}
}
return null;
}
/**
* Creates a stream from the URL possibly referencing KML/KMZ. If this is
* a KMZ, it will be interrogated for the enclosed KML, than that KML will be
* reopened.
* @param xPath XPath
* @param url KML/KMZ url
* @return KML stream or <code>null</code> server provides response code indicating error
* @throws IOException if creating stream failed
* @throws SAXException if error parsing document
* @throws ParserConfigurationException if error obtaining parser
* @throws XPathExpressionException if invalid XPath expression
*/
private static InputStream openKmlStream(XPath xPath, String url)
throws IOException, ParserConfigurationException, SAXException, XPathExpressionException {
URL queryUrl = new URL(url);
HttpURLConnection httpCon = (HttpURLConnection) queryUrl.openConnection();
httpCon.setDoInput(true);
httpCon.setRequestMethod("GET");
InputStream responseStream = httpCon.getInputStream();
if (httpCon.getResponseCode() != HttpURLConnection.HTTP_OK) {
return null;
}
String ct = httpCon.getContentType();
boolean kmz = (ct != null) && (ct.toLowerCase().indexOf("application/vnd.google-earth.kmz") != -1);
InputStream wrappedStream = null;
if (kmz) {
wrappedStream = KmlUtil.extractKmlStream(xPath, responseStream);
} else {
wrappedStream = new BufferedInputStream(responseStream);
}
return wrappedStream;
}
}