1   /**
2    * Distribution License:
3    * BibleDesktop is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: WebResource.java 2099 2011-03-07 17:13:00Z dmsmith $
21   */
22  package org.crosswire.common.util;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.net.ProxySelector;
28  import java.net.URI;
29  import java.util.Date;
30  
31  import org.apache.http.Header;
32  import org.apache.http.HttpEntity;
33  import org.apache.http.HttpHost;
34  import org.apache.http.HttpResponse;
35  import org.apache.http.HttpStatus;
36  import org.apache.http.StatusLine;
37  import org.apache.http.client.HttpClient;
38  import org.apache.http.client.methods.HttpGet;
39  import org.apache.http.client.methods.HttpHead;
40  import org.apache.http.client.methods.HttpRequestBase;
41  import org.apache.http.conn.params.ConnRouteParams;
42  import org.apache.http.impl.client.AbstractHttpClient;
43  import org.apache.http.impl.client.DefaultHttpClient;
44  import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
45  import org.apache.http.params.HttpConnectionParams;
46  import org.apache.http.params.HttpParams;
47  import org.crosswire.common.progress.Progress;
48  import org.crosswire.jsword.JSMsg;
49  
50  /**
51   * A WebResource is backed by an URL and potentially the proxy through which it
52   * need go. It can get basic information about the resource and it can get the
53   * resource. The requests are subject to a timeout, which can be set via the
54   * constructor or previously by a call to set the default timeout. The initial
55   * default timeout is 750 milliseconds.
56   * 
57   * 
58   * @see gnu.lgpl.License for license details.<br>
59   *      The copyright to this program is held by it's authors.
60   * @author DM Smith [dmsmith555 at yahoo dot com]
61   */
62  public class WebResource {
63      /**
64       * Construct a WebResource for the given URL, while timing out if too much
65       * time has passed.
66       * 
67       * @param theURI
68       *            the Resource to get via HTTP
69       */
70      public WebResource(URI theURI) {
71          this(theURI, null, null, timeout);
72      }
73  
74      /**
75       * Construct a WebResource for the given URL, while timing out if too much
76       * time has passed.
77       * 
78       * @param theURI
79       *            the Resource to get via HTTP
80       * @param theTimeout
81       *            the length of time in milliseconds to allow a connection to
82       *            respond before timing out
83       */
84      public WebResource(URI theURI, int theTimeout) {
85          this(theURI, null, null, theTimeout);
86      }
87  
88      /**
89       * Construct a WebResource for the given URL, going through the optional
90       * proxy and default port, while timing out if too much time has passed.
91       * 
92       * @param theURI
93       *            the Resource to get via HTTP
94       * @param theProxyHost
95       *            the proxy host or null
96       */
97      public WebResource(URI theURI, String theProxyHost) {
98          this(theURI, theProxyHost, null, timeout);
99      }
100 
101     /**
102      * Construct a WebResource for the given URL, going through the optional
103      * proxy and default port, while timing out if too much time has passed.
104      * 
105      * @param theURI
106      *            the Resource to get via HTTP
107      * @param theProxyHost
108      *            the proxy host or null
109      * @param theTimeout
110      *            the length of time in milliseconds to allow a connection to
111      *            respond before timing out
112      */
113     public WebResource(URI theURI, String theProxyHost, int theTimeout) {
114         this(theURI, theProxyHost, null, theTimeout);
115     }
116 
117     /**
118      * Construct a WebResource for the given URL, going through the optional
119      * proxy and port, while timing out if too much time has passed.
120      * 
121      * @param theURI
122      *            the Resource to get via HTTP
123      * @param theProxyHost
124      *            the proxy host or null
125      * @param theProxyPort
126      *            the proxy port or null, where null means use the standard port
127      */
128     public WebResource(URI theURI, String theProxyHost, Integer theProxyPort) {
129         this(theURI, theProxyHost, theProxyPort, timeout);
130     }
131 
132     /**
133      * Construct a WebResource for the given URL, going through the optional
134      * proxy and port, while timing out if too much time has passed.
135      * 
136      * @param theURI
137      *            the Resource to get via HTTP
138      * @param theProxyHost
139      *            the proxy host or null
140      * @param theProxyPort
141      *            the proxy port or null, where null means use the standard port
142      * @param theTimeout
143      *            the length of time in milliseconds to allow a connection to
144      *            respond before timing out
145      */
146     public WebResource(URI theURI, String theProxyHost, Integer theProxyPort, int theTimeout) {
147         uri = theURI;
148         client = new DefaultHttpClient();
149         HttpParams params = client.getParams();
150 
151         // Allowable time between packets
152         HttpConnectionParams.setSoTimeout(params, theTimeout);
153         // Allowable time to get a connection
154         HttpConnectionParams.setConnectionTimeout(params, theTimeout);
155 
156         // Configure proxy info if necessary and defined
157         if (theProxyHost != null && theProxyHost.length() > 0) {
158             // Configure the host and port
159             HttpHost proxy = new HttpHost(theProxyHost, theProxyPort == null ? -1 : theProxyPort.intValue());
160             ConnRouteParams.setDefaultProxy(params, proxy);
161         }
162         ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
163                 client.getConnectionManager().getSchemeRegistry(),
164                 ProxySelector.getDefault());
165                 ((AbstractHttpClient) client).setRoutePlanner(routePlanner);
166     }
167 
168     /**
169      * When this WebResource is no longer needed it should be shutdown to return
170      * underlying resources back to the OS.
171      */
172     public void shutdown() {
173         client.getConnectionManager().shutdown();
174     }
175 
176     /**
177      * @return the timeout in milliseconds
178      */
179     public static int getTimeout() {
180         return timeout;
181     }
182 
183     /**
184      * @param timeout
185      *            the timeout to set in milliseconds
186      */
187     public static void setTimeout(int timeout) {
188         WebResource.timeout = timeout;
189     }
190 
191     /**
192      * Determine the size of this WebResource.
193      * <p>
194      * Note that the http client may read the entire file to determine this.
195      * </p>
196      * 
197      * @return the size of the file
198      */
199     public int getSize() {
200         HttpRequestBase method = new HttpHead(uri);
201         HttpResponse response = null;
202         try {
203             // Execute the method.
204             response = client.execute(method);
205             StatusLine statusLine = response.getStatusLine();
206             if (statusLine.getStatusCode() == HttpStatus.SC_OK) {
207                 return getHeaderAsInt(response, "Content-Length");
208             }
209             String reason = response.getStatusLine().getReasonPhrase();
210             // TRANSLATOR: Common error condition: {0} is a placeholder for the
211             // URL of what could not be found.
212             Reporter.informUser(this, JSMsg.gettext("Unable to find: {0}", reason + ':' + uri.getPath()));
213         } catch (IOException e) {
214             return 0;
215         }
216         return 0;
217     }
218 
219     /**
220      * Determine the last modified date of this WebResource.
221      * <p>
222      * Note that the http client may read the entire file.
223      * </p>
224      * 
225      * @return the last mod date of the file
226      */
227     public long getLastModified() {
228         HttpRequestBase method = new HttpHead(uri);
229         HttpResponse response = null;
230         try {
231             // Execute the method.
232             response = client.execute(method);
233             StatusLine statusLine = response.getStatusLine();
234             if (statusLine.getStatusCode() == HttpStatus.SC_OK) {
235                 return getHeaderAsDate(response, "Last-Modified");
236             }
237             String reason = response.getStatusLine().getReasonPhrase();
238             // TRANSLATOR: Common error condition: {0} is a placeholder for the
239             // URL of what could not be found.
240             Reporter.informUser(this, JSMsg.gettext("Unable to find: {0}", reason + ':' + uri.getPath()));
241         } catch (IOException e) {
242             return new Date().getTime();
243         }
244         return new Date().getTime();
245     }
246 
247     /**
248      * Copy this WebResource to the destination and report progress.
249      * 
250      * @param dest
251      *            the URI of the destination, typically a file:///.
252      * @param meter
253      *            the job on which to report progress
254      * @throws LucidException
255      */
256     public void copy(URI dest, Progress meter) throws LucidException  {
257         InputStream in = null;
258         OutputStream out = null;
259         HttpRequestBase method = new HttpGet(uri);
260         HttpResponse response = null;
261         HttpEntity entity = null;
262         try {
263             // Execute the method.
264             response = client.execute(method);
265             // Initialize the meter, if present
266             if (meter != null) {
267                 // Find out how big it is
268                 int size = getHeaderAsInt(response, "Content-Length");
269                 // Sometimes the Content-Length is not given and we have to grab it via HEAD method
270                 if (size == 0) {
271                     size = getSize();
272                 }
273                 meter.setTotalWork(size);
274             }
275 
276             entity = response.getEntity();
277             if (entity != null) {
278                 in = entity.getContent();
279 
280                 // Download the index file
281                 out = NetUtil.getOutputStream(dest);
282 
283                 byte[] buf = new byte[4096];
284                 int count = in.read(buf);
285                 while (-1 != count) {
286                     if (meter != null) {
287                         meter.incrementWorkDone(count);
288                     }
289                     out.write(buf, 0, count);
290                     count = in.read(buf);
291                 }
292             } else {
293                 String reason = response.getStatusLine().getReasonPhrase();
294                 // TRANSLATOR: Common error condition: {0} is a placeholder for
295                 // the URL of what could not be found.
296                 Reporter.informUser(this, JSMsg.gettext("Unable to find: {0}", reason + ':' + uri.getPath()));
297             }
298         } catch (IOException e) {
299             // TRANSLATOR: Common error condition: {0} is a placeholder for the
300             // URL of what could not be found.
301             throw new LucidException(JSMsg.gettext("Unable to find: {0}", uri.toString()), e);
302         } finally {
303             // Close the streams
304             IOUtil.close(in);
305             IOUtil.close(out);
306         }
307     }
308 
309     /**
310      * Copy this WebResource to the destination.
311      * 
312      * @param dest
313      * @throws LucidException
314      */
315     public void copy(URI dest) throws LucidException {
316         copy(dest, null);
317     }
318 
319     /**
320      * Get the field as a long.
321      * 
322      * @param response The response from the request
323      * @param field the header field to check
324      * @return the int value for the field
325      */
326     private int getHeaderAsInt(HttpResponse response, String field) {
327         Header header = response.getFirstHeader(field);
328         String value = header.getValue();
329         try {
330             return Integer.parseInt(value);
331         } catch (NumberFormatException ex) {
332             return 0;
333         }
334     }
335 
336     /**
337      * Get the number of seconds since start of epoch for the field in the response headers as a Date.
338      * 
339      * @param response The response from the request
340      * @param field the header field to check
341      * @return number of seconds since start of epoch
342      */
343     @SuppressWarnings("deprecation")
344     private long getHeaderAsDate(HttpResponse response, String field) {
345         Header header = response.getFirstHeader(field);
346         String value = header.getValue();
347         try {
348             // This date cannot be readily parsed with DateFormatter
349             return Date.parse(value);
350         } catch (IllegalArgumentException ex) {
351             return 0;
352         }
353     }
354     /**
355      * Define a 750 ms timeout to get a connection
356      */
357     private static int timeout = 750;
358 
359     private URI uri;
360     private HttpClient client;
361 }
362