You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2018/01/03 18:47:25 UTC

[1/4] httpcomponents-client git commit: Moved ByteArrayBuilder class (no functional changes)

Repository: httpcomponents-client
Updated Branches:
  refs/heads/master eb1cab46e -> 47a0eb8b6


Moved ByteArrayBuilder class (no functional changes)


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/db4b6db7
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/db4b6db7
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/db4b6db7

Branch: refs/heads/master
Commit: db4b6db79fbb7f36d02d2b401658f36a91a47028
Parents: eb1cab4
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Wed Jan 3 14:35:51 2018 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Wed Jan 3 14:36:20 2018 +0100

----------------------------------------------------------------------
 .../hc/client5/http/auth/ByteArrayBuilder.java  | 211 +++++++++++++++++++
 .../http/auth/util/ByteArrayBuilder.java        | 211 -------------------
 .../hc/client5/http/impl/auth/BasicScheme.java  |   2 +-
 .../hc/client5/http/impl/auth/DigestScheme.java |   2 +-
 .../client5/http/impl/auth/NTLMEngineImpl.java  |   2 +-
 .../http/auth/util/TestByteArrayBuilder.java    |   1 +
 6 files changed, 215 insertions(+), 214 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/db4b6db7/httpclient5/src/main/java/org/apache/hc/client5/http/auth/ByteArrayBuilder.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/ByteArrayBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/ByteArrayBuilder.java
