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/09/12 12:39:03 UTC

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

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

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

commit 9885d2c74a651016d9f28dd8b6e93409f2820f14
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 0e6d70a..f4b44a3 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
@@ -45,7 +45,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;
@@ -412,7 +412,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)));
+    }
+
+}