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