platform: refactoring in the sake of better API
authorRoman Shevchenko <roman.shevchenko@jetbrains.com>
Tue, 25 Nov 2014 17:45:43 +0000 (18:45 +0100)
committerRoman Shevchenko <roman.shevchenko@jetbrains.com>
Wed, 26 Nov 2014 10:45:52 +0000 (11:45 +0100)
platform/platform-api/src/com/intellij/util/net/HttpConfigurable.java
platform/platform-impl/src/com/intellij/ide/plugins/RepositoryHelper.java
platform/platform-impl/src/com/intellij/openapi/updateSettings/impl/PluginDownloader.java
platform/platform-impl/src/com/intellij/openapi/updateSettings/impl/UpdateChecker.java
platform/platform-impl/src/com/intellij/util/HttpRequests.java [deleted file]
platform/platform-impl/src/com/intellij/util/io/HttpRequests.java [new file with mode: 0644]
platform/platform-tests/testSrc/com/intellij/util/io/HttpRequestsTest.java [new file with mode: 0644]

index 115af794d5d685abfbd23acf52b21160fa636492..50f75cd4fb8e7a9c4f510f145e7c83b94848ec30 100644 (file)
@@ -70,6 +70,8 @@ import java.util.Set;
 )
 public class HttpConfigurable implements PersistentStateComponent<HttpConfigurable>, ApplicationComponent {
   public static final int CONNECTION_TIMEOUT = SystemProperties.getIntProperty("idea.connection.timeout", 10000);
+  public static final int READ_TIMEOUT = SystemProperties.getIntProperty("idea.read.timeout", 60000);
+  public static final int REDIRECT_LIMIT = SystemProperties.getIntProperty("idea.redirect.limit", 10);
 
   public boolean PROXY_TYPE_IS_SOCKS;
   public boolean USE_HTTP_PROXY;
@@ -394,7 +396,7 @@ public class HttpConfigurable implements PersistentStateComponent<HttpConfigurab
     }
 
     assert urlConnection != null;
-    urlConnection.setReadTimeout(CONNECTION_TIMEOUT);
+    urlConnection.setReadTimeout(READ_TIMEOUT);
     urlConnection.setConnectTimeout(CONNECTION_TIMEOUT);
     return urlConnection;
   }
index 0a6262ca4eed71ea9a82e9e7ece398c7666f5775..aece5a78cafc68b631fddc9028ec651b24c596ac 100644 (file)
@@ -25,17 +25,20 @@ import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.util.BuildNumber;
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.io.FileUtilRt;
-import com.intellij.util.HttpRequests;
+import com.intellij.util.io.HttpRequests;
 import com.intellij.util.PathUtil;
-import com.intellij.util.ThrowableConvertor;
 import com.intellij.util.net.NetUtils;
-import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.xml.sax.SAXException;
 
+import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
-import java.io.*;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.URLConnection;
 import java.util.List;
