001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2004-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.data.ows;
017:
018: import java.io.BufferedReader;
019: import java.io.ByteArrayInputStream;
020: import java.io.ByteArrayOutputStream;
021: import java.io.IOException;
022: import java.io.InputStream;
023: import java.io.InputStreamReader;
024: import java.io.OutputStream;
025: import java.io.PrintStream;
026: import java.net.HttpURLConnection;
027: import java.net.URL;
028: import java.util.ArrayList;
029: import java.util.Iterator;
030: import java.util.List;
031: import java.util.logging.Level;
032: import java.util.logging.Logger;
033: import java.util.zip.GZIPInputStream;
034:
035: import org.geotools.ows.ServiceException;
036:
037: /**
038: * This abstract class provides a building block for one to implement an
039: * Open Web Service (OWS) client. Each OWS is usually defined by an OGC
040: * specification, available at <a href="http://www.opengeospatial.org">http://www.opengeospatial.org</a>.
041: *
042: * This class provides version negotiation, Capabilities document retrieval,
043: * and a request/response infrastructure. Implementing subclasses need to
044: * provide their own Specifications (representing versions of the OWS to be
045: * implemented) and their own request/response instances.
046: *
047: * @author Richard Gould
048: *
049: */
050: public abstract class AbstractOpenWebService {
051: protected final URL serverURL;
052: protected Capabilities capabilities;
053:
054: /** Contains the specifications that are to be used with this service */
055: protected Specification[] specs;
056: protected Specification specification;
057:
058: private static final Logger LOGGER = org.geotools.util.logging.Logging
059: .getLogger("org.geotools.data.ows");
060:
061: /**
062: * Set up the specifications used and retrieve the Capabilities document
063: * given by serverURL.
064: *
065: * @param serverURL a URL that points to the capabilities document of a server
066: * @throws IOException if there is an error communicating with the server
067: * @throws ServiceException if the server responds with an error
068: */
069: public AbstractOpenWebService(final URL serverURL)
070: throws IOException, ServiceException {
071: if (serverURL == null) {
072: throw new NullPointerException("ServerURL cannot be null");
073: }
074:
075: this .serverURL = serverURL;
076:
077: setupSpecifications();
078:
079: capabilities = negotiateVersion();
080: if (capabilities == null) {
081: throw new ServiceException(
082: "Unable to retrieve or parse Capabilities document.");
083: }
084: }
085:
086: public AbstractOpenWebService(Capabilities capabilties,
087: URL serverURL) {
088: if (capabilties == null) {
089: throw new NullPointerException(
090: "Capabilities cannot be null.");
091: }
092:
093: if (serverURL == null) {
094: throw new NullPointerException("ServerURL cannot be null");
095: }
096:
097: setupSpecifications();
098:
099: for (int i = 0; i < specs.length; i++) {
100: if (specs[i].getVersion().equals(capabilties.getVersion())) {
101: specification = specs[i];
102: break;
103: }
104: }
105:
106: if (specification == null) {
107: specification = specs[specs.length - 1];
108: LOGGER
109: .warning("Unable to choose a specification based on cached capabilities. "
110: + "Arbitrarily choosing spec '"
111: + specification.getVersion() + "'.");
112: }
113:
114: this .serverURL = serverURL;
115: this .capabilities = capabilties;
116: }
117:
118: /**
119: * Sets up the specifications/versions that this server is capable of
120: * communicating with.
121: */
122: protected abstract void setupSpecifications();
123:
124: /**
125: * <p>
126: * Version number negotiation occurs as follows (credit OGC):
127: * <ul>
128: * <li><b>1) </b> If the server implements the requested version number, the server shall send that version.</li>
129: * <li><b>2a) </b> If a version unknown to the server is requested, the server shall send the highest version less
130: * than the requested version.</li>
131: * <li><b>2b) </b> If the client request is for a version lower than any of those known to the server, then the
132: * server shall send the lowest version it knows.</li>
133: * <li><b>3a) </b> If the client does not understand the new version number sent by the server, it may either cease
134: * communicating with the server or send a new request with a new version number that the client does understand but
135: * which is less than that sent by the server (if the server had responded with a lower version).</li>
136: * <li><b>3b) </b> If the server had responded with a higher version (because the request was for a version lower
137: * than any known to the server), and the client does not understand the proposed higher version, then the client
138: * may send a new request with a version number higher than that sent by the server.</li>
139: * </ul>
140: * </p>
141: * <p>
142: * The OGC tells us to repeat this process (or give up). This means we are
143: * actually going to come up with a bit of setup cost in figuring out our
144: * GetCapabilities request. This means that it is possible that we may make
145: * multiple requests before being satisfied with a response.
146: *
147: * Also, if we are unable to parse a given version for some reason,
148: * for example, malformed XML, we will request a lower version until
149: * we have run out of versions to request with. Thus, a server that does
150: * not play nicely may take some time to parse and might not even
151: * succeed.
152: *
153: * @return a capabilities object that represents the Capabilities on the server
154: * @throws IOException if there is an error communicating with the server, or the XML cannot be parsed
155: * @throws ServiceException if the server returns a ServiceException
156: */
157: protected Capabilities negotiateVersion() throws IOException,
158: ServiceException {
159: List versions = new ArrayList(specs.length);
160: Exception exception = null;
161:
162: for (int i = 0; i < specs.length; i++) {
163: versions.add(i, specs[i].getVersion());
164: }
165:
166: int minClient = 0;
167: int maxClient = specs.length - 1;
168:
169: int test = maxClient;
170:
171: while ((minClient <= test) && (test <= maxClient)) {
172: Specification tempSpecification = specs[test];
173: String clientVersion = tempSpecification.getVersion();
174:
175: GetCapabilitiesRequest request = tempSpecification
176: .createGetCapabilitiesRequest(serverURL);
177:
178: //Grab document
179: Capabilities tempCapabilities;
180: try {
181: tempCapabilities = issueRequest(request)
182: .getCapabilities();
183: } catch (ServiceException e) {
184: tempCapabilities = null;
185: exception = e;
186: }
187:
188: int compare = -1;
189: String serverVersion = clientVersion; //Ignored if caps is null
190:
191: if (tempCapabilities != null) {
192:
193: serverVersion = tempCapabilities.getVersion();
194:
195: compare = serverVersion.compareTo(clientVersion);
196: }
197:
198: if (compare == 0) {
199: //we have an exact match and have capabilities as well!
200: this .specification = tempSpecification;
201:
202: return tempCapabilities;
203: }
204:
205: if (tempCapabilities != null
206: && versions.contains(serverVersion)) {
207: // we can communicate with this server
208: int index = versions.indexOf(serverVersion);
209: this .specification = specs[index];
210:
211: return tempCapabilities;
212:
213: } else if (compare < 0) {
214: // server responded lower then we asked - and we don't understand.
215: maxClient = test - 1; // set current version as limit
216:
217: // lets try and go one lower?
218: //
219: clientVersion = before(versions, serverVersion);
220:
221: if (clientVersion == null) {
222: if (exception != null) {
223: if (exception instanceof ServiceException) {
224: throw (ServiceException) exception;
225: }
226: IOException e = new IOException(exception
227: .getMessage());
228: throw e;
229: }
230: return null; // do not know any lower version numbers
231: }
232:
233: test = versions.indexOf(clientVersion);
234: } else {
235: // server responsed higher than we asked - and we don't understand
236: minClient = test + 1; // set current version as lower limit
237:
238: // lets try and go one higher
239: clientVersion = after(versions, serverVersion);
240:
241: if (clientVersion == null) {
242: if (exception != null) {
243: if (exception instanceof ServiceException) {
244: throw (ServiceException) exception;
245: }
246: IOException e = new IOException(exception
247: .getMessage());
248: throw e;
249: }
250: return null; // do not know any lower version numbers
251: }
252:
253: test = versions.indexOf(clientVersion);
254: }
255: }
256:
257: // could not talk to this server
258: if (exception != null) {
259: IOException e = new IOException(exception.getMessage());
260: throw e;
261: }
262: return null;
263: }
264:
265: /**
266: * Utility method returning the known version, just before the provided version
267: *
268: * @param known List<String> of all known versions
269: * @param version the boundary condition
270: * @return the version just below the provided boundary version
271: */
272: String before(List known, String version) {
273: if (known.isEmpty()) {
274: return null;
275: }
276:
277: String before = null;
278:
279: for (Iterator i = known.iterator(); i.hasNext();) {
280: String test = (String) i.next();
281:
282: if (test.compareTo(version) < 0) {
283:
284: if ((before == null) || (before.compareTo(test) < 0)) {
285: before = test;
286: }
287: }
288: }
289:
290: return before;
291: }
292:
293: /**
294: * Utility method returning the known version, just after the provided version
295: *
296: * @param known a List<String> of all known versions
297: * @param version the boundary condition
298: * @return a version just after the provided boundary condition
299: */
300: String after(List known, String version) {
301: if (known.isEmpty()) {
302: return null;
303: }
304:
305: String after = null;
306:
307: for (Iterator i = known.iterator(); i.hasNext();) {
308: String test = (String) i.next();
309:
310: if (test.compareTo(version) > 0) {
311: if ((after == null) || (after.compareTo(test) < 0)) {
312: after = test;
313: }
314: }
315: }
316:
317: return after;
318: }
319:
320: /**
321: * Issues a request to the server and returns that server's response. It
322: * asks the server to send the response gzipped to provide a faster transfer
323: * time.
324: *
325: * @param request the request to be issued
326: * @return a response from the server, which is created according to the specific Request
327: * @throws IOException if there was a problem communicating with the server
328: * @throws ServiceException if the server responds with an exception or returns bad content
329: */
330: protected Response internalIssueRequest(Request request)
331: throws IOException, ServiceException {
332: URL finalURL = request.getFinalURL();
333:
334: HttpURLConnection connection = (HttpURLConnection) finalURL
335: .openConnection();
336:
337: connection.addRequestProperty("Accept-Encoding", "gzip");
338:
339: if (request.requiresPost()) {
340: connection.setRequestMethod("POST");
341: connection.setDoOutput(true);
342: connection.setRequestProperty("Content-type", request
343: .getPostContentType());
344:
345: OutputStream outputStream = connection.getOutputStream();
346:
347: if (LOGGER.isLoggable(Level.FINE)) {
348: ByteArrayOutputStream out = new ByteArrayOutputStream();
349: request.performPostOutput(out);
350:
351: InputStream in = new ByteArrayInputStream(out
352: .toByteArray());
353: BufferedReader reader = new BufferedReader(
354: new InputStreamReader(in));
355:
356: PrintStream stream = new PrintStream(outputStream);
357:
358: String postText = "";
359:
360: while (reader.ready()) {
361: String input = reader.readLine();
362: postText = postText + input;
363: stream.println(input);
364: }
365: LOGGER.fine(postText);
366: System.out.println(postText);
367:
368: out.close();
369: in.close();
370: } else {
371: request.performPostOutput(outputStream);
372: }
373:
374: outputStream.flush();
375: outputStream.close();
376: } else {
377: connection.setRequestMethod("GET");
378: }
379:
380: InputStream inputStream = connection.getInputStream();
381:
382: if (connection.getContentEncoding() != null
383: && connection.getContentEncoding().indexOf("gzip") != -1) { //$NON-NLS-1$
384: inputStream = new GZIPInputStream(inputStream);
385: }
386:
387: String contentType = connection.getContentType();
388:
389: return request.createResponse(contentType, inputStream);
390: }
391:
392: public GetCapabilitiesResponse issueRequest(
393: GetCapabilitiesRequest request) throws IOException,
394: ServiceException {
395: return (GetCapabilitiesResponse) internalIssueRequest(request);
396: }
397:
398: public void setLoggingLevel(Level newLevel) {
399: LOGGER.setLevel(newLevel);
400: }
401: }
|