/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, 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.ows; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import net.opengis.wps10.WPSCapabilitiesType; import org.geotools.data.ResourceInfo; import org.geotools.data.ServiceInfo; import org.geotools.ows.ServiceException; /** * This abstract class provides a building block for one to implement a * WPS client. * * This class provides version negotiation, Capabilities document retrieval, * and a request/response infrastructure. Implementing subclasses need to * provide their own Specifications * * @author gdavis * * * @source $URL$ */ public abstract class AbstractWPS<C extends WPSCapabilitiesType, R extends Object> { private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.ows"); protected HTTPClient httpClient; protected final URL serverURL; protected C capabilities; protected ServiceInfo info; protected Map<R, ResourceInfo> resourceInfo = new HashMap<R, ResourceInfo>(); /** Contains the specifications that are to be used with this service */ protected Specification[] specs; protected Specification specification; /** * Set up the specifications used and retrieve the Capabilities document * given by serverURL. * * @param serverURL a URL that points to the capabilities document of a server * @throws IOException if there is an error communicating with the server * @throws ServiceException if the server responds with an error */ public AbstractWPS(final URL serverURL) throws IOException, ServiceException { this(serverURL, new SimpleHttpClient(), null); capabilities = negotiateVersion(); if (capabilities == null) { throw new ServiceException("Unable to retrieve or parse Capabilities document."); } else if (capabilities != null) { setupSpecification(capabilities); } } /** * @throws IOException * @throws ServiceException * @deprecated use {@link #AbstractWPS(OWSConfig)} */ public AbstractWPS(final URL serverURL, int requestTimeout) throws ServiceException, IOException { this(serverURL, new SimpleHttpClient(), null); this.httpClient.setConnectTimeout(requestTimeout); this.httpClient.setReadTimeout(requestTimeout); } /** * @deprecated use {@link #AbstractWPS(OWSConfig, Capabilities)} */ public AbstractWPS(C capabilties, URL serverURL) throws ServiceException, IOException { this(serverURL, new SimpleHttpClient(), capabilties); } public AbstractWPS(final URL serverURL, final HTTPClient httpClient, final C capabilities) throws ServiceException, IOException { if (serverURL == null) { throw new NullPointerException("serverURL"); } if (httpClient == null) { throw new NullPointerException("httpClient"); } this.serverURL = serverURL; this.httpClient = httpClient; setupSpecifications(); if (capabilities != null) { setupSpecification(capabilities); this.capabilities = capabilities; } else { this.capabilities = negotiateVersion(); if (this.capabilities == null) { throw new ServiceException("Unable to retrieve or parse Capabilities document."); } setupSpecification(this.capabilities); } } /** * @param capabilities */ private void setupSpecification(final C capabilities) { for (int i = 0; i < specs.length; i++) { if (specs[i].getVersion().equals(capabilities.getVersion())) { specification = specs[i]; break; } } if (specification == null) { specification = specs[specs.length - 1]; LOGGER.warning("Unable to choose a specification based on cached capabilities. " + "Arbitrarily choosing spec '" + specification.getVersion() + "'."); } } public void setHttpClient(HTTPClient httpClient) { this.httpClient = httpClient; } public HTTPClient getHTTPClient() { return this.httpClient; } /** * Description of this service. * <p> * Provides a very quick description of the service, for more information * please review the capabilitie document. * <p> * @return description of this service. */ public ServiceInfo getInfo() { synchronized (capabilities) { if (info == null) { info = createInfo(); } return info; } } /** * Implemented by a subclass to describe service * @return ServiceInfo */ protected abstract ServiceInfo createInfo(); public ResourceInfo getInfo(R resource) { synchronized (capabilities) { if (!resourceInfo.containsKey(resource)) { resourceInfo.put(resource, createInfo(resource)); } } return resourceInfo.get(resource); } protected abstract ResourceInfo createInfo(R resource); private void syncrhonized(Capabilities capabilities2) { // TODO Auto-generated method stub } /** * Sets up the specifications/versions that this server is capable of * communicating with. */ protected abstract void setupSpecifications(); /** * <p> * Version number negotiation occurs as follows (credit OGC): * <ul> * <li><b>1) </b> If the server implements the requested version number, the server shall send that version.</li> * <li><b>2a) </b> If a version unknown to the server is requested, the server shall send the highest version less * than the requested version.</li> * <li><b>2b) </b> If the client request is for a version lower than any of those known to the server, then the * server shall send the lowest version it knows.</li> * <li><b>3a) </b> If the client does not understand the new version number sent by the server, it may either cease * communicating with the server or send a new request with a new version number that the client does understand but * which is less than that sent by the server (if the server had responded with a lower version).</li> * <li><b>3b) </b> If the server had responded with a higher version (because the request was for a version lower * than any known to the server), and the client does not understand the proposed higher version, then the client * may send a new request with a version number higher than that sent by the server.</li> * </ul> * </p> * <p> * The OGC tells us to repeat this process (or give up). This means we are * actually going to come up with a bit of setup cost in figuring out our * GetCapabilities request. This means that it is possible that we may make * multiple requests before being satisfied with a response. * * Also, if we are unable to parse a given version for some reason, * for example, malformed XML, we will request a lower version until * we have run out of versions to request with. Thus, a server that does * not play nicely may take some time to parse and might not even * succeed. * * @return a capabilities object that represents the Capabilities on the server * @throws IOException if there is an error communicating with the server, or the XML cannot be parsed * @throws ServiceException if the server returns a ServiceException */ protected C negotiateVersion() throws IOException, ServiceException { List versions = new ArrayList(specs.length); Exception exception = null; for (int i = 0; i < specs.length; i++) { versions.add(i, specs[i].getVersion()); } int minClient = 0; int maxClient = specs.length - 1; int test = maxClient; while ((minClient <= test) && (test <= maxClient)) { Specification tempSpecification = specs[test]; String clientVersion = tempSpecification.getVersion(); GetCapabilitiesRequest request = tempSpecification.createGetCapabilitiesRequest(serverURL); // Grab document C tempCapabilities; try { tempCapabilities = (C) issueRequest(request).getCapabilities(); } catch (ServiceException e) { tempCapabilities = null; exception = e; } int compare = -1; String serverVersion = clientVersion; // Ignored if caps is null if (tempCapabilities != null) { serverVersion = tempCapabilities.getVersion(); compare = serverVersion.compareTo(clientVersion); } if (compare == 0) { // we have an exact match and have capabilities as well! this.specification = tempSpecification; return tempCapabilities; } if ((tempCapabilities != null) && versions.contains(serverVersion)) { // we can communicate with this server int index = versions.indexOf(serverVersion); this.specification = specs[index]; return tempCapabilities; } else if (compare < 0) { // server responded lower then we asked - and we don't understand. maxClient = test - 1; // set current version as limit // lets try and go one lower? // clientVersion = before(versions, serverVersion); if (clientVersion == null) { if (exception != null) { if (exception instanceof ServiceException) { throw (ServiceException) exception; } IOException e = new IOException(exception.getMessage()); throw e; } return null; // do not know any lower version numbers } test = versions.indexOf(clientVersion); } else { // server responsed higher than we asked - and we don't understand minClient = test + 1; // set current version as lower limit // lets try and go one higher clientVersion = after(versions, serverVersion); if (clientVersion == null) { if (exception != null) { if (exception instanceof ServiceException) { throw (ServiceException) exception; } IOException e = new IOException(exception.getMessage()); throw e; } return null; // do not know any lower version numbers } test = versions.indexOf(clientVersion); } } // could not talk to this server if (exception != null) { IOException e = new IOException(exception.getMessage()); throw e; } return null; } /** * Utility method returning the known version, just before the provided version * * @param known List<String> of all known versions * @param version the boundary condition * @return the version just below the provided boundary version */ String before(List known, String version) { if (known.isEmpty()) { return null; } String before = null; for (Iterator i = known.iterator(); i.hasNext();) { String test = (String) i.next(); if (test.compareTo(version) < 0) { if ((before == null) || (before.compareTo(test) < 0)) { before = test; } } } return before; } /** * Utility method returning the known version, just after the provided version * * @param known a List<String> of all known versions * @param version the boundary condition * @return a version just after the provided boundary condition */ String after(List known, String version) { if (known.isEmpty()) { return null; } String after = null; for (Iterator i = known.iterator(); i.hasNext();) { String test = (String) i.next(); if (test.compareTo(version) > 0) { if ((after == null) || (after.compareTo(test) < 0)) { after = test; } } } return after; } /** * Issues a request to the server and returns that server's response. It * asks the server to send the response gzipped to provide a faster transfer * time. * * @param request the request to be issued * @return a response from the server, which is created according to the specific Request * @throws IOException if there was a problem communicating with the server * @throws ServiceException if the server responds with an exception or returns bad content */ protected Response internalIssueRequest(Request request) throws IOException, ServiceException { final URL finalURL = request.getFinalURL(); final HTTPResponse httpResponse; if (request.requiresPost()) { final String postContentType = request.getPostContentType(); ByteArrayOutputStream out = new ByteArrayOutputStream(); request.performPostOutput(out); InputStream in = new ByteArrayInputStream(out.toByteArray()); try { httpResponse = httpClient.post(finalURL, in, postContentType); } finally { in.close(); } } else { httpResponse = httpClient.get(finalURL); } final Response response = request.createResponse(httpResponse); return response; } public AbstractWPSGetCapabilitiesResponse issueRequest(GetCapabilitiesRequest request) throws IOException, ServiceException { return (AbstractWPSGetCapabilitiesResponse) internalIssueRequest(request); } public void setLoggingLevel(Level newLevel) { LOGGER.setLevel(newLevel); } }