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

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

Checkstyle

File size: 15.4 KB
RevLine 
[9168]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;
[9172]13import java.util.List;
[9168]14import java.util.Map;
[9171]15import java.util.Scanner;
[9172]16import java.util.TreeMap;
17import java.util.regex.Matcher;
18import java.util.regex.Pattern;
[9168]19import java.util.zip.GZIPInputStream;
20
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.data.Version;
[9169]23import org.openstreetmap.josm.io.Compression;
[9171]24import org.openstreetmap.josm.io.UTFInputStreamReader;
[9168]25
26/**
27 * Provides a uniform access for a HTTP/HTTPS server. This class should be used in favour of {@link HttpURLConnection}.
28 */
[9173]29public final class HttpClient {
[9168]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;
[9172]37 private final Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
[9168]38 private int maxRedirects = Main.pref.getInteger("socket.maxredirects", 5);
[9171]39 private boolean useCache;
[9172]40 private String reasonForRequest;
[9168]41
42 private HttpClient(URL url, String requestMethod) {
43 this.url = url;
44 this.requestMethod = requestMethod;
[9172]45 this.headers.put("Accept-Encoding", "gzip");
[9168]46 }
47
48 public Response connect() throws IOException {
49 final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
[9172]50 connection.setRequestMethod(requestMethod);
[9168]51 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
52 connection.setConnectTimeout(connectTimeout);
53 connection.setReadTimeout(readTimeout);
[9172]54 connection.setInstanceFollowRedirects(maxRedirects > 0);
[9168]55 if (ifModifiedSince > 0) {
56 connection.setIfModifiedSince(ifModifiedSince);
57 }
[9171]58 connection.setUseCaches(useCache);
59 if (!useCache) {
60 connection.setRequestProperty("Cache-Control", "no-cache");
61 }
[9168]62 for (Map.Entry<String, String> header : headers.entrySet()) {
[9172]63 if (header.getValue() != null) {
64 connection.setRequestProperty(header.getKey(), header.getValue());
65 }
[9168]66 }
67
[9172]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
[9168]76 boolean successfulConnection = false;
77 try {
78 try {
79 connection.connect();
[9172]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 }
[9168]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();
[9172]105 } else if (maxRedirects == 0) {
[9168]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 */
[9173]123 public static final class Response {
[9168]124 private final HttpURLConnection connection;
125 private final int responseCode;
[9172]126 private final String responseMessage;
[9169]127 private boolean uncompress;
[9172]128 private boolean uncompressAccordingToContentDisposition;
[9168]129
130 private Response(HttpURLConnection connection) throws IOException {
[9172]131 CheckParameterUtil.ensureParameterNotNull(connection, "connection");
[9168]132 this.connection = connection;
133 this.responseCode = connection.getResponseCode();
[9172]134 this.responseMessage = connection.getResponseMessage();
[9168]135 }
136
137 /**
[9169]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
[9172]148 public Response uncompressAccordingToContentDisposition(boolean uncompressAccordingToContentDisposition) {
149 this.uncompressAccordingToContentDisposition = uncompressAccordingToContentDisposition;
150 return this;
151 }
152
[9169]153 /**
[9172]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 /**
[9168]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 *
[9172]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 *
[9168]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 }
[9169]184 in = "gzip".equalsIgnoreCase(getContentEncoding()) ? new GZIPInputStream(in) : in;
185 if (uncompress) {
[9172]186 final String contentType = getContentType();
187 Main.debug("Uncompressing input stream according to Content-Type header: {0}", contentType);
188 in = Compression.forContentType(contentType).getUncompressedInputStream(in);
[9169]189 }
[9172]190 if (uncompressAccordingToContentDisposition) {
191 final String contentDisposition = getHeaderField("Content-Disposition");
192 final Matcher matcher = Pattern.compile("filename=\"([^\"]+)\"").matcher(contentDisposition);
193 if (matcher.find()) {
194 Main.debug("Uncompressing input stream according to Content-Disposition header: {0}", contentDisposition);
195 in = Compression.byExtension(matcher.group(1)).getUncompressedInputStream(in);
196 }
197 }
198 return in;
[9168]199 }
200
201 /**
[9171]202 * Returns {@link #getContent()} wrapped in a buffered reader.
203 *
204 * Detects Unicode charset in use utilizing {@link UTFInputStreamReader}.
[9168]205 */
206 public BufferedReader getContentReader() throws IOException {
[9171]207 return new BufferedReader(
208 UTFInputStreamReader.create(getContent())
209 );
[9168]210 }
211
212 /**
[9171]213 * Fetches the HTTP response as String.
214 * @return the response
215 */
216 public String fetchContent() throws IOException {
[9172]217 try (Scanner scanner = new Scanner(getContentReader()).useDelimiter("\\A")) {
218 return scanner.hasNext() ? scanner.next() : "";
[9171]219 }
220 }
221
222 /**
[9168]223 * Gets the response code from this HTTP connection.
224 *
225 * @see HttpURLConnection#getResponseCode()
226 */
227 public int getResponseCode() {
228 return responseCode;
229 }
230
231 /**
[9172]232 * Gets the response message from this HTTP connection.
233 *
234 * @see HttpURLConnection#getResponseMessage()
235 */
236 public String getResponseMessage() {
237 return responseMessage;
238 }
239
240 /**
[9168]241 * Returns the {@code Content-Encoding} header.
242 */
243 public String getContentEncoding() {
244 return connection.getContentEncoding();
245 }
246
247 /**
248 * Returns the {@code Content-Type} header.
249 */
250 public String getContentType() {
251 return connection.getHeaderField("Content-Type");
252 }
253
254 /**
[9171]255 * Returns the {@code Content-Length} header.
256 */
257 public long getContentLength() {
258 return connection.getContentLengthLong();
259 }
260
261 /**
[9172]262 * @see HttpURLConnection#getHeaderField(String)
263 */
264 public String getHeaderField(String name) {
265 return connection.getHeaderField(name);
266 }
267
268 /**
269 * @see HttpURLConnection#getHeaderFields()
270 */
271 public List<String> getHeaderFields(String name) {
272 return connection.getHeaderFields().get(name);
273 }
274
275 /**
[9168]276 * @see HttpURLConnection#disconnect()
277 */
278 public void disconnect() {
[9172]279 // TODO is this block necessary for disconnecting?
280 // Fix upload aborts - see #263
281 connection.setConnectTimeout(100);
282 connection.setReadTimeout(100);
283 try {
284 Thread.sleep(100);
285 } catch (InterruptedException ex) {
286 Main.warn("InterruptedException in " + getClass().getSimpleName() + " during cancel");
287 }
288
[9168]289 connection.disconnect();
290 }
291 }
292
293 /**
294 * Creates a new instance for the given URL and a {@code GET} request
295 *
296 * @param url the URL
297 * @return a new instance
298 */
299 public static HttpClient create(URL url) {
300 return create(url, "GET");
301 }
302
303 /**
304 * Creates a new instance for the given URL and a {@code GET} request
305 *
[9172]306 * @param url the URL
[9168]307 * @param requestMethod the HTTP request method to perform when calling
308 * @return a new instance
309 */
310 public static HttpClient create(URL url, String requestMethod) {
311 return new HttpClient(url, requestMethod);
312 }
313
314 /**
[9172]315 * Returns the URL set for this connection.
316 * @see #create(URL)
317 * @see #create(URL, String)
318 */
319 public URL getURL() {
320 return url;
321 }
322
323 /**
324 * Returns the request method set for this connection.
325 * @see #create(URL, String)
326 */
327 public String getRequestMethod() {
328 return requestMethod;
329 }
330
331 /**
332 * Returns the set value for the given {@code header}.
333 */
334 public String getRequestHeader(String header) {
335 return headers.get(header);
336 }
337
338 /**
[9171]339 * Sets whether not to set header {@code Cache-Control=no-cache}
340 *
341 * @param useCache whether not to set header {@code Cache-Control=no-cache}
[9168]342 * @return {@code this}
[9171]343 * @see HttpURLConnection#setUseCaches(boolean)
344 */
345 public HttpClient useCache(boolean useCache) {
346 this.useCache = useCache;
347 return this;
348 }
349
350 /**
351 * Sets whether not to set header {@code Connection=close}
352 * <p/>
[9173]353 * This might fix #7640, see
354 * <a href='https://web.archive.org/web/20140118201501/http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive'>here</a>.
[9171]355 *
356 * @param keepAlive whether not to set header {@code Connection=close}
357 * @return {@code this}
358 */
359 public HttpClient keepAlive(boolean keepAlive) {
[9172]360 return setHeader("Connection", keepAlive ? null : "close");
[9171]361 }
362
363 /**
364 * @return {@code this}
[9168]365 * @see HttpURLConnection#setConnectTimeout(int)
366 */
367 public HttpClient setConnectTimeout(int connectTimeout) {
368 this.connectTimeout = connectTimeout;
369 return this;
370 }
371
372 /**
373 * @return {@code this}
374 * @see HttpURLConnection#setReadTimeout(int) (int)
375 */
376
377 public HttpClient setReadTimeout(int readTimeout) {
378 this.readTimeout = readTimeout;
379 return this;
380 }
381
382 /**
383 * Sets the {@code Accept} header.
384 *
385 * @return {@code this}
386 */
387 public HttpClient setAccept(String accept) {
[9172]388 return setHeader("Accept", accept);
[9168]389 }
390
391 /**
392 * Sets the request body for {@code PUT}/{@code POST} requests.
393 *
394 * @return {@code this}
395 */
396 public HttpClient setRequestBody(byte[] requestBody) {
397 this.requestBody = requestBody;
398 return this;
399 }
400
401 /**
402 * Sets the {@code If-Modified-Since} header.
403 *
404 * @return {@code this}
405 */
406 public HttpClient setIfModifiedSince(long ifModifiedSince) {
407 this.ifModifiedSince = ifModifiedSince;
408 return this;
409 }
410
411 /**
412 * Sets the maximum number of redirections to follow.
413 *
[9172]414 * Set {@code maxRedirects} to {@code -1} in order to ignore redirects, i.e.,
415 * to not throw an {@link IOException} in {@link #connect()}.
416 *
[9168]417 * @return {@code this}
418 */
419 public HttpClient setMaxRedirects(int maxRedirects) {
420 this.maxRedirects = maxRedirects;
421 return this;
422 }
423
424 /**
425 * Sets an arbitrary HTTP header.
426 *
427 * @return {@code this}
428 */
429 public HttpClient setHeader(String key, String value) {
430 this.headers.put(key, value);
431 return this;
432 }
433
434 /**
435 * Sets arbitrary HTTP headers.
436 *
437 * @return {@code this}
438 */
439 public HttpClient setHeaders(Map<String, String> headers) {
440 this.headers.putAll(headers);
441 return this;
442 }
443
[9172]444 /**
445 * Sets a reason to show on console. Can be {@code null} if no reason is given.
446 */
447 public HttpClient setReasonForRequest(String reasonForRequest) {
448 this.reasonForRequest = reasonForRequest;
449 return this;
450 }
451
[9168]452 private static boolean isRedirect(final int statusCode) {
453 switch (statusCode) {
454 case HttpURLConnection.HTTP_MOVED_PERM: // 301
455 case HttpURLConnection.HTTP_MOVED_TEMP: // 302
456 case HttpURLConnection.HTTP_SEE_OTHER: // 303
457 case 307: // TEMPORARY_REDIRECT:
458 case 308: // PERMANENT_REDIRECT:
459 return true;
460 default:
461 return false;
462 }
463 }
464
465}
Note: See TracBrowser for help on using the repository browser.