You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ck...@apache.org on 2021/04/20 20:55:00 UTC

[httpcomponents-core] 01/02: Improved parsing and formatting of URI components

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

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

commit 95bc9e22773983aeda21e8a66c24077672d24fe7
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Thu Apr 8 18:30:00 2021 +0200

    Improved parsing and formatting of URI components
---
 .../java/org/apache/hc/core5/http/HttpHost.java    |  73 +++-----------
 .../main/java/org/apache/hc/core5/net/Host.java    |  69 ++++++++++---
 .../java/org/apache/hc/core5/net/URIAuthority.java | 109 ++++++++-------------
 .../java/org/apache/hc/core5/net/URISupport.java   |  58 +++++++++++
 .../java/org/apache/hc/core5/net/TestHost.java     |   5 -
 5 files changed, 167 insertions(+), 147 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHost.java b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHost.java
index 40a0a47..d2b38e5 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHost.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpHost.java
@@ -35,8 +35,8 @@ import java.util.Locale;
 
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.net.Host;
 import org.apache.hc.core5.net.NamedEndpoint;
-import org.apache.hc.core5.net.Ports;
 import org.apache.hc.core5.net.URIAuthority;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.LangUtils;
@@ -59,18 +59,8 @@ public final class HttpHost implements NamedEndpoint, Serializable {
     /** The default scheme is "http". */
     public static final URIScheme DEFAULT_SCHEME = URIScheme.HTTP;
 
-    /** The host to use. */
-    private final String hostname;
-
-    /** The lowercase host, for {@link #equals} and {@link #hashCode}. */
-    private final String lcHostname;
-
-    /** The port to use, defaults to -1 if not set. */
-    private final int port;
-
-    /** The scheme (lowercased) */
     private final String schemeName;
-
+    private final Host host;
     private final InetAddress address;
 
     /**
@@ -91,14 +81,9 @@ public final class HttpHost implements NamedEndpoint, Serializable {
      * @since 5.0
      */
     public HttpHost(final String scheme, final InetAddress address, final String hostname, final int port) {
-        this.hostname   = Args.containsNoBlanks(hostname, "Host name");
-        this.port = Ports.checkWithDefault(port);
-        this.lcHostname = hostname.toLowerCase(Locale.ROOT);
-        if (scheme != null) {
-            this.schemeName = scheme.toLowerCase(Locale.ROOT);
-        } else {
-            this.schemeName = DEFAULT_SCHEME.id;
-        }
+        Args.containsNoBlanks(hostname, "Host name");
+        this.host = new Host(hostname, port);
+        this.schemeName = scheme != null ? scheme.toLowerCase(Locale.ROOT) : DEFAULT_SCHEME.id;
         this.address = address;
     }
 
@@ -166,20 +151,8 @@ public final class HttpHost implements NamedEndpoint, Serializable {
             }
             text = text.substring(schemeIdx + 3);
         }
-        int port = -1;
-        final int portIdx = text.lastIndexOf(":");
-        if (portIdx > 0) {
-            try {
-                port = Integer.parseInt(text.substring(portIdx + 1));
-            } catch (final NumberFormatException ex) {
-                throw new URISyntaxException(s, "invalid port");
-            }
-            text = text.substring(0, portIdx);
-        }
-        if (TextUtils.containsBlanks(text)) {
-            throw new URISyntaxException(s, "hostname contains blanks");
-        }
-        return new HttpHost(scheme, null, text, port);
+        final Host host = Host.create(text);
+        return new HttpHost(scheme, host);
     }
 
     /**
@@ -290,7 +263,7 @@ public final class HttpHost implements NamedEndpoint, Serializable {
      */
     @Override
     public String getHostName() {
-        return this.hostname;
+        return this.host.getHostName();
     }
 
     /**
@@ -300,7 +273,7 @@ public final class HttpHost implements NamedEndpoint, Serializable {
      */
     @Override
     public int getPort() {
-        return this.port;
+        return this.host.getPort();
     }
 
     /**
@@ -332,11 +305,7 @@ public final class HttpHost implements NamedEndpoint, Serializable {
         final StringBuilder buffer = new StringBuilder();
         buffer.append(this.schemeName);
         buffer.append("://");
-        buffer.append(this.hostname);
-        if (this.port != -1) {
-            buffer.append(':');
-            buffer.append(this.port);
-        }
+        buffer.append(this.host.toString());
         return buffer.toString();
     }
 
@@ -347,15 +316,7 @@ public final class HttpHost implements NamedEndpoint, Serializable {
      * @return  the host string, for example {@code localhost:8080}
      */
     public String toHostString() {
-        if (this.port != -1) {
-            //the highest port number is 65535, which is length 6 with the addition of the colon
-            final StringBuilder buffer = new StringBuilder(this.hostname.length() + 6);
-            buffer.append(this.hostname);
-            buffer.append(":");
-            buffer.append(this.port);
-            return buffer.toString();
-        }
-        return this.hostname;
+        return this.host.toString();
     }
 
 
