PROTON-1351: [C++ binding] remove dependency on Proton-c url code
- In preparation for moving this to the proton core library
- Improved C++ url tests
diff --git a/proton-c/bindings/cpp/include/proton/url.hpp b/proton-c/bindings/cpp/include/proton/url.hpp
index 57ef586..78271b6 100644
--- a/proton-c/bindings/cpp/include/proton/url.hpp
+++ b/proton-c/bindings/cpp/include/proton/url.hpp
@@ -22,14 +22,13 @@
  *
  */
 
+#include "./internal/pn_unique_ptr.hpp"
 #include "./types_fwd.hpp"
 #include "./error.hpp"
 
 #include <iosfwd>
 #include <string>
 
-struct pn_url_t;
-
 namespace proton {
 
 /// An error encountered during URL parsing.
@@ -124,7 +123,8 @@
     friend PN_CPP_EXTERN std::string to_string(const url&);
 
   private:
-    pn_url_t* url_;
+    struct impl;
+    internal::pn_unique_ptr<impl> impl_;
 
     /// @cond INTERNAL
 
diff --git a/proton-c/bindings/cpp/src/url.cpp b/proton-c/bindings/cpp/src/url.cpp
index 923aaf0..1aa01e1 100644
--- a/proton-c/bindings/cpp/src/url.cpp
+++ b/proton-c/bindings/cpp/src/url.cpp
@@ -23,73 +23,239 @@
 
 #include "proton/error.hpp"
 
-#include <proton/url.h>
-
 #include "proton_bits.hpp"
 
+#include <cstdlib>
+#include <cstring>
+#include <iomanip>
 #include <sstream>
 
-namespace proton {
-
-url_error::url_error(const std::string& s) : error(s) {}
-
 namespace {
 
-pn_url_t* parse_throw(const char* s) {
-    pn_url_t* u = pn_url_parse(s);
-    if (!u) throw url_error("invalid URL: " + std::string(s));
-    return u;
+/** URL-encode src and append to dst. */
+static std::string pni_urlencode(const std::string &src) {
+    static const char *bad = "@:/";
+
+    std::ostringstream dst;
+    dst << std::hex << std::uppercase << std::setfill('0');
+
+    std::size_t i = 0;
+    std::size_t j = src.find_first_of(bad);
+    while (j!=std::string::npos) {
+        dst << src.substr(i, j-i);
+        dst << "%" << std::setw(2) << src[j];
+        i = j + 1;
+        j = src.find_first_of(bad);
+    }
+    dst << src.substr(i);
+    return dst.str();
 }
 
-pn_url_t* parse_allow_empty(const char* s) {
-    return s && *s ? parse_throw(s) : pn_url();
+// Low level url parser
+static void pni_urldecode(const char *src, char *dst)
+{
+  const char *in = src;
+  char *out = dst;
+  while (*in != '\0')
+  {
+    if ('%' == *in)
+    {
+      if ((in[1] != '\0') && (in[2] != '\0'))
+      {
+        char esc[3];
+        esc[0] = in[1];
+        esc[1] = in[2];
+        esc[2] = '\0';
+        unsigned long d = std::strtoul(esc, NULL, 16);
+        *out = (char)d;
+        in += 3;
+        out++;
+      }
+      else
+      {
+        *out = *in;
+        in++;
+        out++;
+      }
+    }
+    else
+    {
+      *out = *in;
+      in++;
+      out++;
+    }
+  }
+  *out = '\0';
 }
 
-void replace(pn_url_t*& var, pn_url_t* val) {
-    if (var) pn_url_free(var);
-    var = val;
-}
+void parse_url(char *url, const char **scheme, const char **user, const char **pass, const char **host, const char **port, const char **path)
+{
+  if (!url) return;
 
-void defaults(pn_url_t* u) {
-  const char* scheme = pn_url_get_scheme(u);
-  const char* port = pn_url_get_port(u);
-  if (!scheme || *scheme=='\0' ) pn_url_set_scheme(u, url::AMQP.c_str());
-  if (!port || *port=='\0' ) pn_url_set_port(u, pn_url_get_scheme(u));
+  char *slash = std::strchr(url, '/');
+
+  if (slash && slash>url) {
+    char *scheme_end = std::strstr(slash-1, "://");
+
+    if (scheme_end && scheme_end<slash) {
+      *scheme_end = '\0';
+      *scheme = url;
+      url = scheme_end + 3;
+      slash = std::strchr(url, '/');
+    }
+  }
+
+  if (slash) {
+    *slash = '\0';
+    *path = slash + 1;
+  }
+
+  char *at = std::strchr(url, '@');
+  if (at) {
+    *at = '\0';
+    char *up = url;
+    url = at + 1;
+    char *colon = std::strchr(up, ':');
+    if (colon) {
+      *colon = '\0';
+      char *p = colon + 1;
+      pni_urldecode(p, p);
+      *pass = p;
+    }
+    pni_urldecode(up, up);
+    *user = up;
+  }
+
+  *host = url;
+  char *open = (*url == '[') ? url : 0;
+  if (open) {
+    char *close = std::strchr(open, ']');
+    if (close) {
+        *host = open + 1;
+        *close = '\0';
+        url = close + 1;
+    }
+  }
+
+  char *colon = std::strchr(url, ':');
+  if (colon) {
+    *colon = '\0';
+    *port = colon + 1;
+  }
 }
 
 } // namespace
 
-url::url(const std::string &s) : url_(parse_throw(s.c_str())) { defaults(url_); }
+namespace proton {
 
-url::url(const std::string &s, bool d) : url_(parse_throw(s.c_str())) { if (d) defaults(url_); }
+struct url::impl {
+    static const char* const default_host;
+    const char* scheme;
+    const char* username;
+    const char* password;
+    const char* host;
+    const char* port;
+    const char* path;
+    char* cstr;
+    mutable std::string str;
 
-url::url(const url& u) : url_(parse_allow_empty(pn_url_str(u.url_))) {}
+    impl(const std::string& s) :
+      scheme(0), username(0), password(0), host(0), port(0), path(0),
+      cstr(new char[s.size()+1])
+    {
+        std::strncpy(cstr, s.c_str(), s.size());
+        cstr[s.size()] = 0;
+        parse_url(cstr, &scheme, &username, &password, &host, &port, &path);
+    }
 
-url::~url() { pn_url_free(url_); }
+    ~impl() {
+        delete [] cstr;
+    }
+
+    void defaults() {
+        if (!scheme || *scheme=='\0' ) scheme = proton::url::AMQP.c_str();
+        if (!host || *host=='\0' ) host = default_host;
+        if (!port || *port=='\0' ) port = scheme;
+    }
+
+    operator std::string() const {
+        if ( str.empty() ) {
+            if (scheme) {
+                str += scheme;
+                str += "://";
+            }
+            if (username) {
+                str += pni_urlencode(username);
+            }
+            if (password) {
+                str += ":";
+                str += pni_urlencode(password);
+            }
+            if (username || password) {
+                str += "@";
+            }
+            if (host) {
+                if (std::strchr(host, ':')) {
+                    str += '[';
+                    str += host;
+                    str += ']';
+                } else {
+                    str += host;
+                }
+            }
+            if (port) {
+                str += ':';
+                str += port;
+            }
+            if (path) {
+                str += '/';
+                str += path;
+            }
+        }
+        return str;
+    }
+
+};
+
+const char* const url::impl::default_host = "localhost";
+
+
+url_error::url_error(const std::string& s) : error(s) {}
+
+url::url(const std::string &s) : impl_(new impl(s)) { impl_->defaults(); }
+
+url::url(const std::string &s, bool d) : impl_(new impl(s)) { if (d) impl_->defaults(); }
+
+url::url(const url& u) : impl_(new impl(u)) {}
+
+url::~url() {}
 
 url& url::operator=(const url& u) {
-    if (this != &u) replace(url_, parse_allow_empty(pn_url_str(u.url_)));
+    if (this != &u) {
+        impl_.reset(new impl(*u.impl_));
+    }
     return *this;
 }
 
-url::operator std::string() const { return str(pn_url_str(url_)); }
+url::operator std::string() const { return *impl_; }
 
-std::string url::scheme() const { return str(pn_url_get_scheme(url_)); }
-std::string url::user() const { return str(pn_url_get_username(url_)); }
-std::string url::password() const { return str(pn_url_get_password(url_)); }
-std::string url::host() const { return str(pn_url_get_host(url_)); }
-std::string url::port() const { return str(pn_url_get_port(url_)); }
-std::string url::path() const { return str(pn_url_get_path(url_)); }
+std::string url::scheme() const { return str(impl_->scheme); }
+std::string url::user() const { return str(impl_->username); }
+std::string url::password() const { return str(impl_->password); }
+std::string url::host() const { return str(impl_->host); }
+std::string url::port() const { return str(impl_->port); }
+std::string url::path() const { return str(impl_->path); }
 
 std::string url::host_port() const { return host() + ":" + port(); }
 
-bool url::empty() const { return *pn_url_str(url_) == '\0'; }
+bool url::empty() const { return impl_->str.empty(); }
 
 const std::string url::AMQP("amqp");
 const std::string url::AMQPS("amqps");
 
 uint16_t url::port_int() const {
     // TODO aconway 2015-10-27: full service name lookup
+    // astitcher 2016-11-17: It is hard to make the full service name lookup platform independent
     if (port() == AMQP) return 5672;
     if (port() == AMQPS) return 5671;
     std::istringstream is(port());
@@ -101,21 +267,21 @@
 }
 
 std::ostream& operator<<(std::ostream& o, const url& u) {
-    return o << pn_url_str(u.url_);
+    return o << std::string(u);
 }
 
 std::string to_string(const url& u) {
-    return std::string(pn_url_str(u.url_));
+    return u;
 }
 
 std::istream& operator>>(std::istream& i, url& u) {
     std::string s;
     i >> s;
     if (!i.fail() && !i.bad()) {
-        pn_url_t* p = pn_url_parse(s.c_str());
-        if (p) {
-            replace(u.url_, p);
-            defaults(u.url_);
+        if (!s.empty()) {
+            url::impl* p = new url::impl(s);
+            p->defaults();
+            u.impl_.reset(p);
         } else {
             i.clear(std::ios::failbit);
         }
diff --git a/proton-c/bindings/cpp/src/url_test.cpp b/proton-c/bindings/cpp/src/url_test.cpp
index ad9eca4..0727e2c 100644
--- a/proton-c/bindings/cpp/src/url_test.cpp
+++ b/proton-c/bindings/cpp/src/url_test.cpp
@@ -22,11 +22,45 @@
 
 namespace {
 
-void parse_to_string_test() {
-    std::string s("amqp://foo:xyz/path");
+void check_url(const std::string& s,
+               const std::string& scheme,
+               const std::string& user,
+               const std::string& pwd,
+               const std::string& host,
+               const std::string& port,
+               const std::string& path
+              )
+{
     proton::url u(s);
-    ASSERT_EQUAL(to_string(u), s);
+    ASSERT_EQUAL(scheme, u.scheme());
+    ASSERT_EQUAL(user, u.user());
+    ASSERT_EQUAL(pwd, u.password());
+    ASSERT_EQUAL(host, u.host());
+    ASSERT_EQUAL(port, u.port());
+    ASSERT_EQUAL(path, u.path());
 }
+
+void parse_to_string_test() {
+    check_url("amqp://foo:xyz/path",
+              "amqp", "", "", "foo", "xyz", "path");
+    check_url("amqp://username:password@host:1234/path",
+              "amqp", "username", "password", "host", "1234", "path");
+    check_url("host:1234",
+              "amqp", "", "", "host", "1234", "");
+    check_url("host",
+              "amqp", "", "", "host", "amqp", "");
+    check_url("host/path",
+              "amqp", "", "", "host", "amqp", "path");
+    check_url("amqps://host",
+              "amqps", "", "", "host", "amqps", "");
+    check_url("/path",
+              "amqp", "", "", "localhost", "amqp", "path");
+    check_url("",
+              "amqp", "", "", "localhost", "amqp", "");
+    check_url(":1234",
+              "amqp", "", "", "localhost", "1234", "");
+}
+
 }
 
 int main(int, char**) {