32108310678df00e59897607102dc62cbb3ff546
[idea/community.git] / platform / platform-api / src / com / intellij / util / io / HttpRequests.java
1 /*
2  * Copyright 2000-2014 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.ide.IdeBundle;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.components.ServiceManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.progress.ProgressIndicator;
23 import com.intellij.openapi.util.SystemInfo;
24 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
25 import com.intellij.openapi.util.io.FileUtilRt;
26 import com.intellij.openapi.util.io.StreamUtil;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vfs.CharsetToolkit;
29 import com.intellij.util.ArrayUtil;
30 import com.intellij.util.ReflectionUtil;
31 import com.intellij.util.SystemProperties;
32 import com.intellij.util.net.HTTPMethod;
33 import com.intellij.util.net.HttpConfigurable;
34 import com.intellij.util.net.NetUtils;
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.HttpURLConnection;
42 import java.net.URL;
43 import java.net.URLClassLoader;
44 import java.net.URLConnection;
45 import java.nio.charset.Charset;
46 import java.util.zip.GZIPInputStream;
47
48 /**
49  * Handy class for reading data from HTTP connections with built-in support for HTTP redirects and gzipped content and automatic cleanup.
50  * Usage: <pre>{@code
51  * int firstByte = HttpRequests.request(url).connect(new HttpRequests.RequestProcessor<Integer>() {
52  *   public Integer process(@NotNull Request request) throws IOException {
53  *     return request.getInputStream().read();
54  *   }
55  * });
56  * }</pre>
57  */
58 public abstract class HttpRequests {
59   private static final boolean ourWrapClassLoader =
60     SystemInfo.isJavaVersionAtLeast("1.7") && !SystemProperties.getBooleanProperty("idea.parallel.class.loader", true);
61
62   public interface Request {
63     @NotNull
64     URLConnection getConnection() throws IOException;
65
66     @NotNull
67     InputStream getInputStream() throws IOException;
68
69     @NotNull
70     BufferedReader getReader() throws IOException;
71
72     @NotNull
73     BufferedReader getReader(@Nullable ProgressIndicator indicator) throws IOException;
74
75     boolean isSuccessful() throws IOException;
76
77     @NotNull
78     File saveToFile(@NotNull File file, @Nullable ProgressIndicator indicator) throws IOException;
79
80     byte[] readBytes(@Nullable ProgressIndicator indicator) throws IOException;
81   }
82
83   public interface RequestProcessor<T> {
84     T process(@NotNull Request request) throws IOException;
85   }
86
87   protected HttpRequests() {
88   }
89
90   @NotNull
91   public static String createErrorMessage(@NotNull IOException e, @NotNull Request request) throws IOException {
92     URLConnection connection = request.getConnection();
93     String errorMessage = "Cannot download '" + connection.getURL().toExternalForm() + "': " + e.getMessage() + "\n, headers: " + connection.getHeaderFields();
94     if (connection instanceof HttpURLConnection) {
95       HttpURLConnection httpConnection = (HttpURLConnection)connection;
96       errorMessage += "\n, response: " + httpConnection.getResponseCode() + ' ' + httpConnection.getResponseMessage();
97     }
98     return errorMessage;
99   }
100
101   public abstract static class RequestBuilder {
102     private final String myUrl;
103     private int myConnectTimeout = HttpConfigurable.CONNECTION_TIMEOUT;
104     private int myTimeout = HttpConfigurable.READ_TIMEOUT;
105     private int myRedirectLimit = HttpConfigurable.REDIRECT_LIMIT;
106     private boolean myGzip = true;
107     private boolean myForceHttps;
108     private HostnameVerifier myHostnameVerifier;
109     private String myUserAgent;
110     private String myAccept;
111
112     private HTTPMethod myMethod;
113
114     protected RequestBuilder(@NotNull String url) {
115       myUrl = url;
116     }
117
118     @NotNull
119     public RequestBuilder connectTimeout(int value) {
120       myConnectTimeout = value;
121       return this;
122     }
123
124     @NotNull
125     public RequestBuilder readTimeout(int value) {
126       myTimeout = value;
127       return this;
128     }
129
130     @NotNull
131     public RequestBuilder redirectLimit(int redirectLimit) {
132       myRedirectLimit = redirectLimit;
133       return this;
134     }
135
136     @NotNull
137     public RequestBuilder gzip(boolean value) {
138       myGzip = value;
139       return this;
140     }
141
142     @NotNull
143     public RequestBuilder forceHttps(boolean forceHttps) {
144       myForceHttps = forceHttps;
145       return this;
146     }
147
148     @NotNull
149     public RequestBuilder hostNameVerifier(@Nullable HostnameVerifier hostnameVerifier) {
150       myHostnameVerifier = hostnameVerifier;
151       return this;
152     }
153
154     @NotNull
155     public RequestBuilder userAgent(@Nullable String userAgent) {
156       myUserAgent = userAgent;
157       return this;
158     }
159
160     @NotNull
161     public abstract RequestBuilder userAgent();
162
163     @NotNull
164     public RequestBuilder accept(@Nullable String mimeType) {
165       myAccept = mimeType;
166       return this;
167     }
168
169     public <T> T connect(@NotNull RequestProcessor<T> processor) throws IOException {
170       // todo[r.sh] drop condition in IDEA 15
171       if (ourWrapClassLoader) {
172         return wrapAndProcess(this, processor);
173       }
174       else {
175         return process(this, processor);
176       }
177     }
178
179     public <T> T connect(@NotNull RequestProcessor<T> processor, T errorValue, @Nullable Logger logger) {
180       try {
181         return connect(processor);
182       }
183       catch (Throwable e) {
184         if (logger != null) {
185           logger.warn(e);
186         }
187         return errorValue;
188       }
189     }
190
191     public void saveToFile(@NotNull final File file, @Nullable final ProgressIndicator indicator) throws IOException {
192       connect(new HttpRequests.RequestProcessor<Void>() {
193         @Override
194         public Void process(@NotNull HttpRequests.Request request) throws IOException {
195           request.saveToFile(file, indicator);
196           return null;
197         }
198       });
199     }
200
201     @NotNull
202     public byte[] readBytes(@Nullable final ProgressIndicator indicator) throws IOException {
203       return connect(new HttpRequests.RequestProcessor<byte[]>() {
204         @Override
205         public byte[] process(@NotNull HttpRequests.Request request) throws IOException {
206           return request.readBytes(indicator);
207         }
208       });
209     }
210
211     @NotNull
212     public String readString(@Nullable final ProgressIndicator indicator) throws IOException {
213       return connect(new HttpRequests.RequestProcessor<String>() {
214         @Override
215         public String process(@NotNull HttpRequests.Request request) throws IOException {
216           int contentLength = request.getConnection().getContentLength();
217           BufferExposingByteArrayOutputStream out = new BufferExposingByteArrayOutputStream(contentLength > 0 ? contentLength : 16 * 1024);
218           NetUtils.copyStreamContent(indicator, request.getInputStream(), out, contentLength);
219           return new String(out.getInternalBuffer(), 0, out.size(), getCharset(request));
220         }
221       });
222     }
223   }
224
225   @NotNull
226   public static RequestBuilder request(@NotNull String url) {
227     if (ApplicationManager.getApplication() == null) {
228       try {
229         return ((HttpRequests)ReflectionUtil.newInstance(Class.forName("com.intellij.util.io.HttpRequestsImpl"))).createRequestBuilder(url);
230       }
231       catch (ClassNotFoundException e) {
232         throw new RuntimeException(e);
233       }
234     }
235     return ServiceManager.getService(HttpRequests.class).createRequestBuilder(url);
236   }
237
238   @NotNull
239   public static RequestBuilder head(@NotNull String url) {
240     RequestBuilder builder = request(url);
241     builder.myMethod = HTTPMethod.HEAD;
242     return builder;
243   }
244
245   protected abstract RequestBuilder createRequestBuilder(@NotNull String url);
246
247   private static <T> T wrapAndProcess(RequestBuilder builder, RequestProcessor<T> processor) throws IOException {
248     // hack-around for class loader lock in sun.net.www.protocol.http.NegotiateAuthentication (IDEA-131621)
249     ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
250     Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[0], oldClassLoader));
251     try {
252       return process(builder, processor);
253     }
254     finally {
255       Thread.currentThread().setContextClassLoader(oldClassLoader);
256     }
257   }
258
259   @NotNull
260   private static Charset getCharset(@NotNull Request request) throws IOException {
261     String contentEncoding = request.getConnection().getContentEncoding();
262     if (contentEncoding != null) {
263       try {
264         return Charset.forName(contentEncoding);
265       }
266       catch (Exception ignored) {
267       }
268     }
269     return CharsetToolkit.UTF8_CHARSET;
270   }
271
272   private static <T> T process(final RequestBuilder builder, RequestProcessor<T> processor) throws IOException {
273     class RequestImpl implements Request {
274       private URLConnection myConnection;
275       private InputStream myInputStream;
276       private BufferedReader myReader;
277
278       @NotNull
279       @Override
280       public URLConnection getConnection() throws IOException {
281         if (myConnection == null) {
282           myConnection = openConnection(builder);
283         }
284         return myConnection;
285       }
286
287       @NotNull
288       @Override
289       public InputStream getInputStream() throws IOException {
290         if (myInputStream == null) {
291           myInputStream = getConnection().getInputStream();
292           if (builder.myGzip && "gzip".equalsIgnoreCase(getConnection().getContentEncoding())) {
293             //noinspection IOResourceOpenedButNotSafelyClosed
294             myInputStream = new GZIPInputStream(myInputStream);
295           }
296         }
297         return myInputStream;
298       }
299
300       @NotNull
301       @Override
302       public BufferedReader getReader() throws IOException {
303         return getReader(null);
304       }
305
306       @NotNull
307       @Override
308       public BufferedReader getReader(@Nullable ProgressIndicator indicator) throws IOException {
309         if (myReader == null) {
310           InputStream inputStream = getInputStream();
311           if (indicator != null) {
312             int contentLength = getConnection().getContentLength();
313             if (contentLength > 0) {
314               //noinspection IOResourceOpenedButNotSafelyClosed
315               inputStream = new ProgressMonitorInputStream(indicator, inputStream, contentLength);
316             }
317           }
318           myReader = new BufferedReader(new InputStreamReader(inputStream, getCharset(this)));
319         }
320         return myReader;
321       }
322
323       @Override
324       public boolean isSuccessful() throws IOException {
325         URLConnection connection = getConnection();
326         return !(connection instanceof HttpURLConnection) || ((HttpURLConnection)connection).getResponseCode() == 200;
327       }
328
329       private void cleanup() {
330         StreamUtil.closeStream(myInputStream);
331         StreamUtil.closeStream(myReader);
332         if (myConnection instanceof HttpURLConnection) {
333           ((HttpURLConnection)myConnection).disconnect();
334         }
335       }
336
337       @NotNull
338       public byte[] readBytes(@Nullable ProgressIndicator indicator) throws IOException {
339         int contentLength = getConnection().getContentLength();
340         BufferExposingByteArrayOutputStream out = new BufferExposingByteArrayOutputStream(contentLength > 0 ? contentLength : 32 * 1024);
341         NetUtils.copyStreamContent(indicator, getInputStream(), out, contentLength);
342         return ArrayUtil.realloc(out.getInternalBuffer(), out.size());
343       }
344
345       @NotNull
346       public File saveToFile(@NotNull File file, @Nullable ProgressIndicator indicator) throws IOException {
347         OutputStream out = null;
348         boolean deleteFile = true;
349         try {
350           if (indicator != null) {
351             indicator.checkCanceled();
352           }
353
354           FileUtilRt.createParentDirs(file);
355           out = new FileOutputStream(file);
356           NetUtils.copyStreamContent(indicator, getInputStream(), out, getConnection().getContentLength());
357           deleteFile = false;
358         }
359         catch (IOException e) {
360           throw new IOException(createErrorMessage(e, this), e);
361         }
362         finally {
363           try {
364             if (out != null) {
365               out.close();
366             }
367           }
368           finally {
369             if (deleteFile) {
370               FileUtilRt.delete(file);
371             }
372           }
373         }
374         return file;
375       }
376     }
377
378     RequestImpl request = new RequestImpl();
379     try {
380       return processor.process(request);
381     }
382     finally {
383       request.cleanup();
384     }
385   }
386
387   private static URLConnection openConnection(RequestBuilder builder) throws IOException {
388     String url = builder.myUrl;
389
390     for (int i = 0; i < builder.myRedirectLimit; i++) {
391       if (builder.myForceHttps && StringUtil.startsWith(url, "http:")) {
392         url = "https:" + url.substring(5);
393       }
394
395       URLConnection connection;
396       if (ApplicationManager.getApplication() == null) {
397         connection = new URL(url).openConnection();
398       }
399       else {
400         connection = HttpConfigurable.getInstance().openConnection(url);
401       }
402
403       connection.setConnectTimeout(builder.myConnectTimeout);
404       connection.setReadTimeout(builder.myTimeout);
405
406       if (builder.myUserAgent != null) {
407         connection.setRequestProperty("User-Agent", builder.myUserAgent);
408       }
409
410       if (builder.myHostnameVerifier != null && connection instanceof HttpsURLConnection) {
411         ((HttpsURLConnection)connection).setHostnameVerifier(builder.myHostnameVerifier);
412       }
413
414       if (builder.myMethod != null) {
415         ((HttpURLConnection)connection).setRequestMethod(builder.myMethod.name());
416       }
417
418       if (builder.myGzip) {
419         connection.setRequestProperty("Accept-Encoding", "gzip");
420       }
421       if (builder.myAccept != null) {
422         connection.setRequestProperty("Accept", builder.myAccept);
423       }
424       connection.setUseCaches(false);
425
426       if (connection instanceof HttpURLConnection) {
427         int responseCode = ((HttpURLConnection)connection).getResponseCode();
428
429         if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) {
430           ((HttpURLConnection)connection).disconnect();
431
432           if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
433             url = connection.getHeaderField("Location");
434             if (url != null) {
435               continue;
436             }
437           }
438
439           throw new IOException(IdeBundle.message("error.connection.failed.with.http.code.N", responseCode));
440         }
441       }
442
443       return connection;
444     }
445
446     throw new IOException(IdeBundle.message("error.connection.failed.redirects"));
447   }
448 }