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 2020/08/05 08:41:55 UTC

[httpcomponents-core] branch 5.1.x updated (eacb0d3 -> 1ca04d6)

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a change to branch 5.1.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git.


 discard eacb0d3  HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection
 discard abf437e  HTTPCORE-643: Implement NullEntity for convenience (#209)
    omit 5f47194  Correct @since for HTTPCORE-642
    omit 19feeae  RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty
    omit 5300ae3  RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec
    omit f62ca75  HTTPCORE-642: Implement ConnectionFactory fluent builders
     new 1b11b32  HTTPCORE-642: Implement ConnectionFactory fluent builders
     new 89d166b  RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec
     new e9d617a  RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty
     new 6b9ed46  HTTPCORE-643: Implement NullEntity for convenience (#209)
     new 1ca04d6  HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (eacb0d3)
            \
             N -- N -- N   refs/heads/5.1.x (1ca04d6)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:


[httpcomponents-core] 01/05: HTTPCORE-642: Implement ConnectionFactory fluent builders

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch 5.1.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 1b11b32c800d9517f7dfaa0d030066e782db9fb1
Author: Carter Kozak <c4...@gmail.com>
AuthorDate: Wed Jul 29 17:40:09 2020 -0400

    HTTPCORE-642: Implement ConnectionFactory fluent builders
    
    This closes #208
---
 .../io/DefaultBHttpClientConnectionFactory.java    | 66 +++++++++++++++++++
 .../io/DefaultBHttpServerConnectionFactory.java    | 73 ++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
index e9781df..384e5d8 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
@@ -106,4 +106,70 @@ public class DefaultBHttpClientConnectionFactory
         return conn;
     }
 
+    /**
+     * Create a new {@link Builder}.
+     *
+     * @since 5.1
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link DefaultBHttpClientConnectionFactory}.
+     *
+     * @since 5.1
+     */
+    public static final class Builder {
+        private Http1Config http1Config;
+        private CharCodingConfig charCodingConfig;
+        private ContentLengthStrategy incomingContentStrategy;
+        private ContentLengthStrategy outgoingContentStrategy;
+        private HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory;
+        private HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory;
+
+        private Builder() {}
+
+        public Builder http1Config(final Http1Config http1Config) {
+            this.http1Config = http1Config;
+            return this;
+        }
+
+        public Builder charCodingConfig(final CharCodingConfig charCodingConfig) {
+            this.charCodingConfig = charCodingConfig;
+            return this;
+        }
+
+        public Builder incomingContentStrategy(final ContentLengthStrategy incomingContentStrategy) {
+            this.incomingContentStrategy = incomingContentStrategy;
+            return this;
+        }
+
+        public Builder outgoingContentStrategy(final ContentLengthStrategy outgoingContentStrategy) {
+            this.outgoingContentStrategy = outgoingContentStrategy;
+            return this;
+        }
+
+        public Builder requestWriterFactory(
+                final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory) {
+            this.requestWriterFactory = requestWriterFactory;
+            return this;
+        }
+
+        public Builder responseParserFactory(
+                final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
+            this.responseParserFactory = responseParserFactory;
+            return this;
+        }
+
+        public DefaultBHttpClientConnectionFactory build() {
+            return new DefaultBHttpClientConnectionFactory(
+                    http1Config,
+                    charCodingConfig,
+                    incomingContentStrategy,
+                    outgoingContentStrategy,
+                    requestWriterFactory,
+                    responseParserFactory);
+        }
+    }
 }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java
index d74c528..607c942 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java
@@ -107,4 +107,77 @@ public class DefaultBHttpServerConnectionFactory implements HttpConnectionFactor
         return conn;
     }
 
+    /**
+     * Create a new {@link Builder}.
+     *
+     * @since 5.1
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link DefaultBHttpServerConnectionFactory}.
+     *
+     * @since 5.1
+     */
+    public static final class Builder {
+        private String scheme;
+        private Http1Config http1Config;
+        private CharCodingConfig charCodingConfig;
+        private ContentLengthStrategy incomingContentStrategy;
+        private ContentLengthStrategy outgoingContentStrategy;
+        private HttpMessageParserFactory<ClassicHttpRequest> requestParserFactory;
+        private HttpMessageWriterFactory<ClassicHttpResponse> responseWriterFactory;
+
+        private Builder() {}
+
+        public Builder scheme(final String scheme) {
+            this.scheme = scheme;
+            return this;
+        }
+
+        public Builder http1Config(final Http1Config http1Config) {
+            this.http1Config = http1Config;
+            return this;
+        }
+
+        public Builder charCodingConfig(final CharCodingConfig charCodingConfig) {
+            this.charCodingConfig = charCodingConfig;
+            return this;
+        }
+
+        public Builder incomingContentStrategy(final ContentLengthStrategy incomingContentStrategy) {
+            this.incomingContentStrategy = incomingContentStrategy;
+            return this;
+        }
+
+        public Builder outgoingContentStrategy(final ContentLengthStrategy outgoingContentStrategy) {
+            this.outgoingContentStrategy = outgoingContentStrategy;
+            return this;
+        }
+
+        public Builder requestParserFactory(
+                final HttpMessageParserFactory<ClassicHttpRequest> requestParserFactory) {
+            this.requestParserFactory = requestParserFactory;
+            return this;
+        }
+
+        public Builder responseWriterFactory(
+                final HttpMessageWriterFactory<ClassicHttpResponse> responseWriterFactory) {
+            this.responseWriterFactory = responseWriterFactory;
+            return this;
+        }
+
+        public DefaultBHttpServerConnectionFactory build() {
+            return new DefaultBHttpServerConnectionFactory(
+                    scheme,
+                    http1Config,
+                    charCodingConfig,
+                    incomingContentStrategy,
+                    outgoingContentStrategy,
+                    requestParserFactory,
+                    responseWriterFactory);
+        }
+    }
 }


[httpcomponents-core] 05/05: HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch 5.1.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 1ca04d66a8fd8b79093a4024da5f177f78fba95a
Author: Carter Kozak <c4...@gmail.com>
AuthorDate: Mon Jul 27 10:17:45 2020 -0400

    HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection
    
    This adds a configurable ResponseOutOfOrderStrategy in place of the
    previous always-enabled behavior, and uses the no-op
    NoResponseOutOfOrderStrategy implementation by default.
    
    The previous behavior can be used by selecting the
    MonitoringResponseOutOfOrderStrategy, which has been updated
    to support more flexible behavior. Note that this strategy
    results in a 1 ms pause for every chunk transferred, limiting
    upload speed using the default 8 KiB chunk size to at most
    8 MiB/second.
    
    The original discussion can be found on the mailing list:
    https://www.mail-archive.com/httpclient-users@hc.apache.org/msg09911.html
    
    This closes #206
---
 ...gResponseOutOfOrderStrategyIntegrationTest.java | 194 +++++++++++++++++++++
 .../http/impl/io/DefaultBHttpClientConnection.java |  76 ++++++--
 .../io/DefaultBHttpClientConnectionFactory.java    |  32 +++-
 .../io/MonitoringResponseOutOfOrderStrategy.java   | 111 ++++++++++++
 .../http/impl/io/NoResponseOutOfOrderStrategy.java |  61 +++++++
 .../core5/http/io/ResponseOutOfOrderStrategy.java  |  66 +++++++
 .../TestMonitoringResponseOutOfOrderStrategy.java  | 132 ++++++++++++++
 7 files changed, 652 insertions(+), 20 deletions(-)

diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java
new file mode 100644
index 0000000..bed4f11
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java
@@ -0,0 +1,194 @@
+/*
+ * ====================================================================
+ * 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.core5.testing.classic;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.impl.bootstrap.HttpRequester;
+import org.apache.hc.core5.http.impl.bootstrap.RequesterBootstrap;
+import org.apache.hc.core5.http.impl.io.DefaultBHttpClientConnectionFactory;
+import org.apache.hc.core5.http.impl.io.MonitoringResponseOutOfOrderStrategy;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.InputStreamEntity;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.testing.SSLTestContexts;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExternalResource;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class MonitoringResponseOutOfOrderStrategyIntegrationTest {
+
+    // Use a 16k buffer for consistent results across systems
+    private static final int BUFFER_SIZE = 16 * 1024;
+    private static final Timeout TIMEOUT = Timeout.ofSeconds(3);
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> protocols() {
+        return Arrays.asList(new Object[][]{
+                { URIScheme.HTTP },
+                { URIScheme.HTTPS }
+        });
+    }
+
+    private final URIScheme scheme;
+    private ClassicTestServer server;
+    private HttpRequester requester;
+
+    public MonitoringResponseOutOfOrderStrategyIntegrationTest(final URIScheme scheme) {
+        this.scheme = scheme;
+    }
+
+    @Rule
+    public ExternalResource serverResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            server = new ClassicTestServer(
+                    scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null,
+                    SocketConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .setSndBufSize(BUFFER_SIZE)
+                            .setRcvBufSize(BUFFER_SIZE)
+                            .setSoKeepAlive(false)
+                            .build());
+        }
+
+        @Override
+        protected void after() {
+            if (server != null) {
+                try {
+                    server.shutdown(CloseMode.IMMEDIATE);
+                    server = null;
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    @Rule
+    public ExternalResource requesterResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            requester = RequesterBootstrap.bootstrap()
+                    .setSslContext(scheme == URIScheme.HTTPS  ? SSLTestContexts.createClientSSLContext() : null)
+                    .setSocketConfig(SocketConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .setRcvBufSize(BUFFER_SIZE)
+                            .setSndBufSize(BUFFER_SIZE)
+                            .setSoKeepAlive(false)
+                            .build())
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE)
+                    .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
+                    .setConnectionFactory(DefaultBHttpClientConnectionFactory.builder()
+                            .responseOutOfOrderStrategy(MonitoringResponseOutOfOrderStrategy.INSTANCE)
+                            .build())
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            if (requester != null) {
+                try {
+                    requester.close(CloseMode.IMMEDIATE);
+                    requester = null;
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    @Test(timeout = 5000) // Failures may hang
+    public void testResponseOutOfOrderWithDefaultStrategy() throws Exception {
+        this.server.registerHandler("*", new HttpRequestHandler() {
+
+            @Override
+            public void handle(
+                    final ClassicHttpRequest request,
+                    final ClassicHttpResponse response,
+                    final HttpContext context) throws IOException {
+                response.setCode(400);
+                response.setEntity(new InputStreamEntity(
+                        new AllOnesInputStream(200000), -1, ContentType.APPLICATION_OCTET_STREAM));
+            }
+
+        });
+
+        this.server.start(null, null, null);
+
+        final HttpCoreContext context = HttpCoreContext.create();
+        final HttpHost host = new HttpHost(scheme.id, "localhost", this.server.getPort());
+
+        final ClassicHttpRequest post = new BasicClassicHttpRequest(Method.POST, "/");
+        post.setEntity(new InputStreamEntity(
+                new AllOnesInputStream(200000), -1, ContentType.APPLICATION_OCTET_STREAM));
+
+        try (final ClassicHttpResponse response = requester.execute(host, post, TIMEOUT, context)) {
+            Assert.assertEquals(400, response.getCode());
+            EntityUtils.consumeQuietly(response.getEntity());
+        }
+    }
+
+    private static final class AllOnesInputStream extends InputStream {
+        private long remaining;
+
+        AllOnesInputStream(final long length) {
+            this.remaining = length;
+        }
+
+        @Override
+        public int read() {
+            if (remaining > 0) {
+                remaining--;
+                return 1;
+            }
+            return -1;
+        }
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java
index 6994ec5..13fc5e0 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java
@@ -34,8 +34,6 @@ import java.net.Socket;
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CharsetEncoder;
 
-import javax.net.ssl.SSLSocket;
-
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.ContentLengthStrategy;
@@ -54,8 +52,8 @@ import org.apache.hc.core5.http.io.HttpMessageParser;
 import org.apache.hc.core5.http.io.HttpMessageParserFactory;
 import org.apache.hc.core5.http.io.HttpMessageWriter;
 import org.apache.hc.core5.http.io.HttpMessageWriterFactory;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
 import org.apache.hc.core5.util.Args;
-import org.apache.hc.core5.util.Timeout;
 
 /**
  * Default implementation of {@link HttpClientConnection}.
@@ -69,6 +67,7 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
     private final HttpMessageWriter<ClassicHttpRequest> requestWriter;
     private final ContentLengthStrategy incomingContentStrategy;
     private final ContentLengthStrategy outgoingContentStrategy;
+    private final ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
     private volatile boolean consistent;
 
     /**
@@ -84,6 +83,8 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
      *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
      * @param outgoingContentStrategy outgoing content length strategy. If {@code null}
      *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
+     * @param responseOutOfOrderStrategy response out of order strategy. If {@code null}
+     *   {@link NoResponseOutOfOrderStrategy#INSTANCE} will be used.
      * @param requestWriterFactory request writer factory. If {@code null}
      *   {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
      * @param responseParserFactory response parser factory. If {@code null}
@@ -95,6 +96,7 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
             final CharsetEncoder charEncoder,
             final ContentLengthStrategy incomingContentStrategy,
             final ContentLengthStrategy outgoingContentStrategy,
+            final ResponseOutOfOrderStrategy responseOutOfOrderStrategy,
             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
         super(http1Config, charDecoder, charEncoder);
@@ -103,12 +105,51 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
         this.responseParser = (responseParserFactory != null ? responseParserFactory :
             DefaultHttpResponseParserFactory.INSTANCE).create(http1Config);
         this.incomingContentStrategy = incomingContentStrategy != null ? incomingContentStrategy :
-                DefaultContentLengthStrategy.INSTANCE;
+            DefaultContentLengthStrategy.INSTANCE;
         this.outgoingContentStrategy = outgoingContentStrategy != null ? outgoingContentStrategy :
-                DefaultContentLengthStrategy.INSTANCE;
+            DefaultContentLengthStrategy.INSTANCE;
+        this.responseOutOfOrderStrategy = responseOutOfOrderStrategy != null ? responseOutOfOrderStrategy :
+            NoResponseOutOfOrderStrategy.INSTANCE;
         this.consistent = true;
     }
 
+    /**
+     * Creates new instance of DefaultBHttpClientConnection.
+     *
+     * @param http1Config Message http1Config. If {@code null}
+     *   {@link Http1Config#DEFAULT} will be used.
+     * @param charDecoder decoder to be used for decoding HTTP protocol elements.
+     *   If {@code null} simple type cast will be used for byte to char conversion.
+     * @param charEncoder encoder to be used for encoding HTTP protocol elements.
+     *   If {@code null} simple type cast will be used for char to byte conversion.
+     * @param incomingContentStrategy incoming content length strategy. If {@code null}
+     *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
+     * @param outgoingContentStrategy outgoing content length strategy. If {@code null}
+     *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
+     * @param requestWriterFactory request writer factory. If {@code null}
+     *   {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
+     * @param responseParserFactory response parser factory. If {@code null}
+     *   {@link DefaultHttpResponseParserFactory#INSTANCE} will be used.
+     */
+    public DefaultBHttpClientConnection(
+            final Http1Config http1Config,
+            final CharsetDecoder charDecoder,
+            final CharsetEncoder charEncoder,
+            final ContentLengthStrategy incomingContentStrategy,
+            final ContentLengthStrategy outgoingContentStrategy,
+            final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
+            final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
+        this(
+                http1Config,
+                charDecoder,
+                charEncoder,
+                incomingContentStrategy,
+                outgoingContentStrategy,
+                null,
+                requestWriterFactory,
+                responseParserFactory);
+    }
+
     public DefaultBHttpClientConnection(
             final Http1Config http1Config,
             final CharsetDecoder charDecoder,
@@ -157,41 +198,40 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
         try (final OutputStream outStream = createContentOutputStream(
                 len, this.outbuffer, new OutputStream() {
 
-                    final boolean ssl = socketHolder.getSocket() instanceof SSLSocket;
-                    final InputStream socketInputStream = socketHolder.getInputStream();
                     final OutputStream socketOutputStream = socketHolder.getOutputStream();
+                    final InputStream socketInputStream = socketHolder.getInputStream();
 
                     long totalBytes = 0;
-                    long chunks = -1;
 
-                    void checkForEarlyResponse() throws IOException {
-                        final long n = totalBytes / (8 * 1024);
-                        if (n > chunks) {
-                            chunks = n;
-                            if (ssl ? isDataAvailable(Timeout.ONE_MILLISECOND) : (socketInputStream.available() > 0)) {
-                                throw new ResponseOutOfOrderException();
-                            }
+                    void checkForEarlyResponse(final long totalBytesSent, final int nextWriteSize) throws IOException {
+                        if (responseOutOfOrderStrategy.isEarlyResponseDetected(
+                                DefaultBHttpClientConnection.this,
+                                request,
+                                socketInputStream,
+                                totalBytesSent,
+                                nextWriteSize)) {
+                            throw new ResponseOutOfOrderException();
                         }
                     }
 
                     @Override
                     public void write(final byte[] b) throws IOException {
+                        checkForEarlyResponse(totalBytes, b.length);
                         totalBytes += b.length;
-                        checkForEarlyResponse();
                         socketOutputStream.write(b);
                     }
 
                     @Override
                     public void write(final byte[] b, final int off, final int len) throws IOException {
+                        checkForEarlyResponse(totalBytes, len);
                         totalBytes += len;
-                        checkForEarlyResponse();
                         socketOutputStream.write(b, off, len);
                     }
 
                     @Override
                     public void write(final int b) throws IOException {
+                        checkForEarlyResponse(totalBytes, 1);
                         totalBytes++;
-                        checkForEarlyResponse();
                         socketOutputStream.write(b);
                     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
index 384e5d8..14cc06b 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
@@ -41,6 +41,7 @@ import org.apache.hc.core5.http.impl.CharCodingSupport;
 import org.apache.hc.core5.http.io.HttpConnectionFactory;
 import org.apache.hc.core5.http.io.HttpMessageParserFactory;
 import org.apache.hc.core5.http.io.HttpMessageWriterFactory;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
 
 /**
  * Default factory for {@link org.apache.hc.core5.http.io.HttpClientConnection}s.
@@ -55,21 +56,23 @@ public class DefaultBHttpClientConnectionFactory
     private final CharCodingConfig charCodingConfig;
     private final ContentLengthStrategy incomingContentStrategy;
     private final ContentLengthStrategy outgoingContentStrategy;
+    private final ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
     private final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory;
     private final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory;
 
-    public DefaultBHttpClientConnectionFactory(
+    private DefaultBHttpClientConnectionFactory(
             final Http1Config http1Config,
             final CharCodingConfig charCodingConfig,
             final ContentLengthStrategy incomingContentStrategy,
             final ContentLengthStrategy outgoingContentStrategy,
+            final ResponseOutOfOrderStrategy responseOutOfOrderStrategy,
             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
-        super();
         this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT;
         this.charCodingConfig = charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT;
         this.incomingContentStrategy = incomingContentStrategy;
         this.outgoingContentStrategy = outgoingContentStrategy;
+        this.responseOutOfOrderStrategy = responseOutOfOrderStrategy;
         this.requestWriterFactory = requestWriterFactory;
         this.responseParserFactory = responseParserFactory;
     }
@@ -77,6 +80,23 @@ public class DefaultBHttpClientConnectionFactory
     public DefaultBHttpClientConnectionFactory(
             final Http1Config http1Config,
             final CharCodingConfig charCodingConfig,
+            final ContentLengthStrategy incomingContentStrategy,
+            final ContentLengthStrategy outgoingContentStrategy,
+            final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
+            final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
+        this(
+                http1Config,
+                charCodingConfig,
+                incomingContentStrategy,
+                outgoingContentStrategy,
+                null,
+                requestWriterFactory,
+                responseParserFactory);
+    }
+
+    public DefaultBHttpClientConnectionFactory(
+            final Http1Config http1Config,
+            final CharCodingConfig charCodingConfig,
             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
         this(http1Config, charCodingConfig, null, null, requestWriterFactory, responseParserFactory);
@@ -100,6 +120,7 @@ public class DefaultBHttpClientConnectionFactory
                 CharCodingSupport.createEncoder(this.charCodingConfig),
                 this.incomingContentStrategy,
                 this.outgoingContentStrategy,
+                this.responseOutOfOrderStrategy,
                 this.requestWriterFactory,
                 this.responseParserFactory);
         conn.bind(socket);
@@ -125,6 +146,7 @@ public class DefaultBHttpClientConnectionFactory
         private CharCodingConfig charCodingConfig;
         private ContentLengthStrategy incomingContentStrategy;
         private ContentLengthStrategy outgoingContentStrategy;
+        private ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
         private HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory;
         private HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory;
 
@@ -150,6 +172,11 @@ public class DefaultBHttpClientConnectionFactory
             return this;
         }
 
+        public Builder responseOutOfOrderStrategy(final ResponseOutOfOrderStrategy responseOutOfOrderStrategy) {
+            this.responseOutOfOrderStrategy = responseOutOfOrderStrategy;
+            return this;
+        }
+
         public Builder requestWriterFactory(
                 final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory) {
             this.requestWriterFactory = requestWriterFactory;
@@ -168,6 +195,7 @@ public class DefaultBHttpClientConnectionFactory
                     charCodingConfig,
                     incomingContentStrategy,
                     outgoingContentStrategy,
+                    responseOutOfOrderStrategy,
                     requestWriterFactory,
                     responseParserFactory);
         }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/MonitoringResponseOutOfOrderStrategy.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/MonitoringResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..5e56af4
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/MonitoringResponseOutOfOrderStrategy.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * 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.core5.http.impl.io;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link ResponseOutOfOrderStrategy} implementation which checks for premature responses every {@link #chunkSize}
+ * bytes. An 8 KiB chunk size is used by default based on testing using values between 4 KiB and 128 KiB. This is
+ * optimized for correctness and results in a maximum upload speed of 8 MiB/s until {@link #maxChunksToCheck} is
+ * reached.
+ *
+ * @since 5.1
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class MonitoringResponseOutOfOrderStrategy implements ResponseOutOfOrderStrategy {
+
+    private static final int DEFAULT_CHUNK_SIZE = 8 * 1024;
+
+    public static final MonitoringResponseOutOfOrderStrategy INSTANCE = new MonitoringResponseOutOfOrderStrategy();
+
+    private final long chunkSize;
+    private final long maxChunksToCheck;
+
+    /**
+     * Instantiates a default {@link MonitoringResponseOutOfOrderStrategy}. {@link #INSTANCE} may be used instead.
+     */
+    public MonitoringResponseOutOfOrderStrategy() {
+        this(DEFAULT_CHUNK_SIZE);
+    }
+
+    /**
+     * Instantiates a {@link MonitoringResponseOutOfOrderStrategy} with unlimited {@link #maxChunksToCheck}.
+     *
+     * @param chunkSize The chunk size after which a response check is executed.
+     */
+    public MonitoringResponseOutOfOrderStrategy(final long chunkSize) {
+        this(chunkSize, Long.MAX_VALUE);
+    }
+
+    /**
+     * Instantiates a {@link MonitoringResponseOutOfOrderStrategy}.
+     *
+     * @param chunkSize The chunk size after which a response check is executed.
+     * @param maxChunksToCheck The maximum number of chunks to check, allowing expensive checks to be avoided
+     *                         after a sufficient portion of the request entity has been transferred.
+     */
+    public MonitoringResponseOutOfOrderStrategy(final long chunkSize, final long maxChunksToCheck) {
+        this.chunkSize = Args.positive(chunkSize, "chunkSize");
+        this.maxChunksToCheck = Args.positive(maxChunksToCheck, "maxChunksToCheck");
+    }
+
+    @Override
+    public boolean isEarlyResponseDetected(
+            final HttpClientConnection connection,
+            final ClassicHttpRequest request,
+            final InputStream inputStream,
+            final long totalBytesSent,
+            final long nextWriteSize) throws IOException {
+        if (nextWriteStartsNewChunk(totalBytesSent, nextWriteSize)) {
+            final boolean ssl = connection.getSSLSession() != null;
+            return ssl ? connection.isDataAvailable(Timeout.ONE_MILLISECOND) : (inputStream.available() > 0);
+        }
+        return false;
+    }
+
+    private boolean nextWriteStartsNewChunk(final long totalBytesSent, final long nextWriteSize) {
+        final long currentChunkIndex = Math.min(totalBytesSent / chunkSize, maxChunksToCheck);
+        final long newChunkIndex = Math.min((totalBytesSent + nextWriteSize) / chunkSize, maxChunksToCheck);
+        return currentChunkIndex < newChunkIndex;
+    }
+
+    @Override
+    public String toString() {
+        return "DefaultResponseOutOfOrderStrategy{chunkSize=" + chunkSize + ", maxChunksToCheck=" + maxChunksToCheck + '}';
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/NoResponseOutOfOrderStrategy.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/NoResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..6eec7e3
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/NoResponseOutOfOrderStrategy.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * 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.core5.http.impl.io;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
+
+import java.io.InputStream;
+
+/**
+ * An implementation of {@link ResponseOutOfOrderStrategy} which does not check for early responses.
+ *
+ * Early response detection requires 1ms blocking reads and incurs a hefty performance cost for
+ * large uploads.
+ *
+ * @see MonitoringResponseOutOfOrderStrategy
+ * @since 5.1
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class NoResponseOutOfOrderStrategy implements ResponseOutOfOrderStrategy {
+
+    public static final NoResponseOutOfOrderStrategy INSTANCE = new NoResponseOutOfOrderStrategy();
+
+    @Override
+    public boolean isEarlyResponseDetected(
+            final HttpClientConnection connection,
+            final ClassicHttpRequest request,
+            final InputStream inputStream,
+            final long totalBytesSent,
+            final long nextWriteSize) {
+        return false;
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/ResponseOutOfOrderStrategy.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/ResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..ae6b79f
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/ResponseOutOfOrderStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * 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.core5.http.io;
+
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Represents a strategy to determine how frequently the client should check for an out of order response.
+ * An out of order response is sent before the server has read the full request. If the client fails to
+ * check for an early response then a {@link java.net.SocketException} or {@link java.net.SocketTimeoutException}
+ * may be thrown while writing the request entity after a timeout is reached on either the client or server.
+ *
+ * @since 5.1
+ */
+@Internal
+public interface ResponseOutOfOrderStrategy {
+
+    /**
+     * Called before each write to the to a socket {@link java.io.OutputStream} with the number of
+     * bytes that have already been sent, and the size of the write that will occur if this check
+     * does not encounter an out of order response.
+     *
+     * @param connection The connection used to send the current request.
+     * @param request The current request.
+     * @param inputStream The response stream, this may be used to check for an early response.
+     * @param totalBytesSent Number of bytes that have already been sent.
+     * @param nextWriteSize The size of a socket write operation that will follow this check.
+     * @return True if an early response was detected, otherwise false.
+     * @throws IOException in case of a network failure while checking for an early response.
+     */
+    boolean isEarlyResponseDetected(
+            HttpClientConnection connection,
+            ClassicHttpRequest request,
+            InputStream inputStream,
+            long totalBytesSent,
+            long nextWriteSize) throws IOException;
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestMonitoringResponseOutOfOrderStrategy.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestMonitoringResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..b15ef16
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestMonitoringResponseOutOfOrderStrategy.java
@@ -0,0 +1,132 @@
+/*
+ * ====================================================================
+ * 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.core5.http.impl.io;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+import javax.net.ssl.SSLSession;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class TestMonitoringResponseOutOfOrderStrategy {
+
+    private static final ClassicHttpRequest REQUEST = new BasicClassicHttpRequest("POST", "/path");
+
+    @Test
+    public void testFirstByteIsNotCheckedSsl() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                connection(true, true),
+                REQUEST,
+                // SSLSocket streams report zero bytes available
+                socketInputStream(0),
+                0,
+                1);
+        Assert.assertFalse(earlyResponse);
+    }
+
+    @Test
+    public void testFirstByteIsNotCheckedPlain() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                connection(true, false),
+                REQUEST,
+                socketInputStream(1),
+                0,
+                1);
+        Assert.assertFalse(earlyResponse);
+    }
+
+    @Test
+    public void testWritesWithinChunkAreNotChecked() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                connection(true, true),
+                REQUEST,
+                socketInputStream(0),
+                1,
+                8190);
+        Assert.assertFalse(
+                "There is data available, but checks shouldn't occur until just prior to the 8192nd byte",
+                earlyResponse);
+    }
+
+    @Test
+    public void testWritesAcrossChunksAreChecked() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                connection(true, true),
+                REQUEST,
+                socketInputStream(0),
+                8191,
+                1);
+        Assert.assertTrue(earlyResponse);
+    }
+
+    @Test
+    public void testMaximumChunks() throws IOException {
+        final ResponseOutOfOrderStrategy strategy = new MonitoringResponseOutOfOrderStrategy(1, 2);
+        Assert.assertTrue(strategy.isEarlyResponseDetected(
+                connection(true, true),
+                REQUEST,
+                socketInputStream(0),
+                0,
+                1));
+        Assert.assertTrue(strategy.isEarlyResponseDetected(
+                connection(true, true),
+                REQUEST,
+                socketInputStream(0),
+                1,
+                2));
+        Assert.assertFalse(strategy.isEarlyResponseDetected(
+                connection(true, true),
+                REQUEST,
+                socketInputStream(0),
+                2,
+                3));
+    }
+
+    private static InputStream socketInputStream(final int available) throws IOException {
+        final InputStream stream = Mockito.mock(InputStream.class);
+        Mockito.when(stream.available()).thenReturn(available);
+        return stream;
+    }
+
+    private static HttpClientConnection connection(final boolean dataAvailable, final boolean ssl) throws IOException {
+        final HttpClientConnection connection = Mockito.mock(HttpClientConnection.class);
+        Mockito.when(connection.isDataAvailable(ArgumentMatchers.any(Timeout.class))).thenReturn(dataAvailable);
+        if (ssl) {
+            Mockito.when(connection.getSSLSession()).thenReturn(Mockito.mock(SSLSession.class));
+        }
+        return connection;
+    }
+}


[httpcomponents-core] 04/05: HTTPCORE-643: Implement NullEntity for convenience (#209)

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch 5.1.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 6b9ed46ee9936353a0eb07cdddc26e8b637564dc
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Aug 3 13:35:42 2020 -0400

    HTTPCORE-643: Implement NullEntity for convenience (#209)
---
 .../hc/core5/http/impl/io/BHttpConnectionBase.java |   1 +
 .../hc/core5/http/impl/io/EmptyInputStream.java    |   2 +
 .../hc/core5/http/impl/io/IncomingHttpEntity.java  |   1 +
 .../hc/core5/http/io/entity/BasicHttpEntity.java   |   1 -
 .../{impl/io => io/entity}/EmptyInputStream.java   |   4 +-
 .../entity/NullEntity.java}                        | 104 ++++++++-------------
 .../core5/http/io/entity/TestBasicHttpEntity.java  |   1 -
 .../http/io/entity/TestInputStreamEntity.java      |   1 -
 .../hc/core5/http/io/entity/TestNullEntity.java    |  90 ++++++++++++++++++
 .../http/protocol/TestStandardInterceptors.java    |   2 +-
 10 files changed, 134 insertions(+), 73 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
index 4ffae0e..b61b177 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
@@ -58,6 +58,7 @@ import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
 import org.apache.hc.core5.http.io.BHttpConnection;
 import org.apache.hc.core5.http.io.SessionInputBuffer;
 import org.apache.hc.core5.http.io.SessionOutputBuffer;
+import org.apache.hc.core5.http.io.entity.EmptyInputStream;
 import org.apache.hc.core5.io.CloseMode;
 import org.apache.hc.core5.io.Closer;
 import org.apache.hc.core5.net.InetAddressUtils;
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
index 8508245..1c3d2e4 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
@@ -31,7 +31,9 @@ import java.io.InputStream;
 
 /**
  * @since 4.4
+ * @deprecated Please use {@link org.apache.hc.core5.http.io.entity.EmptyInputStream}
  */
+@Deprecated
 public final class EmptyInputStream extends InputStream {
 
     public static final EmptyInputStream INSTANCE = new EmptyInputStream();
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
index e831325..ee1228f 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
@@ -38,6 +38,7 @@ import org.apache.hc.core5.function.Supplier;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
+import org.apache.hc.core5.http.io.entity.EmptyInputStream;
 import org.apache.hc.core5.io.Closer;
 
 class IncomingHttpEntity implements HttpEntity {
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java
index 1d9407e..730ac91 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java
@@ -33,7 +33,6 @@ import java.io.InputStream;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
 import org.apache.hc.core5.io.Closer;
 import org.apache.hc.core5.util.Args;
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EmptyInputStream.java
similarity index 97%
copy from httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
copy to httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EmptyInputStream.java
index 8508245..294952b 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EmptyInputStream.java
@@ -25,12 +25,12 @@
  *
  */
 
-package org.apache.hc.core5.http.impl.io;
+package org.apache.hc.core5.http.io.entity;
 
 import java.io.InputStream;
 
 /**
- * @since 4.4
+ * @since 5.1
  */
 public final class EmptyInputStream extends InputStream {
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java
similarity index 53%
copy from httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
copy to httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java
index e831325..b4b1805 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java
@@ -25,7 +25,13 @@
  *
  */
 
-package org.apache.hc.core5.http.impl.io;
+package org.apache.hc.core5.http.io.entity;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -34,103 +40,67 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
-import org.apache.hc.core5.function.Supplier;
-import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpEntity;
-import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
-import org.apache.hc.core5.io.Closer;
-
-class IncomingHttpEntity implements HttpEntity {
-
-    private final InputStream content;
-    private final long len;
-    private final boolean chunked;
-    private final Header contentType;
-    private final Header contentEncoding;
-
-    IncomingHttpEntity(final InputStream content, final long len, final boolean chunked, final Header contentType, final Header contentEncoding) {
-        this.content = content;
-        this.len = len;
-        this.chunked = chunked;
-        this.contentType = contentType;
-        this.contentEncoding = contentEncoding;
-    }
+/**
+ * An empty entity with no content-type. This type may be used for convenience
+ * in place of an empty {@link ByteArrayEntity}.
+ *
+ * @since 5.1
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class NullEntity implements HttpEntity {
 
-    @Override
-    public boolean isRepeatable() {
-        return false;
-    }
+    public static final NullEntity INSTANCE = new NullEntity();
+
+    private NullEntity() {}
 
     @Override
-    public boolean isChunked() {
-        return chunked;
+    public boolean isRepeatable() {
+        return true;
     }
 
     @Override
-    public long getContentLength() {
-        return len;
+    public InputStream getContent() throws IOException, UnsupportedOperationException {
+        return EmptyInputStream.INSTANCE;
     }
 
     @Override
-    public String getContentType() {
-        return contentType != null ? contentType.getValue() : null;
-    }
+    public void writeTo(final OutputStream outStream) throws IOException {}
 
     @Override
-    public String getContentEncoding() {
-        return contentEncoding != null ? contentEncoding.getValue() : null;
+    public boolean isStreaming() {
+        return false;
     }
 
     @Override
-    public InputStream getContent() throws IOException, IllegalStateException {
-        return content;
+    public Supplier<List<? extends Header>> getTrailers() {
+        return null;
     }
 
     @Override
-    public boolean isStreaming() {
-        return content != null && content != EmptyInputStream.INSTANCE;
-    }
+    public void close() throws IOException {}
 
     @Override
-    public void writeTo(final OutputStream outStream) throws IOException {
-        AbstractHttpEntity.writeTo(this, outStream);
+    public long getContentLength() {
+        return 0;
     }
 
     @Override
-    public Supplier<List<? extends Header>> getTrailers() {
+    public String getContentType() {
         return null;
     }
 
     @Override
-    public Set<String> getTrailerNames() {
-        return Collections.emptySet();
+    public String getContentEncoding() {
+        return null;
     }
 
     @Override
-    public void close() throws IOException {
-        Closer.close(content);
+    public boolean isChunked() {
+        return false;
     }
 
     @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append('[');
-        sb.append("Content-Type: ");
-        sb.append(getContentType());
-        sb.append(',');
-        sb.append("Content-Encoding: ");
-        sb.append(getContentEncoding());
-        sb.append(',');
-        final long len = getContentLength();
-        if (len >= 0) {
-            sb.append("Content-Length: ");
-            sb.append(len);
-            sb.append(',');
-        }
-        sb.append("Chunked: ");
-        sb.append(isChunked());
-        sb.append(']');
-        return sb.toString();
+    public Set<String> getTrailerNames() {
+        return Collections.emptySet();
     }
-
 }
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java
index b2ac16e..8012585 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java
@@ -32,7 +32,6 @@ import java.io.ByteArrayOutputStream;
 import java.nio.charset.StandardCharsets;
 
 import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java
index 04f937b..35e0e17 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java
@@ -33,7 +33,6 @@ import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 
 import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestNullEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestNullEntity.java
new file mode 100644
index 0000000..c2abce3
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestNullEntity.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * 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.core5.http.io.entity;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+public class TestNullEntity {
+
+    @Test
+    public void testLength() {
+        assertEquals(0, NullEntity.INSTANCE.getContentLength());
+    }
+
+    @Test
+    public void testContentType() {
+        assertNull(NullEntity.INSTANCE.getContentType());
+    }
+
+    @Test
+    public void testContentEncoding() {
+        assertNull(NullEntity.INSTANCE.getContentEncoding());
+    }
+
+    @Test
+    public void testTrailerNames() {
+        assertEquals(Collections.emptySet(), NullEntity.INSTANCE.getTrailerNames());
+    }
+
+    @Test
+    public void testContentStream() throws IOException {
+        try (InputStream content = NullEntity.INSTANCE.getContent()) {
+            assertEquals(-1, content.read());
+        }
+        // Closing the resource should have no impact
+        try (InputStream content = NullEntity.INSTANCE.getContent()) {
+            assertEquals(-1, content.read());
+        }
+    }
+
+    @Test
+    public void testWriteTo() throws IOException {
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        NullEntity.INSTANCE.writeTo(baos);
+        assertEquals(0, baos.size());
+    }
+
+    @Test
+    public void testIsStreaming() {
+        assertFalse(NullEntity.INSTANCE.isStreaming());
+    }
+
+    @Test
+    public void testIsChunked() {
+        assertFalse(NullEntity.INSTANCE.isChunked());
+    }
+}
\ No newline at end of file
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
index 8ed0f1e..cef1f8a 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
@@ -38,7 +38,7 @@ import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.Method;
 import org.apache.hc.core5.http.ProtocolException;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
+import org.apache.hc.core5.http.io.entity.EmptyInputStream;
 import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
 import org.apache.hc.core5.http.io.entity.HttpEntities;
 import org.apache.hc.core5.http.io.entity.StringEntity;


[httpcomponents-core] 02/05: RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch 5.1.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 89d166bf85fde5beaf220d37c3b3bad470be41cc
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Mon Jul 20 17:30:22 2020 +0200

    RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec
---
 .../hc/core5/http2/examples/H2GreetingServer.java  |   4 +-
 .../hc/core5/testing/framework/FrameworkTest.java  |   5 +-
 .../framework/TestingFrameworkRequestHandler.java  |   7 +-
 .../hc/core5/http/io/entity/EntityUtils.java       |   4 +-
 .../hc/core5/http/io/entity/HttpEntities.java      |   4 +-
 .../http/nio/entity/AsyncEntityProducers.java      |   4 +-
 .../http/nio/support/AsyncRequestBuilder.java      |   4 +-
 .../java/org/apache/hc/core5/net/PercentCodec.java | 163 +++++++++
 .../java/org/apache/hc/core5/net/URIBuilder.java   | 233 +++++++++---
 .../org/apache/hc/core5/net/URLEncodedUtils.java   | 303 ++--------------
 .../java/org/apache/hc/core5/net/WWWFormCodec.java |  83 +++++
 .../hc/core5/http/NameValuePairListMatcher.java    |  85 +++++
 .../hc/core5/http/io/entity/TestEntityUtils.java   |   4 +-
 .../org/apache/hc/core5/net/TestPercentCodec.java  |  70 ++++
 .../org/apache/hc/core5/net/TestURIBuilder.java    | 206 ++++++++---
 .../apache/hc/core5/net/TestURLEncodedUtils.java   | 394 ---------------------
 .../org/apache/hc/core5/net/TestWWWFormCodec.java  | 123 +++++++
 17 files changed, 913 insertions(+), 783 deletions(-)

diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java
index b1dc3e1..551be6f 100644
--- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java
@@ -63,7 +63,7 @@ import org.apache.hc.core5.http2.HttpVersionPolicy;
 import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap;
 import org.apache.hc.core5.io.CloseMode;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.reactor.IOReactorConfig;
 import org.apache.hc.core5.reactor.ListenerEndpoint;
 import org.apache.hc.core5.util.TimeValue;
@@ -174,7 +174,7 @@ public class H2GreetingServer {
             if (contentType != null && contentType.isSameMimeType(ContentType.APPLICATION_FORM_URLENCODED)) {
 
                 // decoding the form entity into key/value pairs:
-                final List<NameValuePair> args = URLEncodedUtils.parse(httpEntity, contentType.getCharset());
+                final List<NameValuePair> args = WWWFormCodec.parse(httpEntity, contentType.getCharset());
                 if (!args.isEmpty()) {
                     name = args.get(0).getValue();
                 }
diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java
index 326a8f1..9685487 100644
--- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java
+++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java
@@ -46,7 +46,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.hc.core5.http.NameValuePair;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.URIBuilder;
 
 public class FrameworkTest {
 
@@ -112,7 +112,8 @@ public class FrameworkTest {
             if (path != null) {
                 final URI uri = path.startsWith("/") ? new URI("http://localhost:8080" + path) :
                                                  new URI("http://localhost:8080/");
-                final List<NameValuePair> params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8);
+                final URIBuilder uriBuilder = new URIBuilder(uri, StandardCharsets.UTF_8);
+                final List<NameValuePair> params = uriBuilder.getQueryParams();
                 @SuppressWarnings("unchecked")
                 final Map<String, Object> queryMap = (Map<String, Object>) request.get(QUERY);
                 for (final NameValuePair param : params) {
diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java
index 07bd7d5..e757b62 100644
--- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java
+++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java
@@ -45,17 +45,17 @@ import java.util.Map.Entry;
 
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.ProtocolVersion;
 import org.apache.hc.core5.http.io.HttpRequestHandler;
-import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.URIBuilder;
 
 public class TestingFrameworkRequestHandler implements HttpRequestHandler {
     protected Throwable thrown;
@@ -137,7 +137,8 @@ public class TestingFrameworkRequestHandler implements HttpRequestHandler {
             final Map<String, String> expectedQuery = (Map<String, String>) requestExpectations.get(QUERY);
             if (expectedQuery != null) {
                 final URI uri = request.getUri();
-                final List<NameValuePair> actualParams = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8);
+                final URIBuilder uriBuilder = new URIBuilder(uri, StandardCharsets.UTF_8);
+                final List<NameValuePair> actualParams = uriBuilder.getQueryParams();
                 final Map<String, String> actualParamsMap = new HashMap<>();
                 for (final NameValuePair actualParam : actualParams) {
                     actualParamsMap.put(actualParam.getName(), actualParam.getValue());
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java
index 4b95c39..ecf7f71 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java
@@ -43,7 +43,7 @@ import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.ParseException;
 import org.apache.hc.core5.io.Closer;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.ByteArrayBuffer;
 import org.apache.hc.core5.util.CharArrayBuffer;
@@ -390,7 +390,7 @@ public final class EntityUtils {
         if (buf.isEmpty()) {
             return Collections.emptyList();
         }
-        return URLEncodedUtils.parse(buf, charset, '&');
+        return WWWFormCodec.parse(buf, charset);
     }
 
 }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java
index 597e1ea..6331733 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java
@@ -46,7 +46,7 @@ import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.io.IOCallback;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.util.Args;
 
 /**
@@ -88,7 +88,7 @@ public final class HttpEntities {
         final ContentType contentType = charset != null ?
                 ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset) :
                 ContentType.APPLICATION_FORM_URLENCODED;
-        return create(URLEncodedUtils.format(parameters, contentType.getCharset()), contentType);
+        return create(WWWFormCodec.format(parameters, contentType.getCharset()), contentType);
     }
 
     public static HttpEntity create(final IOCallback<OutputStream> callback, final ContentType contentType) {
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java
index 3fd20d1..b34438d 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java
@@ -45,7 +45,7 @@ import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.nio.AsyncEntityProducer;
 import org.apache.hc.core5.http.nio.DataStreamChannel;
 import org.apache.hc.core5.http.nio.StreamChannel;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 
 /**
  * {AsyncEntityProducer} factory methods.
@@ -82,7 +82,7 @@ public final class AsyncEntityProducers {
         final ContentType contentType = charset != null ?
                 ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset) :
                 ContentType.APPLICATION_FORM_URLENCODED;
-        return create(URLEncodedUtils.format(parameters, contentType.getCharset()), contentType);
+        return create(WWWFormCodec.format(parameters, contentType.getCharset()), contentType);
     }
 
     public static AsyncEntityProducer createBinary(
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java
index 81149b2..720980e 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java
@@ -50,7 +50,7 @@ import org.apache.hc.core5.http.nio.AsyncRequestProducer;
 import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer;
 import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
 import org.apache.hc.core5.net.URIBuilder;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.util.Args;
 
 /**
@@ -368,7 +368,7 @@ public class AsyncRequestBuilder {
         AsyncEntityProducer entityProducerCopy = entityProducer;
         if (parameters != null && !parameters.isEmpty()) {
             if (entityProducerCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method))) {
-                final String content = URLEncodedUtils.format(
+                final String content = WWWFormCodec.format(
                         parameters,
                         charset != null ? charset : ContentType.APPLICATION_FORM_URLENCODED.getCharset());
                 entityProducerCopy = new StringAsyncEntityProducer(
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
new file mode 100644
index 0000000..2782282
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
@@ -0,0 +1,163 @@
+/*
+ * ====================================================================
+ * 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.core5.net;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+
+/**
+ * Percent-encoding mechanism defined in RFC 3986
+ *
+ * @since 5.1
+ */
+public class PercentCodec {
+
+    static final BitSet GEN_DELIMS = new BitSet(256);
+    static final BitSet SUB_DELIMS = new BitSet(256);
+    static final BitSet UNRESERVED = new BitSet(256);
+    static final BitSet URIC = new BitSet(256);
+
+    static {
+        GEN_DELIMS.set(':');
+        GEN_DELIMS.set('/');
+        GEN_DELIMS.set('?');
+        GEN_DELIMS.set('#');
+        GEN_DELIMS.set('[');
+        GEN_DELIMS.set(']');
+        GEN_DELIMS.set('@');
+
+        SUB_DELIMS.set('!');
+        SUB_DELIMS.set('$');
+        SUB_DELIMS.set('&');
+        SUB_DELIMS.set('\'');
+        SUB_DELIMS.set('(');
+        SUB_DELIMS.set(')');
+        SUB_DELIMS.set('*');
+        SUB_DELIMS.set('+');
+        SUB_DELIMS.set(',');
+        SUB_DELIMS.set(';');
+        SUB_DELIMS.set('=');
+
+        for (int i = 'a'; i <= 'z'; i++) {
+            UNRESERVED.set(i);
+        }
+        for (int i = 'A'; i <= 'Z'; i++) {
+            UNRESERVED.set(i);
+        }
+        // numeric characters
+        for (int i = '0'; i <= '9'; i++) {
+            UNRESERVED.set(i);
+        }
+        UNRESERVED.set('-');
+        UNRESERVED.set('.');
+        UNRESERVED.set('_');
+        UNRESERVED.set('~');
+        URIC.or(SUB_DELIMS);
+        URIC.or(UNRESERVED);
+    }
+
+    private static final int RADIX = 16;
+
+    static void encode(final StringBuilder buf, final CharSequence content, final Charset charset,
+                       final BitSet safechars, final boolean blankAsPlus) {
+        if (content == null) {
+            return;
+        }
+        final CharBuffer cb = CharBuffer.wrap(content);
+        final ByteBuffer bb = (charset != null ? charset : StandardCharsets.UTF_8).encode(cb);
+        while (bb.hasRemaining()) {
+            final int b = bb.get() & 0xff;
+            if (safechars.get(b)) {
+                buf.append((char) b);
+            } else if (blankAsPlus && b == ' ') {
+                buf.append("+");
+            } else {
+                buf.append("%");
+                final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
+                final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
+                buf.append(hex1);
+                buf.append(hex2);
+            }
+        }
+    }
+
+    static void encode(final StringBuilder buf, final CharSequence content, final Charset charset, final boolean blankAsPlus) {
+        encode(buf, content, charset, UNRESERVED, blankAsPlus);
+    }
+
+    public static void encode(final StringBuilder buf, final CharSequence content, final Charset charset) {
+        encode(buf, content, charset, UNRESERVED, false);
+    }
+
+    public static String encode(final CharSequence content, final Charset charset) {
+        if (content == null) {
+            return null;
+        }
+        final StringBuilder buf = new StringBuilder();
+        encode(buf, content, charset, UNRESERVED, false);
+        return buf.toString();
+    }
+
+    static String decode(final CharSequence content, final Charset charset, final boolean plusAsBlank) {
+        if (content == null) {
+            return null;
+        }
+        final ByteBuffer bb = ByteBuffer.allocate(content.length());
+        final CharBuffer cb = CharBuffer.wrap(content);
+        while (cb.hasRemaining()) {
+            final char c = cb.get();
+            if (c == '%' && cb.remaining() >= 2) {
+                final char uc = cb.get();
+                final char lc = cb.get();
+                final int u = Character.digit(uc, RADIX);
+                final int l = Character.digit(lc, RADIX);
+                if (u != -1 && l != -1) {
+                    bb.put((byte) ((u << 4) + l));
+                } else {
+                    bb.put((byte) '%');
+                    bb.put((byte) uc);
+                    bb.put((byte) lc);
+                }
+            } else if (plusAsBlank && c == '+') {
+                bb.put((byte) ' ');
+            } else {
+                bb.put((byte) c);
+            }
+        }
+        bb.flip();
+        return (charset != null ? charset : StandardCharsets.UTF_8).decode(bb).toString();
+    }
+
+    public static String decode(final CharSequence content, final Charset charset) {
+        return decode(content, charset, false);
+    }
+
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
index 3fbe2d7..70358e2 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
@@ -34,6 +34,7 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -41,7 +42,9 @@ import java.util.List;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.apache.hc.core5.http.message.ParserCursor;
 import org.apache.hc.core5.util.TextUtils;
+import org.apache.hc.core5.util.Tokenizer;
 
 /**
  * Builder for {@link URI} instances.
@@ -75,6 +78,7 @@ public class URIBuilder {
     private String host;
     private int port;
     private String encodedPath;
+    private boolean pathRootless;
     private List<String> pathSegments;
     private String encodedQuery;
     private List<NameValuePair> queryParams;
@@ -98,7 +102,7 @@ public class URIBuilder {
      * @throws URISyntaxException if the input is not a valid URI
      */
     public URIBuilder(final String string) throws URISyntaxException {
-        this(new URI(string), null);
+        this(new URI(string), StandardCharsets.UTF_8);
     }
 
     /**
@@ -106,7 +110,7 @@ public class URIBuilder {
      * @param uri
      */
     public URIBuilder(final URI uri) {
-        this(uri, null);
+        this(uri, StandardCharsets.UTF_8);
     }
 
     /**
@@ -125,8 +129,7 @@ public class URIBuilder {
      */
     public URIBuilder(final URI uri, final Charset charset) {
         super();
-        setCharset(charset);
-        digestURI(uri);
+        digestURI(uri, charset);
     }
 
     public URIBuilder setCharset(final Charset charset) {
@@ -138,18 +141,118 @@ public class URIBuilder {
         return charset;
     }
 
-    private List <NameValuePair> parseQuery(final String query, final Charset charset) {
-        if (query != null && !query.isEmpty()) {
-            return URLEncodedUtils.parse(query, charset);
+    private static final char QUERY_PARAM_SEPARATOR = '&';
+    private static final char PARAM_VALUE_SEPARATOR = '=';
+    private static final char PATH_SEPARATOR = '/';
+
+    private static final BitSet QUERY_PARAM_SEPARATORS = new BitSet(256);
+    private static final BitSet QUERY_VALUE_SEPARATORS = new BitSet(256);
+    private static final BitSet PATH_SEPARATORS = new BitSet(256);
+
+    static {
+        QUERY_PARAM_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
+        QUERY_PARAM_SEPARATORS.set(PARAM_VALUE_SEPARATOR);
+        QUERY_VALUE_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
+        PATH_SEPARATORS.set(PATH_SEPARATOR);
+    }
+
+    static List<NameValuePair> parseQuery(final CharSequence s, final Charset charset, final boolean plusAsBlank) {
+        if (s == null) {
+            return null;
+        }
+        final Tokenizer tokenParser = Tokenizer.INSTANCE;
+        final ParserCursor cursor = new ParserCursor(0, s.length());
+        final List<NameValuePair> list = new ArrayList<>();
+        while (!cursor.atEnd()) {
+            final String name = tokenParser.parseToken(s, cursor, QUERY_PARAM_SEPARATORS);
+            String value = null;
+            if (!cursor.atEnd()) {
+                final int delim = s.charAt(cursor.getPos());
+                cursor.updatePos(cursor.getPos() + 1);
+                if (delim == PARAM_VALUE_SEPARATOR) {
+                    value = tokenParser.parseToken(s, cursor, QUERY_VALUE_SEPARATORS);
+                    if (!cursor.atEnd()) {
+                        cursor.updatePos(cursor.getPos() + 1);
+                    }
+                }
+            }
+            if (!name.isEmpty()) {
+                list.add(new BasicNameValuePair(
+                        PercentCodec.decode(name, charset, plusAsBlank),
+                        PercentCodec.decode(value, charset, plusAsBlank)));
+            }
         }
-        return null;
+        return list;
     }
 
-    private List <String> parsePath(final String path, final Charset charset) {
-        if (path != null && !path.isEmpty()) {
-            return URLEncodedUtils.parsePathSegments(path, charset);
+    static List<String> splitPath(final CharSequence s) {
+        if (s == null) {
+            return null;
+        }
+        final ParserCursor cursor = new ParserCursor(0, s.length());
+        // Skip leading separator
+        if (cursor.atEnd()) {
+            return new ArrayList<>(0);
+        }
+        if (PATH_SEPARATORS.get(s.charAt(cursor.getPos()))) {
+            cursor.updatePos(cursor.getPos() + 1);
+        }
+        final List<String> list = new ArrayList<>();
+        final StringBuilder buf = new StringBuilder();
+        for (;;) {
+            if (cursor.atEnd()) {
+                list.add(buf.toString());
+                break;
+            }
+            final char current = s.charAt(cursor.getPos());
+            if (PATH_SEPARATORS.get(current)) {
+                list.add(buf.toString());
+                buf.setLength(0);
+            } else {
+                buf.append(current);
+            }
+            cursor.updatePos(cursor.getPos() + 1);
+        }
+        return list;
+    }
+
+    static List<String> parsePath(final CharSequence s, final Charset charset) {
+        if (s == null) {
+            return null;
+        }
+        final List<String> segments = splitPath(s);
+        final List<String> list = new ArrayList<>(segments.size());
+        for (final String segment: segments) {
+            list.add(PercentCodec.decode(segment, charset));
+        }
+        return list;
+    }
+
+    static void formatPath(final StringBuilder buf, final Iterable<String> segments, final boolean rootless, final Charset charset) {
+        int i = 0;
+        for (final String segment : segments) {
+            if (i > 0 || !rootless) {
+                buf.append(PATH_SEPARATOR);
+            }
+            PercentCodec.encode(buf, segment, charset);
+            i++;
+        }
+    }
+
+    static void formatQuery(final StringBuilder buf, final Iterable<? extends NameValuePair> params, final Charset charset,
+                            final boolean blankAsPlus) {
+        int i = 0;
+        for (final NameValuePair parameter : params) {
+            if (i > 0) {
+                buf.append(QUERY_PARAM_SEPARATOR);
+            }
+            PercentCodec.encode(buf, parameter.getName(), charset, blankAsPlus);
+            if (parameter.getValue() != null) {
+                buf.append(PARAM_VALUE_SEPARATOR);
+                PercentCodec.encode(buf, parameter.getValue(), charset, blankAsPlus);
+            }
+            i++;
         }
-        return null;
     }
 
     /**
@@ -167,14 +270,23 @@ public class URIBuilder {
         if (this.encodedSchemeSpecificPart != null) {
             sb.append(this.encodedSchemeSpecificPart);
         } else {
+            final boolean authoritySpecified;
             if (this.encodedAuthority != null) {
                 sb.append("//").append(this.encodedAuthority);
+                authoritySpecified = true;
             } else if (this.host != null) {
                 sb.append("//");
                 if (this.encodedUserInfo != null) {
                     sb.append(this.encodedUserInfo).append("@");
                 } else if (this.userInfo != null) {
-                    encodeUserInfo(sb, this.userInfo);
+                    final int idx = this.userInfo.indexOf(':');
+                    if (idx != -1) {
+                        PercentCodec.encode(sb, this.userInfo.substring(0, idx), this.charset);
+                        sb.append(':');
+                        PercentCodec.encode(sb, this.userInfo.substring(idx + 1), this.charset);
+                    } else {
+                        PercentCodec.encode(sb, this.userInfo, this.charset);
+                    }
                     sb.append("@");
                 }
                 if (InetAddressUtils.isIPv6Address(this.host)) {
@@ -185,43 +297,38 @@ public class URIBuilder {
                 if (this.port >= 0) {
                     sb.append(":").append(this.port);
                 }
+                authoritySpecified = true;
+            } else {
+                authoritySpecified = false;
             }
             if (this.encodedPath != null) {
-                sb.append(normalizePath(this.encodedPath, sb.length() == 0));
+                if (authoritySpecified && !TextUtils.isEmpty(this.encodedPath) && !this.encodedPath.startsWith("/")) {
+                    sb.append('/');
+                }
+                sb.append(this.encodedPath);
             } else if (this.pathSegments != null) {
-                encodePath(sb, this.pathSegments);
+                formatPath(sb, this.pathSegments, !authoritySpecified && this.pathRootless, this.charset);
             }
             if (this.encodedQuery != null) {
                 sb.append("?").append(this.encodedQuery);
             } else if (this.queryParams != null && !this.queryParams.isEmpty()) {
                 sb.append("?");
-                encodeUrlForm(sb, this.queryParams);
+                formatQuery(sb, this.queryParams, this.charset, false);
             } else if (this.query != null) {
                 sb.append("?");
-                encodeUric(sb, this.query);
+                PercentCodec.encode(sb, this.query, this.charset, PercentCodec.URIC, false);
             }
         }
         if (this.encodedFragment != null) {
             sb.append("#").append(this.encodedFragment);
         } else if (this.fragment != null) {
             sb.append("#");
-            encodeUric(sb, this.fragment);
+            PercentCodec.encode(sb, this.fragment, this.charset);
         }
         return sb.toString();
     }
 
-    private static String normalizePath(final String path, final boolean relative) {
-        String s = path;
-        if (TextUtils.isBlank(s)) {
-            return "";
-        }
-        if (!relative && !s.startsWith("/")) {
-            s = "/" + s;
-        }
-        return s;
-    }
-
-    private void digestURI(final URI uri) {
+    private void digestURI(final URI uri, final Charset charset) {
         this.scheme = uri.getScheme();
         this.encodedSchemeSpecificPart = uri.getRawSchemeSpecificPart();
         this.encodedAuthority = uri.getRawAuthority();
@@ -230,27 +337,13 @@ public class URIBuilder {
         this.encodedUserInfo = uri.getRawUserInfo();
         this.userInfo = uri.getUserInfo();
         this.encodedPath = uri.getRawPath();
-        this.pathSegments = parsePath(uri.getRawPath(), this.charset != null ? this.charset : StandardCharsets.UTF_8);
+        this.pathSegments = parsePath(uri.getRawPath(), charset);
+        this.pathRootless = uri.getRawPath() != null && !uri.getRawPath().startsWith("/");
         this.encodedQuery = uri.getRawQuery();
-        this.queryParams = parseQuery(uri.getRawQuery(), this.charset != null ? this.charset : StandardCharsets.UTF_8);
+        this.queryParams = parseQuery(uri.getRawQuery(), charset, false);
         this.encodedFragment = uri.getRawFragment();
         this.fragment = uri.getFragment();
-    }
-
-    private void encodeUserInfo(final StringBuilder buf, final String userInfo) {
-        URLEncodedUtils.encUserInfo(buf, userInfo, this.charset != null ? this.charset : StandardCharsets.UTF_8);
-    }
-
-    private void encodePath(final StringBuilder buf, final List<String> pathSegments) {
-        URLEncodedUtils.formatSegments(buf, pathSegments, this.charset != null ? this.charset : StandardCharsets.UTF_8);
-    }
-
-    private void encodeUrlForm(final StringBuilder buf, final List<NameValuePair> params) {
-        URLEncodedUtils.formatParameters(buf, params, this.charset != null ? this.charset : StandardCharsets.UTF_8);
-    }
-
-    private void encodeUric(final StringBuilder buf, final String fragment) {
-        URLEncodedUtils.encUric(buf, fragment, this.charset != null ? this.charset : StandardCharsets.UTF_8);
+        this.charset = charset;
     }
 
     /**
@@ -301,7 +394,7 @@ public class URIBuilder {
             final StringBuilder sb = new StringBuilder(schemeSpecificPart);
             if (nvps != null && !nvps.isEmpty()) {
                 sb.append("?");
-                encodeUrlForm(sb, nvps);
+                formatQuery(sb, nvps, this.charset, false);
             }
             this.encodedSchemeSpecificPart = sb.toString();
         }
@@ -327,7 +420,11 @@ public class URIBuilder {
      * be unescaped and may contain non ASCII characters.
      *
      * @return this.
+     *
+     * @deprecated The use of clear-text passwords in {@link URI}s has been deprecated and is strongly
+     * discouraged.
      */
+    @Deprecated
     public URIBuilder setUserInfo(final String username, final String password) {
         return setUserInfo(username + ':' + password);
     }
@@ -387,7 +484,9 @@ public class URIBuilder {
      * @return this.
      */
     public URIBuilder setPath(final String path) {
-        return setPathSegments(path != null ? URLEncodedUtils.splitPathSegments(path) : null);
+        setPathSegments(path != null ? splitPath(path) : null);
+        this.pathRootless = path != null && !path.startsWith("/");
+        return this;
     }
 
     /**
@@ -399,6 +498,23 @@ public class URIBuilder {
         this.pathSegments = pathSegments.length > 0 ? Arrays.asList(pathSegments) : null;
         this.encodedSchemeSpecificPart = null;
         this.encodedPath = null;
+        this.pathRootless = false;
+        return this;
+    }
+
+    /**
+     * Sets rootless URI path (the first segment does not start with a /).
+     * The value is expected to be unescaped and may contain non ASCII characters.
+     *
+     * @return this.
+     *
+     * @since 5.1
+     */
+    public URIBuilder setPathSegmentsRootless(final String... pathSegments) {
+        this.pathSegments = pathSegments.length > 0 ? Arrays.asList(pathSegments) : null;
+        this.encodedSchemeSpecificPart = null;
+        this.encodedPath = null;
+        this.pathRootless = true;
         return this;
     }
 
@@ -411,6 +527,23 @@ public class URIBuilder {
         this.pathSegments = pathSegments != null && pathSegments.size() > 0 ? new ArrayList<>(pathSegments) : null;
         this.encodedSchemeSpecificPart = null;
         this.encodedPath = null;
+        this.pathRootless = false;
+        return this;
+    }
+
+    /**
+     * Sets rootless URI path (the first segment does not start with a /).
+     * The value is expected to be unescaped and may contain non ASCII characters.
+     *
+     * @return this.
+     *
+     * @since 5.1
+     */
+    public URIBuilder setPathSegmentsRootless(final List<String> pathSegments) {
+        this.pathSegments = pathSegments != null && pathSegments.size() > 0 ? new ArrayList<>(pathSegments) : null;
+        this.encodedSchemeSpecificPart = null;
+        this.encodedPath = null;
+        this.pathRootless = true;
         return this;
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java
index c2131ea..a3e3676 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java
@@ -28,14 +28,11 @@
 package org.apache.hc.core5.net;
 
 import java.net.URI;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
-import java.util.Collections;
 import java.util.List;
 
 import org.apache.hc.core5.http.NameValuePair;
@@ -47,18 +44,15 @@ import org.apache.hc.core5.util.Tokenizer;
  * A collection of utilities for encoding URLs.
  *
  * @since 4.0
+ *
+ * @deprecated Use {@link URIBuilder} to parse and format {@link URI}s and
+ * {@link WWWFormCodec} to parse and format {@code application/x-www-form-urlencoded} forms.
  */
+@Deprecated
 public class URLEncodedUtils {
 
     private static final char QP_SEP_A = '&';
     private static final char QP_SEP_S = ';';
-    private static final String NAME_VALUE_SEPARATOR = "=";
-    private static final char PATH_SEPARATOR = '/';
-
-    private static final BitSet PATH_SEPARATORS     = new BitSet(256);
-    static {
-        PATH_SEPARATORS.set(PATH_SEPARATOR);
-    }
 
     /**
      * Returns a list of {@link NameValuePair}s URI query parameters.
@@ -76,7 +70,7 @@ public class URLEncodedUtils {
         if (query != null && !query.isEmpty()) {
             return parse(query, charset);
         }
-        return createEmptyList();
+        return new ArrayList<>(0);
     }
 
     /**
@@ -91,7 +85,7 @@ public class URLEncodedUtils {
      */
     public static List<NameValuePair> parse(final CharSequence s, final Charset charset) {
         if (s == null) {
-            return createEmptyList();
+            return new ArrayList<>(0);
         }
         return parse(s, charset, QP_SEP_A, QP_SEP_S);
     }
@@ -133,45 +127,13 @@ public class URLEncodedUtils {
             }
             if (!name.isEmpty()) {
                 list.add(new BasicNameValuePair(
-                        decodeFormFields(name, charset),
-                        decodeFormFields(value, charset)));
-            }
-        }
-        return list;
-    }
-
-    static List<String> splitSegments(final CharSequence s, final BitSet separators) {
-        final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, s.length());
-        // Skip leading separator
-        if (cursor.atEnd()) {
-            return Collections.emptyList();
-        }
-        if (separators.get(s.charAt(cursor.getPos()))) {
-            cursor.updatePos(cursor.getPos() + 1);
-        }
-        final List<String> list = new ArrayList<>();
-        final StringBuilder buf = new StringBuilder();
-        for (;;) {
-            if (cursor.atEnd()) {
-                list.add(buf.toString());
-                break;
-            }
-            final char current = s.charAt(cursor.getPos());
-            if (separators.get(current)) {
-                list.add(buf.toString());
-                buf.setLength(0);
-            } else {
-                buf.append(current);
+                        PercentCodec.decode(name, charset, true),
+                        PercentCodec.decode(value, charset, true)));
             }
-            cursor.updatePos(cursor.getPos() + 1);
         }
         return list;
     }
 
-    static List<String> splitPathSegments(final CharSequence s) {
-        return splitSegments(s, PATH_SEPARATORS);
-    }
-
     /**
      * Returns a list of URI path segments.
      *
@@ -182,12 +144,7 @@ public class URLEncodedUtils {
      * @since 4.5
      */
     public static List<String> parsePathSegments(final CharSequence s, final Charset charset) {
-        Args.notNull(s, "Char sequence");
-        final List<String> list = splitPathSegments(s);
-        for (int i = 0; i < list.size(); i++) {
-            list.set(i, urlDecode(list.get(i), charset != null ? charset : StandardCharsets.UTF_8, false));
-        }
-        return list;
+        return URIBuilder.parsePath(s, charset);
     }
 
     /**
@@ -202,13 +159,6 @@ public class URLEncodedUtils {
         return parsePathSegments(s, StandardCharsets.UTF_8);
     }
 
-    static void formatSegments(final StringBuilder buf, final Iterable<String> segments, final Charset charset) {
-        for (final String segment : segments) {
-            buf.append(PATH_SEPARATOR);
-            urlEncode(buf, segment, charset, PATHSAFE);
-        }
-    }
-
     /**
      * Returns a string consisting of joint encoded path segments.
      *
@@ -221,7 +171,7 @@ public class URLEncodedUtils {
     public static String formatSegments(final Iterable<String> segments, final Charset charset) {
         Args.notNull(segments, "Segments");
         final StringBuilder buf = new StringBuilder();
-        formatSegments(buf, segments, charset);
+        URIBuilder.formatPath(buf, segments, false, charset);
         return buf.toString();
     }
 
@@ -237,32 +187,6 @@ public class URLEncodedUtils {
         return formatSegments(Arrays.asList(segments), StandardCharsets.UTF_8);
     }
 
-    static void formatNameValuePairs(
-            final StringBuilder buf,
-            final Iterable<? extends NameValuePair> parameters,
-            final char parameterSeparator,
-            final Charset charset) {
-        int i = 0;
-        for (final NameValuePair parameter : parameters) {
-            if (i > 0) {
-                buf.append(parameterSeparator);
-            }
-            encodeFormFields(buf, parameter.getName(), charset);
-            if (parameter.getValue() != null) {
-                buf.append(NAME_VALUE_SEPARATOR);
-                encodeFormFields(buf, parameter.getValue(), charset);
-            }
-            i++;
-        }
-    }
-
-    static void formatParameters(
-            final StringBuilder buf,
-            final Iterable<? extends NameValuePair> parameters,
-            final Charset charset) {
-        formatNameValuePairs(buf, parameters, QP_SEP_A, charset);
-    }
-
     /**
      * Returns a String that is suitable for use as an {@code application/x-www-form-urlencoded}
      * list of parameters in an HTTP PUT or HTTP POST.
@@ -280,7 +204,18 @@ public class URLEncodedUtils {
             final Charset charset) {
         Args.notNull(parameters, "Parameters");
         final StringBuilder buf = new StringBuilder();
-        formatNameValuePairs(buf, parameters, parameterSeparator, charset);
+        int i = 0;
+        for (final NameValuePair parameter : parameters) {
+            if (i > 0) {
+                buf.append(parameterSeparator);
+            }
+            PercentCodec.encode(buf, parameter.getName(), charset, URLENCODER, true);
+            if (parameter.getValue() != null) {
+                buf.append('=');
+                PercentCodec.encode(buf, parameter.getValue(), charset, URLENCODER, true);
+            }
+            i++;
+        }
         return buf.toString();
     }
 
@@ -300,205 +235,25 @@ public class URLEncodedUtils {
         return format(parameters, QP_SEP_A, charset);
     }
 
-    /**
-     * Unreserved characters, i.e. alphanumeric, plus: {@code _ - ! . ~ ' ( ) *}
-     * <p>
-     *  This list is the same as the {@code unreserved} list in
-     *  <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>
-     */
-    private static final BitSet UNRESERVED   = new BitSet(256);
-    /**
-     * Punctuation characters: , ; : $ & + =
-     * <p>
-     * These are the additional characters allowed by userinfo.
-     */
-    private static final BitSet PUNCT        = new BitSet(256);
-    /** Characters which are safe to use in userinfo,
-     * i.e. {@link #UNRESERVED} plus {@link #PUNCT}uation */
-    private static final BitSet USERINFO     = new BitSet(256);
-    /** Characters which are safe to use in a path,
-     * i.e. {@link #UNRESERVED} plus {@link #PUNCT}uation plus / @ */
-    private static final BitSet PATHSAFE     = new BitSet(256);
-    /** Characters which are safe to use in a query or a fragment,
-     * i.e. {@link #RESERVED} plus {@link #UNRESERVED} */
-    private static final BitSet URIC     = new BitSet(256);
-
-    /**
-     * Reserved characters, i.e. {@code ;/?:@&=+$,[]}
-     * <p>
-     *  This list is the same as the {@code reserved} list in
-     *  <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>
-     *  as augmented by
-     *  <a href="http://www.ietf.org/rfc/rfc2732.txt">RFC 2732</a>
-     */
-    private static final BitSet RESERVED     = new BitSet(256);
-
-
-    /**
-     * Safe characters for x-www-form-urlencoded data, as per java.net.URLEncoder and browser behaviour,
-     * i.e. alphanumeric plus {@code "-", "_", ".", "*"}
-     */
     private static final BitSet URLENCODER   = new BitSet(256);
 
-    private static final BitSet PATH_SPECIAL = new BitSet(256);
-
     static {
         // unreserved chars
         // alpha characters
         for (int i = 'a'; i <= 'z'; i++) {
-            UNRESERVED.set(i);
+            URLENCODER.set(i);
         }
         for (int i = 'A'; i <= 'Z'; i++) {
-            UNRESERVED.set(i);
+            URLENCODER.set(i);
         }
         // numeric characters
         for (int i = '0'; i <= '9'; i++) {
-            UNRESERVED.set(i);
-        }
-        UNRESERVED.set('_'); // these are the charactes of the "mark" list
-        UNRESERVED.set('-');
-        UNRESERVED.set('.');
-        UNRESERVED.set('*');
-        URLENCODER.or(UNRESERVED); // skip remaining unreserved characters
-        UNRESERVED.set('!');
-        UNRESERVED.set('~');
-        UNRESERVED.set('\'');
-        UNRESERVED.set('(');
-        UNRESERVED.set(')');
-        // punct chars
-        PUNCT.set(',');
-        PUNCT.set(';');
-        PUNCT.set(':');
-        PUNCT.set('$');
-        PUNCT.set('&');
-        PUNCT.set('+');
-        PUNCT.set('=');
-        // Safe for userinfo
-        USERINFO.or(UNRESERVED);
-        USERINFO.or(PUNCT);
-
-        // URL path safe
-        PATHSAFE.or(UNRESERVED);
-        PATHSAFE.set(';'); // param separator
-        PATHSAFE.set(':'); // RFC 2396
-        PATHSAFE.set('@');
-        PATHSAFE.set('&');
-        PATHSAFE.set('=');
-        PATHSAFE.set('+');
-        PATHSAFE.set('$');
-        PATHSAFE.set(',');
-
-        PATH_SPECIAL.or(PATHSAFE);
-        PATH_SPECIAL.set('/');
-
-        RESERVED.set(';');
-        RESERVED.set('/');
-        RESERVED.set('?');
-        RESERVED.set(':');
-        RESERVED.set('@');
-        RESERVED.set('&');
-        RESERVED.set('=');
-        RESERVED.set('+');
-        RESERVED.set('$');
-        RESERVED.set(',');
-        RESERVED.set('['); // added by RFC 2732
-        RESERVED.set(']'); // added by RFC 2732
-
-        URIC.or(RESERVED);
-        URIC.or(UNRESERVED);
-    }
-
-    private static final int RADIX = 16;
-
-    private static List<NameValuePair> createEmptyList() {
-        return new ArrayList<>(0);
-    }
-
-    private static void urlEncode(
-            final StringBuilder buf,
-            final String content,
-            final Charset charset,
-            final BitSet safechars) {
-        if (content == null) {
-            return;
-        }
-        final ByteBuffer bb = charset.encode(content);
-        while (bb.hasRemaining()) {
-            final int b = bb.get() & 0xff;
-            if (safechars.get(b)) {
-                buf.append((char) b);
-            } else {
-                buf.append("%");
-                final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
-                final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
-                buf.append(hex1);
-                buf.append(hex2);
-            }
-        }
-    }
-
-    private static String urlDecode(
-            final String content,
-            final Charset charset,
-            final boolean plusAsBlank) {
-        if (content == null) {
-            return null;
-        }
-        final ByteBuffer bb = ByteBuffer.allocate(content.length());
-        final CharBuffer cb = CharBuffer.wrap(content);
-        while (cb.hasRemaining()) {
-            final char c = cb.get();
-            if (c == '%' && cb.remaining() >= 2) {
-                final char uc = cb.get();
-                final char lc = cb.get();
-                final int u = Character.digit(uc, 16);
-                final int l = Character.digit(lc, 16);
-                if (u != -1 && l != -1) {
-                    bb.put((byte) ((u << 4) + l));
-                } else {
-                    bb.put((byte) '%');
-                    bb.put((byte) uc);
-                    bb.put((byte) lc);
-                }
-            } else if (plusAsBlank && c == '+') {
-                bb.put((byte) ' ');
-            } else {
-                bb.put((byte) c);
-            }
-        }
-        bb.flip();
-        return charset.decode(bb).toString();
-    }
-
-    static String decodeFormFields(final String content, final Charset charset) {
-        if (content == null) {
-            return null;
+            URLENCODER.set(i);
         }
-        return urlDecode(content, charset != null ? charset : StandardCharsets.UTF_8, true);
-    }
-
-    static void encodeFormFields(final StringBuilder buf, final String content, final Charset charset) {
-        if (content == null) {
-            return;
-        }
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, URLENCODER);
-    }
-
-    static String encodeFormFields(final String content, final Charset charset) {
-        if (content == null) {
-            return null;
-        }
-        final StringBuilder buf = new StringBuilder();
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, URLENCODER);
-        return buf.toString();
-    }
-
-    static void encUserInfo(final StringBuilder buf, final String content, final Charset charset) {
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, USERINFO);
-    }
-
-    static void encUric(final StringBuilder buf, final String content, final Charset charset) {
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, URIC);
+        URLENCODER.set('_'); // these are the characters of the "mark" list
+        URLENCODER.set('-');
+        URLENCODER.set('.');
+        URLENCODER.set('*');
     }
 
 }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/WWWFormCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/WWWFormCodec.java
new file mode 100644
index 0000000..900918e
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/WWWFormCodec.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * 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.core5.net;
+
+import java.nio.charset.Charset;
+import java.util.List;
+
+import org.apache.hc.core5.http.NameValuePair;
+
+/**
+ * {@code application/x-www-form-urlencoded} codec.
+ *
+ * @since 5.1
+ */
+public class WWWFormCodec {
+
+    private static final char QP_SEP_A = '&';
+
+    /**
+     * Returns a list of {@link NameValuePair} parameters parsed
+     * from the {@code application/x-www-form-urlencoded} content.
+     *
+     * @param s input text.
+     * @param charset parameter charset.
+     * @return list of form parameters.
+     */
+    public static List<NameValuePair> parse(final CharSequence s, final Charset charset) {
+        return URIBuilder.parseQuery(s, charset, true);
+    }
+
+    /**
+     * Formats the list of {@link NameValuePair} parameters into a {@code application/x-www-form-urlencoded}
+     * content.
+     *
+     * @param buf the content buffer
+     * @param params  The from parameters.
+     * @param charset The encoding to use.
+     */
+    public static void format(
+            final StringBuilder buf, final Iterable<? extends NameValuePair> params, final Charset charset) {
+        URIBuilder.formatQuery(buf, params, charset, true);
+    }
+
+    /**
+     * Formats the list of {@link NameValuePair} parameters into a {@code application/x-www-form-urlencoded}
+     * content string.
+     *
+     * @param params  The from parameters.
+     * @param charset The encoding to use.
+     * @return content string
+     */
+    public static String format(final Iterable<? extends NameValuePair> params, final Charset charset) {
+        final StringBuilder buf = new StringBuilder();
+        URIBuilder.formatQuery(buf, params, charset, true);
+        return buf.toString();
+    }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/NameValuePairListMatcher.java b/httpcore5/src/test/java/org/apache/hc/core5/http/NameValuePairListMatcher.java
new file mode 100644
index 0000000..7ecf449
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/NameValuePairListMatcher.java
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * 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.core5.http;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hc.core5.util.LangUtils;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+
+public class NameValuePairListMatcher extends BaseMatcher<List<NameValuePair>> {
+
+    private List<? extends NameValuePair> nvps;
+
+    public NameValuePairListMatcher(final List<? extends NameValuePair> nvps) {
+        this.nvps = nvps;
+    }
+
+    @Override
+    public boolean matches(final Object item) {
+        if (item instanceof List<?>) {
+            final List<?> objects = (List<?>) item;
+            if (objects.size() != nvps.size()) {
+                return false;
+            }
+            for (int i = 1; i < objects.size(); i++) {
+                final Object obj = objects.get(i);
+                if (obj instanceof NameValuePair) {
+                    final NameValuePair nvp = (NameValuePair) obj;
+                    final NameValuePair expected = nvps.get(i);
+                    if (!LangUtils.equals(nvp.getName(), expected.getName())
+                            || !LangUtils.equals(nvp.getValue(), expected.getValue())) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void describeTo(final Description description) {
+        description.appendText("equals ").appendValueList("[", ";", "]", nvps);
+    }
+
+    @Factory
+    public static Matcher<List<NameValuePair>> equalsTo(final NameValuePair... nvps) {
+        return new NameValuePairListMatcher(Arrays.asList(nvps));
+    }
+
+    @Factory
+    public static Matcher<List<NameValuePair>> isEmpty() {
+        return new NameValuePairListMatcher(Collections.<NameValuePair>emptyList());
+    }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java
index d50131c..f83ac7f 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java
@@ -42,7 +42,7 @@ import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.message.BasicNameValuePair;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -228,7 +228,7 @@ public class TestEntityUtils {
         parameters.add(new BasicNameValuePair("russian", ru_hello));
         parameters.add(new BasicNameValuePair("swiss", ch_hello));
 
-        final String s = URLEncodedUtils.format(parameters, StandardCharsets.UTF_8);
+        final String s = WWWFormCodec.format(parameters, StandardCharsets.UTF_8);
 
         Assert.assertEquals("russian=%D0%92%D1%81%D0%B5%D0%BC_%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82" +
                 "&swiss=Gr%C3%BCezi_z%C3%A4m%C3%A4", s);
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
new file mode 100644
index 0000000..a19b617
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * 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.core5.net;
+
+import java.nio.charset.StandardCharsets;
+
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link PercentCodec}.
+ */
+public class TestPercentCodec {
+
+    @Test
+    public void testCoding() {
+        final StringBuilder buf = new StringBuilder();
+        PercentCodec.encode(buf, "blah!", StandardCharsets.UTF_8);
+        PercentCodec.encode(buf, " ~ ", StandardCharsets.UTF_8);
+        PercentCodec.encode(buf, "huh?", StandardCharsets.UTF_8);
+        MatcherAssert.assertThat(buf.toString(), CoreMatchers.equalTo("blah%21%20~%20huh%3F"));
+    }
+
+    @Test
+    public void testDecoding() {
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20~%20huh%3F", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! ~ huh?"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21+~%20huh%3F", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah!+~ huh?"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21+~%20huh%3F", StandardCharsets.UTF_8, true),
+                CoreMatchers.equalTo("blah! ~ huh?"));
+    }
+
+    @Test
+    public void testDecodingPartialContent() {
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20%", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! %"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20%a", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! %a"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20%wa", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! %wa"));
+    }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
index f86d345..ea153cf 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
@@ -32,17 +32,138 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.NameValuePairListMatcher;
 import org.apache.hc.core5.http.message.BasicNameValuePair;
 import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.Assert;
 import org.junit.Test;
 
 public class TestURIBuilder {
 
+    private static final String CH_HELLO = "\u0047\u0072\u00FC\u0065\u007A\u0069\u005F\u007A\u00E4\u006D\u00E4";
+    private static final String RU_HELLO = "\u0412\u0441\u0435\u043C\u005F\u043F\u0440\u0438\u0432\u0435\u0442";
+
+    static List<String> parsePath(final CharSequence s) {
+        return URIBuilder.parsePath(s, null);
+    }
+
+    @Test
+    public void testParseSegments() throws Exception {
+        MatcherAssert.assertThat(parsePath("/this/that"), CoreMatchers.equalTo(Arrays.asList("this", "that")));
+        MatcherAssert.assertThat(parsePath("this/that"), CoreMatchers.equalTo(Arrays.asList("this", "that")));
+        MatcherAssert.assertThat(parsePath("this//that"), CoreMatchers.equalTo(Arrays.asList("this", "", "that")));
+        MatcherAssert.assertThat(parsePath("this//that/"), CoreMatchers.equalTo(Arrays.asList("this", "", "that", "")));
+        MatcherAssert.assertThat(parsePath("this//that/%2fthis%20and%20that"),
+                CoreMatchers.equalTo(Arrays.asList("this", "", "that", "/this and that")));
+        MatcherAssert.assertThat(parsePath("this///that//"),
+                CoreMatchers.equalTo(Arrays.asList("this", "", "", "that", "", "")));
+        MatcherAssert.assertThat(parsePath("/"), CoreMatchers.equalTo(Collections.singletonList("")));
+        MatcherAssert.assertThat(parsePath(""), CoreMatchers.equalTo(Collections.<String>emptyList()));
+    }
+
+    static String formatPath(final String... pathSegments) {
+        final StringBuilder buf = new StringBuilder();
+        URIBuilder.formatPath(buf, Arrays.asList(pathSegments), false, null);
+        return buf.toString();
+    }
+
+    @Test
+    public void testFormatSegments() throws Exception {
+        MatcherAssert.assertThat(formatPath("this", "that"), CoreMatchers.equalTo("/this/that"));
+        MatcherAssert.assertThat(formatPath("this", "", "that"), CoreMatchers.equalTo("/this//that"));
+        MatcherAssert.assertThat(formatPath("this", "", "that", "/this and that"),
+                CoreMatchers.equalTo("/this//that/%2Fthis%20and%20that"));
+        MatcherAssert.assertThat(formatPath("this", "", "", "that", "", ""),
+                CoreMatchers.equalTo("/this///that//"));
+        MatcherAssert.assertThat(formatPath(""), CoreMatchers.equalTo("/"));
+        MatcherAssert.assertThat(formatPath(), CoreMatchers.equalTo(""));
+    }
+
+    static List<NameValuePair> parseQuery(final CharSequence s) {
+        return URIBuilder.parseQuery(s, null, false);
+    }
+
+    @Test
+    public void testParseQuery() throws Exception {
+        MatcherAssert.assertThat(parseQuery(""), NameValuePairListMatcher.isEmpty());
+        MatcherAssert.assertThat(parseQuery("Name0"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name0", null)));
+        MatcherAssert.assertThat(parseQuery("Name1=Value1"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name1", "Value1")));
+        MatcherAssert.assertThat(parseQuery("Name2="),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name2", "")));
+        MatcherAssert.assertThat(parseQuery(" Name3  "),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name3", null)));
+        MatcherAssert.assertThat(parseQuery("Name4=Value%204%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4!")));
+        MatcherAssert.assertThat(parseQuery("Name4=Value%2B4%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value+4!")));
+        MatcherAssert.assertThat(parseQuery("Name4=Value%204%21%20%214"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4! !4")));
+        MatcherAssert.assertThat(parseQuery("Name5=aaa&Name6=bbb"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name5", "aaa"),
+                        new BasicNameValuePair("Name6", "bbb")));
+        MatcherAssert.assertThat(parseQuery("Name7=aaa&Name7=b%2Cb&Name7=ccc"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name7", "aaa"),
+                        new BasicNameValuePair("Name7", "b,b"),
+                        new BasicNameValuePair("Name7", "ccc")));
+        MatcherAssert.assertThat(parseQuery("Name8=xx%2C%20%20yy%20%20%2Czz"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name8", "xx,  yy  ,zz")));
+        MatcherAssert.assertThat(parseQuery("price=10%20%E2%82%AC"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("price", "10 \u20AC")));
+        MatcherAssert.assertThat(parseQuery("a=b\"c&d=e"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("a", "b\"c"),
+                        new BasicNameValuePair("d", "e")));
+        MatcherAssert.assertThat(parseQuery("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("russian", RU_HELLO),
+                        new BasicNameValuePair("swiss", CH_HELLO)));
+    }
+
+    static String formatQuery(final NameValuePair... params) {
+        final StringBuilder buf = new StringBuilder();
+        URIBuilder.formatQuery(buf, Arrays.asList(params), null, false);
+        return buf.toString();
+    }
+
+    @Test
+    public void testFormatQuery() throws Exception {
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name0", null)), CoreMatchers.equalTo("Name0"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name1", "Value1")), CoreMatchers.equalTo("Name1=Value1"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name2", "")), CoreMatchers.equalTo("Name2="));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name4", "Value 4&")),
+                CoreMatchers.equalTo("Name4=Value%204%26"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name4", "Value+4&")),
+                CoreMatchers.equalTo("Name4=Value%2B4%26"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name4", "Value 4& =4")),
+                CoreMatchers.equalTo("Name4=Value%204%26%20%3D4"));
+        MatcherAssert.assertThat(formatQuery(
+                new BasicNameValuePair("Name5", "aaa"),
+                new BasicNameValuePair("Name6", "bbb")), CoreMatchers.equalTo("Name5=aaa&Name6=bbb"));
+        MatcherAssert.assertThat(formatQuery(
+                new BasicNameValuePair("Name7", "aaa"),
+                new BasicNameValuePair("Name7", "b,b"),
+                new BasicNameValuePair("Name7", "ccc")
+        ), CoreMatchers.equalTo("Name7=aaa&Name7=b%2Cb&Name7=ccc"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name8", "xx,  yy  ,zz")),
+                CoreMatchers.equalTo("Name8=xx%2C%20%20yy%20%20%2Czz"));
+        MatcherAssert.assertThat(formatQuery(
+                new BasicNameValuePair("russian", RU_HELLO),
+                new BasicNameValuePair("swiss", CH_HELLO)),
+                CoreMatchers.equalTo("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)));
+    }
+
     @Test
     public void testHierarchicalUri() throws Exception {
         final URI uri = new URI("http", "stuff", "localhost", 80, "/some stuff", "param=stuff", "fragment");
@@ -169,14 +290,6 @@ public class TestURIBuilder {
     }
 
     @Test
-    public void testSetUserInfo() throws Exception {
-        final URI uri = new URI("http", null, "localhost", 80, "/", "param=stuff", null);
-        final URIBuilder uribuilder = new URIBuilder(uri).setUserInfo("user", "password");
-        final URI result = uribuilder.build();
-        Assert.assertEquals(new URI("http://user:password@localhost:80/?param=stuff"), result);
-    }
-
-    @Test
     public void testRemoveParameters() throws Exception {
         final URI uri = new URI("http", null, "localhost", 80, "/", "param=stuff", null);
         final URIBuilder uribuilder = new URIBuilder(uri).removeQuery();
@@ -290,35 +403,6 @@ public class TestURIBuilder {
     }
 
     @Test
-    public void testAgainstURIEncoded() throws Exception {
-        // Check that the encoded URI generated by URI builder agrees with that generated by using URI directly
-        final String scheme="https";
-        final String host="localhost";
-        final String specials="/ abcd!$&*()_-+.,=:;'~<>/@[]|#^%\"{}\\`xyz"; // N.B. excludes \u00a3\u00ac\u00a6
-        final URI uri = new URI(scheme, specials, host, 80, specials, specials, specials);
-
-        final URI bld = new URIBuilder()
-                .setScheme(scheme)
-                .setHost(host)
-                .setUserInfo(specials)
-                .setPath(specials)
-                .setCustomQuery(specials)
-                .setFragment(specials)
-                .build();
-
-        Assert.assertEquals(uri.getHost(), bld.getHost());
-
-        Assert.assertEquals(uri.getRawUserInfo(), bld.getRawUserInfo());
-
-        Assert.assertEquals(uri.getRawPath(), bld.getRawPath());
-
-        Assert.assertEquals(uri.getRawQuery(), bld.getRawQuery());
-
-        Assert.assertEquals(uri.getRawFragment(), bld.getRawFragment());
-
-    }
-
-    @Test
     public void testBuildAddParametersUTF8() throws Exception {
         assertAddParameters(StandardCharsets.UTF_8);
     }
@@ -355,8 +439,8 @@ public class TestURIBuilder {
     }
 
     public void assertBuild(final Charset charset, final URI uri) throws Exception {
-        final String encodedData1 = URLEncodedUtils.encodeFormFields("\"1\u00aa position\"", charset);
-        final String encodedData2 = URLEncodedUtils.encodeFormFields("Jos\u00e9 Abra\u00e3o", charset);
+        final String encodedData1 = PercentCodec.encode("\"1\u00aa position\"", charset);
+        final String encodedData2 = PercentCodec.encode("Jos\u00e9 Abra\u00e3o", charset);
 
         final String uriExpected = String.format("https://somehost.com/stuff?parameter1=value1&parameter2=%s&parameter3=%s", encodedData1, encodedData2);
 
@@ -392,7 +476,7 @@ public class TestURIBuilder {
 
     @Test
     public void testTolerateNullInput() throws Exception {
-        Assert.assertThat(new URIBuilder()
+        MatcherAssert.assertThat(new URIBuilder()
                         .setScheme(null)
                         .setHost("localhost")
                         .setUserInfo(null)
@@ -406,7 +490,7 @@ public class TestURIBuilder {
 
     @Test
     public void testTolerateBlankInput() throws Exception {
-        Assert.assertThat(new URIBuilder()
+        MatcherAssert.assertThat(new URIBuilder()
                         .setScheme("")
                         .setHost("localhost")
                         .setUserInfo("")
@@ -424,8 +508,7 @@ public class TestURIBuilder {
         final HttpHost httpHost = new HttpHost("http", "example.com", 1234);
         final URIBuilder uribuilder = new URIBuilder();
         uribuilder.setHttpHost(httpHost);
-        final URI result = uribuilder.build();
-        Assert.assertEquals(URI.create(httpHost.toURI()), result);
+        Assert.assertEquals(URI.create("http://example.com:1234"), uribuilder.build());
     }
 
     @Test
@@ -435,23 +518,50 @@ public class TestURIBuilder {
                 .setHost("somehost")
                 .setPath("//blah//blah")
                 .build();
-        Assert.assertThat(uri, CoreMatchers.equalTo(URI.create("ftp://somehost//blah//blah")));
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("ftp://somehost//blah//blah")));
     }
 
     @Test
-    public void testPathNoLeadingSlash() throws Exception {
+    public void testNoAuthorityAndPath() throws Exception {
         final URI uri = new URIBuilder()
-                .setScheme("ftp")
+                .setScheme("file")
+                .setPath("/blah")
+                .build();
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:/blah")));
+    }
+
+    @Test
+    public void testNoAuthorityAndPathSegments() throws Exception {
+        final URI uri = new URIBuilder()
+                .setScheme("file")
+                .setPathSegments("this", "that")
+                .build();
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:/this/that")));
+    }
+
+    @Test
+    public void testNoAuthorityAndRootlessPath() throws Exception {
+        final URI uri = new URIBuilder()
+                .setScheme("file")
                 .setPath("blah")
                 .build();
-        Assert.assertThat(uri, CoreMatchers.equalTo(URI.create("ftp:/blah")));
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:blah")));
+    }
+
+    @Test
+    public void testNoAuthorityAndRootlessPathSegments() throws Exception {
+        final URI uri = new URIBuilder()
+                .setScheme("file")
+                .setPathSegmentsRootless("this", "that")
+                .build();
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:this/that")));
     }
 
     @Test
     public void testOpaque() throws Exception {
         final URIBuilder uriBuilder = new URIBuilder("http://host.com");
         final URI uri = uriBuilder.build();
-        Assert.assertThat(uriBuilder.isOpaque(), CoreMatchers.equalTo(uri.isOpaque()));
+        MatcherAssert.assertThat(uriBuilder.isOpaque(), CoreMatchers.equalTo(uri.isOpaque()));
     }
 
     @Test
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURLEncodedUtils.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURLEncodedUtils.java
deleted file mode 100644
index e7687a4..0000000
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURLEncodedUtils.java
+++ /dev/null
@@ -1,394 +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.core5.net;
-
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.hc.core5.http.NameValuePair;
-import org.apache.hc.core5.http.message.BasicNameValuePair;
-import org.hamcrest.CoreMatchers;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class TestURLEncodedUtils {
-
-    @Test
-    public void testParseURLCodedContent() throws Exception {
-        List <NameValuePair> result;
-
-        result = parse("");
-        Assert.assertTrue(result.isEmpty());
-
-        result = parse("Name0");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name0", null);
-
-        result = parse("Name1=Value1");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name1", "Value1");
-
-        result = parse("Name2=");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name2", "");
-
-        result = parse("Name3");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name3", null);
-
-        result = parse("Name4=Value%204%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4!");
-
-        result = parse("Name4=Value%2B4%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value+4!");
-
-        result = parse("Name4=Value%204%21%20%214");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4! !4");
-
-        result = parse("Name5=aaa&Name6=bbb");
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "Name5", "aaa");
-        assertNameValuePair(result.get(1), "Name6", "bbb");
-
-        result = parse("Name7=aaa&Name7=b%2Cb&Name7=ccc");
-        Assert.assertEquals(3, result.size());
-        assertNameValuePair(result.get(0), "Name7", "aaa");
-        assertNameValuePair(result.get(1), "Name7", "b,b");
-        assertNameValuePair(result.get(2), "Name7", "ccc");
-
-        result = parse("Name8=xx%2C%20%20yy%20%20%2Czz");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name8", "xx,  yy  ,zz");
-
-        result = parse("price=10%20%E2%82%AC");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "price", "10 \u20AC");
-    }
-
-    @Test
-    public void testParseSegments() throws Exception {
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("/this/that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this/that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this//that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this//that/"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "that", "")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this//that/%2fthis%20and%20that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "that", "/this and that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this///that//"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "", "that", "", "")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("/"),
-                CoreMatchers.equalTo(Collections.singletonList("")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments(""),
-                CoreMatchers.equalTo(Collections.<String>emptyList()));
-    }
-
-    @Test
-    public void testFormatSegments() throws Exception {
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "that"),
-                CoreMatchers.equalTo("/this/that"));
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "", "that"),
-                CoreMatchers.equalTo("/this//that"));
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "", "that", "/this and that"),
-                CoreMatchers.equalTo("/this//that/%2Fthis%20and%20that"));
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "", "", "that", "", ""),
-                CoreMatchers.equalTo("/this///that//"));
-        Assert.assertThat(URLEncodedUtils.formatSegments(""),
-                CoreMatchers.equalTo("/"));
-        Assert.assertThat(URLEncodedUtils.formatSegments(),
-                CoreMatchers.equalTo(""));
-    }
-
-    @Test
-    public void testParseURLCodedContentString() throws Exception {
-        List <NameValuePair> result;
-
-        result = parseString("");
-        Assert.assertTrue(result.isEmpty());
-
-        result = parseString("Name0");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name0", null);
-
-        result = parseString("Name1=Value1");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name1", "Value1");
-
-        result = parseString("Name2=");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name2", "");
-
-        result = parseString("Name3");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name3", null);
-
-        result = parseString("Name4=Value%204%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4!");
-
-        result = parseString("Name4=Value%2B4%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value+4!");
-
-        result = parseString("Name4=Value%204%21%20%214");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4! !4");
-
-        result = parseString("Name5=aaa&Name6=bbb");
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "Name5", "aaa");
-        assertNameValuePair(result.get(1), "Name6", "bbb");
-
-        result = parseString("Name7=aaa&Name7=b%2Cb&Name7=ccc");
-        Assert.assertEquals(3, result.size());
-        assertNameValuePair(result.get(0), "Name7", "aaa");
-        assertNameValuePair(result.get(1), "Name7", "b,b");
-        assertNameValuePair(result.get(2), "Name7", "ccc");
-
-        result = parseString("Name8=xx%2C%20%20yy%20%20%2Czz");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name8", "xx,  yy  ,zz");
-
-        result = parseString("price=10%20%E2%82%AC");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "price", "10 \u20AC");
-
-        result = parse("a=b\"c&d=e");
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "a", "b\"c");
-        assertNameValuePair(result.get(1), "d", "e");
-    }
-
-    @Test
-    public void testParseInvalidURLCodedContent() throws Exception {
-        List <NameValuePair> result;
-
-        result = parse("name=%");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "name", "%");
-
-        result = parse("name=%a");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "name", "%a");
-
-        result = parse("name=%wa%20");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "name", "%wa ");
-    }
-
-    private static final int SWISS_GERMAN_HELLO [] = {
-        0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4
-    };
-
-    private static final int RUSSIAN_HELLO [] = {
-        0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438,
-        0x432, 0x435, 0x442
-    };
-
-    private static String constructString(final int [] unicodeChars) {
-        final StringBuilder buffer = new StringBuilder();
-        if (unicodeChars != null) {
-            for (final int unicodeChar : unicodeChars) {
-                buffer.append((char)unicodeChar);
-            }
-        }
-        return buffer.toString();
-    }
-
-    @Test
-    public void testParseUTF8Ampersand1String() throws Exception {
-        final String ru_hello = constructString(RUSSIAN_HELLO);
-        final String ch_hello = constructString(SWISS_GERMAN_HELLO);
-        final List <NameValuePair> parameters = new ArrayList<>();
-        parameters.add(new BasicNameValuePair("russian", ru_hello));
-        parameters.add(new BasicNameValuePair("swiss", ch_hello));
-
-        final String s = URLEncodedUtils.format(parameters, StandardCharsets.UTF_8);
-
-        final List <NameValuePair> result = URLEncodedUtils.parse(s, StandardCharsets.UTF_8);
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "russian", ru_hello);
-        assertNameValuePair(result.get(1), "swiss", ch_hello);
-    }
-
-    @Test
-    public void testParseUTF8Ampersand2String() throws Exception {
-        testParseUTF8String('&');
-    }
-
-    @Test
-    public void testParseUTF8SemicolonString() throws Exception {
-        testParseUTF8String(';');
-    }
-
-    private void testParseUTF8String(final char parameterSeparator) throws Exception {
-        final String ru_hello = constructString(RUSSIAN_HELLO);
-        final String ch_hello = constructString(SWISS_GERMAN_HELLO);
-        final List <NameValuePair> parameters = new ArrayList<>();
-        parameters.add(new BasicNameValuePair("russian", ru_hello));
-        parameters.add(new BasicNameValuePair("swiss", ch_hello));
-
-        final String s = URLEncodedUtils.format(parameters, parameterSeparator, StandardCharsets.UTF_8);
-
-        final List<NameValuePair> result1 = URLEncodedUtils.parse(s, StandardCharsets.UTF_8);
-        Assert.assertEquals(2, result1.size());
-        assertNameValuePair(result1.get(0), "russian", ru_hello);
-        assertNameValuePair(result1.get(1), "swiss", ch_hello);
-
-        final List<NameValuePair> result2 = URLEncodedUtils.parse(s, StandardCharsets.UTF_8, parameterSeparator);
-        Assert.assertEquals(2, result2.size());
-        assertNameValuePair(result2.get(0), "russian", ru_hello);
-        assertNameValuePair(result2.get(1), "swiss", ch_hello);
-    }
-
-    @Test
-    public void testEmptyQuery() throws Exception {
-        final List<NameValuePair> result = URLEncodedUtils.parse("", StandardCharsets.UTF_8);
-        Assert.assertEquals(0, result.size());
-        // [HTTPCLIENT-1889]:
-        result.add(new BasicNameValuePair("key", "value"));
-    }
-
-    @Test
-    public void testFormat() throws Exception {
-        final List <NameValuePair> params = new ArrayList<>();
-        Assert.assertEquals(0, URLEncodedUtils.format(params, StandardCharsets.US_ASCII).length());
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name0", null));
-        Assert.assertEquals("Name0", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name1", "Value1"));
-        Assert.assertEquals("Name1=Value1", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name2", ""));
-        Assert.assertEquals("Name2=", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4&"));
-        Assert.assertEquals("Name4=Value%204%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value+4&"));
-        Assert.assertEquals("Name4=Value%2B4%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4& =4"));
-        Assert.assertEquals("Name4=Value%204%26%20%3D4", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name5", "aaa"));
-        params.add(new BasicNameValuePair("Name6", "bbb"));
-        Assert.assertEquals("Name5=aaa&Name6=bbb", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name7", "aaa"));
-        params.add(new BasicNameValuePair("Name7", "b,b"));
-        params.add(new BasicNameValuePair("Name7", "ccc"));
-        Assert.assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-        Assert.assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", URLEncodedUtils.format(params, '&', StandardCharsets.US_ASCII));
-        Assert.assertEquals("Name7=aaa;Name7=b%2Cb;Name7=ccc", URLEncodedUtils.format(params, ';', StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name8", "xx,  yy  ,zz"));
-        Assert.assertEquals("Name8=xx%2C%20%20yy%20%20%2Czz", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-    }
-
-    @Test
-    public void testFormatString() throws Exception { // as above, using String
-        final List <NameValuePair> params = new ArrayList<>();
-        Assert.assertEquals(0, URLEncodedUtils.format(params, StandardCharsets.US_ASCII).length());
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name0", null));
-        Assert.assertEquals("Name0", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name1", "Value1"));
-        Assert.assertEquals("Name1=Value1", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name2", ""));
-        Assert.assertEquals("Name2=", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4&"));
-        Assert.assertEquals("Name4=Value%204%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value+4&"));
-        Assert.assertEquals("Name4=Value%2B4%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4& =4"));
-        Assert.assertEquals("Name4=Value%204%26%20%3D4", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name5", "aaa"));
-        params.add(new BasicNameValuePair("Name6", "bbb"));
-        Assert.assertEquals("Name5=aaa&Name6=bbb", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name7", "aaa"));
-        params.add(new BasicNameValuePair("Name7", "b,b"));
-        params.add(new BasicNameValuePair("Name7", "ccc"));
-        Assert.assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name8", "xx,  yy  ,zz"));
-        Assert.assertEquals("Name8=xx%2C%20%20yy%20%20%2Czz", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-    }
-
-    private List <NameValuePair> parse (final String params) {
-        return URLEncodedUtils.parse(params, StandardCharsets.UTF_8);
-    }
-
-    private List <NameValuePair> parseString (final String uri) throws Exception {
-        return URLEncodedUtils.parse(new URI("?"+uri), StandardCharsets.UTF_8);
-    }
-
-    private static void assertNameValuePair (
-            final NameValuePair parameter,
-            final String expectedName,
-            final String expectedValue) {
-        Assert.assertEquals(parameter.getName(), expectedName);
-        Assert.assertEquals(parameter.getValue(), expectedValue);
-    }
-
-}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestWWWFormCodec.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestWWWFormCodec.java
new file mode 100644
index 0000000..88d4362
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestWWWFormCodec.java
@@ -0,0 +1,123 @@
+/*
+ * ====================================================================
+ * 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.core5.net;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.NameValuePairListMatcher;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+public class TestWWWFormCodec {
+
+    private static final String CH_HELLO = "\u0047\u0072\u00FC\u0065\u007A\u0069\u005F\u007A\u00E4\u006D\u00E4";
+    private static final String RU_HELLO = "\u0412\u0441\u0435\u043C\u005F\u043F\u0440\u0438\u0432\u0435\u0442";
+
+    private static List<NameValuePair> parse(final String params) {
+        return WWWFormCodec.parse(params, StandardCharsets.UTF_8);
+    }
+
+    @Test
+    public void testParse() throws Exception {
+        MatcherAssert.assertThat(parse(""), NameValuePairListMatcher.isEmpty());
+        MatcherAssert.assertThat(parse("Name0"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name0", null)));
+        MatcherAssert.assertThat(parse("Name1=Value1"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name1", "Value1")));
+        MatcherAssert.assertThat(parse("Name2="),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name2", "")));
+        MatcherAssert.assertThat(parse(" Name3  "),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name3", null)));
+        MatcherAssert.assertThat(parse("Name4=Value%204%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4!")));
+        MatcherAssert.assertThat(parse("Name4=Value%2B4%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value+4!")));
+        MatcherAssert.assertThat(parse("Name4=Value%204%21%20%214"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4! !4")));
+        MatcherAssert.assertThat(parse("Name5=aaa&Name6=bbb"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name5", "aaa"),
+                        new BasicNameValuePair("Name6", "bbb")));
+        MatcherAssert.assertThat(parse("Name7=aaa&Name7=b%2Cb&Name7=ccc"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name7", "aaa"),
+                        new BasicNameValuePair("Name7", "b,b"),
+                        new BasicNameValuePair("Name7", "ccc")));
+        MatcherAssert.assertThat(parse("Name8=xx%2C%20%20yy%20%20%2Czz"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name8", "xx,  yy  ,zz")));
+        MatcherAssert.assertThat(parse("price=10%20%E2%82%AC"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("price", "10 \u20AC")));
+        MatcherAssert.assertThat(parse("a=b\"c&d=e"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("a", "b\"c"),
+                        new BasicNameValuePair("d", "e")));
+        MatcherAssert.assertThat(parse("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("russian", RU_HELLO),
+                        new BasicNameValuePair("swiss", CH_HELLO)));
+    }
+
+    private static String format(final NameValuePair... nvps) {
+        return WWWFormCodec.format(Arrays.asList(nvps), StandardCharsets.UTF_8);
+    }
+
+    @Test
+    public void testFormat() throws Exception {
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name0", null)), CoreMatchers.equalTo("Name0"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name1", "Value1")), CoreMatchers.equalTo("Name1=Value1"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name2", "")), CoreMatchers.equalTo("Name2="));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name4", "Value 4&")),
+                CoreMatchers.equalTo("Name4=Value+4%26"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name4", "Value+4&")),
+                CoreMatchers.equalTo("Name4=Value%2B4%26"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name4", "Value 4& =4")),
+                CoreMatchers.equalTo("Name4=Value+4%26+%3D4"));
+        MatcherAssert.assertThat(format(
+                new BasicNameValuePair("Name5", "aaa"),
+                new BasicNameValuePair("Name6", "bbb")), CoreMatchers.equalTo("Name5=aaa&Name6=bbb"));
+        MatcherAssert.assertThat(format(
+                new BasicNameValuePair("Name7", "aaa"),
+                new BasicNameValuePair("Name7", "b,b"),
+                new BasicNameValuePair("Name7", "ccc")
+        ), CoreMatchers.equalTo("Name7=aaa&Name7=b%2Cb&Name7=ccc"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name8", "xx,  yy  ,zz")),
+                CoreMatchers.equalTo("Name8=xx%2C++yy++%2Czz"));
+        MatcherAssert.assertThat(format(
+                new BasicNameValuePair("russian", RU_HELLO),
+                new BasicNameValuePair("swiss", CH_HELLO)),
+                CoreMatchers.equalTo("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)));
+    }
+
+}


[httpcomponents-core] 03/05: RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch 5.1.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit e9d617a8b53cc969a2f719213600a32344b5d7bf
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sat Jul 25 12:47:27 2020 +0200

    RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty
---
 .../main/java/org/apache/hc/core5/net/URIAuthority.java    | 10 +---------
 .../src/main/java/org/apache/hc/core5/net/URIBuilder.java  | 14 ++++++++++++--
 .../java/org/apache/hc/core5/net/TestURIAuthority.java     | 14 ++++----------
 .../test/java/org/apache/hc/core5/net/TestURIBuilder.java  | 14 ++++++++++++++
 4 files changed, 31 insertions(+), 21 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
index 1cae817..920c426 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
@@ -34,7 +34,6 @@ import java.util.Locale;
 
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
-import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.LangUtils;
 import org.apache.hc.core5.util.TextUtils;
 import org.apache.hc.core5.util.Tokenizer;
@@ -104,9 +103,6 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
         } else {
             hostName = token;
         }
-        if (TextUtils.isBlank(hostName)) {
-            throw createException(s, cursor, "Authority host is empty");
-        }
         final int port;
         if (!TextUtils.isBlank(portText)) {
             try {
@@ -162,12 +158,8 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
      */
     public URIAuthority(final String userInfo, final String hostname, final int port) {
         super();
-        Args.containsNoBlanks(hostname, "Host name");
-        if (userInfo != null) {
-            Args.containsNoBlanks(userInfo, "User info");
-        }
         this.userInfo = userInfo;
-        this.hostname = hostname.toLowerCase(Locale.ROOT);
+        this.hostname = hostname != null ? hostname.toLowerCase(Locale.ROOT) : null;
         this.port = Ports.checkWithDefault(port);
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
index 70358e2..c8f3fa7 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
@@ -292,7 +292,7 @@ public class URIBuilder {
                 if (InetAddressUtils.isIPv6Address(this.host)) {
                     sb.append("[").append(this.host).append("]");
                 } else {
-                    sb.append(this.host);
+                    sb.append(PercentCodec.encode(this.host, this.charset));
                 }
                 if (this.port >= 0) {
                     sb.append(":").append(this.port);
@@ -336,6 +336,16 @@ public class URIBuilder {
         this.port = uri.getPort();
         this.encodedUserInfo = uri.getRawUserInfo();
         this.userInfo = uri.getUserInfo();
+        if (this.encodedAuthority != null && this.host == null) {
+            try {
+                final URIAuthority uriAuthority = URIAuthority.parse(this.encodedAuthority);
+                this.encodedUserInfo = uriAuthority.getUserInfo();
+                this.userInfo = PercentCodec.decode(uriAuthority.getUserInfo(), charset);
+                this.host = PercentCodec.decode(uriAuthority.getHostName(), charset);
+                this.port = uriAuthority.getPort();
+            } catch (final URISyntaxException ignore) {
+            }
+        }
         this.encodedPath = uri.getRawPath();
         this.pathSegments = parsePath(uri.getRawPath(), charset);
         this.pathRootless = uri.getRawPath() != null && !uri.getRawPath().startsWith("/");
@@ -447,7 +457,7 @@ public class URIBuilder {
      * @return this.
      */
     public URIBuilder setHost(final String host) {
-        this.host = !TextUtils.isBlank(host) ? host : null;
+        this.host = host;
         this.encodedSchemeSpecificPart = null;
         this.encodedAuthority = null;
         return this;
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java
index 76c3b41..3aa1986 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java
@@ -129,16 +129,10 @@ public class TestURIAuthority {
                 CoreMatchers.equalTo(new URIAuthority("somehost", -1)));
         MatcherAssert.assertThat(URIAuthority.parse("somehost#blah"),
                 CoreMatchers.equalTo(new URIAuthority("somehost", -1)));
-        try {
-            URIAuthority.create("aaaa@:8080");
-            Assert.fail("URISyntaxException expected");
-        } catch (final URISyntaxException expected) {
-        }
-        try {
-            URIAuthority.create("@:");
-            Assert.fail("URISyntaxException expected");
-        } catch (final URISyntaxException expected) {
-        }
+        MatcherAssert.assertThat(URIAuthority.parse("aaaa@:8080"),
+                CoreMatchers.equalTo(new URIAuthority("aaaa", "", 8080)));
+        MatcherAssert.assertThat(URIAuthority.parse("@:"),
+                CoreMatchers.equalTo(new URIAuthority(null, "", -1)));
         MatcherAssert.assertThat(URIAuthority.parse("somehost:8080"),
                 CoreMatchers.equalTo(new URIAuthority("somehost", 8080)));
         MatcherAssert.assertThat(URIAuthority.parse("somehost:8080/blah"),
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
index ea153cf..8e5b334 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
@@ -512,6 +512,20 @@ public class TestURIBuilder {
     }
 
     @Test
+    public void testSetHostWithReservedChars() throws Exception {
+        final URIBuilder uribuilder = new URIBuilder();
+        uribuilder.setScheme("http").setHost("!example!.com");
+        Assert.assertEquals(URI.create("http://%21example%21.com"), uribuilder.build());
+    }
+
+    @Test
+    public void testGetHostWithReservedChars() throws Exception {
+        final URIBuilder uribuilder = new URIBuilder("http://someuser%21@%21example%21.com/");
+        Assert.assertEquals("!example!.com", uribuilder.getHost());
+        Assert.assertEquals("someuser!", uribuilder.getUserInfo());
+    }
+
+    @Test
     public void testMultipleLeadingPathSlashes() throws Exception {
         final URI uri = new URIBuilder()
                 .setScheme("ftp")