/*
* 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.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
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 java.util.zip.GZIPInputStream;
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 an
* Open Web Service (OWS) client. Each OWS is usually defined by an OGC
* specification, available at <a href="http://www.opengeospatial.org">http://www.opengeospatial.org</a>.
*
* This class provides version negotiation, Capabilities document retrieval,
* and a request/response infrastructure. Implementing subclasses need to
* provide their own Specifications (representing versions of the OWS to be
* implemented) and their own request/response instances.
*
* @author Richard Gould
*
* @source $URL$
*/
public abstract class AbstractOpenWebService<C extends Capabilities, R extends Object> {
/** Define a interval to wait a server response */
protected int requestTimeout = 30000; // 30 seconds
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;
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.ows");
private static final int DEFAULT_TIMEOUT = 0;
/**
* 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 AbstractOpenWebService(final URL serverURL) throws IOException, ServiceException {
this( serverURL, DEFAULT_TIMEOUT );
}
public AbstractOpenWebService(final URL serverURL, int requestTimeout) throws IOException,
ServiceException {
if (serverURL == null) {
throw new NullPointerException("ServerURL cannot be null");
}
this.serverURL = serverURL;
this.requestTimeout = requestTimeout;
setupSpecifications();
capabilities = negotiateVersion();
if (capabilities == null) {
throw new ServiceException("Unable to retrieve or parse Capabilities document.");
}
}
public AbstractOpenWebService(C capabilties, URL serverURL) {
if (capabilties == null) {
throw new NullPointerException("Capabilities cannot be null.");
}
if (serverURL == null) {
throw new NullPointerException("ServerURL cannot be null");
}
setupSpecifications();
for (int i = 0; i < specs.length; i++) {
if (specs[i].getVersion().equals(capabilties.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()+"'.");
}
this.serverURL = serverURL;
this.capabilities = capabilties;
}
/**
* 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 && capabilities != 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 {
URL finalURL = request.getFinalURL();
HttpURLConnection connection = (HttpURLConnection) finalURL.openConnection();
connection.addRequestProperty("Accept-Encoding", "gzip");
connection.setConnectTimeout(this.requestTimeout);
if (request.requiresPost()) {
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-type", request.getPostContentType());
OutputStream outputStream = connection.getOutputStream();
if (LOGGER.isLoggable(Level.FINE)) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
request.performPostOutput(out);
InputStream in = new ByteArrayInputStream(out.toByteArray());
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
PrintStream stream = new PrintStream(outputStream);
String postText = "";
while (reader.ready()) {
String input = reader.readLine();
postText = postText + input;
stream.println(input);
}
LOGGER.fine(postText);
System.out.println(postText);
out.close();
in.close();
} else {
request.performPostOutput(outputStream);
}
outputStream.flush();
outputStream.close();
} else {
connection.setRequestMethod("GET");
}
InputStream inputStream = connection.getInputStream();
if (connection.getContentEncoding() != null && connection.getContentEncoding().indexOf("gzip") != -1) { //$NON-NLS-1$
inputStream = new GZIPInputStream(inputStream);
}
String contentType = connection.getContentType();
return request.createResponse(contentType, inputStream);
}
public GetCapabilitiesResponse issueRequest(GetCapabilitiesRequest request) throws IOException, ServiceException {
return (GetCapabilitiesResponse) internalIssueRequest(request);
}
public void setLoggingLevel(Level newLevel) {
LOGGER.setLevel(newLevel);
}
}