Ported FaceUri from ndn-cxx

Change-Id: Ie88eebcce0f27c80ee96d27aac04e152bf5f1ef0
diff --git a/src/main/java/net/named_data/jndn_xx/util/FaceUri.java b/src/main/java/net/named_data/jndn_xx/util/FaceUri.java
new file mode 100644
index 0000000..f05b27d
--- /dev/null
+++ b/src/main/java/net/named_data/jndn_xx/util/FaceUri.java
@@ -0,0 +1,456 @@
+/* -*- Mode:jde; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2015 Alexander Afanasyev.
+ *
+ * This file is part of jndn-xx library.
+ *
+ * jndn-xx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * jndn-xx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with jndn-xx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of jndn-xx authors and contributors.
+ */
+
+package net.named_data.jndn_xx.util;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.net.InetAddresses;
+
+/**
+ * Represents the underlying protocol and address used by Face
+ */
+public class FaceUri implements Cloneable {
+  public static class Error extends IllegalArgumentException {
+    Error(String error) {
+      super(error);
+    }
+  }
+
+  public static class CanonizeError extends Exception {
+    CanonizeError(String error) {
+      super(error);
+    }
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+
+  public FaceUri()
+  {
+  }
+
+  /**
+   * Construct FaceUri from string
+   * @param uri scheme://host[:port]/path
+   * @throws Error if URI cannot be parsed
+   */
+  public FaceUri(String uri) throws Error {
+    if (!parse(uri)) {
+      throw new Error("Malformed URI: " + uri);
+    }
+  }
+
+  /**
+   * Exception-safe parsing
+   * @param uri FaceUri to parse
+   * @return true if <pre>uri</pre> is successfully parsed
+   */
+  public boolean
+  parse(String uri) {
+    m_scheme = "";
+    m_host = "";
+    m_isV6 = false;
+    m_port = "";
+    m_path = "";
+
+    Pattern protocolExp = Pattern.compile("(\\w+\\d?)://([^/]*)(\\/[^?]*)?");
+    Matcher protocolMatch = protocolExp.matcher(uri);
+    if (!protocolMatch.matches()) {
+      return false;
+    }
+    m_scheme = protocolMatch.group(1);
+    if (m_scheme == null)
+      m_scheme = "";
+    String authority = protocolMatch.group(2);
+    if (authority == null)
+      authority = "";
+    m_path = protocolMatch.group(3);
+    if (m_path == null)
+      m_path = "";
+
+    // pattern for IPv6 address enclosed in [ ], with optional port number
+    final Pattern v6Exp = Pattern.compile("^\\[([a-fA-F0-9:]+)\\](?:\\:(\\d+))?$");
+    // pattern for Ethernet address in standard hex-digits-and-colons notation
+    final Pattern etherExp = Pattern.compile("^\\[((?:[a-fA-F0-9]{1,2}\\:){5}(?:[a-fA-F0-9]{1,2}))\\]$");
+    // pattern for IPv4-mapped IPv6 address, with optional port number
+    final Pattern v4MappedV6Exp = Pattern.compile("^\\[::ffff:(\\d+(?:\\.\\d+){3})\\](?:\\:(\\d+))?$");
+    // pattern for IPv4/hostname/fd/ifname, with optional port number
+    final Pattern v4HostExp = Pattern.compile("^([^:]+)(?:\\:(\\d+))?$");
+
+    if (authority.equals("")) {
+      // UNIX, internal
+    } else {
+      Matcher match = v6Exp.matcher(authority);
+      m_isV6 = match.matches();
+      if (m_isV6 ||
+        (match = etherExp.matcher(authority)).matches() ||
+        (match = v4MappedV6Exp.matcher(authority)).matches() ||
+        (match = v4HostExp.matcher(authority)).matches()) {
+        m_host = match.group(1);
+        if (m_host == null)
+          m_host = "";
+        m_port = match.group(2);
+        if (m_port == null)
+          m_port = "";
+      } else {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+  // getters
+
+  /**
+   * Get scheme (protocol)
+   * @return scheme (return "" when empty)
+   */
+  public String
+  getScheme() {
+    return m_scheme;
+  }
+
+  /**
+   * Get host (domain)
+   * @return host (return "" when empty)
+   */
+  public String
+  getHost() {
+    return m_host;
+  }
+
+  /**
+   * Get port
+   * @return port number (return "" when empty)
+   */
+  public String
+  getPort() {
+    return m_port;
+  }
+
+  /**
+   * Get path
+   * @return path (return "" when empty)
+   */
+  public String
+  getPath() {
+    return m_path;
+  }
+
+  /**
+   * Convert FaceUri instance to a string
+   * @return string represenation of FaceUri
+   */
+  public String
+  toString() {
+    String out;
+    out = m_scheme + "://";
+    if (m_isV6) {
+      out += "[" + m_host + "]";
+    } else {
+      out += m_host;
+    }
+    if (!m_port.equals("")) {
+      out += ":" + m_port;
+    }
+    out += m_path;
+    return out;
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+  // comparator
+
+  /**
+   * Compare FaceUris
+   * @param rhs FaceUri to compare with
+   * @return true if <pre>this</pre> is equal to <pre>rhs</pre>
+   */
+  boolean
+  equals(FaceUri rhs) {
+    return (m_scheme == rhs.m_scheme &&
+      m_host == rhs.m_host &&
+      m_isV6 == rhs.m_isV6 &&
+      m_port == rhs.m_port &&
+      m_path == rhs.m_path);
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+  // canonical FaceUri
+
+  /**
+   * Check whether FaceUri of the scheme can be canonized
+   * @param scheme scheme to check
+   * @return true if FaceUri supports canonization for the scheme
+   */
+  static public boolean
+  canCanonize(String scheme) {
+    return s_canonizeProviders.containsKey(scheme);
+  }
+
+  /**
+   * Determine whether this FaceUri is in canonical form
+   * <p>
+   * Note that this method can block for DNS resolution process
+   * <p>
+   * @return true if this FaceUri is in canonical form,
+   * false if this FaceUri is not in canonical form or
+   * or it's undetermined whether this FaceUri is in canonical form
+   */
+  public boolean
+  isCanonical() {
+    CanonizeProvider provider = s_canonizeProviders.get(m_scheme);
+    if (provider == null)
+      return false;
+
+    return provider.isCanonical(this);
+  }
+
+  /**
+   * Convert this FaceUri to canonical form
+   * <p>
+   * Note that this method can block for DNS resolution process
+   * <p>
+   * @return A new FaceUri in canonical form; this FaceUri is unchanged
+   * @throws CanonizeError when canonization fails
+   */
+  public FaceUri
+  canonize() throws CanonizeError {
+    CanonizeProvider provider = s_canonizeProviders.get(m_scheme);
+    if (provider == null) {
+      throw new CanonizeError(this.toString() + " does not support canonization");
+    }
+
+    return provider.canonize(this);
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+
+  /**
+   * Interface that provides FaceUri canonization functionality for a group of schemes
+   */
+  private interface CanonizeProvider {
+    public Set<String>
+    getSchemes();
+
+    public boolean
+    isCanonical(FaceUri faceUri);
+
+    public FaceUri
+    canonize(FaceUri faceUri) throws CanonizeError;
+  }
+
+  /**
+   * Canonizer for IPv4 and IPv6-based schemes
+   */
+  private static class IpHostCanonizeProvider implements CanonizeProvider {
+    public Set<String>
+    getSchemes() {
+      Set<String> schemes = new HashSet<String>();
+      schemes.add(m_baseScheme);
+      schemes.add(m_v4Scheme);
+      schemes.add(m_v6Scheme);
+      return schemes;
+    }
+
+    public boolean
+    isCanonical(FaceUri faceUri) {
+      if (faceUri.getPort().equals("")) {
+        return false;
+      }
+      if (!faceUri.getPath().equals("")) {
+        return false;
+      }
+
+      try {
+        InetAddress addr;
+        if (faceUri.getScheme().equals(m_v4Scheme)) {
+          addr = Inet4Address.getByName(faceUri.getHost());
+        } else if (faceUri.getScheme().equals(m_v6Scheme)) {
+          addr = Inet6Address.getByName(faceUri.getHost());
+        } else {
+          return false;
+        }
+        return InetAddresses.toAddrString(addr).equals(faceUri.getHost()) && this.checkAddress(addr);
+      } catch (UnknownHostException e) {
+        return false;
+      }
+    }
+
+    public FaceUri
+    canonize(FaceUri faceUri) throws CanonizeError {
+
+      if (this.isCanonical(faceUri)) {
+        try {
+          return (FaceUri)faceUri.clone();
+        }
+        catch (CloneNotSupportedException e) {
+          assert false;
+          return null;
+        }
+      }
+
+      InetAddress addr = null;
+      try {
+        if (faceUri.getScheme().equals(m_v4Scheme)) {
+          for (InetAddress a : InetAddress.getAllByName(faceUri.getHost())) {
+            if (a instanceof Inet4Address) {
+              addr = a;
+              break;
+            }
+          }
+        } else if (faceUri.getScheme().equals(m_v6Scheme)) {
+          for (InetAddress a : InetAddress.getAllByName(faceUri.getHost())) {
+            if (a instanceof Inet6Address) {
+              addr = a;
+              break;
+            }
+          }
+        } else {
+          addr = InetAddress.getByName(faceUri.getHost());
+        }
+      } catch (UnknownHostException e) {
+        throw new CanonizeError("Cannot resolve " + faceUri.getHost());
+      }
+
+      if (addr == null) {
+        throw new CanonizeError("Could not resolve " + faceUri.getHost() + " for scheme " + faceUri.getScheme());
+      }
+
+      if (!this.checkAddress(addr)) {
+        throw new CanonizeError("Resolved to " + addr.getHostAddress() + ", which is prohibied by the CanonizeProvider");
+      }
+
+      int port = 0;
+      if (faceUri.getPort().equals("")) {
+        port = addr.isMulticastAddress() ? m_defaultMulticastPort : m_defaultUnicastPort;
+      } else {
+        try {
+          port = Integer.valueOf(faceUri.getPort());
+        } catch (NumberFormatException e) {
+          throw new CanonizeError("Invalid port number");
+        }
+      }
+
+      faceUri = new FaceUri();
+      if (addr instanceof Inet4Address) {
+        faceUri.parse(m_v4Scheme + "://" + InetAddresses.toAddrString(addr) + ":" + String.valueOf(port));
+      } else if (addr instanceof Inet6Address) {
+        faceUri.parse(m_v6Scheme + "://[" + InetAddresses.toAddrString(addr) + "]:" + String.valueOf(port));
+      } else {
+        throw new CanonizeError("Unknown type of address: " + addr.getHostAddress());
+      }
+
+      return faceUri;
+    }
+
+    //////////////////////////////////////////////////////////////////////////////
+
+    protected IpHostCanonizeProvider(String baseScheme, int defaultUnicastPort, int defaultMulticastPort) {
+      m_baseScheme = baseScheme;
+      m_v4Scheme = baseScheme + "4";
+      m_v6Scheme = baseScheme + "6";
+      m_defaultUnicastPort = defaultUnicastPort;
+      m_defaultMulticastPort = defaultMulticastPort;
+    }
+
+    protected IpHostCanonizeProvider(String baseScheme) {
+      this(baseScheme, 6363, 56363);
+    }
+
+    /**
+     * @return (true, ignored) if the address is allowable;
+     * (false,reason) if the address is not allowable.
+     * @brief when overriden in a subclass, check the IP address is allowable
+     */
+    protected boolean
+    checkAddress(InetAddress ipAddress) {
+      return true;
+    }
+
+    //////////////////////////////////////////////////////////////////////////////
+
+    private String m_baseScheme = "";
+    private String m_v4Scheme = "";
+    private String m_v6Scheme = "";
+    private int m_defaultUnicastPort = 6363;
+    private int m_defaultMulticastPort = 56363;
+  }
+
+  private static class UdpCanonizeProvider extends IpHostCanonizeProvider {
+    public UdpCanonizeProvider() {
+      super("udp");
+    }
+  }
+
+  private static class TcpCanonizeProvider extends IpHostCanonizeProvider {
+    public TcpCanonizeProvider() {
+      super("tcp");
+    }
+
+    protected boolean
+    checkAddress(InetAddress ipAddress) {
+      return !ipAddress.isMulticastAddress();
+    }
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+
+  static private Map<String, CanonizeProvider>
+  initCanonizeProviders()
+  {
+    Map<String, CanonizeProvider> providers = new HashMap<String, CanonizeProvider>();
+    addCanonizeProvider(providers, new TcpCanonizeProvider());
+    addCanonizeProvider(providers, new UdpCanonizeProvider());
+    return providers;
+  }
+
+  private static void
+  addCanonizeProvider(Map<String, CanonizeProvider> providers, CanonizeProvider provider) {
+    Set<String> schemes = provider.getSchemes();
+    assert !schemes.isEmpty();
+
+    for (String scheme : schemes) {
+      assert !providers.containsKey(scheme);
+      providers.put(scheme, provider);
+    }
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+
+  private String m_scheme = "";
+  private String m_host = "";
+  boolean m_isV6 = false; ///< whether to add [] around host when writing string
+  private String m_port = "";
+  private String m_path = "";
+
+  static private final Map<String, CanonizeProvider> s_canonizeProviders = initCanonizeProviders();
+}
diff --git a/src/test/java/net/named_data/jndn_xx/util/FaceUriTest.java b/src/test/java/net/named_data/jndn_xx/util/FaceUriTest.java
new file mode 100644
index 0000000..b05bfcf
--- /dev/null
+++ b/src/test/java/net/named_data/jndn_xx/util/FaceUriTest.java
@@ -0,0 +1,341 @@
+/* -*- Mode:jde; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2015 Alexander Afanasyev.
+ *
+ * This file is part of jndn-xx library.
+ *
+ * jndn-xx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * jndn-xx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with jndn-xx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of jndn-xx authors and contributors.
+ */
+
+package net.named_data.jndn_xx.util;
+
+import org.junit.Test;
+
+import java.net.Inet4Address;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class FaceUriTest {
+
+  @Test
+  public void ParseInternal() {
+    FaceUri uri = new FaceUri();
+
+    assertTrue(uri.parse("internal://"));
+    assertEquals("internal", uri.getScheme());
+    assertEquals("", uri.getHost());
+    assertEquals("", uri.getPort());
+    assertEquals("", uri.getPath());
+
+    assertEquals(uri.parse("internal:"), false);
+    assertEquals(uri.parse("internal:/"), false);
+  }
+
+  @Test
+  public void ParseUdp()
+  {
+    new FaceUri("udp://hostname:6363");
+
+    try {
+      new FaceUri("udp//hostname:6363");
+      fail("FaceUri.Error exception is expected");
+    }
+    catch (FaceUri.Error e) {
+    }
+
+    try {
+      new FaceUri("udp://hostname:port");
+      fail("FaceUri.Error exception is expected");
+    }
+    catch (FaceUri.Error e) {
+    }
+
+    FaceUri uri = new FaceUri();
+    assertEquals(uri.parse("udp//hostname:6363"), false);
+
+    assertTrue(uri.parse("udp://hostname:80"));
+    assertEquals(uri.getScheme(), "udp");
+    assertEquals(uri.getHost(), "hostname");
+    assertEquals(uri.getPort(), "80");
+    assertEquals(uri.getPath(), "");
+
+    assertTrue(uri.parse("udp4://192.0.2.1:20"));
+    assertEquals(uri.getScheme(), "udp4");
+    assertEquals(uri.getHost(), "192.0.2.1");
+    assertEquals(uri.getPort(), "20");
+    assertEquals(uri.getPath(), "");
+
+    assertTrue(uri.parse("udp6://[2001:db8:3f9:0::1]:6363"));
+    assertEquals(uri.getScheme(), "udp6");
+    assertEquals(uri.getHost(), "2001:db8:3f9:0::1");
+    assertEquals(uri.getPort(), "6363");
+    assertEquals(uri.getPath(), "");
+
+    assertTrue(uri.parse("udp6://[2001:db8:3f9:0:3025:ccc5:eeeb:86d3]:6363"));
+    assertEquals(uri.getScheme(), "udp6");
+    assertEquals(uri.getHost(), "2001:db8:3f9:0:3025:ccc5:eeeb:86d3");
+    assertEquals(uri.getPort(), "6363");
+    assertEquals(uri.getPath(), "");
+
+    assertEquals(uri.parse("udp6://[2001:db8:3f9:0:3025:ccc5:eeeb:86dg]:6363"), false);
+  }
+
+  @Test
+  public void CheckCanonicalUdp() {
+   assertEquals(FaceUri.canCanonize("udp"), true);
+   assertEquals(FaceUri.canCanonize("udp4"), true);
+   assertEquals(FaceUri.canCanonize("udp6"), true);
+
+   assertEquals(new FaceUri("udp4://192.0.2.1:6363").isCanonical(), true);
+   assertEquals(new FaceUri("udp://192.0.2.1:6363").isCanonical(), false);
+   assertEquals(new FaceUri("udp4://192.0.2.1").isCanonical(), false);
+   assertEquals(new FaceUri("udp4://192.0.2.1:6363/").isCanonical(), false);
+   assertEquals(new FaceUri("udp6://[2001:db8::1]:6363").isCanonical(), true);
+   assertEquals(new FaceUri("udp6://[2001:db8::01]:6363").isCanonical(), false);
+   assertEquals(new FaceUri("udp://example.net:6363").isCanonical(), false);
+   assertEquals(new FaceUri("udp4://example.net:6363").isCanonical(), false);
+   assertEquals(new FaceUri("udp6://example.net:6363").isCanonical(), false);
+   assertEquals(new FaceUri("udp4://224.0.23.170:56363").isCanonical(), true);
+  }
+
+  private static void
+  addTest(String testUri, boolean shouldSucceed, String canonicalUri) throws FaceUri.CanonizeError
+  {
+    FaceUri uri = new FaceUri(testUri);
+    if (shouldSucceed) {
+      assertEquals(canonicalUri, uri.canonize().toString());
+    }
+    else {
+      try {
+        uri.canonize();
+        fail("Canonization should have failed");
+      }
+      catch (FaceUri.CanonizeError e) {
+      }
+    }
+  }
+
+  @Test
+  public void CanonizeUdpV4() throws FaceUri.CanonizeError
+  {
+    // IPv4 unicast
+    addTest("udp4://192.0.2.1:6363", true, "udp4://192.0.2.1:6363");
+    addTest("udp://192.0.2.2:6363", true, "udp4://192.0.2.2:6363");
+    addTest("udp4://192.0.2.3", true, "udp4://192.0.2.3:6363");
+    addTest("udp4://192.0.2.4:6363/", true, "udp4://192.0.2.4:6363");
+    addTest("udp4://192.0.2.5:9695", true, "udp4://192.0.2.5:9695");
+    addTest("udp4://192.0.2.666:6363", false, "");
+    addTest("udp4://google-public-dns-a.google.com", true, "udp4://8.8.8.8:6363");
+    addTest("udp4://invalid.invalid", false, "");
+
+    // IPv4 multicast
+    addTest("udp4://224.0.23.170:56363", true, "udp4://224.0.23.170:56363");
+    addTest("udp4://224.0.23.170", true, "udp4://224.0.23.170:56363");
+    addTest("udp4://all-routers.mcast.net:56363", true, "udp4://224.0.0.2:56363");
+  }
+
+  @Test
+  public void CanonizeUdpV6() throws FaceUri.CanonizeError
+  {
+    // IPv6 unicast
+    addTest("udp6://[2001:db8::1]:6363", true, "udp6://[2001:db8::1]:6363");
+    addTest("udp://[2001:db8::1]:6363", true, "udp6://[2001:db8::1]:6363");
+    addTest("udp6://[2001:db8::01]:6363", true, "udp6://[2001:db8::1]:6363");
+    addTest("udp6://google-public-dns-a.google.com", true, "udp6://[2001:4860:4860::8888]:6363");
+    addTest("udp6://invalid.invalid", false, "");
+    addTest("udp://invalid.invalid", false, "");
+
+    // IPv6 multicast
+    addTest("udp6://[ff02::2]:56363", true, "udp6://[ff02::2]:56363");
+    addTest("udp6://[ff02::2]", true, "udp6://[ff02::2]:56363");
+  }
+
+  @Test
+  public void ParseTcp()
+  {
+    FaceUri uri = new FaceUri();
+
+    assertTrue(uri.parse("tcp://random.host.name"));
+    assertEquals(uri.getScheme(), "tcp");
+    assertEquals(uri.getHost(), "random.host.name");
+    assertEquals(uri.getPort(), "");
+    assertEquals(uri.getPath(), "");
+
+    assertEquals(uri.parse("tcp://192.0.2.1:"), false);
+    assertEquals(uri.parse("tcp://[::zzzz]"), false);
+  }
+
+  @Test
+  public void CheckCanonicalTcp()
+  {
+    assertEquals(FaceUri.canCanonize("tcp"), true);
+    assertEquals(FaceUri.canCanonize("tcp4"), true);
+    assertEquals(FaceUri.canCanonize("tcp6"), true);
+
+    assertEquals(new FaceUri("tcp4://192.0.2.1:6363").isCanonical(), true);
+    assertEquals(new FaceUri("tcp://192.0.2.1:6363").isCanonical(), false);
+    assertEquals(new FaceUri("tcp4://192.0.2.1").isCanonical(), false);
+    assertEquals(new FaceUri("tcp4://192.0.2.1:6363/").isCanonical(), false);
+    assertEquals(new FaceUri("tcp6://[2001:db8::1]:6363").isCanonical(), true);
+    assertEquals(new FaceUri("tcp6://[2001:db8::01]:6363").isCanonical(), false);
+    assertEquals(new FaceUri("tcp://example.net:6363").isCanonical(), false);
+    assertEquals(new FaceUri("tcp4://example.net:6363").isCanonical(), false);
+    assertEquals(new FaceUri("tcp6://example.net:6363").isCanonical(), false);
+    assertEquals(new FaceUri("tcp4://224.0.23.170:56363").isCanonical(), false);
+  }
+
+  @Test
+  public void CanonizeTcpV4() throws FaceUri.CanonizeError
+  {
+    // IPv4 unicast
+    addTest("tcp4://192.0.2.1:6363", true, "tcp4://192.0.2.1:6363");
+    addTest("tcp://192.0.2.2:6363", true, "tcp4://192.0.2.2:6363");
+    addTest("tcp4://192.0.2.3", true, "tcp4://192.0.2.3:6363");
+    addTest("tcp4://192.0.2.4:6363/", true, "tcp4://192.0.2.4:6363");
+    addTest("tcp4://192.0.2.5:9695", true, "tcp4://192.0.2.5:9695");
+    addTest("tcp4://192.0.2.666:6363", false, "");
+    addTest("tcp4://google-public-dns-a.google.com", true, "tcp4://8.8.8.8:6363");
+    addTest("tcp4://invalid.invalid", false, "");
+
+    // IPv4 multicast
+    addTest("tcp4://224.0.23.170:56363", false, "");
+    addTest("tcp4://224.0.23.170", false, "");
+    addTest("tcp4://all-routers.mcast.net:56363", false, "");
+  }
+
+  @Test
+  public void CanonizeTcpV6() throws FaceUri.CanonizeError
+  {
+    // IPv6 unicast
+    addTest("tcp6://[2001:db8::1]:6363", true, "tcp6://[2001:db8::1]:6363");
+    addTest("tcp://[2001:db8::1]:6363", true, "tcp6://[2001:db8::1]:6363");
+    addTest("tcp6://[2001:db8::01]:6363", true, "tcp6://[2001:db8::1]:6363");
+    addTest("tcp6://google-public-dns-a.google.com", true, "tcp6://[2001:4860:4860::8888]:6363");
+    addTest("tcp6://invalid.invalid", false, "");
+    addTest("tcp://invalid.invalid", false, "");
+
+    // IPv6 multicast
+    addTest("tcp6://[ff02::2]:56363", false, "");
+    addTest("tcp6://[ff02::2]", false, "");
+  }
+
+  @Test
+  public void ParseUnix()
+  {
+    FaceUri uri = new FaceUri();
+
+    assertTrue(uri.parse("unix:///var/run/example.sock"));
+    assertEquals(uri.getScheme(), "unix");
+    assertEquals(uri.getHost(), "");
+    assertEquals(uri.getPort(), "");
+    assertEquals(uri.getPath(), "/var/run/example.sock");
+
+    //assertEquals(uri.parse("unix://var/run/example.sock"), false);
+    // This is not a valid unix:/ URI, but the parse would treat "var" as host
+  }
+
+  @Test
+  public void ParseFd()
+  {
+    FaceUri uri = new FaceUri();
+
+    assertTrue(uri.parse("fd://6"));
+    assertEquals(uri.getScheme(), "fd");
+    assertEquals(uri.getHost(), "6");
+    assertEquals(uri.getPort(), "");
+    assertEquals(uri.getPath(), "");
+  }
+
+  @Test
+  public void ParseEther()
+  {
+    FaceUri uri = new FaceUri();
+
+    assertTrue(uri.parse("ether://[08:00:27:01:dd:01]"));
+    assertEquals(uri.getScheme(), "ether");
+    assertEquals(uri.getHost(), "08:00:27:01:dd:01");
+    assertEquals(uri.getPort(), "");
+    assertEquals(uri.getPath(), "");
+
+    assertEquals(uri.parse("ether://[08:00:27:zz:dd:01]"), false);
+  }
+
+  // @Test
+  // public void CanonizeEther()
+  // {
+  //   // not supported (yet?)
+  //   assertEquals(FaceUri.canCanonize("ether"), true);
+
+  //   assertEquals(FaceUri("ether://[08:00:27:01:01:01]").isCanonical(), true);
+  //   assertEquals(FaceUri("ether://[08:00:27:1:1:1]").isCanonical(), false);
+  //   assertEquals(FaceUri("ether://[08:00:27:01:01:01]/").isCanonical(), false);
+  //   assertEquals(FaceUri("ether://[33:33:01:01:01:01]").isCanonical(), true);
+
+  //   addTest("ether://[08:00:27:01:01:01]", true, "ether://[08:00:27:01:01:01]");
+  //   addTest("ether://[08:00:27:1:1:1]", true, "ether://[08:00:27:01:01:01]");
+  //   addTest("ether://[08:00:27:01:01:01]/", true, "ether://[08:00:27:01:01:01]");
+  //   addTest("ether://[33:33:01:01:01:01]", true, "ether://[33:33:01:01:01:01]");
+  // }
+
+  @Test
+  public void ParseDev()
+  {
+    FaceUri uri = new FaceUri();
+
+    assertTrue(uri.parse("dev://eth0"));
+    assertEquals(uri.getScheme(), "dev");
+    assertEquals(uri.getHost(), "eth0");
+    assertEquals(uri.getPort(), "");
+    assertEquals(uri.getPath(), "");
+  }
+
+  @Test
+  public void CanonizeUnsupported() throws FaceUri.CanonizeError
+  {
+    assertEquals(FaceUri.canCanonize("internal"), false);
+    assertEquals(FaceUri.canCanonize("null"), false);
+    assertEquals(FaceUri.canCanonize("unix"), false);
+    assertEquals(FaceUri.canCanonize("fd"), false);
+    assertEquals(FaceUri.canCanonize("dev"), false);
+
+    assertEquals(new FaceUri("internal://").isCanonical(), false);
+    assertEquals(new FaceUri("null://").isCanonical(), false);
+    assertEquals(new FaceUri("unix:///var/run/nfd.sock").isCanonical(), false);
+    assertEquals(new FaceUri("fd://0").isCanonical(), false);
+    assertEquals(new FaceUri("dev://eth1").isCanonical(), false);
+
+    addTest("internal://", false, "");
+    addTest("null://", false, "");
+    addTest("unix:///var/run/nfd.sock", false, "");
+    addTest("fd://0", false, "");
+    addTest("dev://eth1", false, "");
+  }
+
+  @Test
+  public void Bug1635()
+  {
+    FaceUri uri = new FaceUri();
+
+    assertTrue(uri.parse("wsclient://[::ffff:76.90.11.239]:56366"));
+    assertEquals(uri.getScheme(), "wsclient");
+    assertEquals(uri.getHost(), "76.90.11.239");
+    assertEquals(uri.getPort(), "56366");
+    assertEquals(uri.getPath(), "");
+    assertEquals(uri.toString(), "wsclient://76.90.11.239:56366");
+  }
+
+}