@@ -372,10 +333,9 @@ public final class HttpHost implements NamedEndpoint, Serializable {
         }
         if (obj instanceof HttpHost) {
             final HttpHost that = (HttpHost) obj;
-            return this.lcHostname.equals(that.lcHostname)
-                && this.port == that.port
-                && this.schemeName.equals(that.schemeName)
-                && LangUtils.equals(this.address, that.address);
+            return this.schemeName.equals(that.schemeName) &&
+                    this.host.equals(that.host) &&
+                    LangUtils.equals(this.address, that.address);
         }
         return false;
     }
@@ -386,10 +346,9 @@ public final class HttpHost implements NamedEndpoint, Serializable {
     @Override
     public int hashCode() {
         int hash = LangUtils.HASH_SEED;
-        hash = LangUtils.hashCode(hash, this.lcHostname);
-        hash = LangUtils.hashCode(hash, this.port);
         hash = LangUtils.hashCode(hash, this.schemeName);
-        hash = LangUtils.hashCode(hash, address);
+        hash = LangUtils.hashCode(hash, this.host);
+        hash = LangUtils.hashCode(hash, this.address);
         return hash;
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java b/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java
index f03b790..d915b0e 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java
@@ -35,6 +35,7 @@ import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.LangUtils;
 import org.apache.hc.core5.util.TextUtils;
+import org.apache.hc.core5.util.Tokenizer;
 
 /**
  * Component that holds all details needed to describe a network connection
@@ -52,28 +53,66 @@ public final class Host implements NamedEndpoint, Serializable {
 
     public Host(final String name, final int port) {
         super();
-        this.name   = Args.containsNoBlanks(name, "Host name");
-        this.port = Ports.check(port);
+        this.name = Args.notNull(name, "Host name");
+        this.port = Ports.checkWithDefault(port);
         this.lcName = this.name.toLowerCase(Locale.ROOT);
     }
 
-    public static Host create(final String s) throws URISyntaxException {
-        Args.notEmpty(s, "HTTP Host");
-        final int portIdx = s.lastIndexOf(":");
+    static Host parse(final CharSequence s, final Tokenizer.Cursor cursor) throws URISyntaxException {
+        final Tokenizer tokenizer = Tokenizer.INSTANCE;
+        final String hostName = tokenizer.parseContent(s, cursor, URISupport.PORT_SEPARATORS);
+        String portText = null;
+        if (!cursor.atEnd() && s.charAt(cursor.getPos()) == ':') {
+            cursor.updatePos(cursor.getPos() + 1);
+            portText = tokenizer.parseContent(s, cursor, URISupport.TERMINATORS);
+        }
         final int port;
-        if (portIdx > 0) {
+        if (!TextUtils.isBlank(portText)) {
             try {
-                port = Integer.parseInt(s.substring(portIdx + 1));
+                port = Integer.parseInt(portText);
             } catch (final NumberFormatException ex) {
-                throw new URISyntaxException(s, "invalid port");
+                throw URISupport.createException(s, cursor, "Port is invalid");
             }
-            final String hostname = s.substring(0, portIdx);
-            if (TextUtils.containsBlanks(hostname)) {
-                throw new URISyntaxException(s, "hostname contains blanks");
-            }
-            return new Host(hostname, port);
+        } else {
+            port = -1;
+        }
+        return new Host(hostName, port);
+    }
+
+    static Host parse(final CharSequence s) throws URISyntaxException {
+        final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, s.length());
+        return parse(s, cursor);
+    }
+
+    static void format(final StringBuilder buf, final NamedEndpoint endpoint) {
+        buf.append(endpoint.getHostName());
+        if (endpoint.getPort() != -1) {
+            buf.append(":");
+            buf.append(endpoint.getPort());
+        }
+    }
+
+    static void format(final StringBuilder buf, final Host host) {
+        format(buf, (NamedEndpoint) host);
+    }
+
+    static String format(final Host host) {
+        final StringBuilder buf = new StringBuilder();
+        format(buf, host);
+        return buf.toString();
+    }
+
+    public static Host create(final String s) throws URISyntaxException {
+        Args.notEmpty(s, "HTTP Host");
+        final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, s.length());
+        final Host host = parse(s, cursor);
+        if (TextUtils.isBlank(host.getHostName())) {
+            throw URISupport.createException(s, cursor, "Hostname is invalid");
+        }
+        if (!cursor.atEnd()) {
+            throw URISupport.createException(s, cursor, "Unexpected content");
         }
-        throw new URISyntaxException(s, "port not found");
+        return host;
     }
 
     @Override
@@ -108,7 +147,7 @@ public final class Host implements NamedEndpoint, Serializable {
 
     @Override
     public String toString() {
-        return name + ":" + port;
+        return format(this);
     }
 
 }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
index 2907f9d..939f27d 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
@@ -29,11 +29,10 @@ package org.apache.hc.core5.net;
 
 import java.io.Serializable;
 import java.net.URISyntaxException;
-import java.util.BitSet;
-import java.util.Locale;
 
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.LangUtils;
 import org.apache.hc.core5.util.TextUtils;
 import org.apache.hc.core5.util.Tokenizer;
@@ -48,36 +47,13 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
 
     private static final long serialVersionUID = 1L;
     private final String userInfo;
-    private final String hostname;
-    private final int port;
-
-    private static final BitSet HOST_SEPARATORS = new BitSet(256);
-    private static final BitSet PORT_SEPARATORS = new BitSet(256);
-    private static final BitSet TERMINATORS = new BitSet(256);
-
-    static {
-        TERMINATORS.set('/');
-        TERMINATORS.set('#');
-        TERMINATORS.set('?');
-        HOST_SEPARATORS.or(TERMINATORS);
-        HOST_SEPARATORS.set('@');
-        PORT_SEPARATORS.or(TERMINATORS);
-        PORT_SEPARATORS.set(':');
-    }
-
-    static URISyntaxException createException(
-            final CharSequence input, final Tokenizer.Cursor cursor, final String reason) {
-        return new URISyntaxException(
-                input.subSequence(cursor.getLowerBound(), cursor.getUpperBound()).toString(),
-                reason,
-                cursor.getPos());
-    }
+    private final Host host;
 
     static URIAuthority parse(final CharSequence s, final Tokenizer.Cursor cursor) throws URISyntaxException {
         final Tokenizer tokenizer = Tokenizer.INSTANCE;
         String userInfo = null;
         final int initPos = cursor.getPos();
-        final String token = tokenizer.parseContent(s, cursor, HOST_SEPARATORS);
+        final String token = tokenizer.parseContent(s, cursor, URISupport.HOST_SEPARATORS);
         if (!cursor.atEnd() && s.charAt(cursor.getPos()) == '@') {
             cursor.updatePos(cursor.getPos() + 1);
             if (!TextUtils.isBlank(token)) {
@@ -87,23 +63,8 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
             //Rewind
             cursor.updatePos(initPos);
         }
-        final String hostName = tokenizer.parseContent(s, cursor, PORT_SEPARATORS);
-        String portText = null;
-        if (!cursor.atEnd() && s.charAt(cursor.getPos()) == ':') {
-            cursor.updatePos(cursor.getPos() + 1);
-            portText = tokenizer.parseContent(s, cursor, TERMINATORS);
-        }
-        final int port;
-        if (!TextUtils.isBlank(portText)) {
-            try {
-                port = Integer.parseInt(portText);
-            } catch (final NumberFormatException ex) {
-                throw createException(s, cursor, "Authority port is invalid");
-            }
-        } else {
-            port = -1;
-        }
-        return new URIAuthority(userInfo, hostName.toLowerCase(Locale.ROOT), port, true);
+        final Host host = Host.parse(s, cursor);
+        return new URIAuthority(userInfo, host);
     }
 
     static URIAuthority parse(final CharSequence s) throws URISyntaxException {
@@ -112,15 +73,11 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
     }
 
     static void format(final StringBuilder buf, final URIAuthority uriAuthority) {
-        if (uriAuthority.userInfo != null) {
-            buf.append(uriAuthority.userInfo);
+        if (uriAuthority.getUserInfo() != null) {
+            buf.append(uriAuthority.getUserInfo());
             buf.append("@");
         }
-        buf.append(uriAuthority.hostname);
-        if (uriAuthority.port != -1) {
-            buf.append(":");
-            buf.append(uriAuthority.port);
-        }
+        Host.format(buf, uriAuthority);
     }
 
     static String format(final URIAuthority uriAuthority) {
@@ -134,31 +91,45 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
      *             If the port parameter is outside the specified range of valid port values, which is between 0 and
      *             65535, inclusive. {@code -1} indicates the scheme default port.
      */
-    private URIAuthority(final String userInfo, final String hostname, final int port, final boolean internal) {
+    public URIAuthority(final String userInfo, final String hostname, final int port) {
         super();
         this.userInfo = userInfo;
-        this.hostname = hostname;
-        this.port = Ports.checkWithDefault(port);
+        this.host = new Host(hostname, port);
+    }
+
+    public URIAuthority(final String hostname, final int port) {
+        this(null, hostname, port);
     }
 
     /**
-     * @throws IllegalArgumentException
-     *             If the port parameter is outside the specified range of valid port values, which is between 0 and
-     *             65535, inclusive. {@code -1} indicates the scheme default port.
+     * @since 5.2
      */
-    public URIAuthority(final String userInfo, final String hostname, final int port) {
+    public URIAuthority(final String userInfo, final Host host) {
         super();
+        Args.notNull(host, "Host");
         this.userInfo = userInfo;
-        this.hostname = hostname != null ? hostname.toLowerCase(Locale.ROOT) : null;
-        this.port = Ports.checkWithDefault(port);
+        this.host = host;
     }
 
-    public URIAuthority(final String hostname, final int port) {
-        this(null, hostname, port);
+    /**
+     * @since 5.2
+     */
+    public URIAuthority(final Host host) {
+        this(null, host);
+    }
+
+    /**
+     * @since 5.2
+     */
+    public URIAuthority(final String userInfo, final NamedEndpoint endpoint) {
+        super();
+        Args.notNull(endpoint, "Endpoint");
+        this.userInfo = userInfo;
+        this.host = new Host(endpoint.getHostName(), endpoint.getPort());
     }
 
     public URIAuthority(final NamedEndpoint namedEndpoint) {
-        this(null, namedEndpoint.getHostName(), namedEndpoint.getPort());
+        this(null, namedEndpoint);
     }
 
     /**
@@ -171,7 +142,7 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
         final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, s.length());
         final URIAuthority uriAuthority = parse(s, cursor);
         if (!cursor.atEnd()) {
-            throw createException(s, cursor, "Unexpected content");
+            throw URISupport.createException(s, cursor, "Unexpected content");
         }
         return uriAuthority;
     }
@@ -186,12 +157,12 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
 
     @Override
     public String getHostName() {
-        return hostname;
+        return host.getHostName();
     }
 
     @Override
     public int getPort() {
-        return port;
+        return host.getPort();
     }
 
     @Override
@@ -207,8 +178,7 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
         if (obj instanceof URIAuthority) {
             final URIAuthority that = (URIAuthority) obj;
             return LangUtils.equals(this.userInfo, that.userInfo) &&
-                    LangUtils.equals(this.hostname, that.hostname) &&
-                    this.port == that.port;
+                    LangUtils.equals(this.host, that.host);
         }
         return false;
     }
@@ -217,8 +187,7 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
     public int hashCode() {
         int hash = LangUtils.HASH_SEED;
         hash = LangUtils.hashCode(hash, userInfo);
-        hash = LangUtils.hashCode(hash, hostname);
-        hash = LangUtils.hashCode(hash, port);
+        hash = LangUtils.hashCode(hash, host);
         return hash;
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URISupport.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URISupport.java
new file mode 100644
index 0000000..6948dc8
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URISupport.java
@@ -0,0 +1,58 @@
+/*
+ * ====================================================================
+ * 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.URISyntaxException;
+import java.util.BitSet;
+
+import org.apache.hc.core5.util.Tokenizer;
+
+final class URISupport {
+
+    static final BitSet HOST_SEPARATORS = new BitSet(256);
+    static final BitSet PORT_SEPARATORS = new BitSet(256);
+    static final BitSet TERMINATORS = new BitSet(256);
+
+    static {
+        TERMINATORS.set('/');
+        TERMINATORS.set('#');
+        TERMINATORS.set('?');
+        HOST_SEPARATORS.or(TERMINATORS);
+        HOST_SEPARATORS.set('@');
+        PORT_SEPARATORS.or(TERMINATORS);
+        PORT_SEPARATORS.set(':');
+    }
+
+    static URISyntaxException createException(
+            final CharSequence input, final Tokenizer.Cursor cursor, final String reason) {
+        return new URISyntaxException(
+                input.subSequence(cursor.getLowerBound(), cursor.getUpperBound()).toString(),
+                reason,
+                cursor.getPos());
+    }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestHost.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestHost.java
index 23c5880..71f35d5 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestHost.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestHost.java
@@ -55,11 +55,6 @@ public class TestHost {
             Assert.fail("NullPointerException should have been thrown");
         } catch (final NullPointerException expected) {
         }
-        try {
-            new Host("blah", -1);
-            Assert.fail("IllegalArgumentException should have been thrown");
-        } catch (final IllegalArgumentException expected) {
-        }
     }
 
     @Test