clear code by converting AtomicLong to long as GZIPInputStream is designed for single...
[idea/community.git] / platform / platform-api / src / com / intellij / util / io / HttpRequests.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.util.io;
17
18 import com.intellij.Patches;
19 import com.intellij.ide.IdeBundle;
20 import com.intellij.openapi.application.Application;
21 import com.intellij.openapi.application.ApplicationInfo;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.progress.ProgressIndicator;
25 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
26 import com.intellij.openapi.util.io.FileUtilRt;
27 import com.intellij.openapi.util.io.StreamUtil;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.openapi.vfs.CharsetToolkit;
30 import com.intellij.util.ArrayUtil;
31 import com.intellij.util.lang.UrlClassLoader;
32 import com.intellij.util.net.HttpConfigurable;
33 import com.intellij.util.net.NetUtils;
34 import com.intellij.util.net.ssl.CertificateManager;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import javax.net.ssl.HostnameVerifier;
39 import javax.net.ssl.HttpsURLConnection;
40 import java.io.*;
41 import java.net.*;
42 import java.nio.charset.Charset;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45
46 /**
47  * Handy class for reading data from HTTP connections with built-in support for HTTP redirects and gzipped content and automatic cleanup.
48  * Usage: <pre>{@code
49  * int firstByte = HttpRequests.request(url).connect(new HttpRequests.RequestProcessor<Integer>() {
50  *   public Integer process(@NotNull Request request) throws IOException {
51  *     return request.getInputStream().read();
52  *   }
53  * });
54  * }</pre>
55  */
56 public final class HttpRequests {
57   private static final Logger LOG = Logger.getInstance(HttpRequests.class);
58
59   private static final int BLOCK_SIZE = 16 * 1024;
60   private static final Pattern CHARSET_PATTERN = Pattern.compile("charset=([^;]+)");
61
62   private HttpRequests() { }
63
64
65   public interface Request {
66     @NotNull
67     String getURL();
68
69     @NotNull
70     URLConnection getConnection() throws IOException;
71
72     @NotNull
73     InputStream getInputStream() throws IOException;
74
75     @NotNull
76     BufferedReader getReader() throws IOException;
77
78     @NotNull
79     BufferedReader getReader(@Nullable ProgressIndicator indicator) throws IOException;
80
81     /** @deprecated Called automatically on open connection. Use {@link RequestBuilder#tryConnect()} to get response code */
82     boolean isSuccessful() throws IOException;
83
84     @NotNull
85     File saveToFile(@NotNull File file, @Nullable ProgressIndicator indicator) throws IOException;
86
87     @NotNull
88     byte[] readBytes(@Nullable ProgressIndicator indicator) throws IOException;
89
90     @NotNull
91     String readString(@Nullable ProgressIndicator indicator) throws IOException;
92   }
93
94   public interface RequestProcessor<T> {
95     T process(@NotNull Request request) throws IOException;
96   }
97
98   public interface ConnectionTuner {
99     void tune(@NotNull URLConnection connection) throws IOException;
100   }
101
102   public static class HttpStatusException extends IOException {
103     private int myStatusCode;
104     private String myUrl;
105
106     public HttpStatusException(@NotNull String message, int statusCode, @NotNull String url) {
107       super(message);
108       myStatusCode = statusCode;
109       myUrl = url;
110     }
111
112     public int getStatusCode() {
113       return myStatusCode;
114     }
115
116     @NotNull
117     public String getUrl() {
118       return myUrl;
119     }
120
121     @Override
122     public String toString() {
123       return super.toString() + ". Status=" + myStatusCode + ", Url=" + myUrl;
124     }
125   }
126
127
128   @NotNull
129   public static RequestBuilder request(@NotNull String url) {
130     return new RequestBuilderImpl(url);
131   }
132
133   @NotNull
134   public static String createErrorMessage(@NotNull IOException e, @NotNull Request request, boolean includeHeaders) {
135     StringBuilder builder = new StringBuilder();
136
137     builder.append("Cannot download '").append(request.getURL()).append("': ").append(e.getMessage());
138
139     try {
140       URLConnection connection = request.getConnection();
141       if (includeHeaders) {
142         builder.append("\n, headers: ").append(connection.getHeaderFields());
143       }
144       if (connection instanceof HttpURLConnection) {
145         HttpURLConnection httpConnection = (HttpURLConnection)connection;
146         builder.append("\n, response: ").append(httpConnection.getResponseCode()).append(' ').append(httpConnection.getResponseMessage());
147       }
148     }
149     catch (Throwable ignored) { }
150
151     return builder.toString();
152   }
153
154
155   private static class RequestBuilderImpl extends RequestBuilder {
156     private final String myUrl;
157     private int myConnectTimeout = HttpConfigurable.CONNECTION_TIMEOUT;
158     private int myTimeout = HttpConfigurable.READ_TIMEOUT;
159     private int myRedirectLimit = HttpConfigurable.REDIRECT_LIMIT;
160     private boolean myGzip = true;
161     private boolean myForceHttps;
162     private boolean myUseProxy = true;
163     private HostnameVerifier myHostnameVerifier;
164     private String myUserAgent;
165     private String myAccept;
166     private ConnectionTuner myTuner;
167
168     private RequestBuilderImpl(@NotNull String url) {
169       myUrl = url;
170     }
171
172     @Override
173     public RequestBuilder connectTimeout(int value) {
174       myConnectTimeout = value;
175       return this;
176     }
177
178     @Override
179     public RequestBuilder readTimeout(int value) {
180       myTimeout = value;
181       return this;
182     }
183
184     @Override
185     public RequestBuilder redirectLimit(int redirectLimit) {
186       myRedirectLimit = redirectLimit;
187       return this;
188     }
189
190     @Override
191     public RequestBuilder gzip(boolean value) {
192       myGzip = value;
193       return this;
194     }
195
196     @Override
197     public RequestBuilder forceHttps(boolean forceHttps) {
198       myForceHttps = forceHttps;
199       return this;
200     }
201
202     @Override
203     public RequestBuilder useProxy(boolean useProxy) {
204       myUseProxy = useProxy;
205       return this;
206     }
207
208     @Override
209     public RequestBuilder hostNameVerifier(@Nullable HostnameVerifier hostnameVerifier) {
210       myHostnameVerifier = hostnameVerifier;
211       return this;
212     }
213
214     @Override
215     public RequestBuilder userAgent(@Nullable String userAgent) {
216       myUserAgent = userAgent;
217       return this;
218     }
219
220     @Override
221     public RequestBuilder productNameAsUserAgent() {
222       Application app = ApplicationManager.getApplication();
223       if (app != null && !app.isDisposed()) {
224         ApplicationInfo info = ApplicationInfo.getInstance();
225         return userAgent(info.getVersionName() + '/' + info.getBuild().asStringWithoutProductCode());
226       }
227       else {
228         return userAgent("IntelliJ");
229       }
230     }
231
232     @Override
233     public RequestBuilder accept(@Nullable String mimeType) {
234       myAccept = mimeType;
235       return this;
236     }
237
238     @Override
239     public RequestBuilder tuner(@Nullable ConnectionTuner tuner) {
240       myTuner = tuner;
241       return this;
242     }
243
244     @Override
245     public <T> T connect(@NotNull HttpRequests.RequestProcessor<T> processor) throws IOException {
246       return process(this, processor);
247     }
248   }
249
250   private static class RequestImpl implements Request, AutoCloseable {
251     private final RequestBuilderImpl myBuilder;
252     private URLConnection myConnection;
253     private InputStream myInputStream;
254     private BufferedReader myReader;
255
256     private RequestImpl(RequestBuilderImpl builder) {
257       myBuilder = builder;
258     }
259
260     @NotNull
261     @Override
262     public String getURL() {
263       return myBuilder.myUrl;
264     }
265
266     @NotNull
267     @Override
268     public URLConnection getConnection() throws IOException {
269       if (myConnection == null) {
270         myConnection = openConnection(myBuilder);
271       }
272       return myConnection;
273     }
274
275     @NotNull
276     @Override
277     public InputStream getInputStream() throws IOException {
278       if (myInputStream == null) {
279         myInputStream = getConnection().getInputStream();
280         if (myBuilder.myGzip && "gzip".equalsIgnoreCase(getConnection().getContentEncoding())) {
281           myInputStream = CountingGZIPInputStream.create(myInputStream);
282         }
283       }
284       return myInputStream;
285     }
286
287     @NotNull
288     @Override
289     public BufferedReader getReader() throws IOException {
290       return getReader(null);
291     }
292
293     @NotNull
294     @Override
295     public BufferedReader getReader(@Nullable ProgressIndicator indicator) throws IOException {
296       if (myReader == null) {
297         InputStream inputStream = getInputStream();
298         if (indicator != null) {
299           int contentLength = getConnection().getContentLength();
300           if (contentLength > 0) {
301             //noinspection IOResourceOpenedButNotSafelyClosed
302             inputStream = new ProgressMonitorInputStream(indicator, inputStream, contentLength);
303           }
304         }
305         myReader = new BufferedReader(new InputStreamReader(inputStream, getCharset(this)));
306       }
307       return myReader;
308     }
309
310     @Override
311     public boolean isSuccessful() throws IOException {
312       URLConnection connection = getConnection();
313       return !(connection instanceof HttpURLConnection) || ((HttpURLConnection)connection).getResponseCode() == 200;
314     }
315
316     @Override
317     @NotNull
318     public byte[] readBytes(@Nullable ProgressIndicator indicator) throws IOException {
319       int contentLength = getConnection().getContentLength();
320       BufferExposingByteArrayOutputStream out = new BufferExposingByteArrayOutputStream(contentLength > 0 ? contentLength : BLOCK_SIZE);
321       NetUtils.copyStreamContent(indicator, getInputStream(), out, contentLength);
322       return ArrayUtil.realloc(out.getInternalBuffer(), out.size());
323     }
324
325     @NotNull
326     @Override
327     public String readString(@Nullable ProgressIndicator indicator) throws IOException {
328       Charset cs = getCharset(this);
329       byte[] bytes = readBytes(indicator);
330       return new String(bytes, cs);
331     }
332
333     @Override
334     @NotNull
335     public File saveToFile(@NotNull File file, @Nullable ProgressIndicator indicator) throws IOException {
336       FileUtilRt.createParentDirs(file);
337
338       boolean deleteFile = true;
339       try {
340         OutputStream out = new FileOutputStream(file);
341         try {
342           NetUtils.copyStreamContent(indicator, getInputStream(), out, getConnection().getContentLength());
343           deleteFile = false;
344         }
345         catch (IOException e) {
346           throw new IOException(createErrorMessage(e, this, false), e);
347         }
348         finally {
349           out.close();
350         }
351       }
352       finally {
353         if (deleteFile) {
354           FileUtilRt.delete(file);
355         }
356       }
357
358       return file;
359     }
360
361     @Override
362     public void close() {
363       StreamUtil.closeStream(myInputStream);
364       StreamUtil.closeStream(myReader);
365       if (myConnection instanceof HttpURLConnection) {
366         ((HttpURLConnection)myConnection).disconnect();
367       }
368     }
369   }
370
371   private static <T> T process(RequestBuilderImpl builder, RequestProcessor<T> processor) throws IOException {
372     LOG.assertTrue(ApplicationManager.getApplication() == null ||
373                    ApplicationManager.getApplication().isUnitTestMode() ||
374                    !ApplicationManager.getApplication().isReadAccessAllowed(),
375                    "Network shouldn't be accessed in EDT or inside read action");
376
377     ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
378     if (Patches.JDK_BUG_ID_8032832 && !UrlClassLoader.isRegisteredAsParallelCapable(contextLoader)) {
379       // hack-around for class loader lock in sun.net.www.protocol.http.NegotiateAuthentication (IDEA-131621)
380       try (URLClassLoader cl = new URLClassLoader(new URL[0], contextLoader)) {
381         Thread.currentThread().setContextClassLoader(cl);
382         return doProcess(builder, processor);
383       }
384       finally {
385         Thread.currentThread().setContextClassLoader(contextLoader);
386       }
387     }
388     else {
389       return doProcess(builder, processor);
390     }
391   }
392
393   private static <T> T doProcess(RequestBuilderImpl builder, RequestProcessor<T> processor) throws IOException {
394     try (RequestImpl request = new RequestImpl(builder)) {
395       return processor.process(request);
396     }
397   }
398
399   private static Charset getCharset(Request request) throws IOException {
400     String contentType = request.getConnection().getContentType();
401     if (!StringUtil.isEmptyOrSpaces(contentType)) {
402       Matcher m = CHARSET_PATTERN.matcher(contentType);
403       if (m.find()) {
404         try {
405           return Charset.forName(StringUtil.unquoteString(m.group(1)));
406         }
407         catch (IllegalArgumentException e) {
408           throw new IOException("unknown charset (" + contentType + ")", e);
409         }
410       }
411     }
412
413     return CharsetToolkit.UTF8_CHARSET;
414   }
415
416   private static URLConnection openConnection(RequestBuilderImpl builder) throws IOException {
417     String url = builder.myUrl;
418
419     for (int i = 0; i < builder.myRedirectLimit; i++) {
420       if (builder.myForceHttps && StringUtil.startsWith(url, "http:")) {
421         url = "https:" + url.substring(5);
422       }
423
424       if (url.startsWith("https:") && ApplicationManager.getApplication() != null) {
425         CertificateManager.getInstance();
426       }
427
428       URLConnection connection;
429       if (!builder.myUseProxy) {
430         connection = new URL(url).openConnection(Proxy.NO_PROXY);
431       }
432       else if (ApplicationManager.getApplication() == null) {
433         connection = new URL(url).openConnection();
434       }
435       else {
436         connection = HttpConfigurable.getInstance().openConnection(url);
437       }
438
439       connection.setConnectTimeout(builder.myConnectTimeout);
440       connection.setReadTimeout(builder.myTimeout);
441
442       if (builder.myUserAgent != null) {
443         connection.setRequestProperty("User-Agent", builder.myUserAgent);
444       }
445
446       if (builder.myHostnameVerifier != null && connection instanceof HttpsURLConnection) {
447         ((HttpsURLConnection)connection).setHostnameVerifier(builder.myHostnameVerifier);
448       }
449
450       if (builder.myGzip) {
451         connection.setRequestProperty("Accept-Encoding", "gzip");
452       }
453
454       if (builder.myAccept != null) {
455         connection.setRequestProperty("Accept", builder.myAccept);
456       }
457
458       connection.setUseCaches(false);
459
460       if (builder.myTuner != null) {
461         builder.myTuner.tune(connection);
462       }
463
464       if (connection instanceof HttpURLConnection) {
465         int responseCode = ((HttpURLConnection)connection).getResponseCode();
466
467         if (responseCode < 200 || responseCode >= 300 && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) {
468           ((HttpURLConnection)connection).disconnect();
469
470           if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
471             url = connection.getHeaderField("Location");
472             if (url != null) {
473               continue;
474             }
475           }
476
477           String message = IdeBundle.message("error.connection.failed.with.http.code.N", responseCode);
478           throw new HttpStatusException(message, responseCode, StringUtil.notNullize(url, "Empty URL"));
479         }
480       }
481
482       return connection;
483     }
484
485     throw new IOException(IdeBundle.message("error.connection.failed.redirects"));
486   }
487 }