/* 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.control.publication;
import com.esri.gpt.framework.http.HttpClientRequest;
import com.esri.gpt.framework.util.Val;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;
/**
* Reads the XML content associated with a resource. The source for the resource can be an XML file,
* an XML HTTP response file, or an ArcGIS JSON response.
*/
public class ResourceReader {
/** class variables ========================================================= */
/** The Logger. */
private static Logger LOGGER = Logger.getLogger(ResourceReader.class.getName());
/** instance variables ====================================================== */
private ArrayList<String> agsServiceUrls = new ArrayList<String>();
private boolean autoRecurse = true;
private String baseUrl = "";
private String systemId = "";
private boolean wasJson = false;
/** Default constructor. */
public ResourceReader() {}
/** properties ============================================================== */
/**
* Gets a list of ArcGIS server service URLs associated with the last
* ArcGIS server or folder read.
* @return the list of urls
*/
public List<String> getAgsServiceUrls() {
return this.agsServiceUrls;
}
/** methods ================================================================= */
/**
* Reads a JSON response from an ArcGIS server Rest end-point.
* <br/>The supplied rest URL will be appended with "f=json" if required.
* @param restUrl the rest URL to the ArcGIS service
* @return a JSON object representing the response
* @throws MalformedURLException if the URL is invalid
* @throws IOException if the communication fails
* @throws JSONException if a non-empty response could not be
* loaded as a JSON onject
*/
private JSONObject readAgsJson(String restUrl)
throws MalformedURLException, IOException, JSONException {
if (!restUrl.toLowerCase().startsWith("http")) return null;
// ensure that we are asking for a JSON response
if ((restUrl.toLowerCase().indexOf("f=json") == -1) &&
(restUrl.toLowerCase().indexOf("f=pjson") == -1)) {
if (restUrl.indexOf("?") == -1) {
restUrl += "?f=json";
} else {
restUrl += "&f=json";
}
}
LOGGER.finer("Attempting to read ArcGIS Server JSON response for:\n"+restUrl);
HttpClientRequest client = HttpClientRequest.newRequest();
client.setUrl(restUrl);
String sResponse = Val.chkStr(client.readResponseAsCharacters());
// attempt to create a JSON object from the response
if (sResponse.length() > 0) {
JSONObject jso = new JSONObject(sResponse);
this.wasJson = true;
return jso;
}
return null;
}
/**
* Reads a JSON response from an ArcGIS server Rest end-point and converts it to an
* XML string.
* @param url the rest URL to the ArcGIS service
* @return the rest response as an XML String
* @throws MalformedURLException if the URL is invalid
* @throws IOException if the communication fails
* @throws JSONException if a non-empty response could not be
* loaded as a JSON onject or conversion to XML fails
* @throws TransformerFactoryConfigurationError configuration related exception
* @throws TransformerException transformation related exception
*/
private String readAgsXml(String url)
throws MalformedURLException, IOException, JSONException,
TransformerFactoryConfigurationError, TransformerException {
String xml = "";
JSONObject jso = readAgsJson(url);
if (jso != null) {
try {
JSONObject jsoError = jso.getJSONObject("error");
if (jsoError != null) {
throw new IOException(jsoError.toString());
}
} catch (JSONException e) {}
if (autoRecurse) {
parseAgsTree(jso);
for (String service: this.agsServiceUrls) System.err.println(service);
}
xml = toAgsXml(jso);
LOGGER.finer("ArcGIS Rest/JSON to XML\n"+xml);
}
return xml;
}
/**
* 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 String readCharacters(InputStream stream, String charset)
throws IOException {
StringBuffer sb = new StringBuffer();
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();
}
/**
* Reads the XML content behind a file or URL.
* <br/>If the content returns is ArcGIS/JSON, the JSON object is converted to XML.
* @param systemId the system id of the source (file path or URL)
* @return the associated XML
* @throws Exception if an exception occurs
*/
public String readXml(String systemId) throws Exception {
// initialize
String xml = "";
this.systemId = Val.chkStr(systemId);
this.baseUrl = "";
this.agsServiceUrls = new ArrayList<String>();
boolean isHttp = false;
try {
URL url = new URL(systemId);
String tmp = url.getProtocol();
isHttp = (tmp != null) && (tmp.equals("http") || tmp.equals("https"));
} catch (MalformedURLException e) {
isHttp = false;
}
if (isHttp) {
this.baseUrl = this.systemId;
int nIdx = this.baseUrl.indexOf("?");
if (nIdx != -1) this.baseUrl = this.baseUrl.substring(0,nIdx);
// guess which to try first
boolean tryAgsFirst = false;
if (this.baseUrl.length() > 0) {
String lc = this.systemId.toLowerCase();
if (lc.indexOf("/rest/") != -1) {
if (lc.indexOf("service=") == -1) tryAgsFirst = true;
}
}
// execute
HttpClientRequest client = HttpClientRequest.newRequest();
client.setUrl(this.systemId);
if (tryAgsFirst) {
try {
xml = this.readAgsXml(this.systemId);
} catch (Exception e) {
if (this.wasJson) {
throw e;
} else {
String result = client.readResponseAsCharacters();
StringReader reader = new StringReader(result);
StringWriter writer = new StringWriter();
transform(new StreamSource(reader),new StreamResult(writer));
xml = Val.chkStr(writer.toString());
}
}
} else {
try {
String result = client.readResponseAsCharacters();
StringReader reader = new StringReader(result);
StringWriter writer = new StringWriter();
transform(new StreamSource(reader),new StreamResult(writer));
xml = Val.chkStr(writer.toString());
} catch (TransformerException te) {
try {
xml = this.readAgsXml(this.systemId);
} catch (MalformedURLException mue) {
throw te;
} catch (JSONException e) {
throw te;
}
}
}
} else {
// read the contexts of a file path
StringWriter writer = new StringWriter();
transform(new StreamSource(this.systemId),new StreamResult(writer)) ;
xml = Val.chkStr(writer.toString());
}
return xml;
}
/**
* Recursively parses an ArcGIS service tree.
* <br/>Located services are appended to the "agsServicePaths" list.
* @param jsoParent the JSON parent object (the server or a folder)
*/
private void parseAgsTree(JSONObject jsoParent) {
// parse folders if present
ArrayList<String> folders = new ArrayList<String>();
try {
JSONArray jsoFolders = jsoParent.getJSONArray("folders");
if (jsoFolders != null) {
for (int i=0;i<jsoFolders.length();i++) {
String name = Val.chkStr(jsoFolders.getString(i));
if (name.length() > 0) {
folders.add(this.baseUrl+"/"+name);
}
}
}
} catch (JSONException e) {
LOGGER.finest("No ArcGIS folders: "+e.toString());
}
// parse services if present
try {
JSONArray jsoServices = jsoParent.getJSONArray("services");
if (jsoServices != null) {
for (int i=0;i<jsoServices.length();i++) {
JSONObject service = jsoServices.getJSONObject(i);
if (service != null) {
String name = Val.chkStr(service.getString("name"));
String type = Val.chkStr(service.getString("type"));
// for some reason, service names within a folder are partial paths
// e.g baseurl = http://server.arcgisonline.com/ArcGIS/rest/services/Elevation
// name = Elevation/ESRI_Elevation_World
if (name.lastIndexOf("/") != -1) {
if (this.baseUrl.lastIndexOf("/") != -1) {
String folder = this.baseUrl.substring(this.baseUrl.lastIndexOf("/")+1);
if (name.startsWith(folder+"/")) {
name = name.substring(name.lastIndexOf("/")+1);
}
}
}
if ((name.length() > 0) && (type.length() > 0)) {
this.agsServiceUrls.add(this.baseUrl+"/"+name+"/"+type);
}
}
}
}
} catch (JSONException e) {
LOGGER.finest("No ArcGIS services: "+e.toString());
}
// recurse folders
for (String folder: folders) {
try {
JSONObject jsoFolder = readAgsJson(folder);
if (jsoFolder != null) {
parseAgsTree(jsoFolder);
}
} catch (MalformedURLException e) {
LOGGER.finer("Error reading folder: "+folder+", urlerr="+e.toString());
} catch (IOException e) {
LOGGER.finer("Error reading folder: "+folder+", ioerr="+e.toString());
} catch (JSONException e) {
LOGGER.finer("Error reading folder: "+folder+", jsonerr="+e.toString());
}
}
}
/**
* Create an XML string from an ArcGIS Rest response.
* <br/>The XML string is wrappoed in a parent tag <ags-rest>.
* @param jso the JSON object representing the response
* @return the XML String
* @throws JSONException if the JDON object cannot be converted to an XML
* @throws TransformerFactoryConfigurationError configuration related exception
* @throws TransformerException transformation related exception
*/
private String toAgsXml(JSONObject jso)
throws JSONException, TransformerFactoryConfigurationError, TransformerException {
String xml = XML.toString(jso,"ags-rest");
StreamSource source = new StreamSource(new StringReader(xml));
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT,"yes");
transformer.transform(source,result);
return Val.chkStr(writer.toString());
}
/**
* Executes a transformation.
* <br/>The output encoding is set to UTF-8
* <br/>The indent is set to "yes"
* @param source the transformation source
* @param result the transformation result
* @throws TransformerException if an exception occurs
*/
private void transform(javax.xml.transform.Source source,
javax.xml.transform.Result result)
throws TransformerException {
Transformer transformer = TransformerFactory.newInstance().newTransformer() ;
transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT,"yes");
transformer.transform(source,result);
}
}