/**
* Copyright (C) 2007 - 2016 52°North Initiative for Geospatial Open Source
* Software GmbH
*
* Licensed 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 org.n52.wps.client;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import net.opengis.ows.x11.ExceptionReportDocument;
import net.opengis.ows.x11.OperationDocument.Operation;
import net.opengis.wps.x100.CapabilitiesDocument;
import net.opengis.wps.x100.ExecuteDocument;
import net.opengis.wps.x100.ExecuteResponseDocument;
import net.opengis.wps.x100.ProcessDescriptionType;
import net.opengis.wps.x100.ProcessDescriptionsDocument;
/**
* Contains some convenient methods to access and manage WebProcessingSerivces in a very
* generic way.
*
* This is implemented as a singleton.
* @author foerster
*/
public class WPSClientSession {
private static Logger LOGGER = LoggerFactory.getLogger(WPSClientSession.class);
private static final String OGC_OWS_URI = "http://www.opengeospatial.net/ows";
private static String SUPPORTED_VERSION = "1.0.0";
private static WPSClientSession session;
private Map<String, CapabilitiesDocument> loggedServices;
private XmlOptions options = null;
private boolean useConnectURL = false;
private String connectURL;
// a Map of <url, all available process descriptions>
private Map<String, ProcessDescriptionsDocument> processDescriptions;
/**
* Initializes a WPS client session.
*
*/
private WPSClientSession() {
options = new XmlOptions();
options.setLoadStripWhitespace();
options.setLoadTrimTextBuffer();
loggedServices = new HashMap<String, CapabilitiesDocument>();
processDescriptions = new HashMap<String, ProcessDescriptionsDocument>();
}
/*
* @result An instance of a WPS Client session.
*/
public static WPSClientSession getInstance() {
if(session == null) {
session = new WPSClientSession();
}
return session;
}
/**
* This resets the WPSClientSession. This might be necessary, to get rid of old service entries/descriptions. However, the session has to be repopulated afterwards.
*/
public static void reset() {
session = new WPSClientSession();
}
/**
* Connects to a WPS and retrieves Capabilities plus puts all available Descriptions into cache.
* @param url the entry point for the service. This is used as id for further identification of the service.
* @return true, if connect succeeded, false else.
* @throws WPSClientException
*/
public boolean connect(String url) throws WPSClientException {
return connect(url, false);
}
/**
* Connects to a WPS and retrieves Capabilities plus puts all available Descriptions into cache.
* @param url the entry point for the service. This is used as id for further identification of the service.
* @param useConnectURL use the URL that was passed for communication with the WPS and not the URLs from the Operations Metadata
* @return true, if connect succeeded, false else.
* @throws WPSClientException
*/
public boolean connect(String url, boolean useConnectURL) throws WPSClientException {
LOGGER.info("CONNECT");
this.useConnectURL = useConnectURL;
this.connectURL = url;
if(useConnectURL){
LOGGER.info("Use connect URL for further communication: " + connectURL);
}
if(loggedServices.containsKey(url)) {
LOGGER.info("Service already registered: " + url);
return false;
}
CapabilitiesDocument capsDoc = retrieveCapsViaGET(url);
if(capsDoc != null) {
loggedServices.put(url, retrieveCapsViaGET(url));
}
ProcessDescriptionsDocument processDescs = describeAllProcesses(url);
if(processDescs != null && capsDoc != null) {
processDescriptions.put(url, processDescs);
return true;
}
LOGGER.warn("retrieving caps failed, caps are null");
return false;
}
/**
* removes a service from the session
* @param url
*/
public void disconnect(String url) {
if(loggedServices.containsKey(url)) {
loggedServices.remove(url);
processDescriptions.remove(url);
LOGGER.info("service removed successfully: " + url);
}
}
/**
* returns the serverIDs of all loggedServices
* @return
*/
public List<String> getLoggedServices() {
return new ArrayList<String>(loggedServices.keySet());
}
/**
* informs you if the descriptions for the specified service is already in the session.
* in normal case it should return true :)
* @param serverID
* @return success
*/
public boolean descriptionsAvailableInCache(String serverID) {
return processDescriptions.containsKey(serverID);
}
/**
* returns the cached processdescriptions of a service.
* @param serverID
* @return success
* @throws IOException
*/
private ProcessDescriptionsDocument getProcessDescriptionsFromCache(String wpsUrl) throws IOException {
if(! descriptionsAvailableInCache(wpsUrl)) {
try{
connect(wpsUrl);
}
catch(WPSClientException e) {
throw new IOException("Could not initialize WPS " + wpsUrl);
}
}
return processDescriptions.get(wpsUrl);
}
/**
* return the processDescription for a specific process from Cache.
* @param serverID
* @param processID
* @return a ProcessDescription for a specific process from Cache.
* @throws IOException
*/
public ProcessDescriptionType getProcessDescription(String serverID, String processID) throws IOException {
ProcessDescriptionType[] processes = getProcessDescriptionsFromCache(serverID).getProcessDescriptions().getProcessDescriptionArray();
for(ProcessDescriptionType process : processes) {
if(process.getIdentifier().getStringValue().equals(processID)) {
return process;
}
}
return null;
}
/**
* Delivers all ProcessDescriptions from a WPS
*
* @param wpsUrl the URL of the WPS
* @return An Array of ProcessDescriptions
* @throws IOException
*/
public ProcessDescriptionType[] getAllProcessDescriptions(String wpsUrl) throws IOException{
return getProcessDescriptionsFromCache(wpsUrl).getProcessDescriptions().getProcessDescriptionArray();
}
/**
* looks up, if the service exists already in session.
*/
public boolean serviceAlreadyRegistered(String serverID) {
return loggedServices.containsKey(serverID);
}
/**
* provides you the cached capabilities for a specified service.
* @param url
* @return
*/
public CapabilitiesDocument getWPSCaps(String url) {
return loggedServices.get(url);
}
/**
* retrieves all current available ProcessDescriptions of a WPS. Mention: to get the current list
* of all processes, which will be requested, the cached capabilities will be used. Please keep that in mind.
* the retrieved descriptions will not be cached, so only transient information!
* @param url
* @return
* @throws WPSClientException
*/
public ProcessDescriptionsDocument describeAllProcesses(String url) throws WPSClientException {
CapabilitiesDocument doc = loggedServices.get(url);
if(doc == null) {
LOGGER.warn("serviceCaps are null, perhaps server does not exist");
return null;
}
String[] processIDs = new String[]{"all"};
return describeProcess(processIDs, url);
}
/**
* retrieves the desired description for a service. the retrieved information will not be held in cache!
* @param processIDs one or more processIDs
* @param serverID
* @throws WPSClientException
*/
public ProcessDescriptionsDocument describeProcess(String[] processIDs, String serverID) throws WPSClientException {
String url = connectURL;
if (!useConnectURL) {
CapabilitiesDocument caps = this.loggedServices.get(serverID);
Operation[] operations = caps.getCapabilities().getOperationsMetadata().getOperationArray();
for (Operation operation : operations) {
if (operation.getName().equals("DescribeProcess")) {
url = operation.getDCPArray()[0].getHTTP().getGetArray()[0].getHref();
}
}
if (url == null) {
throw new WPSClientException("Capabilities do not contain any information about the entry point for DescribeProcess operation.");
}
}
return retrieveDescriptionViaGET(processIDs, url);
}
/**
* Executes a process at a WPS
*
* @param url url of server not the entry additionally defined in the caps.
* @param execute Execute document
* @return either an ExecuteResponseDocument or an InputStream if asked for RawData or an Exception Report
*/
private Object execute(String serverID, ExecuteDocument execute, boolean rawData) throws WPSClientException{
String url = connectURL;
if (!useConnectURL) {
CapabilitiesDocument caps = loggedServices.get(serverID);
Operation[] operations = caps.getCapabilities().getOperationsMetadata().getOperationArray();
for (Operation operation : operations) {
if (operation.getName().equals("Execute")) {
url = operation.getDCPArray()[0].getHTTP().getPostArray()[0].getHref();
}
}
if (url == null) {
throw new WPSClientException(
"Capabilities do not contain any information about the entry point for Execute operation.");
}
}
execute.getExecute().setVersion(SUPPORTED_VERSION);
return retrieveExecuteResponseViaPOST(url, execute,rawData);
}
/**
* Executes a process at a WPS
*
* @param url url of server not the entry additionally defined in the caps.
* @param execute Execute document
* @return either an ExecuteResponseDocument or an InputStream if asked for RawData or an Exception Report
*/
public Object execute(String serverID, ExecuteDocument execute) throws WPSClientException{
if(execute.getExecute().isSetResponseForm()==true && execute.getExecute().isSetResponseForm()==true && execute.getExecute().getResponseForm().isSetRawDataOutput()==true){
return execute(serverID, execute,true);
}else{
return execute(serverID, execute,false);
}
}
private CapabilitiesDocument retrieveCapsViaGET(String url) throws WPSClientException {
ClientCapabiltiesRequest req = new ClientCapabiltiesRequest();
url = req.getRequest(url);
try {
URL urlObj = new URL(url);
urlObj.getContent();
InputStream is = urlObj.openStream();
Document doc = checkInputStream(is);
return CapabilitiesDocument.Factory.parse(doc, options);
} catch (MalformedURLException e) {
throw new WPSClientException("Capabilities URL seems to be unvalid: " + url, e);
} catch (IOException e) {
throw new WPSClientException("Error occured while retrieving capabilities from url: " + url, e);
} catch (XmlException e) {
throw new WPSClientException("Error occured while parsing XML", e);
}
}
private ProcessDescriptionsDocument retrieveDescriptionViaGET(String[] processIDs, String url) throws WPSClientException{
ClientDescribeProcessRequest req = new ClientDescribeProcessRequest();
req.setIdentifier(processIDs);
String requestURL = req.getRequest(url);
try {
URL urlObj = new URL(requestURL);
InputStream is = urlObj.openStream();
Document doc = checkInputStream(is);
return ProcessDescriptionsDocument.Factory.parse(doc, options);
} catch (MalformedURLException e) {
throw new WPSClientException("URL seems not to be valid", e);
}
catch (IOException e) {
throw new WPSClientException("Error occured while receiving data", e);
}
catch(XmlException e) {
throw new WPSClientException("Error occured while parsing ProcessDescription document", e);
}
}
private InputStream retrieveDataViaPOST(XmlObject obj, String urlString) throws WPSClientException{
try {
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
conn.setRequestProperty("Accept-Encoding", "gzip");
conn.setRequestProperty("Content-Type", "text/xml");
conn.setDoOutput(true);
obj.save(conn.getOutputStream());
InputStream input = null;
String encoding = conn.getContentEncoding();
if(encoding != null && encoding.equalsIgnoreCase("gzip")) {
input = new GZIPInputStream(conn.getInputStream());
}
else {
input = conn.getInputStream();
}
return input;
} catch (MalformedURLException e) {
throw new WPSClientException("URL seems to be unvalid", e);
} catch (IOException e) {
throw new WPSClientException("Error while transmission", e);
}
}
private Document checkInputStream(InputStream is) throws WPSClientException {
DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
fac.setNamespaceAware(true);
try {
Document doc = fac.newDocumentBuilder().parse(is);
if(getFirstElementNode(doc.getFirstChild()).getLocalName().equals("ExceptionReport") && getFirstElementNode(doc.getFirstChild()).getNamespaceURI().equals(OGC_OWS_URI)) {
try {
ExceptionReportDocument exceptionDoc = ExceptionReportDocument.Factory.parse(doc);
LOGGER.debug(exceptionDoc.xmlText(options));
throw new WPSClientException("Error occured while executing query", exceptionDoc);
}
catch(XmlException e) {
throw new WPSClientException("Error while parsing ExceptionReport retrieved from server", e);
}
}
return doc;
} catch (SAXException e) {
throw new WPSClientException("Error while parsing input.", e);
} catch (IOException e) {
throw new WPSClientException("Error occured while transfer", e);
} catch (ParserConfigurationException e) {
throw new WPSClientException("Error occured, parser is not correctly configured", e);
}
}
private Node getFirstElementNode(Node node) {
if(node == null) {
return null;
}
if(node.getNodeType() == Node.ELEMENT_NODE) {
return node;
}
else {
return getFirstElementNode(node.getNextSibling());
}
}
/**
* either an ExecuteResponseDocument or an InputStream if asked for RawData or an Exception Report
* @param url
* @param doc
* @param rawData
* @return
* @throws WPSClientException
*/
private Object retrieveExecuteResponseViaPOST(String url, ExecuteDocument doc, boolean rawData) throws WPSClientException{
InputStream is = retrieveDataViaPOST(doc, url);
if(rawData) {
return is;
}
Document documentObj = checkInputStream(is);
ExceptionReportDocument erDoc = null;
try {
return ExecuteResponseDocument.Factory.parse(documentObj);
}
catch(XmlException e) {
try {
erDoc = ExceptionReportDocument.Factory.parse(documentObj);
} catch (XmlException e1) {
throw new WPSClientException("Error occured while parsing executeResponse", e);
}
return erDoc;
}
}
public String[] getProcessNames(String url) throws IOException {
ProcessDescriptionType[] processes = getProcessDescriptionsFromCache(url).getProcessDescriptions().getProcessDescriptionArray();
String[] processNames = new String[processes.length];
for(int i = 0; i<processNames.length; i++){
processNames[i] = processes[i].getIdentifier().getStringValue();
}
return processNames;
}
/**
* Executes a process at a WPS
*
* @param url url of server not the entry additionally defined in the caps.
* @param executeAsGETString KVP Execute request
* @return either an ExecuteResponseDocument or an InputStream if asked for RawData or an Exception Report
*/
public Object executeViaGET(String url, String executeAsGETString) throws WPSClientException {
url = url + executeAsGETString;
try {
URL urlObj = new URL(url);
InputStream is = urlObj.openStream();
if(executeAsGETString.toUpperCase().contains("RAWDATA")){
return is;
}
Document doc = checkInputStream(is);
ExceptionReportDocument erDoc = null;
try {
return ExecuteResponseDocument.Factory.parse(doc);
}
catch(XmlException e) {
try {
erDoc = ExceptionReportDocument.Factory.parse(doc);
} catch (XmlException e1) {
throw new WPSClientException("Error occured while parsing executeResponse", e);
}
throw new WPSClientException("Error occured while parsing executeResponse", erDoc);
}
} catch (MalformedURLException e) {
throw new WPSClientException("Capabilities URL seems to be unvalid: " + url, e);
} catch (IOException e) {
throw new WPSClientException("Error occured while retrieving capabilities from url: " + url, e);
}
}
}