@@ -45,14 +48,14 @@ import java.util.List;
  * @since Mar 28, 2003
  */
 public class RepositoryHelper {
-  @NonNls public static final String PLUGIN_LIST_FILE = "availables.xml";
+  @SuppressWarnings("SpellCheckingInspection") public static final String PLUGIN_LIST_FILE = "availables.xml";
 
   public static List<IdeaPluginDescriptor> loadPluginsFromRepository(@Nullable ProgressIndicator indicator) throws Exception {
     return loadPluginsFromRepository(indicator, null);
   }
 
-  @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
-  public static List<IdeaPluginDescriptor> loadPluginsFromRepository(@Nullable final ProgressIndicator indicator, @Nullable BuildNumber buildnumber) throws Exception {
+  public static List<IdeaPluginDescriptor> loadPluginsFromRepository(@Nullable final ProgressIndicator indicator,
+                                                                     @Nullable BuildNumber buildnumber) throws Exception {
     final ApplicationInfoEx appInfo = ApplicationInfoImpl.getShadowInstance();
 
     String url = appInfo.getPluginsListUrl() + "?build=" + (buildnumber != null ? buildnumber.asString() : appInfo.getApiVersion());
@@ -65,7 +68,7 @@ public class RepositoryHelper {
     if (pluginListFile.length() > 0) {
       try {
         //noinspection SpellCheckingInspection
-        url = url + "&crc32=" + Files.hash(pluginListFile, Hashing.crc32()).toString();
+        url += "&crc32=" + Files.hash(pluginListFile, Hashing.crc32()).toString();
       }
       catch (NoSuchMethodError e) {
         String guavaPath = PathUtil.getJarPathForClass(Hashing.class);
@@ -73,15 +76,20 @@ public class RepositoryHelper {
       }
     }
 
-    return HttpRequests.request(url).get(new ThrowableConvertor<URLConnection, List<IdeaPluginDescriptor>, Exception>() {
+    if (indicator != null) {
+      indicator.setText2(IdeBundle.message("progress.waiting.for.reply.from.plugin.manager", appInfo.getPluginManagerUrl()));
+    }
+
+    return HttpRequests.request(url).connect(new HttpRequests.RequestProcessor<List<IdeaPluginDescriptor>>() {
       @Override
-      public List<IdeaPluginDescriptor> convert(URLConnection connection) throws Exception {
+      public List<IdeaPluginDescriptor> process(@NotNull HttpRequests.Request request) throws IOException {
         if (indicator != null) {
           indicator.checkCanceled();
-          indicator.setText2(IdeBundle.message("progress.waiting.for.reply.from.plugin.manager", appInfo.getPluginManagerUrl()));
         }
 
-        if (connection instanceof HttpURLConnection && ((HttpURLConnection)connection).getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+        URLConnection connection = request.getConnection();
+        if (connection instanceof HttpURLConnection &&
+            ((HttpURLConnection)connection).getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
           return loadPluginList(pluginListFile);
         }
 
@@ -89,37 +97,35 @@ public class RepositoryHelper {
           indicator.checkCanceled();
           indicator.setText2(IdeBundle.message("progress.downloading.list.of.plugins"));
         }
-        return readPluginsStream(connection, indicator, PLUGIN_LIST_FILE);
+
+        synchronized (RepositoryHelper.class) {
+          File localFile = createLocalPluginsDescriptions(PLUGIN_LIST_FILE);
+          OutputStream output = new FileOutputStream(localFile);
+          try {
+            NetUtils.copyStreamContent(indicator, request.getInputStream(), output, connection.getContentLength());
+            return loadPluginList(localFile);
+          }
+          finally {
+            output.close();
+          }
+        }
       }
     });
   }
 
-  public synchronized static List<IdeaPluginDescriptor> readPluginsStream(@NotNull URLConnection connection,
-                                                                          @Nullable ProgressIndicator indicator,
-                                                                          @NotNull String file) throws Exception {
-    File localFile;
-    InputStream input = HttpRequests.getInputStream(connection);
+  private static List<IdeaPluginDescriptor> loadPluginList(File file) throws IOException{
     try {
-      localFile = createLocalPluginsDescriptions(file);
-      OutputStream output = new FileOutputStream(localFile);
-      try {
-        NetUtils.copyStreamContent(indicator, input, output, connection.getContentLength());
-      }
-      finally {
-        output.close();
-      }
+      SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
+      RepositoryContentHandler handler = new RepositoryContentHandler();
+      parser.parse(file, handler);
+      return handler.getPluginsList();
     }
-    finally {
-      input.close();
+    catch (ParserConfigurationException e) {
+      throw new IOException(e);
+    }
+    catch (SAXException e) {
+      throw new IOException(e);
     }
-    return loadPluginList(localFile);
-  }
-
-  private static List<IdeaPluginDescriptor> loadPluginList(File file) throws Exception {
-    SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
-    RepositoryContentHandler handler = new RepositoryContentHandler();
-    parser.parse(file, handler);
-    return handler.getPluginsList();
   }
 
   @NotNull
index fd6fce0474f6d7e03ce6418f023b8c4cfe4ccc15..ed34961d8b30d17a37a61e61da890b2e81f19e05 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2000-2013 JetBrains s.r.o.
+ * Copyright 2000-2014 JetBrains s.r.o.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,10 +30,8 @@ import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.openapi.vfs.VirtualFileManager;
 import com.intellij.openapi.vfs.VirtualFileSystem;
-import com.intellij.util.Consumer;
-import com.intellij.util.HttpRequests;
+import com.intellij.util.io.HttpRequests;
 import com.intellij.util.PathUtil;
-import com.intellij.util.ThrowableConvertor;
 import com.intellij.util.io.ZipUtil;
 import com.intellij.util.net.NetUtils;
 import org.jetbrains.annotations.NonNls;
@@ -236,28 +234,18 @@ public class PluginDownloader {
     progressIndicator.checkCanceled();
     progressIndicator.setText(IdeBundle.message("progress.downloading.plugin", getPluginName()));
 
-    return HttpRequests.request(myPluginUrl).supportGzip(false).effectiveUrlConsumer(new Consumer<String>() {
+    return HttpRequests.request(myPluginUrl).gzip(false).connect(new HttpRequests.RequestProcessor<File>() {
       @Override
-      public void consume(String url) {
-        myPluginUrl = url;
-      }
-    }).get(new ThrowableConvertor<URLConnection, File, IOException>() {
-      @Override
-      public File convert(URLConnection connection) throws IOException {
+      public File process(@NotNull HttpRequests.Request request) throws IOException {
         progressIndicator.checkCanceled();
 
-        InputStream input = HttpRequests.getInputStream(connection);
+        URLConnection connection = request.getConnection();
+        OutputStream fileOut = new FileOutputStream(file);
         try {
-          OutputStream fileOut = new FileOutputStream(file);
-          try {
-            NetUtils.copyStreamContent(progressIndicator, input, fileOut, connection.getContentLength());
-          }
-          finally {
-            fileOut.close();
-          }
+          NetUtils.copyStreamContent(progressIndicator, request.getInputStream(), fileOut, connection.getContentLength());
         }
         finally {
-          input.close();
+          fileOut.close();
         }
 
         if (myFileName == null) {
@@ -291,9 +279,9 @@ public class PluginDownloader {
     if (fileName == null) {
       // try to find a filename in an URL
       final String usedURL = connection.getURL().toString();
-      fileName = usedURL.substring(usedURL.lastIndexOf("/") + 1);
+      fileName = usedURL.substring(usedURL.lastIndexOf('/') + 1);
       if (fileName.length() == 0 || fileName.contains("?")) {
-        fileName = myPluginUrl.substring(myPluginUrl.lastIndexOf("/") + 1);
+        fileName = myPluginUrl.substring(myPluginUrl.lastIndexOf('/') + 1);
       }
     }
 
index bd6ebc3859c5f76d8470208bf02a2680fe76ea82..fa58f145f7719104d2d8e8873796c4ce82ddd86f 100644 (file)
@@ -40,10 +40,12 @@ import com.intellij.openapi.vfs.StandardFileSystems;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.util.*;
 import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.io.HttpRequests;
 import com.intellij.util.io.UrlConnectionUtil;
 import com.intellij.util.net.HttpConfigurable;
 import com.intellij.util.net.NetUtils;
 import com.intellij.util.ui.UIUtil;
+import org.jdom.Document;
 import org.jdom.Element;
 import org.jdom.JDOMException;
 import org.jetbrains.annotations.Contract;
@@ -359,26 +361,19 @@ public final class UpdateChecker {
       url = host + (host.contains("?") ? '&' : '?') + "build=" + ApplicationInfo.getInstance().getBuild().asString();
     }
 
-    BufferExposingByteArrayOutputStream bytes = HttpRequests.request(url)
-      .get(new ThrowableConvertor<URLConnection, BufferExposingByteArrayOutputStream, Exception>() {
-        @Override
-        public BufferExposingByteArrayOutputStream convert(URLConnection connection) throws Exception {
-          InputStream input = HttpRequests.getInputStream(connection);
-          try {
-            BufferExposingByteArrayOutputStream output = new BufferExposingByteArrayOutputStream();
-            try {
-              NetUtils.copyStreamContent(indicator, input, output, connection.getContentLength());
-            }
-            finally {
-              output.close();
-            }
-            return output;
-          }
-          finally {
-            input.close();
-          }
+    BufferExposingByteArrayOutputStream bytes = HttpRequests.request(url).connect(new HttpRequests.RequestProcessor<BufferExposingByteArrayOutputStream>() {
+      @Override
+      public BufferExposingByteArrayOutputStream process(@NotNull HttpRequests.Request request) throws IOException {
+        BufferExposingByteArrayOutputStream output = new BufferExposingByteArrayOutputStream();
+        try {
+          NetUtils.copyStreamContent(indicator, request.getInputStream(), output, request.getConnection().getContentLength());
         }
-      });
+        finally {
+          output.close();
+        }
+        return output;
+      }
+    });
 
     ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes.getInternalBuffer(), 0, bytes.size());
     Element element;
@@ -499,21 +494,23 @@ public final class UpdateChecker {
       return null;
     }
 
-    return HttpRequests.request(updateUrl.startsWith("file:") ? updateUrl : updateUrl + '?' + prepareUpdateCheckArgs())
-      .get(new ThrowableConvertor<URLConnection, UpdatesInfo, Exception>() {
-        @Override
-        public UpdatesInfo convert(URLConnection connection) throws Exception {
-          InputStream inputStream = HttpRequests.getInputStream(connection);
-          try {
-            return new UpdatesInfo(JDOMUtil.load(inputStream));
-          }
-          catch (JDOMException e) {
-            // Broken xml downloaded. Don't bother telling user.
-            LOG.info(e);
-            return null;
-          }
+    if (!updateUrl.startsWith("file:")) {
+      updateUrl = updateUrl + '?' + prepareUpdateCheckArgs();
+    }
+    return HttpRequests.request(updateUrl).connect(new HttpRequests.RequestProcessor<UpdatesInfo>() {
+      @Override
+      public UpdatesInfo process(@NotNull HttpRequests.Request request) throws IOException {
+        try {
+          Document document = JDOMUtil.loadDocument(request.getInputStream());
+          return new UpdatesInfo(document.detachRootElement());
         }
-      });
+        catch (JDOMException e) {
+          // corrupted content, don't bother telling user
+          LOG.info(e);
+          return null;
+        }
+      }
+    });
   }
 
   @NotNull
diff --git a/platform/platform-impl/src/com/intellij/util/HttpRequests.java b/platform/platform-impl/src/com/intellij/util/HttpRequests.java
deleted file mode 100644 (file)
index 9bc99d1..0000000
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright 2000-2014 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.intellij.util;
-
-import com.intellij.ide.IdeBundle;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.util.net.HttpConfigurable;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.net.URLConnection;
-import java.util.zip.GZIPInputStream;
-
-/**
- * GZip supported by default, so, you must use {@link #getInputStream(java.net.URLConnection)} to get connection input stream.
- */
-public final class HttpRequests {
-  public static class HttpRequestBuilder {
-    private final String url;
-    private int connectTimeout = HttpConfigurable.CONNECTION_TIMEOUT;
-    private int readTimeout = HttpConfigurable.CONNECTION_TIMEOUT;
-
-    private Consumer<String> effectiveUrlConsumer;
-
-    private boolean supportGzip = true;
-
-    private HttpRequestBuilder(@NotNull String url) {
-      this.url = url;
-    }
-
-    @NotNull
-    public HttpRequestBuilder connectTimeout(int value) {
-      connectTimeout = value;
-      return this;
-    }
-
-    @SuppressWarnings("unused")
-    @NotNull
-    public HttpRequestBuilder readTimeout(int value) {
-      readTimeout = value;
-      return this;
-    }
-
-    @NotNull
-    public HttpRequestBuilder supportGzip(boolean value) {
-      supportGzip = value;
-      return this;
-    }
-
-    @NotNull
-    public HttpRequestBuilder effectiveUrlConsumer(Consumer<String> value) {
-      effectiveUrlConsumer = value;
-      return this;
-    }
-
-    public <T, E extends Throwable> T get(@NotNull final ThrowableConvertor<URLConnection, T, E> handler) throws E, IOException {
-      return loadData(this, handler);
-    }
-  }
-
-  @NotNull
-  public static HttpRequestBuilder request(@NotNull String url) {
-    return new HttpRequestBuilder(url);
-  }
-
-  @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
-  @NotNull
-  public static InputStream getInputStream(@NotNull URLConnection connection) throws IOException {
-    InputStream inputStream = connection.getInputStream();
-    if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) {
-      try {
-        return new GZIPInputStream(inputStream);
-      }
-      catch (IOException e) {
-        inputStream.close();
-        throw e;
-      }
-    }
-    else {
-      return inputStream;
-    }
-  }
-
-  private static <T, E extends Throwable> T loadData(@NotNull HttpRequestBuilder requestBuilder, @NotNull ThrowableConvertor<URLConnection, T, E> handler)
-    throws E, IOException {
-    ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
-    Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[0], oldClassLoader));
-    try {
-      URLConnection connection = openConnection(requestBuilder);
-      try {
-        return handler.convert(connection);
-      }
-      finally {
-        if (connection instanceof HttpURLConnection) {
-          ((HttpURLConnection)connection).disconnect();
-        }
-      }
-    }
-    finally {
-      Thread.currentThread().setContextClassLoader(oldClassLoader);
-    }
-  }
-
-  @NotNull
-  private static URLConnection openConnection(@NotNull HttpRequestBuilder requestBuilder) throws IOException {
-    int i = 0;
-    String url = requestBuilder.url;
-    while (i++ < 99) {
-      URLConnection connection;
-      if (ApplicationManager.getApplication() == null) {
-        connection = new URL(url).openConnection();
-      }
-      else {
-        connection = HttpConfigurable.getInstance().openConnection(url);
-      }
-
-      connection.setConnectTimeout(requestBuilder.connectTimeout);
-      connection.setReadTimeout(requestBuilder.readTimeout);
-
-      if (requestBuilder.supportGzip) {
-        connection.setRequestProperty("Accept-Encoding", "gzip");
-      }
-
-      connection.setUseCaches(false);
-
-      if (connection instanceof HttpURLConnection) {
-        int responseCode = ((HttpURLConnection)connection).getResponseCode();
-        if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) {
-          if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
-            url = connection.getHeaderField("Location");
-          }
-          else {
-            url = null;
-          }
-
-          if (url == null) {
-            throw new IOException(IdeBundle.message("error.connection.failed.with.http.code.N", responseCode));
-          }
-          else {
-            ((HttpURLConnection)connection).disconnect();
-            continue;
-          }
-        }
-      }
-
-      if (url != requestBuilder.url && requestBuilder.effectiveUrlConsumer != null) {
-        requestBuilder.effectiveUrlConsumer.consume(url);
-      }
-      return connection;
-    }
-    throw new IOException("Infinite redirection");
-  }
-}
\ No newline at end of file
diff --git a/platform/platform-impl/src/com/intellij/util/io/HttpRequests.java b/platform/platform-impl/src/com/intellij/util/io/HttpRequests.java
new file mode 100644 (file)
index 0000000..8051801
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.util.io;
+
+import com.intellij.ide.IdeBundle;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.util.net.HttpConfigurable;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.net.URLConnection;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Handy class for reading data from HTTP connections with built-in support for HTTP redirects and gzipped content and automatic cleanup.
+ * Usage: <pre>{@code
+ * int firstByte = HttpRequests.request(url).connect(new HttpRequests.RequestProcessor<Integer>() {
+ *   public Integer process(@NotNull Request request) throws IOException {
+ *     return request.getInputStream().read();
+ *   }
+ * });
+ * }</pre>
+ */
+public final class HttpRequests {
+  public interface Request {
+    @NotNull URLConnection getConnection() throws IOException;
+    @NotNull InputStream getInputStream() throws IOException;
+  }
+
+  public interface RequestProcessor<T> {
+    T process(@NotNull Request request) throws IOException;
+  }
+
+  public static class RequestBuilder {
+    private final String myUrl;
+    private int myConnectTimeout = HttpConfigurable.CONNECTION_TIMEOUT;
+    private int myTimeout = HttpConfigurable.READ_TIMEOUT;
+    private int myRedirectLimit = HttpConfigurable.REDIRECT_LIMIT;
+    private boolean myGzip = true;
+
+    private RequestBuilder(@NotNull String url) {
+      myUrl = url;
+    }
+
+    @NotNull
+    public RequestBuilder connectTimeout(int value) {
+      myConnectTimeout = value;
+      return this;
+    }
+
+    @NotNull
+    public RequestBuilder readTimeout(int value) {
+      myTimeout = value;
+      return this;
+    }
+
+    @NotNull
+    public RequestBuilder redirectLimit(int redirectLimit) {
+      myRedirectLimit = redirectLimit;
+      return this;
+    }
+
+    @NotNull
+    public RequestBuilder gzip(boolean value) {
+      myGzip = value;
+      return this;
+    }
+
+    public <T> T connect(@NotNull RequestProcessor<T> processor) throws IOException {
+      return process(this, processor);
+    }
+  }
+
+  @NotNull
+  public static RequestBuilder request(@NotNull String url) {
+    return new RequestBuilder(url);
+  }
+
+  private static <T> T process(RequestBuilder builder, RequestProcessor<T> processor) throws IOException {
+    // hack-around for class loader lock in sun.net.www.protocol.http.NegotiateAuthentication (IDEA-131621)
+    ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
+    Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[0], oldClassLoader));
+    try {
+      return doProcess(builder, processor);
+    }
+    finally {
+      Thread.currentThread().setContextClassLoader(oldClassLoader);
+    }
+  }
+
+  private static <T> T doProcess(final RequestBuilder builder, RequestProcessor<T> processor) throws IOException {
+    class RequestImpl implements Request {
+      private URLConnection myConnection;
+      private InputStream myInputStream;
+
+      @NotNull
+      @Override
+      public URLConnection getConnection() throws IOException {
+        if (myConnection == null) {
+          myConnection = openConnection(builder);
+        }
+        return myConnection;
+      }
+
+      @NotNull
+      @Override
+      public InputStream getInputStream() throws IOException {
+        if (myInputStream == null) {
+          myInputStream = getConnection().getInputStream();
+          if (builder.myGzip && "gzip".equalsIgnoreCase(getConnection().getContentEncoding())) {
+            //noinspection IOResourceOpenedButNotSafelyClosed
+            myInputStream = new GZIPInputStream(myInputStream);
+          }
+        }
+        return myInputStream;
+      }
+
+      private void cleanup() throws IOException {
+        if (myInputStream != null) {
+          myInputStream.close();
+        }
+        if (myConnection instanceof HttpURLConnection) {
+          ((HttpURLConnection)myConnection).disconnect();
+        }
+      }
+    }
+
+    RequestImpl request = new RequestImpl();
+    try {
+      return processor.process(request);
+    }
+    finally {
+      request.cleanup();
+    }
+  }
+
+  private static URLConnection openConnection(RequestBuilder builder) throws IOException {
+    String url = builder.myUrl;
+
+    for (int i = 0; i < builder.myRedirectLimit; i++) {
+      URLConnection connection;
+      if (ApplicationManager.getApplication() == null) {
+        connection = new URL(url).openConnection();
+      }
+      else {
+        connection = HttpConfigurable.getInstance().openConnection(url);
+      }
+
+      connection.setConnectTimeout(builder.myConnectTimeout);
+      connection.setReadTimeout(builder.myTimeout);
+      if (builder.myGzip) {
+        connection.setRequestProperty("Accept-Encoding", "gzip");
+      }
+      connection.setUseCaches(false);
+
+      if (connection instanceof HttpURLConnection) {
+        int responseCode = ((HttpURLConnection)connection).getResponseCode();
+        if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) {
+          if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
+            url = connection.getHeaderField("Location");
+          }
+          else {
+            url = null;
+          }
+
+          if (url == null) {
+            throw new IOException(IdeBundle.message("error.connection.failed.with.http.code.N", responseCode));
+          }
+
+          ((HttpURLConnection)connection).disconnect();
+          continue;
+        }
+      }
+
+      return connection;
+    }
+
+    throw new IOException("Too many redirects");
+  }
+}
\ No newline at end of file
diff --git a/platform/platform-tests/testSrc/com/intellij/util/io/HttpRequestsTest.java b/platform/platform-tests/testSrc/com/intellij/util/io/HttpRequestsTest.java
new file mode 100644 (file)
index 0000000..2a68a2c
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.util.io;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class HttpRequestsTest {
+  private final HttpRequests.RequestProcessor<Void> myProcessor = new HttpRequests.RequestProcessor<Void>() {
+    @SuppressWarnings("ResultOfMethodCallIgnored")
+    @Override
+    public Void process(@NotNull HttpRequests.Request request) throws IOException {
+      request.getInputStream().read();
+      return null;
+    }
+  };
+
+  @Test
+  public void testLimit() {
+    try {
+      HttpRequests.request("").redirectLimit(0).connect(myProcessor);
+      fail();
+    }
+    catch (IOException e) {
+      assertEquals("Too many redirects", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testConnectTimeout() {
+    try {
+      HttpRequests.request("http://openjdk.java.net").connectTimeout(1).connect(myProcessor);
+      fail();
+    }
+    catch (SocketTimeoutException ignore) { }
+    catch (IOException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  @Test
+  public void testReadTimeout() {
+    try {
+      HttpRequests.request("http://openjdk.java.net").readTimeout(1).connect(myProcessor);
+      fail();
+    }
+    catch (SocketTimeoutException ignore) { }
+    catch (IOException e) {
+      fail(e.getMessage());
+    }
+  }
+}