source: josm/trunk/src/org/openstreetmap/josm/tools/HttpClient.java@ 9174

Last change on this file since 9174 was 9174, checked in by simon04, 8 years ago

see #12231 - Update unit tests, fix regression

File size: 15.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedOutputStream;
7import java.io.BufferedReader;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.OutputStream;
11import java.net.HttpURLConnection;
12import java.net.URL;
13import java.util.List;
14import java.util.Map;
15import java.util.Scanner;
16import java.util.TreeMap;
17import java.util.regex.Matcher;
18import java.util.regex.Pattern;
19import java.util.zip.GZIPInputStream;
20
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.data.Version;
23import org.openstreetmap.josm.io.Compression;
24import org.openstreetmap.josm.io.UTFInputStreamReader;
25
26/**
27 * Provides a uniform access for a HTTP/HTTPS server. This class should be used in favour of {@link HttpURLConnection}.
28 */
29public final class HttpClient {
30
31 private URL url;
32 private final String requestMethod;
33 private int connectTimeout = Main.pref.getInteger("socket.timeout.connect", 15) * 1000;
34 private int readTimeout = Main.pref.getInteger("socket.timeout.read", 30) * 1000;
35 private byte[] requestBody;
36 private long ifModifiedSince;
37 private final Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
38 private int maxRedirects = Main.pref.getInteger("socket.maxredirects", 5);
39 private boolean useCache;
40 private String reasonForRequest;
41
42 private HttpClient(URL url, String requestMethod) {
43 this.url = url;
44 this.requestMethod = requestMethod;
45 this.headers.put("Accept-Encoding", "gzip");
46 }
47
48 public Response connect() throws IOException {
49 final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
50 connection.setRequestMethod(requestMethod);
51 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
52 connection.setConnectTimeout(connectTimeout);
53 connection.setReadTimeout(readTimeout);
54 connection.setInstanceFollowRedirects(maxRedirects > 0);
55 if (ifModifiedSince > 0) {
56 connection.setIfModifiedSince(ifModifiedSince);
57 }
58 connection.setUseCaches(useCache);
59 if (!useCache) {
60 connection.setRequestProperty("Cache-Control", "no-cache");
61 }
62 for (Map.Entry<String, String> header : headers.entrySet()) {
63 if (header.getValue() != null) {
64 connection.setRequestProperty(header.getKey(), header.getValue());
65 }
66 }
67
68 if ("PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod)) {
69 headers.put("Content-Length", String.valueOf(requestBody.length));
70 connection.setDoOutput(true);
71 try (OutputStream out = new BufferedOutputStream(connection.getOutputStream())) {
72 out.write(requestBody);
73 }
74 }
75
76 boolean successfulConnection = false;
77 try {
78 try {
79 connection.connect();
80 if (reasonForRequest != null && "".equalsIgnoreCase(reasonForRequest)) {
81 Main.info("{0} {1} ({2}) -> {3}", requestMethod, url, reasonForRequest, connection.getResponseCode());
82 } else {
83 Main.info("{0} {1} -> {2}", requestMethod, url, connection.getResponseCode());
84 }
85 if (Main.isDebugEnabled()) {
86 Main.debug("RESPONSE: " + connection.getHeaderFields());
87 }
88 } catch (IOException e) {
89 //noinspection ThrowableResultOfMethodCallIgnored
90 Main.addNetworkError(url, Utils.getRootCause(e));
91 throw e;
92 }
93 if (isRedirect(connection.getResponseCode())) {
94 final String redirectLocation = connection.getHeaderField("Location");
95 if (redirectLocation == null) {
96 /* I18n: argument is HTTP response code */
97 String msg = tr("Unexpected response from HTTP server. Got {0} response without ''Location'' header." +
98 " Can''t redirect. Aborting.", connection.getResponseCode());
99 throw new IOException(msg);
100 } else if (maxRedirects > 0) {
101 url = new URL(redirectLocation);
102 maxRedirects--;
103 Main.info(tr("Download redirected to ''{0}''", redirectLocation));
104 return connect();
105 } else if (maxRedirects == 0) {
106 String msg = tr("Too many redirects to the download URL detected. Aborting.");
107 throw new IOException(msg);
108 }
109 }
110 Response response = new Response(connection);
111 successfulConnection = true;
112 return response;
113 } finally {
114 if (!successfulConnection) {
115 connection.disconnect();
116 }
117 }
118 }
119
120 /**
121 * A wrapper for the HTTP response.
122 */
123 public static final class Response {
124 private final HttpURLConnection connection;
125 private final int responseCode;
126 private final String responseMessage;
127 private boolean uncompress;
128 private boolean uncompressAccordingToContentDisposition;
129
130 private Response(HttpURLConnection connection) throws IOException {
131 CheckParameterUtil.ensureParameterNotNull(connection, "connection");
132 this.connection = connection;
133 this.responseCode = connection.getResponseCode();
134 this.responseMessage = connection.getResponseMessage();
135 }
136
137 /**
138 * Sets whether {@link #getContent()} should uncompress the input stream if necessary.
139 *
140 * @param uncompress whether the input stream should be uncompressed if necessary
141 * @return {@code this}
142 */
143 public Response uncompress(boolean uncompress) {
144 this.uncompress = uncompress;
145 return this;
146 }
147
148 public Response uncompressAccordingToContentDisposition(boolean uncompressAccordingToContentDisposition) {
149 this.uncompressAccordingToContentDisposition = uncompressAccordingToContentDisposition;
150 return this;
151 }
152
153 /**
154 * @see HttpURLConnection#getURL()
155 */
156 public URL getURL() {
157 return connection.getURL();
158 }
159
160 /**
161 * @see HttpURLConnection#getRequestMethod()
162 */
163 public String getRequestMethod() {
164 return connection.getRequestMethod();
165 }
166
167 /**
168 * Returns an input stream that reads from this HTTP connection, or,
169 * error stream if the connection failed but the server sent useful data.
170 *
171 * Note: the return value can be null, if both the input and the error stream are null.
172 * Seems to be the case if the OSM server replies a 401 Unauthorized, see #3887
173 *
174 * @see HttpURLConnection#getInputStream()
175 * @see HttpURLConnection#getErrorStream()
176 */
177 public InputStream getContent() throws IOException {
178 InputStream in;
179 try {
180 in = connection.getInputStream();
181 } catch (IOException ioe) {
182 in = connection.getErrorStream();
183 }
184 in = "gzip".equalsIgnoreCase(getContentEncoding()) ? new GZIPInputStream(in) : in;
185 Compression compression = Compression.NONE;
186 if (uncompress) {
187 final String contentType = getContentType();
188 Main.debug("Uncompressing input stream according to Content-Type header: {0}", contentType);
189 compression = Compression.forContentType(contentType);
190 }
191 if (uncompressAccordingToContentDisposition && Compression.NONE.equals(compression)) {
192 final String contentDisposition = getHeaderField("Content-Disposition");
193 final Matcher matcher = Pattern.compile("filename=\"([^\"]+)\"").matcher(contentDisposition);
194 if (matcher.find()) {
195 Main.debug("Uncompressing input stream according to Content-Disposition header: {0}", contentDisposition);
196 compression = Compression.byExtension(matcher.group(1));
197 }
198 }
199 in = compression.getUncompressedInputStream(in);
200 return in;
201 }
202
203 /**
204 * Returns {@link #getContent()} wrapped in a buffered reader.
205 *
206 * Detects Unicode charset in use utilizing {@link UTFInputStreamReader}.
207 */
208 public BufferedReader getContentReader() throws IOException {
209 return new BufferedReader(
210 UTFInputStreamReader.create(getContent())
211 );
212 }
213
214 /**
215 * Fetches the HTTP response as String.
216 * @return the response
217 */
218 public String fetchContent() throws IOException {
219 try (Scanner scanner = new Scanner(getContentReader()).useDelimiter("\\A")) {
220 return scanner.hasNext() ? scanner.next() : "";
221 }
222 }
223
224 /**
225 * Gets the response code from this HTTP connection.
226 *
227 * @see HttpURLConnection#getResponseCode()
228 */
229 public int getResponseCode() {
230 return responseCode;
231 }
232
233 /**
234 * Gets the response message from this HTTP connection.
235 *
236 * @see HttpURLConnection#getResponseMessage()
237 */
238 public String getResponseMessage() {
239 return responseMessage;
240 }
241
242 /**
243 * Returns the {@code Content-Encoding} header.
244 */
245 public String getContentEncoding() {
246 return connection.getContentEncoding();
247 }
248
249 /**
250 * Returns the {@code Content-Type} header.
251 */
252 public String getContentType() {
253 return connection.getHeaderField("Content-Type");
254 }
255
256 /**
257 * Returns the {@code Content-Length} header.
258 */
259 public long getContentLength() {
260 return connection.getContentLengthLong();
261 }
262
263 /**
264 * @see HttpURLConnection#getHeaderField(String)
265 */
266 public String getHeaderField(String name) {
267 return connection.getHeaderField(name);
268 }
269
270 /**
271 * @see HttpURLConnection#getHeaderFields()
272 */
273 public List<String> getHeaderFields(String name) {
274 return connection.getHeaderFields().get(name);
275 }
276
277 /**
278 * @see HttpURLConnection#disconnect()
279 */
280 public void disconnect() {
281 // TODO is this block necessary for disconnecting?
282 // Fix upload aborts - see #263
283 connection.setConnectTimeout(100);
284 connection.setReadTimeout(100);
285 try {
286 Thread.sleep(100);
287 } catch (InterruptedException ex) {
288 Main.warn("InterruptedException in " + getClass().getSimpleName() + " during cancel");
289 }
290
291 connection.disconnect();
292 }
293 }
294
295 /**
296 * Creates a new instance for the given URL and a {@code GET} request
297 *
298 * @param url the URL
299 * @return a new instance
300 */
301 public static HttpClient create(URL url) {
302 return create(url, "GET");
303 }
304
305 /**
306 * Creates a new instance for the given URL and a {@code GET} request
307 *
308 * @param url the URL
309 * @param requestMethod the HTTP request method to perform when calling
310 * @return a new instance
311 */
312 public static HttpClient create(URL url, String requestMethod) {
313 return new HttpClient(url, requestMethod);
314 }
315
316 /**
317 * Returns the URL set for this connection.
318 * @see #create(URL)
319 * @see #create(URL, String)
320 */
321 public URL getURL() {
322 return url;
323 }
324
325 /**
326 * Returns the request method set for this connection.
327 * @see #create(URL, String)
328 */
329 public String getRequestMethod() {
330 return requestMethod;
331 }
332
333 /**
334 * Returns the set value for the given {@code header}.
335 */
336 public String getRequestHeader(String header) {
337 return headers.get(header);
338 }
339
340 /**
341 * Sets whether not to set header {@code Cache-Control=no-cache}
342 *
343 * @param useCache whether not to set header {@code Cache-Control=no-cache}
344 * @return {@code this}
345 * @see HttpURLConnection#setUseCaches(boolean)
346 */
347 public HttpClient useCache(boolean useCache) {
348 this.useCache = useCache;
349 return this;
350 }
351
352 /**
353 * Sets whether not to set header {@code Connection=close}
354 * <p/>
355 * This might fix #7640, see
356 * <a href='https://web.archive.org/web/20140118201501/http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive'>here</a>.
357 *
358 * @param keepAlive whether not to set header {@code Connection=close}
359 * @return {@code this}
360 */
361 public HttpClient keepAlive(boolean keepAlive) {
362 return setHeader("Connection", keepAlive ? null : "close");
363 }
364
365 /**
366 * @return {@code this}
367 * @see HttpURLConnection#setConnectTimeout(int)
368 */
369 public HttpClient setConnectTimeout(int connectTimeout) {
370 this.connectTimeout = connectTimeout;
371 return this;
372 }
373
374 /**
375 * @return {@code this}
376 * @see HttpURLConnection#setReadTimeout(int) (int)
377 */
378
379 public HttpClient setReadTimeout(int readTimeout) {
380 this.readTimeout = readTimeout;
381 return this;
382 }
383
384 /**
385 * Sets the {@code Accept} header.
386 *
387 * @return {@code this}
388 */
389 public HttpClient setAccept(String accept) {
390 return setHeader("Accept", accept);
391 }
392
393 /**
394 * Sets the request body for {@code PUT}/{@code POST} requests.
395 *
396 * @return {@code this}
397 */
398 public HttpClient setRequestBody(byte[] requestBody) {
399 this.requestBody = requestBody;
400 return this;
401 }
402
403 /**
404 * Sets the {@code If-Modified-Since} header.
405 *
406 * @return {@code this}
407 */
408 public HttpClient setIfModifiedSince(long ifModifiedSince) {
409 this.ifModifiedSince = ifModifiedSince;
410 return this;
411 }
412
413 /**
414 * Sets the maximum number of redirections to follow.
415 *
416 * Set {@code maxRedirects} to {@code -1} in order to ignore redirects, i.e.,
417 * to not throw an {@link IOException} in {@link #connect()}.
418 *
419 * @return {@code this}
420 */
421 public HttpClient setMaxRedirects(int maxRedirects) {
422 this.maxRedirects = maxRedirects;
423 return this;
424 }
425
426 /**
427 * Sets an arbitrary HTTP header.
428 *
429 * @return {@code this}
430 */
431 public HttpClient setHeader(String key, String value) {
432 this.headers.put(key, value);
433 return this;
434 }
435
436 /**
437 * Sets arbitrary HTTP headers.
438 *
439 * @return {@code this}
440 */
441 public HttpClient setHeaders(Map<String, String> headers) {
442 this.headers.putAll(headers);
443 return this;
444 }
445
446 /**
447 * Sets a reason to show on console. Can be {@code null} if no reason is given.
448 */
449 public HttpClient setReasonForRequest(String reasonForRequest) {
450 this.reasonForRequest = reasonForRequest;
451 return this;
452 }
453
454 private static boolean isRedirect(final int statusCode) {
455 switch (statusCode) {
456 case HttpURLConnection.HTTP_MOVED_PERM: // 301
457 case HttpURLConnection.HTTP_MOVED_TEMP: // 302
458 case HttpURLConnection.HTTP_SEE_OTHER: // 303
459 case 307: // TEMPORARY_REDIRECT:
460 case 308: // PERMANENT_REDIRECT:
461 return true;
462 default:
463 return false;
464 }
465 }
466
467}
Note: See TracBrowser for help on using the repository browser.