new file mode 100644
index 0000000..1d12878
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/ByteArrayBuilder.java
@@ -0,0 +1,211 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.auth;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+/**
+ * Builder class for sequences of bytes.
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class ByteArrayBuilder {
+
+    private CharsetEncoder charsetEncoder;
+    private ByteBuffer buffer;
+
+    public ByteArrayBuilder() {
+    }
+
+    public ByteArrayBuilder(final int initialCapacity) {
+        this.buffer = ByteBuffer.allocate(initialCapacity);
+    }
+
+    public int capacity() {
+        return this.buffer != null ? this.buffer.capacity() : 0;
+    }
+
+    static ByteBuffer ensureFreeCapacity(final ByteBuffer buffer, final int capacity) {
+        if (buffer == null) {
+            return ByteBuffer.allocate(capacity);
+        }
+        if (buffer.remaining() < capacity) {
+            final ByteBuffer newBuffer = ByteBuffer.allocate(buffer.position() + capacity);
+            buffer.flip();
+            newBuffer.put(buffer);
+            return newBuffer;
+        }
+        return buffer;
+    }
+
+    static ByteBuffer encode(
+            final ByteBuffer buffer, final CharBuffer in, final CharsetEncoder encoder) throws CharacterCodingException {
+
+        final int capacity = (int) (in.remaining() * encoder.averageBytesPerChar());
+        ByteBuffer out = ensureFreeCapacity(buffer, capacity);
+        for (;;) {
+            CoderResult result = in.hasRemaining() ? encoder.encode(in, out, true) : CoderResult.UNDERFLOW;
+            if (result.isError()) {
+                result.throwException();
+            }
+            if (result.isUnderflow()) {
+                result = encoder.flush(out);
+            }
+            if (result.isUnderflow()) {
+                break;
+            }
+            if (result.isOverflow()) {
+                out = ensureFreeCapacity(out, capacity);
+            }
+        }
+        return out;
+    }
+
+    public void ensureFreeCapacity(final int freeCapacity) {
+        this.buffer = ensureFreeCapacity(this.buffer, freeCapacity);
+    }
+
+    private void doAppend(final CharBuffer charBuffer) {
+        if (this.charsetEncoder == null) {
+            this.charsetEncoder = StandardCharsets.US_ASCII.newEncoder()
+                    .onMalformedInput(CodingErrorAction.IGNORE)
+                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
+        }
+        this.charsetEncoder.reset();
+        try {
+            this.buffer = encode(this.buffer, charBuffer, this.charsetEncoder);
+        } catch (final CharacterCodingException ex) {
+            // Should never happen
+            throw new IllegalStateException("Unexpected character coding error", ex);
+        }
+    }
+
+    public ByteArrayBuilder charset(final Charset charset) {
+        if (charset == null) {
+            this.charsetEncoder = null;
+        } else {
+            this.charsetEncoder = charset.newEncoder()
+                    .onMalformedInput(CodingErrorAction.IGNORE)
+                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
+        }
+        return this;
+    }
+
+    public ByteArrayBuilder append(final byte[] b, final int off, final int len) {
+        if (b == null) {
+            return this;
+        }
+        if ((off < 0) || (off > b.length) || (len < 0) ||
+                ((off + len) < 0) || ((off + len) > b.length)) {
+            throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length);
+        }
+        ensureFreeCapacity(len);
+        this.buffer.put(b, off, len);
+        return this;
+    }
+
+    public ByteArrayBuilder append(final byte[] b) {
+        if (b == null) {
+            return this;
+        }
+        return append(b, 0, b.length);
+    }
+
+    public ByteArrayBuilder append(final CharBuffer charBuffer) {
+        if (charBuffer == null) {
+            return this;
+        }
+        doAppend(charBuffer);
+        return this;
+    }
+
+    public ByteArrayBuilder append(final char[] b, final int off, final int len) {
+        if (b == null) {
+            return this;
+        }
+        if ((off < 0) || (off > b.length) || (len < 0) ||
+                ((off + len) < 0) || ((off + len) > b.length)) {
+            throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length);
+        }
+        return append(CharBuffer.wrap(b, off, len));
+    }
+
+    public ByteArrayBuilder append(final char[] b) {
+        if (b == null) {
+            return this;
+        }
+        return append(b, 0, b.length);
+    }
+
+    public ByteArrayBuilder append(final String s) {
+        if (s == null) {
+            return this;
+        }
+        return append(CharBuffer.wrap(s));
+    }
+
+    public ByteBuffer toByteBuffer() {
+        return this.buffer != null ? this.buffer.duplicate() : ByteBuffer.allocate(0);
+    }
+
+    public byte[] toByteArray() {
+        if (this.buffer == null) {
+            return new byte[] {};
+        }
+        this.buffer.flip();
+        final byte[] b = new byte[this.buffer.remaining()];
+        this.buffer.get(b);
+        this.buffer.clear();
+        return b;
+    }
+
+    public void reset() {
+        if (this.charsetEncoder != null) {
+            this.charsetEncoder.reset();
+        }
+        if (this.buffer != null) {
+            this.buffer.clear();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return this.buffer != null ? this.buffer.toString() : "null";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/db4b6db7/httpclient5/src/main/java/org/apache/hc/client5/http/auth/util/ByteArrayBuilder.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/util/ByteArrayBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/util/ByteArrayBuilder.java
deleted file mode 100644
index 138f681..0000000
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/util/ByteArrayBuilder.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.auth.util;
-
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetEncoder;
-import java.nio.charset.CoderResult;
-import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
-
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
-
-/**
- * Builder class for sequences of bytes.
- *
- * @since 5.0
- */
-@Contract(threading = ThreadingBehavior.IMMUTABLE)
-public class ByteArrayBuilder {
-
-    private CharsetEncoder charsetEncoder;
-    private ByteBuffer buffer;
-
-    public ByteArrayBuilder() {
-    }
-
-    public ByteArrayBuilder(final int initialCapacity) {
-        this.buffer = ByteBuffer.allocate(initialCapacity);
-    }
-
-    public int capacity() {
-        return this.buffer != null ? this.buffer.capacity() : 0;
-    }
-
-    static ByteBuffer ensureFreeCapacity(final ByteBuffer buffer, final int capacity) {
-        if (buffer == null) {
-            return ByteBuffer.allocate(capacity);
-        }
-        if (buffer.remaining() < capacity) {
-            final ByteBuffer newBuffer = ByteBuffer.allocate(buffer.position() + capacity);
-            buffer.flip();
-            newBuffer.put(buffer);
-            return newBuffer;
-        }
-        return buffer;
-    }
-
-    static ByteBuffer encode(
-            final ByteBuffer buffer, final CharBuffer in, final CharsetEncoder encoder) throws CharacterCodingException {
-
-        final int capacity = (int) (in.remaining() * encoder.averageBytesPerChar());
-        ByteBuffer out = ensureFreeCapacity(buffer, capacity);
-        for (;;) {
-            CoderResult result = in.hasRemaining() ? encoder.encode(in, out, true) : CoderResult.UNDERFLOW;
-            if (result.isError()) {
-                result.throwException();
-            }
-            if (result.isUnderflow()) {
-                result = encoder.flush(out);
-            }
-            if (result.isUnderflow()) {
-                break;
-            }
-            if (result.isOverflow()) {
-                out = ensureFreeCapacity(out, capacity);
-            }
-        }
-        return out;
-    }
-
-    public void ensureFreeCapacity(final int freeCapacity) {
-        this.buffer = ensureFreeCapacity(this.buffer, freeCapacity);
-    }
-
-    private void doAppend(final CharBuffer charBuffer) {
-        if (this.charsetEncoder == null) {
-            this.charsetEncoder = StandardCharsets.US_ASCII.newEncoder()
-                    .onMalformedInput(CodingErrorAction.IGNORE)
-                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
-        }
-        this.charsetEncoder.reset();
-        try {
-            this.buffer = encode(this.buffer, charBuffer, this.charsetEncoder);
-        } catch (final CharacterCodingException ex) {
-            // Should never happen
-            throw new IllegalStateException("Unexpected character coding error", ex);
-        }
-    }
-
-    public ByteArrayBuilder charset(final Charset charset) {
-        if (charset == null) {
-            this.charsetEncoder = null;
-        } else {
-            this.charsetEncoder = charset.newEncoder()
-                    .onMalformedInput(CodingErrorAction.IGNORE)
-                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
-        }
-        return this;
-    }
-
-    public ByteArrayBuilder append(final byte[] b, final int off, final int len) {
-        if (b == null) {
-            return this;
-        }
-        if ((off < 0) || (off > b.length) || (len < 0) ||
-                ((off + len) < 0) || ((off + len) > b.length)) {
-            throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length);
-        }
-        ensureFreeCapacity(len);
-        this.buffer.put(b, off, len);
-        return this;
-    }
-
-    public ByteArrayBuilder append(final byte[] b) {
-        if (b == null) {
-            return this;
-        }
-        return append(b, 0, b.length);
-    }
-
-    public ByteArrayBuilder append(final CharBuffer charBuffer) {
-        if (charBuffer == null) {
-            return this;
-        }
-        doAppend(charBuffer);
-        return this;
-    }
-
-    public ByteArrayBuilder append(final char[] b, final int off, final int len) {
-        if (b == null) {
-            return this;
-        }
-        if ((off < 0) || (off > b.length) || (len < 0) ||
-                ((off + len) < 0) || ((off + len) > b.length)) {
-            throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length);
-        }
-        return append(CharBuffer.wrap(b, off, len));
-    }
-
-    public ByteArrayBuilder append(final char[] b) {
-        if (b == null) {
-            return this;
-        }
-        return append(b, 0, b.length);
-    }
-
-    public ByteArrayBuilder append(final String s) {
-        if (s == null) {
-            return this;
-        }
-        return append(CharBuffer.wrap(s));
-    }
-
-    public ByteBuffer toByteBuffer() {
-        return this.buffer != null ? this.buffer.duplicate() : ByteBuffer.allocate(0);
-    }
-
-    public byte[] toByteArray() {
-        if (this.buffer == null) {
-            return new byte[] {};
-        }
-        this.buffer.flip();
-        final byte[] b = new byte[this.buffer.remaining()];
-        this.buffer.get(b);
-        this.buffer.clear();
-        return b;
-    }
-
-    public void reset() {
-        if (this.charsetEncoder != null) {
-            this.charsetEncoder.reset();
-        }
-        if (this.buffer != null) {
-            this.buffer.clear();
-        }
-    }
-
-    @Override
-    public String toString() {
-        return this.buffer != null ? this.buffer.toString() : "null";
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/db4b6db7/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicScheme.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicScheme.java
index d700184..a198b8f 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicScheme.java
@@ -48,7 +48,7 @@ import org.apache.hc.client5.http.auth.AuthenticationException;
 import org.apache.hc.client5.http.auth.Credentials;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.MalformedChallengeException;
-import org.apache.hc.client5.http.auth.util.ByteArrayBuilder;
+import org.apache.hc.client5.http.auth.ByteArrayBuilder;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.NameValuePair;

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/db4b6db7/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
index 2c754f1..ed87755 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
@@ -51,7 +51,7 @@ import org.apache.hc.client5.http.auth.AuthenticationException;
 import org.apache.hc.client5.http.auth.Credentials;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.MalformedChallengeException;
-import org.apache.hc.client5.http.auth.util.ByteArrayBuilder;
+import org.apache.hc.client5.http.auth.ByteArrayBuilder;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpHost;

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/db4b6db7/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java
index c8e5db0..e3a4dc3 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java
@@ -41,7 +41,7 @@ import javax.crypto.Cipher;
 import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.commons.codec.binary.Base64;
-import org.apache.hc.client5.http.auth.util.ByteArrayBuilder;
+import org.apache.hc.client5.http.auth.ByteArrayBuilder;
 
 /**
  * Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/db4b6db7/httpclient5/src/test/java/org/apache/hc/client5/http/auth/util/TestByteArrayBuilder.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/auth/util/TestByteArrayBuilder.java b/httpclient5/src/test/java/org/apache/hc/client5/http/auth/util/TestByteArrayBuilder.java
index 3438ad9..a6a2f4e 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/auth/util/TestByteArrayBuilder.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/auth/util/TestByteArrayBuilder.java
@@ -29,6 +29,7 @@ package org.apache.hc.client5.http.auth.util;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 
+import org.apache.hc.client5.http.auth.ByteArrayBuilder;
 import org.junit.Assert;
 import org.junit.Test;
 


[3/4] httpcomponents-client git commit: Improved flaky test case

Posted by ol...@apache.org.
Improved flaky test case


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/71060c7e
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/71060c7e
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/71060c7e

Branch: refs/heads/master
Commit: 71060c7ee147e6de5540fd6b72f1eefcd868ff23
Parents: b45b72e
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Wed Jan 3 15:58:34 2018 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Wed Jan 3 19:37:19 2018 +0100

----------------------------------------------------------------------
 .../testing/async/AbstractHttpAsyncFundamentalsTest.java      | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/71060c7e/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java
----------------------------------------------------------------------
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java
index dc8839a..6f36fd8 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncFundamentalsTest.java
@@ -179,7 +179,9 @@ public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpA
 
                 @Override
                 public void run() {
-                    httpclient.execute(SimpleHttpRequest.get(target, "/random/2048"), callback);
+                    if (!Thread.currentThread().isInterrupted()) {
+                        httpclient.execute(SimpleHttpRequest.get(target, "/random/2048"), callback);
+                    }
                 }
 
             });
@@ -187,6 +189,9 @@ public abstract class AbstractHttpAsyncFundamentalsTest<T extends CloseableHttpA
 
         Assert.assertThat(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), CoreMatchers.equalTo(true));
 
+        executorService.shutdownNow();
+        executorService.awaitTermination(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
         for (;;) {
             final SimpleHttpResponse response = resultQueue.poll();
             if (response == null) {


[2/4] httpcomponents-client git commit: Added HTTP routing support class; simplified HttpRoutePlanner API

Posted by ol...@apache.org.
Added HTTP routing support class; simplified HttpRoutePlanner API


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/b45b72ef
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/b45b72ef
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/b45b72ef

Branch: refs/heads/master
Commit: b45b72ef774dce7df3ec594bd92122fa6ba70b1c
Parents: db4b6db
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Wed Jan 3 15:36:27 2018 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Wed Jan 3 19:37:12 2018 +0100

----------------------------------------------------------------------
 .../hc/client5/http/SchemePortResolver.java     |  2 +-
 .../http/impl/DefaultSchemePortResolver.java    |  5 +-
 .../impl/async/InternalHttp2AsyncClient.java    |  5 +-
 .../impl/async/InternalHttpAsyncClient.java     |  5 +-
 .../client5/http/impl/auth/BasicAuthCache.java  | 27 +++-----
 .../http/impl/classic/InternalHttpClient.java   |  3 +-
 .../http/impl/classic/MinimalHttpClient.java    | 10 +--
 .../DefaultAsyncClientConnectionOperator.java   | 11 +---
 .../http/impl/routing/DefaultRoutePlanner.java  | 37 ++---------
 .../client5/http/routing/HttpRoutePlanner.java  | 14 ----
 .../hc/client5/http/routing/RoutingSupport.java | 68 ++++++++++++++++++++
 .../http/impl/auth/TestBasicAuthCache.java      | 20 ------
 .../impl/routing/TestDefaultRoutePlanner.java   | 26 --------
 .../http/impl/routing/TestRoutingSupport.java   | 62 ++++++++++++++++++
 14 files changed, 159 insertions(+), 136 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
index 7c10e04..c3caa83 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
@@ -38,6 +38,6 @@ public interface SchemePortResolver {
     /**
      * Returns the actual port for the host based on the protocol scheme.
      */
-    int resolve(HttpHost host) throws UnsupportedSchemeException;
+    int resolve(HttpHost host);
 
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
index 4611b08..37555f7 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
@@ -27,7 +27,6 @@
 package org.apache.hc.client5.http.impl;
 
 import org.apache.hc.client5.http.SchemePortResolver;
-import org.apache.hc.client5.http.UnsupportedSchemeException;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
@@ -44,7 +43,7 @@ public class DefaultSchemePortResolver implements SchemePortResolver {
     public static final DefaultSchemePortResolver INSTANCE = new DefaultSchemePortResolver();
 
     @Override
-    public int resolve(final HttpHost host) throws UnsupportedSchemeException {
+    public int resolve(final HttpHost host) {
         Args.notNull(host, "HTTP host");
         final int port = host.getPort();
         if (port > 0) {
@@ -56,7 +55,7 @@ public class DefaultSchemePortResolver implements SchemePortResolver {
         } else if (name.equalsIgnoreCase("https")) {
             return 443;
         } else {
-            throw new UnsupportedSchemeException(name + " protocol is not supported");
+            return -1;
         }
     }
 

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttp2AsyncClient.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttp2AsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttp2AsyncClient.java
index f68c69a..8be84b5 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttp2AsyncClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttp2AsyncClient.java
@@ -39,8 +39,8 @@ import org.apache.hc.client5.http.cookie.CookieSpecProvider;
 import org.apache.hc.client5.http.cookie.CookieStore;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
+import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.config.Lookup;
 import org.apache.hc.core5.http2.nio.pool.H2ConnPool;
@@ -77,8 +77,7 @@ class InternalHttp2AsyncClient extends InternalAbstractHttpAsyncClient {
 
     @Override
     HttpRoute determineRoute(final HttpRequest request, final HttpClientContext clientContext) throws HttpException {
-        final HttpHost target = routePlanner.determineTargetHost(request, clientContext);
-        final HttpRoute route = routePlanner.determineRoute(target, clientContext);
+        final HttpRoute route = routePlanner.determineRoute(RoutingSupport.determineHost(request), clientContext);
         if (route.isTunnelled()) {
             throw new HttpException("HTTP/2 tunneling not supported");
         }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java
index 8d23479..eed92f1 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java
@@ -40,8 +40,8 @@ import org.apache.hc.client5.http.cookie.CookieStore;
 import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
+import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.ProtocolVersion;
@@ -83,8 +83,7 @@ class InternalHttpAsyncClient extends InternalAbstractHttpAsyncClient {
 
     @Override
     HttpRoute determineRoute(final HttpRequest request, final HttpClientContext clientContext) throws HttpException {
-        final HttpHost target = routePlanner.determineTargetHost(request, clientContext);
-        final HttpRoute route = routePlanner.determineRoute(target, clientContext);
+        final HttpRoute route = routePlanner.determineRoute(RoutingSupport.determineHost(request), clientContext);
         final ProtocolVersion protocolVersion = clientContext.getProtocolVersion();
         if (route.isTunnelled() && protocolVersion.greaterEquals(HttpVersion.HTTP_2_0)) {
             throw new HttpException("HTTP/2 tunneling not supported");

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
index dfd1aa4..d9b2980 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
@@ -36,10 +36,10 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.hc.client5.http.SchemePortResolver;
-import org.apache.hc.client5.http.UnsupportedSchemeException;
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
@@ -73,27 +73,13 @@ public class BasicAuthCache implements AuthCache {
     public BasicAuthCache(final SchemePortResolver schemePortResolver) {
         super();
         this.map = new ConcurrentHashMap<>();
-        this.schemePortResolver = schemePortResolver != null ? schemePortResolver :
-            DefaultSchemePortResolver.INSTANCE;
+        this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
     }
 
     public BasicAuthCache() {
         this(null);
     }
 
-    protected HttpHost getKey(final HttpHost host) {
-        if (host.getPort() <= 0) {
-            final int port;
-            try {
-                port = schemePortResolver.resolve(host);
-            } catch (final UnsupportedSchemeException ignore) {
-                return host;
-            }
-            return new HttpHost(host.getHostName(), port, host.getSchemeName());
-        }
-        return host;
-    }
-
     @Override
     public void put(final HttpHost host, final AuthScheme authScheme) {
         Args.notNull(host, "HTTP host");
@@ -106,7 +92,8 @@ public class BasicAuthCache implements AuthCache {
                 try (final ObjectOutputStream out = new ObjectOutputStream(buf)) {
                     out.writeObject(authScheme);
                 }
-                this.map.put(getKey(host), buf.toByteArray());
+                final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
+                this.map.put(key, buf.toByteArray());
             } catch (final IOException ex) {
                 if (log.isWarnEnabled()) {
                     log.warn("Unexpected I/O error while serializing auth scheme", ex);
@@ -122,7 +109,8 @@ public class BasicAuthCache implements AuthCache {
     @Override
     public AuthScheme get(final HttpHost host) {
         Args.notNull(host, "HTTP host");
-        final byte[] bytes = this.map.get(getKey(host));
+        final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
+        final byte[] bytes = this.map.get(key);
         if (bytes != null) {
             try {
                 final ByteArrayInputStream buf = new ByteArrayInputStream(bytes);
@@ -147,7 +135,8 @@ public class BasicAuthCache implements AuthCache {
     @Override
     public void remove(final HttpHost host) {
         Args.notNull(host, "HTTP host");
-        this.map.remove(getKey(host));
+        final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
+        this.map.remove(key);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
index 795938a..46d99e1 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
@@ -46,6 +46,7 @@ import org.apache.hc.client5.http.impl.ExecSupport;
 import org.apache.hc.client5.http.io.HttpClientConnectionManager;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
+import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.ClassicHttpRequest;
@@ -111,7 +112,7 @@ class InternalHttpClient extends CloseableHttpClient implements Configurable {
             final HttpHost host,
             final HttpRequest request,
             final HttpContext context) throws HttpException {
-        final HttpHost target = host != null ? host : this.routePlanner.determineTargetHost(request, context);
+        final HttpHost target = host != null ? host : RoutingSupport.determineHost(request);
         return this.routePlanner.determineRoute(target, context);
     }
 

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java
index e335abd..4c39405 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java
@@ -33,6 +33,7 @@ import java.io.InterruptedIOException;
 import org.apache.hc.client5.http.CancellableAware;
 import org.apache.hc.client5.http.ClientProtocolException;
 import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.classic.ExecRuntime;
 import org.apache.hc.client5.http.config.Configurable;
 import org.apache.hc.client5.http.config.RequestConfig;
@@ -41,6 +42,7 @@ import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.io.HttpClientConnectionManager;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.protocol.RequestClientConnControl;
+import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.ClassicHttpRequest;
@@ -77,6 +79,7 @@ public class MinimalHttpClient extends CloseableHttpClient {
 
     private final HttpClientConnectionManager connManager;
     private final ConnectionReuseStrategy reuseStrategy;
+    private final SchemePortResolver schemePortResolver;
     private final HttpRequestExecutor requestExecutor;
     private final HttpProcessor httpProcessor;
 
@@ -84,6 +87,7 @@ public class MinimalHttpClient extends CloseableHttpClient {
         super();
         this.connManager = Args.notNull(connManager, "HTTP connection manager");
         this.reuseStrategy = DefaultConnectionReuseStrategy.INSTANCE;
+        this.schemePortResolver = DefaultSchemePortResolver.INSTANCE;
         this.requestExecutor = new HttpRequestExecutor(this.reuseStrategy);
         this.httpProcessor = new DefaultHttpProcessor(
                 new RequestContent(),
@@ -116,11 +120,7 @@ public class MinimalHttpClient extends CloseableHttpClient {
             clientContext.setRequestConfig(config);
         }
 
-        final HttpRoute route = new HttpRoute(target.getPort() > 0 ? target : new HttpHost(
-                target.getHostName(),
-                DefaultSchemePortResolver.INSTANCE.resolve(target),
-                target.getSchemeName()));
-
+        final HttpRoute route = new HttpRoute(RoutingSupport.normalize(target, schemePortResolver));
         final ExecRuntime execRuntime = new ExecRuntimeImpl(log, connManager, requestExecutor,
                 request instanceof CancellableAware ? (CancellableAware) request : null);
         try {

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java
index 75b285a..33503da 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java
@@ -34,10 +34,10 @@ import java.util.concurrent.Future;
 
 import org.apache.hc.client5.http.DnsResolver;
 import org.apache.hc.client5.http.SchemePortResolver;
-import org.apache.hc.client5.http.UnsupportedSchemeException;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
 import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.annotation.Internal;
 import org.apache.hc.core5.concurrent.ComplexFuture;
 import org.apache.hc.core5.concurrent.FutureCallback;
@@ -81,14 +81,7 @@ final class DefaultAsyncClientConnectionOperator implements AsyncClientConnectio
         Args.notNull(connectionInitiator, "Connection initiator");
         Args.notNull(host, "Host");
         final ComplexFuture<ManagedAsyncClientConnection> future = new ComplexFuture<>(callback);
-        final HttpHost remoteEndpoint;
-        try {
-            remoteEndpoint = host.getPort() > 0 ? host :
-                    new HttpHost(host.getHostName(), schemePortResolver.resolve(host), host.getSchemeName());
-        } catch (final UnsupportedSchemeException ex) {
-            future.failed(ex);
-            return future;
-        }
+        final HttpHost remoteEndpoint = RoutingSupport.normalize(host, schemePortResolver);
         final InetAddress remoteAddress = host.getAddress();
         final TlsStrategy tlsStrategy = tlsStrategyLookup != null ? tlsStrategyLookup.lookup(host.getSchemeName()) : null;
         final Future<IOSession> sessionFuture = sessionRequester.connect(

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/DefaultRoutePlanner.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/DefaultRoutePlanner.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/DefaultRoutePlanner.java
index 02cc5ab..513d8a1 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/DefaultRoutePlanner.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/DefaultRoutePlanner.java
@@ -31,19 +31,17 @@ import java.net.InetAddress;
 
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.SchemePortResolver;
-import org.apache.hc.client5.http.UnsupportedSchemeException;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
+import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.net.URIAuthority;
 
 /**
  * Default implementation of an {@link HttpRoutePlanner}. It will not make use of
@@ -58,8 +56,7 @@ public class DefaultRoutePlanner implements HttpRoutePlanner {
 
     public DefaultRoutePlanner(final SchemePortResolver schemePortResolver) {
         super();
-        this.schemePortResolver = schemePortResolver != null ? schemePortResolver :
-            DefaultSchemePortResolver.INSTANCE;
+        this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
     }
 
     @Override
@@ -73,19 +70,9 @@ public class DefaultRoutePlanner implements HttpRoutePlanner {
         if (proxy == null) {
             proxy = determineProxy(host, context);
         }
-
-        final HttpHost target;
-        if (host.getPort() <= 0) {
-            try {
-                target = new HttpHost(
-                        host.getHostName(),
-                        this.schemePortResolver.resolve(host),
-                        host.getSchemeName());
-            } catch (final UnsupportedSchemeException ex) {
-                throw new HttpException(ex.getMessage());
-            }
-        } else {
-            target = host;
+        final HttpHost target = RoutingSupport.normalize(host, schemePortResolver);
+        if (target.getPort() < 0) {
+            throw new ProtocolException("Unroutable protocol scheme: " + target);
         }
         final boolean secure = target.getSchemeName().equalsIgnoreCase("https");
         if (proxy == null) {
@@ -95,20 +82,6 @@ public class DefaultRoutePlanner implements HttpRoutePlanner {
         }
     }
 
-    @Override
-    public final HttpHost determineTargetHost(final HttpRequest request, final HttpContext context) throws HttpException {
-        final URIAuthority authority = request.getAuthority();
-        if (authority != null) {
-            final String scheme = request.getScheme();
-            if (scheme == null) {
-                throw new ProtocolException("Protocol scheme is not specified");
-            }
-            return new HttpHost(authority, scheme);
-        } else {
-            return null;
-        }
-    }
-
     /**
      * This implementation returns null.
      *

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/routing/HttpRoutePlanner.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/routing/HttpRoutePlanner.java b/httpclient5/src/main/java/org/apache/hc/client5/http/routing/HttpRoutePlanner.java
index 8dade56..03b6599 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/routing/HttpRoutePlanner.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/routing/HttpRoutePlanner.java
@@ -30,7 +30,6 @@ package org.apache.hc.client5.http.routing;
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.protocol.HttpContext;
 
 /**
@@ -60,17 +59,4 @@ public interface HttpRoutePlanner {
      */
     HttpRoute determineRoute(HttpHost target, HttpContext context) throws HttpException;
 
-    /**
-     * Determines the target host for the given request.
-     *
-     * @param request   the request to be executed
-     * @param context   the context to use for the subsequent execution.
-     *                  Implementations may accept {@code null}.
-     *
-     * @return  the route that the request should take
-     *
-     * @throws HttpException    in case of a problem
-     */
-    HttpHost determineTargetHost(HttpRequest request, HttpContext context) throws HttpException;
-
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/main/java/org/apache/hc/client5/http/routing/RoutingSupport.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/routing/RoutingSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/routing/RoutingSupport.java
new file mode 100644
index 0000000..946ac14
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/routing/RoutingSupport.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.routing;
+
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.net.URIAuthority;
+
+public final class RoutingSupport {
+
+    public static HttpHost determineHost(final HttpRequest request) throws HttpException {
+        if (request == null) {
+            return null;
+        }
+        final URIAuthority authority = request.getAuthority();
+        if (authority != null) {
+            final String scheme = request.getScheme();
+            if (scheme == null) {
+                throw new ProtocolException("Protocol scheme is not specified");
+            }
+            return new HttpHost(authority, scheme);
+        } else {
+            return null;
+        }
+    }
+
+    public static HttpHost normalize(final HttpHost host, final SchemePortResolver schemePortResolver) {
+        if (host == null) {
+            return null;
+        }
+        if (host.getPort() < 0) {
+            final int port = (schemePortResolver != null ? schemePortResolver: DefaultSchemePortResolver.INSTANCE).resolve(host);
+            if (port > 0) {
+                return new HttpHost(host.getHostName(), port, host.getSchemeName());
+            }
+        }
+        return host;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
index 7a37dfb..6279805 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
@@ -27,12 +27,10 @@
 
 package org.apache.hc.client5.http.impl.auth;
 
-import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.core5.http.HttpHost;
 import org.junit.Assert;
 import org.junit.Test;
-import org.mockito.Mockito;
 
 /**
  * Unit tests for {@link BasicAuthCache}.
@@ -68,24 +66,6 @@ public class TestBasicAuthCache {
     }
 
     @Test
-    public void testGetKey() throws Exception {
-        final BasicAuthCache cache = new BasicAuthCache();
-        final HttpHost target = new HttpHost("localhost", 443, "https");
-        Assert.assertSame(target, cache.getKey(target));
-        Assert.assertEquals(target, cache.getKey(new HttpHost("localhost", -1, "https")));
-    }
-
-    @Test
-    public void testGetKeyWithSchemeRegistry() throws Exception {
-        final SchemePortResolver schemePortResolver = Mockito.mock(SchemePortResolver.class);
-        final BasicAuthCache cache = new BasicAuthCache(schemePortResolver);
-        Mockito.when(schemePortResolver.resolve(new HttpHost("localhost", -1, "https"))).thenReturn(443);
-        final HttpHost target = new HttpHost("localhost", 443, "https");
-        Assert.assertSame(target, cache.getKey(target));
-        Assert.assertEquals(target, cache.getKey(new HttpHost("localhost", -1, "https")));
-    }
-
-    @Test
     public void testStoreNonserializable() throws Exception {
         final BasicAuthCache cache = new BasicAuthCache();
         final AuthScheme authScheme = new NTLMScheme();

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestDefaultRoutePlanner.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestDefaultRoutePlanner.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestDefaultRoutePlanner.java
index 0ca9166..6ec2e1f 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestDefaultRoutePlanner.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestDefaultRoutePlanner.java
@@ -27,20 +27,14 @@
 
 package org.apache.hc.client5.http.impl.routing;
 
-import java.net.URI;
-
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.ProtocolException;
-import org.apache.hc.core5.http.message.BasicHttpRequest;
 import org.apache.hc.core5.http.protocol.BasicHttpContext;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.net.URIAuthority;
-import org.hamcrest.CoreMatchers;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -109,24 +103,4 @@ public class TestDefaultRoutePlanner {
         routePlanner.determineRoute(null, context);
     }
 
-    @Test
-    public void testDetermineHost() throws Exception {
-        final HttpContext context = new BasicHttpContext();
-        final HttpRequest request1 = new BasicHttpRequest("GET", "/");
-        final HttpHost host1 = routePlanner.determineTargetHost(request1, context);
-        Assert.assertThat(host1, CoreMatchers.nullValue());
-
-        final HttpRequest request2 = new BasicHttpRequest("GET", new URI("https://somehost:8443/"));
-        final HttpHost host2 = routePlanner.determineTargetHost(request2, context);
-        Assert.assertThat(host2, CoreMatchers.equalTo(new HttpHost("somehost", 8443, "https")));
-    }
-
-    @Test(expected = ProtocolException.class)
-    public void testDetermineHostMissingScheme() throws Exception {
-        final HttpContext context = new BasicHttpContext();
-        final HttpRequest request1 = new BasicHttpRequest("GET", "/");
-        request1.setAuthority(new URIAuthority("host"));
-        routePlanner.determineTargetHost(request1, context);
-    }
-
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/b45b72ef/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestRoutingSupport.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestRoutingSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestRoutingSupport.java
new file mode 100644
index 0000000..57b4f51
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestRoutingSupport.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.routing;
+
+import java.net.URI;
+
+import org.apache.hc.client5.http.routing.RoutingSupport;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.net.URIAuthority;
+import org.hamcrest.CoreMatchers;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestRoutingSupport {
+
+    @Test
+    public void testDetermineHost() throws Exception {
+        final HttpRequest request1 = new BasicHttpRequest("GET", "/");
+        final HttpHost host1 = RoutingSupport.determineHost(request1);
+        Assert.assertThat(host1, CoreMatchers.nullValue());
+
+        final HttpRequest request2 = new BasicHttpRequest("GET", new URI("https://somehost:8443/"));
+        final HttpHost host2 = RoutingSupport.determineHost(request2);
+        Assert.assertThat(host2, CoreMatchers.equalTo(new HttpHost("somehost", 8443, "https")));
+    }
+
+    @Test(expected = ProtocolException.class)
+    public void testDetermineHostMissingScheme() throws Exception {
+        final HttpRequest request1 = new BasicHttpRequest("GET", "/");
+        request1.setAuthority(new URIAuthority("host"));
+        RoutingSupport.determineHost(request1);
+    }
+
+}


[4/4] httpcomponents-client git commit: HTTPCLIENT-1893: Moved HttpContext state logic out of DefaultRedirectStrategy to the redirect execution interceptors; redesigned RedirectLocations class; refactored classic redirect execution interceptor unit tests

Posted by ol...@apache.org.
HTTPCLIENT-1893: Moved HttpContext state logic out of DefaultRedirectStrategy to the redirect execution interceptors; redesigned RedirectLocations class; refactored classic redirect execution interceptor unit tests


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/47a0eb8b
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/47a0eb8b
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/47a0eb8b

Branch: refs/heads/master
Commit: 47a0eb8b65c2a98b50567bf6e0b4229a08035098
Parents: 71060c7
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Wed Jan 3 19:36:04 2018 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Wed Jan 3 19:37:19 2018 +0100

----------------------------------------------------------------------
 .../sync/TestClientRequestExecution.java        |   6 +-
 .../hc/client5/testing/sync/TestRedirects.java  |   9 +-
 .../http/impl/DefaultRedirectStrategy.java      |  46 +---
 .../http/impl/async/AsyncRedirectExec.java      |  63 ++++--
 .../client5/http/impl/classic/RedirectExec.java |  64 ++++--
 .../http/protocol/HttpClientContext.java        |   9 +-
 .../http/protocol/RedirectLocations.java        | 121 +---------
 .../http/impl/TestDefaultRedirectStrategy.java  |  49 ----
 .../http/impl/classic/TestRedirectExec.java     | 226 +++++++++++--------
 .../http/protocol/TestRedirectLocation.java     |  12 +-
 10 files changed, 227 insertions(+), 378 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
----------------------------------------------------------------------
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
index f89d4a1..d0dd55d 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
@@ -29,12 +29,12 @@ package org.apache.hc.client5.testing.sync;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.URI;
-import java.util.List;
 
 import org.apache.hc.client5.http.HttpRequestRetryHandler;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.classic.methods.HttpPost;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.protocol.RedirectLocations;
 import org.apache.hc.client5.http.utils.URIUtils;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
@@ -252,8 +252,8 @@ public class TestClientRequestExecution extends LocalServerTestBase {
         final HttpRequest request = context.getRequest();
         Assert.assertEquals("/stuff", request.getRequestUri());
 
-        final List<URI> redirectLocations = context.getRedirectLocations();
-        final URI location = URIUtils.resolve(uri, target, redirectLocations);
+        final RedirectLocations redirectLocations = context.getRedirectLocations();
+        final URI location = URIUtils.resolve(uri, target, redirectLocations.getAll());
         Assert.assertEquals(uri, location);
     }
 

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
----------------------------------------------------------------------
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
index ab8ec57..aedefd6 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
@@ -30,7 +30,6 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Arrays;
-import java.util.List;
 
 import org.apache.hc.client5.http.CircularRedirectException;
 import org.apache.hc.client5.http.ClientProtocolException;
@@ -42,6 +41,7 @@ import org.apache.hc.client5.http.cookie.BasicCookieStore;
 import org.apache.hc.client5.http.cookie.CookieStore;
 import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.protocol.RedirectLocations;
 import org.apache.hc.client5.http.utils.URIUtils;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
@@ -282,8 +282,9 @@ public class TestRedirects extends LocalServerTestBase {
         Assert.assertEquals(HttpStatus.SC_MULTIPLE_CHOICES, response.getCode());
         Assert.assertEquals(URIUtils.create(target, "/oldlocation/"), reqWrapper.getUri());
 
-        final List<URI> redirects = context.getRedirectLocations();
-        Assert.assertNull(redirects);
+        final RedirectLocations redirects = context.getRedirectLocations();
+        Assert.assertNotNull(redirects);
+        Assert.assertEquals(0, redirects.size());
     }
 
     @Test
@@ -304,7 +305,7 @@ public class TestRedirects extends LocalServerTestBase {
         Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
         Assert.assertEquals(URIUtils.create(target, "/newlocation/"), reqWrapper.getUri());
 
-        final List<URI> redirects = context.getRedirectLocations();
+        final RedirectLocations redirects = context.getRedirectLocations();
         Assert.assertNotNull(redirects);
         Assert.assertEquals(1, redirects.size());
 

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java
index 75c3cda..54c9048 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java
@@ -30,13 +30,7 @@ package org.apache.hc.client5.http.impl;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Locale;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 
-import org.apache.hc.client5.http.CircularRedirectException;
-import org.apache.hc.client5.http.config.RequestConfig;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.http.protocol.RedirectLocations;
 import org.apache.hc.client5.http.protocol.RedirectStrategy;
 import org.apache.hc.client5.http.utils.URIUtils;
 import org.apache.hc.core5.annotation.Contract;
@@ -52,8 +46,6 @@ import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.net.URIBuilder;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.TextUtils;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
 
 /**
  * Default implementation of {@link RedirectStrategy}. This strategy honors the restrictions
@@ -64,27 +56,11 @@ import org.apache.logging.log4j.Logger;
  *
  * @since 4.1
  */
-@Contract(threading = ThreadingBehavior.IMMUTABLE)
+@Contract(threading = ThreadingBehavior.STATELESS)
 public class DefaultRedirectStrategy implements RedirectStrategy {
 
-    private final Logger log = LogManager.getLogger(getClass());
-
     public static final DefaultRedirectStrategy INSTANCE = new DefaultRedirectStrategy();
 
-    private final ConcurrentMap<String, Boolean> safeMethods;
-
-    public DefaultRedirectStrategy(final String... safeMethods) {
-        super();
-        this.safeMethods = new ConcurrentHashMap<>();
-        for (final String safeMethod: safeMethods) {
-            this.safeMethods.put(safeMethod.toUpperCase(Locale.ROOT), Boolean.TRUE);
-        }
-    }
-
-    public DefaultRedirectStrategy() {
-        this("GET", "HEAD", "OPTIONS", "TRACE");
-    }
-
     @Override
     public boolean isRedirected(
             final HttpRequest request,
@@ -117,20 +93,12 @@ public class DefaultRedirectStrategy implements RedirectStrategy {
         Args.notNull(response, "HTTP response");
         Args.notNull(context, "HTTP context");
 
-        final HttpClientContext clientContext = HttpClientContext.adapt(context);
-
         //get the location header to find out where to redirect to
         final Header locationHeader = response.getFirstHeader("location");
         if (locationHeader == null) {
             throw new HttpException("Redirect location is missing");
         }
         final String location = locationHeader.getValue();
-        if (this.log.isDebugEnabled()) {
-            this.log.debug("Redirect requested to location '" + location + "'");
-        }
-
-        final RequestConfig config = clientContext.getRequestConfig();
-
         URI uri = createLocationURI(location);
         try {
             if (!uri.isAbsolute()) {
@@ -141,18 +109,6 @@ public class DefaultRedirectStrategy implements RedirectStrategy {
             throw new ProtocolException(ex.getMessage(), ex);
         }
 
-        RedirectLocations redirectLocations = (RedirectLocations) clientContext.getAttribute(
-                HttpClientContext.REDIRECT_LOCATIONS);
-        if (redirectLocations == null) {
-            redirectLocations = new RedirectLocations();
-            context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
-        }
-        if (!config.isCircularRedirectsAllowed()) {
-            if (redirectLocations.contains(uri)) {
-                throw new CircularRedirectException("Circular redirect to '" + uri + "'");
-            }
-        }
-        redirectLocations.add(uri);
         return uri;
     }
 

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java
index 4f34bf6..cdd86b5 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java
@@ -28,8 +28,8 @@ package org.apache.hc.client5.http.impl.async;
 
 import java.io.IOException;
 import java.net.URI;
-import java.util.List;
 
+import org.apache.hc.client5.http.CircularRedirectException;
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.RedirectException;
 import org.apache.hc.client5.http.StandardMethods;
@@ -40,6 +40,7 @@ import org.apache.hc.client5.http.auth.AuthExchange;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.protocol.RedirectLocations;
 import org.apache.hc.client5.http.protocol.RedirectStrategy;
 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
 import org.apache.hc.client5.http.utils.URIUtils;
@@ -53,6 +54,7 @@ import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.message.BasicHttpRequest;
 import org.apache.hc.core5.http.nio.AsyncDataConsumer;
 import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.util.LangUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -75,6 +77,7 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
         volatile int redirectCount;
         volatile HttpRequest currentRequest;
         volatile AsyncEntityProducer currentEntityProducer;
+        volatile RedirectLocations redirectLocations;
         volatile AsyncExecChain.Scope currentScope;
         volatile boolean reroute;
 
@@ -107,6 +110,16 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
                     state.redirectCount++;
 
                     final URI redirectUri = redirectStrategy.getLocationURI(request, response, clientContext);
+                    if (log.isDebugEnabled()) {
+                        log.debug("Redirect requested to location '" + redirectUri + "'");
+                    }
+                    if (!config.isCircularRedirectsAllowed()) {
+                        if (state.redirectLocations.contains(redirectUri)) {
+                            throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
+                        }
+                    }
+                    state.redirectLocations.add(redirectUri);
+
                     final int statusCode = response.getCode();
 
                     switch (statusCode) {
@@ -128,27 +141,27 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
                         throw new ProtocolException("Redirect URI does not specify a valid host name: " + redirectUri);
                     }
 
-                    if (!currentRoute.getTargetHost().equals(newTarget)) {
-                        log.debug("New route required");
-                        state.reroute = true;
-                        final AuthExchange targetAuthExchange = clientContext.getAuthExchange(currentRoute.getTargetHost());
-                        log.debug("Resetting target auth state");
-                        targetAuthExchange.reset();
-                        if (currentRoute.getProxyHost() != null) {
-                            final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(currentRoute.getProxyHost());
-                            final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
-                            if (authScheme != null && authScheme.isConnectionBased()) {
-                                log.debug("Resetting proxy auth state");
-                                proxyAuthExchange.reset();
+                    state.reroute = false;
+                    state.redirectURI = redirectUri;
+
+                    if (!LangUtils.equals(currentRoute.getTargetHost(), newTarget)) {
+                        final HttpRoute newRoute = routePlanner.determineRoute(newTarget, clientContext);
+                        if (!LangUtils.equals(currentRoute, newRoute)) {
+                            state.reroute = true;
+                            final AuthExchange targetAuthExchange = clientContext.getAuthExchange(currentRoute.getTargetHost());
+                            log.debug("Resetting target auth state");
+                            targetAuthExchange.reset();
+                            if (currentRoute.getProxyHost() != null) {
+                                final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(currentRoute.getProxyHost());
+                                final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
+                                if (authScheme != null && authScheme.isConnectionBased()) {
+                                    log.debug("Resetting proxy auth state");
+                                    proxyAuthExchange.reset();
+                                }
                             }
+                            state.currentScope = new AsyncExecChain.Scope(scope.exchangeId, newRoute,
+                                    scope.originalRequest, scope.future, clientContext, scope.execRuntime);
                         }
-                        final HttpRoute newRoute = routePlanner.determineRoute(newTarget, clientContext);
-                        state.currentScope = new AsyncExecChain.Scope(scope.exchangeId, newRoute,
-                                scope.originalRequest, scope.future, clientContext, scope.execRuntime);
-                        state.redirectURI = redirectUri;
-                    } else {
-                        state.reroute = false;
-                        state.redirectURI = redirectUri;
                     }
                 }
                 if (state.redirectURI != null) {
@@ -200,10 +213,13 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
             final AsyncExecChain chain,
             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
         final HttpClientContext clientContext = scope.clientContext;
-        final List<URI> redirectLocations = clientContext.getRedirectLocations();
-        if (redirectLocations != null) {
-            redirectLocations.clear();
+        RedirectLocations redirectLocations = clientContext.getRedirectLocations();
+        if (redirectLocations == null) {
+            redirectLocations = new RedirectLocations();
+            clientContext.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
         }
+        redirectLocations.clear();
+
         final RequestConfig config = clientContext.getRequestConfig();
 
         final State state = new State();
@@ -211,6 +227,7 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
         state.redirectCount = 0;
         state.currentRequest = request;
         state.currentEntityProducer = entityProducer;
+        state.redirectLocations = redirectLocations;
         state.currentScope = scope;
 
         internalExecute(state, chain, asyncExecCallback);

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java
index 99b02cb..80e5cbf 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java
@@ -29,8 +29,8 @@ package org.apache.hc.client5.http.impl.classic;
 
 import java.io.IOException;
 import java.net.URI;
-import java.util.List;
 
+import org.apache.hc.client5.http.CircularRedirectException;
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.RedirectException;
 import org.apache.hc.client5.http.StandardMethods;
@@ -42,6 +42,7 @@ import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.classic.methods.RequestBuilder;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.protocol.RedirectLocations;
 import org.apache.hc.client5.http.protocol.RedirectStrategy;
 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
 import org.apache.hc.client5.http.utils.URIUtils;
@@ -56,6 +57,7 @@ import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -97,11 +99,12 @@ final class RedirectExec implements ExecChainHandler {
         Args.notNull(scope, "Scope");
 
         final HttpClientContext context = scope.clientContext;
-
-        final List<URI> redirectLocations = context.getRedirectLocations();
-        if (redirectLocations != null) {
-            redirectLocations.clear();
+        RedirectLocations redirectLocations = context.getRedirectLocations();
+        if (redirectLocations == null) {
+            redirectLocations = new RedirectLocations();
+            context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
         }
+        redirectLocations.clear();
 
         final RequestConfig config = context.getRequestConfig();
         final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
@@ -122,6 +125,16 @@ final class RedirectExec implements ExecChainHandler {
                     redirectCount++;
 
                     final URI redirectUri = this.redirectStrategy.getLocationURI(currentRequest, response, context);
+                    if (this.log.isDebugEnabled()) {
+                        this.log.debug("Redirect requested to location '" + redirectUri + "'");
+                    }
+                    if (!config.isCircularRedirectsAllowed()) {
+                        if (redirectLocations.contains(redirectUri)) {
+                            throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
+                        }
+                    }
+                    redirectLocations.add(redirectUri);
+
                     final ClassicHttpRequest originalRequest = scope.originalRequest;
                     ClassicHttpRequest redirect = null;
                     final int statusCode = response.getCode();
@@ -147,28 +160,29 @@ final class RedirectExec implements ExecChainHandler {
                                 redirectUri);
                     }
 
-                    HttpRoute currentRoute = currentScope.route;
-                    final boolean crossSiteRedirect = !currentRoute.getTargetHost().equals(newTarget);
-                    if (crossSiteRedirect) {
-
-                        final AuthExchange targetAuthExchange = context.getAuthExchange(currentRoute.getTargetHost());
-                        this.log.debug("Resetting target auth state");
-                        targetAuthExchange.reset();
-                        if (currentRoute.getProxyHost() != null) {
-                            final AuthExchange proxyAuthExchange = context.getAuthExchange(currentRoute.getProxyHost());
-                            final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
-                            if (authScheme != null && authScheme.isConnectionBased()) {
-                                this.log.debug("Resetting proxy auth state");
-                                proxyAuthExchange.reset();
+                    final HttpRoute currentRoute = currentScope.route;
+                    if (!LangUtils.equals(currentRoute.getTargetHost(), newTarget)) {
+                        final HttpRoute newRoute = this.routePlanner.determineRoute(newTarget, context);
+                        if (!LangUtils.equals(currentRoute, newRoute)) {
+                            log.debug("New route required");
+                            final AuthExchange targetAuthExchange = context.getAuthExchange(currentRoute.getTargetHost());
+                            this.log.debug("Resetting target auth state");
+                            targetAuthExchange.reset();
+                            if (currentRoute.getProxyHost() != null) {
+                                final AuthExchange proxyAuthExchange = context.getAuthExchange(currentRoute.getProxyHost());
+                                final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
+                                if (authScheme != null && authScheme.isConnectionBased()) {
+                                    this.log.debug("Resetting proxy auth state");
+                                    proxyAuthExchange.reset();
+                                }
                             }
+                            currentScope = new ExecChain.Scope(
+                                    currentScope.exchangeId,
+                                    newRoute,
+                                    currentScope.originalRequest,
+                                    currentScope.execRuntime,
+                                    currentScope.clientContext);
                         }
-                        currentRoute = this.routePlanner.determineRoute(newTarget, context);
-                        currentScope = new ExecChain.Scope(
-                                currentScope.exchangeId,
-                                currentRoute,
-                                currentScope.originalRequest,
-                                currentScope.execRuntime,
-                                currentScope.clientContext);
                     }
 
                     if (this.log.isDebugEnabled()) {

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java
index e717a90..7be4c16 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java
@@ -27,9 +27,7 @@
 
 package org.apache.hc.client5.http.protocol;
 
-import java.net.URI;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 import org.apache.hc.client5.http.HttpRoute;
@@ -67,7 +65,7 @@ public class HttpClientContext extends HttpCoreContext {
     public static final String HTTP_ROUTE   = "http.route";
 
     /**
-     * Attribute name of a {@link List} object that represents a collection of all
+     * Attribute name of a {@link RedirectLocations} object that represents a collection of all
      * redirect locations received in the process of request execution.
      */
     public static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";
@@ -156,9 +154,8 @@ public class HttpClientContext extends HttpCoreContext {
         return getAttribute(HTTP_ROUTE, HttpRoute.class);
     }
 
-    @SuppressWarnings("unchecked")
-    public List<URI> getRedirectLocations() {
-        return getAttribute(REDIRECT_LOCATIONS, List.class);
+    public RedirectLocations getRedirectLocations() {
+        return getAttribute(REDIRECT_LOCATIONS, RedirectLocations.class);
     }
 
     public CookieStore getCookieStore() {

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RedirectLocations.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RedirectLocations.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RedirectLocations.java
index effcbee..22fb6ff 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RedirectLocations.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RedirectLocations.java
@@ -28,10 +28,8 @@
 package org.apache.hc.client5.http.protocol;
 
 import java.net.URI;
-import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
@@ -41,7 +39,7 @@ import java.util.Set;
  *
  * @since 4.0
  */
-public class RedirectLocations extends AbstractList<Object> {
+public final class RedirectLocations {
 
     private final Set<URI> unique;
     private final List<URI> all;
@@ -68,23 +66,6 @@ public class RedirectLocations extends AbstractList<Object> {
     }
 
     /**
-     * Removes a URI from the collection.
-     */
-    public boolean remove(final URI uri) {
-        final boolean removed = this.unique.remove(uri);
-        if (removed) {
-            final Iterator<URI> it = this.all.iterator();
-            while (it.hasNext()) {
-                final URI current = it.next();
-                if (current.equals(uri)) {
-                    it.remove();
-                }
-            }
-        }
-        return removed;
-    }
-
-    /**
      * Returns all redirect {@link URI}s in the order they were added to the collection.
      *
      * @return list of all URIs
@@ -106,7 +87,6 @@ public class RedirectLocations extends AbstractList<Object> {
      *             {@code index &lt; 0 || index &gt;= size()})
      * @since 4.3
      */
-    @Override
     public URI get(final int index) {
         return this.all.get(index);
     }
@@ -119,106 +99,13 @@ public class RedirectLocations extends AbstractList<Object> {
      * @return the number of elements in this list
      * @since 4.3
      */
-    @Override
     public int size() {
         return this.all.size();
     }
 
-    /**
-     * Replaces the URI at the specified position in this list with the
-     * specified element (must be a URI).
-     *
-     * @param index
-     *            index of the element to replace
-     * @param element
-     *            URI to be stored at the specified position
-     * @return the URI previously at the specified position
-     * @throws UnsupportedOperationException
-     *             if the {@code set} operation is not supported by this list
-     * @throws ClassCastException
-     *             if the element is not a {@link URI}
-     * @throws NullPointerException
-     *             if the specified element is null and this list does not
-     *             permit null elements
-     * @throws IndexOutOfBoundsException
-     *             if the index is out of range (
-     *             {@code index &lt; 0 || index &gt;= size()})
-     * @since 4.3
-     */
-    @Override
-    public Object set(final int index, final Object element) {
-        final URI removed = this.all.set(index, (URI) element);
-        this.unique.remove(removed);
-        this.unique.add((URI) element);
-        if (this.all.size() != this.unique.size()) {
-            this.unique.addAll(this.all);
-        }
-        return removed;
-    }
-
-    /**
-     * Inserts the specified element at the specified position in this list
-     * (must be a URI). Shifts the URI currently at that position (if any) and
-     * any subsequent URIs to the right (adds one to their indices).
-     *
-     * @param index
-     *            index at which the specified element is to be inserted
-     * @param element
-     *            URI to be inserted
-     * @throws UnsupportedOperationException
-     *             if the {@code add} operation is not supported by this list
-     * @throws ClassCastException
-     *             if the element is not a {@link URI}
-     * @throws NullPointerException
-     *             if the specified element is null and this list does not
-     *             permit null elements
-     * @throws IndexOutOfBoundsException
-     *             if the index is out of range (
-     *             {@code index &lt; 0 || index &gt; size()})
-     * @since 4.3
-     */
-    @Override
-    public void add(final int index, final Object element) {
-        this.all.add(index, (URI) element);
-        this.unique.add((URI) element);
-    }
-
-    /**
-     * Removes the URI at the specified position in this list. Shifts any
-     * subsequent URIs to the left (subtracts one from their indices). Returns
-     * the URI that was removed from the list.
-     *
-     * @param index
-     *            the index of the URI to be removed
-     * @return the URI previously at the specified position
-     * @throws IndexOutOfBoundsException
-     *             if the index is out of range (
-     *             {@code index &lt; 0 || index &gt;= size()})
-     * @since 4.3
-     */
-    @Override
-    public URI remove(final int index) {
-        final URI removed = this.all.remove(index);
-        this.unique.remove(removed);
-        if (this.all.size() != this.unique.size()) {
-            this.unique.addAll(this.all);
-        }
-        return removed;
-    }
-
-    /**
-     * Returns {@code true} if this collection contains the specified element.
-     * More formally, returns {@code true} if and only if this collection
-     * contains at least one element {@code e} such that
-     * {@code (o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))}.
-     *
-     * @param o element whose presence in this collection is to be tested
-     * @return {@code true} if this collection contains the specified
-     *         element
-     */
-    @Override
-    public boolean contains(final Object o) {
-        return this.unique.contains(o);
+    public void clear() {
+        unique.clear();
+        all.clear();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java
index 1fc2eb2..6ca7e60 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java
@@ -27,11 +27,9 @@
 package org.apache.hc.client5.http.impl;
 
 import java.net.URI;
-import java.util.List;
 
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.classic.methods.HttpPost;
-import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHeaders;
@@ -201,53 +199,6 @@ public class TestDefaultRedirectStrategy {
     }
 
     @Test
-    public void testGetLocationUriAllowCircularRedirects() throws Exception {
-        final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
-        final HttpClientContext context = HttpClientContext.create();
-        final RequestConfig config = RequestConfig.custom().setCircularRedirectsAllowed(true).build();
-        context.setRequestConfig(config);
-        final URI uri1 = URI.create("http://localhost/stuff1");
-        final URI uri2 = URI.create("http://localhost/stuff2");
-        final URI uri3 = URI.create("http://localhost/stuff3");
-        final HttpGet httpget1 = new HttpGet("http://localhost/");
-        final HttpResponse response1 = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
-        response1.addHeader("Location", uri1.toASCIIString());
-        final HttpGet httpget2 = new HttpGet(uri1.toASCIIString());
-        final HttpResponse response2 = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
-        response2.addHeader("Location", uri2.toASCIIString());
-        final HttpGet httpget3 = new HttpGet(uri2.toASCIIString());
-        final HttpResponse response3 = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
-        response3.addHeader("Location", uri3.toASCIIString());
-        Assert.assertEquals(uri1, redirectStrategy.getLocationURI(httpget1, response1, context));
-        Assert.assertEquals(uri2, redirectStrategy.getLocationURI(httpget2, response2, context));
-        Assert.assertEquals(uri3, redirectStrategy.getLocationURI(httpget3, response3, context));
-
-        final List<URI> uris = context.getRedirectLocations();
-        Assert.assertNotNull(uris);
-        Assert.assertTrue(uris.contains(uri1));
-        Assert.assertTrue(uris.contains(uri2));
-        Assert.assertTrue(uris.contains(uri3));
-        Assert.assertEquals(3, uris.size());
-        Assert.assertEquals(uri1, uris.get(0));
-        Assert.assertEquals(uri2, uris.get(1));
-        Assert.assertEquals(uri3, uris.get(2));
-    }
-
-    @Test(expected=ProtocolException.class)
-    public void testGetLocationUriDisallowCircularRedirects() throws Exception {
-        final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
-        final HttpClientContext context = HttpClientContext.create();
-        final HttpGet httpget = new HttpGet("http://localhost/stuff");
-        final RequestConfig config = RequestConfig.custom().setCircularRedirectsAllowed(false).build();
-        context.setRequestConfig(config);
-        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
-        response.addHeader("Location", "http://localhost/stuff");
-        final URI uri = URI.create("http://localhost/stuff");
-        Assert.assertEquals(uri, redirectStrategy.getLocationURI(httpget, response, context));
-        redirectStrategy.getLocationURI(httpget, response, context);
-    }
-
-    @Test
     public void testGetLocationUriInvalidInput() throws Exception {
         final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
         final HttpClientContext context = HttpClientContext.create();

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRedirectExec.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRedirectExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRedirectExec.java
index 7fdb4cc..80cca6a 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRedirectExec.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRedirectExec.java
@@ -30,8 +30,10 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Arrays;
 import java.util.List;
 
+import org.apache.hc.client5.http.CircularRedirectException;
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.RedirectException;
 import org.apache.hc.client5.http.auth.AuthExchange;
@@ -40,48 +42,52 @@ import org.apache.hc.client5.http.classic.ExecRuntime;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.entity.EntityBuilder;
+import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.impl.auth.NTLMScheme;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.protocol.RedirectLocations;
 import org.apache.hc.client5.http.protocol.RedirectStrategy;
 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
 import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnitRunner;
 
-@SuppressWarnings({"boxing","static-access"}) // test code
+@RunWith(MockitoJUnitRunner.class)
 public class TestRedirectExec {
 
     @Mock
     private HttpRoutePlanner httpRoutePlanner;
     @Mock
-    private RedirectStrategy redirectStrategy;
-    @Mock
     private ExecChain chain;
     @Mock
     private ExecRuntime endpoint;
 
+    private RedirectStrategy redirectStrategy;
     private RedirectExec redirectExec;
     private HttpHost target;
 
     @Before
     public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        redirectExec = new RedirectExec(httpRoutePlanner, redirectStrategy);
         target = new HttpHost("localhost", 80);
+        redirectStrategy = Mockito.spy(new DefaultRedirectStrategy());
+        redirectExec = new RedirectExec(httpRoutePlanner, redirectStrategy);
     }
 
     @Test
@@ -90,19 +96,20 @@ public class TestRedirectExec {
         final HttpGet request = new HttpGet("/test");
         final HttpClientContext context = HttpClientContext.create();
 
-        final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
+        final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
+        final URI redirect = new URI("http://localhost:80/redirect");
+        response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
         final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
         final HttpEntity entity1 = EntityBuilder.create()
                 .setStream(instream1)
                 .build();
-        Mockito.when(response1.getEntity()).thenReturn(entity1);
-        final ClassicHttpResponse response2 = Mockito.mock(ClassicHttpResponse.class);
+        response1.setEntity(entity1);
+        final ClassicHttpResponse response2 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_OK));
         final InputStream instream2 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
         final HttpEntity entity2 = EntityBuilder.create()
                 .setStream(instream2)
                 .build();
-        Mockito.when(response2.getEntity()).thenReturn(entity2);
-        final URI redirect = new URI("http://localhost:80/redirect");
+        response2.setEntity(entity2);
 
         Mockito.when(chain.proceed(
                 Mockito.same(request),
@@ -110,26 +117,12 @@ public class TestRedirectExec {
         Mockito.when(chain.proceed(
                 HttpRequestMatcher.matchesRequestUri(redirect),
                 Mockito.<ExecChain.Scope>any())).thenReturn(response2);
-        Mockito.when(redirectStrategy.isRedirected(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
-        Mockito.when(redirectStrategy.getLocationURI(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(redirect);
-        Mockito.when(httpRoutePlanner.determineRoute(
-                Mockito.eq(target),
-                Mockito.<HttpClientContext>any())).thenReturn(route);
 
         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
         redirectExec.execute(request, scope, chain);
 
-        final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(
-                ClassicHttpRequest.class);
-        Mockito.verify(chain, Mockito.times(2)).proceed(
-                reqCaptor.capture(),
-                Mockito.same(scope));
+        final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
+        Mockito.verify(chain, Mockito.times(2)).proceed(reqCaptor.capture(), Mockito.same(scope));
 
         final List<ClassicHttpRequest> allValues = reqCaptor.getAllValues();
         Assert.assertNotNull(allValues);
@@ -137,7 +130,7 @@ public class TestRedirectExec {
         Assert.assertSame(request, allValues.get(0));
 
         Mockito.verify(response1, Mockito.times(1)).close();
-        Mockito.verify(instream1, Mockito.times(1)).close();
+        Mockito.verify(instream1, Mockito.times(2)).close();
         Mockito.verify(response2, Mockito.never()).close();
         Mockito.verify(instream2, Mockito.never()).close();
     }
@@ -153,23 +146,11 @@ public class TestRedirectExec {
                 .build();
         context.setRequestConfig(config);
 
-        final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
+        final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
         final URI redirect = new URI("http://localhost:80/redirect");
+        response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
 
-        Mockito.when(chain.proceed(
-                Mockito.<ClassicHttpRequest>any(),
-                Mockito.<ExecChain.Scope>any())).thenReturn(response1);
-        Mockito.when(redirectStrategy.isRedirected(
-                Mockito.<ClassicHttpRequest>any(),
-                Mockito.<HttpResponse>any(),
-                Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
-        Mockito.when(redirectStrategy.getLocationURI(
-                Mockito.<ClassicHttpRequest>any(),
-                Mockito.<HttpResponse>any(),
-                Mockito.<HttpClientContext>any())).thenReturn(redirect);
-        Mockito.when(httpRoutePlanner.determineRoute(
-                Mockito.eq(target),
-                Mockito.<HttpClientContext>any())).thenReturn(route);
+        Mockito.when(chain.proceed(Mockito.<ClassicHttpRequest>any(), Mockito.<ExecChain.Scope>any())).thenReturn(response1);
 
         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
         redirectExec.execute(request, scope, chain);
@@ -181,26 +162,12 @@ public class TestRedirectExec {
         final HttpGet request = new HttpGet("/test");
         final HttpClientContext context = HttpClientContext.create();
 
-        final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
-        final ClassicHttpResponse response2 = Mockito.mock(ClassicHttpResponse.class);
+        final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
         final URI redirect = new URI("/redirect");
+        response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
         Mockito.when(chain.proceed(
                 Mockito.same(request),
                 Mockito.<ExecChain.Scope>any())).thenReturn(response1);
-        Mockito.when(chain.proceed(
-                HttpRequestMatcher.matchesRequestUri(redirect),
-                Mockito.<ExecChain.Scope>any())).thenReturn(response2);
-        Mockito.when(redirectStrategy.isRedirected(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
-        Mockito.when(redirectStrategy.getLocationURI(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(redirect);
-        Mockito.when(httpRoutePlanner.determineRoute(
-                Mockito.eq(target),
-                Mockito.<HttpClientContext>any())).thenReturn(route);
 
         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
         redirectExec.execute(request, scope, chain);
@@ -223,27 +190,17 @@ public class TestRedirectExec {
         context.setAuthExchange(target, targetAuthExchange);
         context.setAuthExchange(proxy, proxyAuthExchange);
 
-        final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
-        final ClassicHttpResponse response2 = Mockito.mock(ClassicHttpResponse.class);
-        final HttpHost otherHost = new HttpHost("otherhost", 8888);
+        final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
         final URI redirect = new URI("http://otherhost:8888/redirect");
+        response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
+        final ClassicHttpResponse response2 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_OK));
+        final HttpHost otherHost = new HttpHost("otherhost", 8888);
         Mockito.when(chain.proceed(
                 Mockito.same(request),
                 Mockito.<ExecChain.Scope>any())).thenReturn(response1);
         Mockito.when(chain.proceed(
                 HttpRequestMatcher.matchesRequestUri(redirect),
                 Mockito.<ExecChain.Scope>any())).thenReturn(response2);
-        Mockito.when(redirectStrategy.isRedirected(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
-        Mockito.when(redirectStrategy.getLocationURI(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(redirect);
-        Mockito.when(httpRoutePlanner.determineRoute(
-                Mockito.eq(target),
-                Mockito.<HttpClientContext>any())).thenReturn(new HttpRoute(target));
         Mockito.when(httpRoutePlanner.determineRoute(
                 Mockito.eq(otherHost),
                 Mockito.<HttpClientContext>any())).thenReturn(new HttpRoute(otherHost));
@@ -261,24 +218,101 @@ public class TestRedirectExec {
         Assert.assertEquals(null, authExchange2.getAuthScheme());
     }
 
+    @Test
+    public void testAllowCircularRedirects() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpClientContext context = HttpClientContext.create();
+        context.setRequestConfig(RequestConfig.custom()
+                .setCircularRedirectsAllowed(true)
+                .build());
+
+        final URI uri = URI.create("http://localhost/");
+        final HttpGet request = new HttpGet(uri);
+
+        final URI uri1 = URI.create("http://localhost/stuff1");
+        final URI uri2 = URI.create("http://localhost/stuff2");
+        final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
+        response1.addHeader("Location", uri1.toASCIIString());
+        final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
+        response2.addHeader("Location", uri2.toASCIIString());
+        final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
+        response3.addHeader("Location", uri1.toASCIIString());
+        final ClassicHttpResponse response4 = new BasicClassicHttpResponse(HttpStatus.SC_OK);
+
+        Mockito.when(chain.proceed(
+                HttpRequestMatcher.matchesRequestUri(uri),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response1);
+        Mockito.when(chain.proceed(
+                HttpRequestMatcher.matchesRequestUri(uri1),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response2, response4);
+        Mockito.when(chain.proceed(
+                HttpRequestMatcher.matchesRequestUri(uri2),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response3);
+        Mockito.when(httpRoutePlanner.determineRoute(
+                Mockito.eq(new HttpHost("localhost")),
+                Mockito.<HttpClientContext>any())).thenReturn(route);
+
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
+        redirectExec.execute(request, scope, chain);
+
+        final RedirectLocations uris = context.getRedirectLocations();
+        Assert.assertNotNull(uris);
+        Assert.assertEquals(Arrays.asList(uri1, uri2, uri1), uris.getAll());
+    }
+
+    @Test(expected=CircularRedirectException.class)
+    public void testGetLocationUriDisallowCircularRedirects() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpClientContext context = HttpClientContext.create();
+        context.setRequestConfig(RequestConfig.custom()
+                .setCircularRedirectsAllowed(false)
+                .build());
+
+        final URI uri = URI.create("http://localhost/");
+        final HttpGet request = new HttpGet(uri);
+
+        final URI uri1 = URI.create("http://localhost/stuff1");
+        final URI uri2 = URI.create("http://localhost/stuff2");
+        final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
+        response1.addHeader("Location", uri1.toASCIIString());
+        final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
+        response2.addHeader("Location", uri2.toASCIIString());
+        final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
+        response3.addHeader("Location", uri1.toASCIIString());
+        Mockito.when(httpRoutePlanner.determineRoute(
+                Mockito.eq(new HttpHost("localhost")),
+                Mockito.<HttpClientContext>any())).thenReturn(route);
+
+        Mockito.when(chain.proceed(
+                HttpRequestMatcher.matchesRequestUri(uri),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response1);
+        Mockito.when(chain.proceed(
+                HttpRequestMatcher.matchesRequestUri(uri1),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response2);
+        Mockito.when(chain.proceed(
+                HttpRequestMatcher.matchesRequestUri(uri2),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response3);
+
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
+        redirectExec.execute(request, scope, chain);
+    }
+
     @Test(expected = RuntimeException.class)
     public void testRedirectRuntimeException() throws Exception {
         final HttpRoute route = new HttpRoute(target);
         final HttpGet request = new HttpGet("/test");
         final HttpClientContext context = HttpClientContext.create();
 
-        final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
+        final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
+        final URI redirect = new URI("http://localhost:80/redirect");
+        response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
         Mockito.when(chain.proceed(
                 Mockito.same(request),
                 Mockito.<ExecChain.Scope>any())).thenReturn(response1);
-        Mockito.when(redirectStrategy.isRedirected(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
-        Mockito.doThrow(new RuntimeException("Oppsie")).when(redirectStrategy.getLocationURI(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any()));
+        Mockito.doThrow(new RuntimeException("Oppsie")).when(redirectStrategy).getLocationURI(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<ClassicHttpResponse>any(),
+                Mockito.<HttpClientContext>any());
 
         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
         try {
@@ -295,29 +329,27 @@ public class TestRedirectExec {
         final HttpGet request = new HttpGet("/test");
         final HttpClientContext context = HttpClientContext.create();
 
-        final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
+        final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
+        final URI redirect = new URI("http://localhost:80/redirect");
+        response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
         final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
         final HttpEntity entity1 = EntityBuilder.create()
                 .setStream(instream1)
                 .build();
-        Mockito.when(response1.getEntity()).thenReturn(entity1);
+        response1.setEntity(entity1);
         Mockito.when(chain.proceed(
                 Mockito.same(request),
                 Mockito.<ExecChain.Scope>any())).thenReturn(response1);
-        Mockito.when(redirectStrategy.isRedirected(
-                Mockito.same(request),
-                Mockito.same(response1),
-                Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
         Mockito.doThrow(new ProtocolException("Oppsie")).when(redirectStrategy).getLocationURI(
-                Mockito.same(request),
-                Mockito.same(response1),
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<ClassicHttpResponse>any(),
                 Mockito.<HttpClientContext>any());
 
         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
         try {
             redirectExec.execute(request, scope, chain);
         } catch (final Exception ex) {
-            Mockito.verify(instream1).close();
+            Mockito.verify(instream1, Mockito.times(2)).close();
             Mockito.verify(response1).close();
             throw ex;
         }
@@ -325,17 +357,21 @@ public class TestRedirectExec {
 
     private static class HttpRequestMatcher implements ArgumentMatcher<ClassicHttpRequest> {
 
-        private final URI requestUri;
+        private final URI expectedRequestUri;
 
         HttpRequestMatcher(final URI requestUri) {
             super();
-            this.requestUri = requestUri;
+            this.expectedRequestUri = requestUri;
         }
 
         @Override
         public boolean matches(final ClassicHttpRequest argument) {
+            if (argument == null) {
+                return false;
+            }
             try {
-                return requestUri.equals(argument.getUri());
+                final URI requestUri = argument.getUri();
+                return this.expectedRequestUri.equals(requestUri);
             } catch (final URISyntaxException ex) {
                 return false;
             }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/47a0eb8b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRedirectLocation.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRedirectLocation.java b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRedirectLocation.java
index 95f4693..3923afc 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRedirectLocation.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRedirectLocation.java
@@ -56,7 +56,7 @@ public class TestRedirectLocation {
         Assert.assertTrue(locations.contains(uri3));
         Assert.assertFalse(locations.contains(new URI("/")));
 
-        List<URI> list = locations.getAll();
+        final List<URI> list = locations.getAll();
         Assert.assertNotNull(list);
         Assert.assertEquals(5, list.size());
         Assert.assertEquals(uri1, list.get(0));
@@ -64,16 +64,6 @@ public class TestRedirectLocation {
         Assert.assertEquals(uri2, list.get(2));
         Assert.assertEquals(uri3, list.get(3));
         Assert.assertEquals(uri3, list.get(4));
-
-        Assert.assertTrue(locations.remove(uri3));
-        Assert.assertTrue(locations.remove(uri1));
-        Assert.assertFalse(locations.remove(new URI("/")));
-
-        list = locations.getAll();
-        Assert.assertNotNull(list);
-        Assert.assertEquals(2, list.size());
-        Assert.assertEquals(uri2, list.get(0));
-        Assert.assertEquals(uri2, list.get(1));
     }
 
 }