This commit is contained in:
Jörg Prante 2019-01-17 21:30:38 +01:00
parent b23fe28c42
commit a63557cdd9
8 changed files with 102 additions and 67 deletions

View file

@ -2,7 +2,6 @@ package org.xbib.net;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.BitSet; import java.util.BitSet;
import static java.nio.charset.CodingErrorAction.REPORT; import static java.nio.charset.CodingErrorAction.REPORT;
/** /**

View file

@ -5,5 +5,5 @@ package org.xbib.net;
*/ */
public enum ProtocolVersion { public enum ProtocolVersion {
IPV4, IPV6, IPV46, NONE IPV4, IPV6, NONE
} }

View file

@ -63,6 +63,10 @@ public class URL implements Comparable<URL> {
private static final char AT_CHAR = '@'; private static final char AT_CHAR = '@';
private static final char LEFT_BRACKET_CHAR = '[';
private static final char RIGHT_BRACKET_CHAR = ']';
private static final String DOUBLE_SLASH = "//"; private static final String DOUBLE_SLASH = "//";
private static final String EMPTY = ""; private static final String EMPTY = "";
@ -71,10 +75,20 @@ public class URL implements Comparable<URL> {
private final transient Builder builder; private final transient Builder builder;
private final transient Charset charset;
private final transient Scheme scheme; private final transient Scheme scheme;
private final transient PercentEncoder queryParamEncoder;
private final transient PercentEncoder regNameEncoder;
private final transient PercentEncoder pathEncoder;
private final transient PercentEncoder matrixEncoder;
private final transient PercentEncoder queryEncoder;
private final transient PercentEncoder fragmentEncoder;
private final String hostinfo; private final String hostinfo;
private final String path; private final String path;
@ -89,19 +103,19 @@ public class URL implements Comparable<URL> {
private URL(Builder builder) { private URL(Builder builder) {
this.builder = builder; this.builder = builder;
this.charset = builder.charset;
this.scheme = SchemeRegistry.getInstance().getScheme(builder.scheme); this.scheme = SchemeRegistry.getInstance().getScheme(builder.scheme);
this.queryParamEncoder = PercentEncoders.getQueryParamEncoder(builder.charset);
this.regNameEncoder = PercentEncoders.getRegNameEncoder(builder.charset);
this.pathEncoder = PercentEncoders.getPathEncoder(builder.charset);
this.matrixEncoder = PercentEncoders.getMatrixEncoder(builder.charset);
this.queryEncoder = PercentEncoders.getQueryEncoder(builder.charset);
this.fragmentEncoder = PercentEncoders.getFragmentEncoder(builder.charset);
this.hostinfo = encodeHostInfo(); this.hostinfo = encodeHostInfo();
this.path = encodePath(); this.path = encodePath();
this.query = encodeQuery(); this.query = encodeQuery();
this.fragment = encodeFragment(); this.fragment = encodeFragment();
} }
/**
* A special, scheme-less URL denoting the fact that this URL should be considered as invalid.
*/
private static final URL INVALID = URL.builder().build();
public static Builder file() { public static Builder file() {
return new Builder().scheme(Scheme.FILE); return new Builder().scheme(Scheme.FILE);
} }
@ -234,8 +248,14 @@ public class URL implements Comparable<URL> {
return new Resolver(URL.create(base)); return new Resolver(URL.create(base));
} }
public static URL getInvalid() { private static final URL NULL_URL = URL.builder().build();
return INVALID;
/**
* Return a special URL denoting the fact that this URL should be considered as invalid.
* The URL has a null scheme.
*/
public static URL nullUrl() {
return NULL_URL;
} }
public static URL from(String input) { public static URL from(String input) {
@ -331,6 +351,10 @@ public class URL implements Comparable<URL> {
return builder.userInfo; return builder.userInfo;
} }
/**
* Get the user of the user info.
* @return the user
*/
public String getUser() { public String getUser() {
if (builder.userInfo == null) { if (builder.userInfo == null) {
return null; return null;
@ -355,6 +379,10 @@ public class URL implements Comparable<URL> {
return builder.host; return builder.host;
} }
/**
* Get the decoded host name.
* @return the decoded host name
*/
public String getDecodedHost() { public String getDecodedHost() {
return decode(builder.host); return decode(builder.host);
} }
@ -372,13 +400,16 @@ public class URL implements Comparable<URL> {
} }
/** /**
* Get the encoded path ('/path/to/my/file.html') of the {@code URL} if it exists. * Get the path ('/path/to/my/file.html') of the {@code URL} if it exists.
* @return the encoded path * @return the path
*/ */
public String getPath() { public String getPath() {
return path; return path;
} }
/**
* Get the percent-decoded path of the {@code URL} if it exists.
*/
public String getDecodedPath() { public String getDecodedPath() {
return decode(path); return decode(path);
} }
@ -458,18 +489,16 @@ public class URL implements Comparable<URL> {
private String toInternalForm(boolean withFragment) { private String toInternalForm(boolean withFragment) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (!isNullOrEmpty(builder.scheme)) { if (!isNullOrEmpty(builder.scheme)) {
sb.append(builder.scheme).append(':'); sb.append(builder.scheme).append(COLON_CHAR);
} }
if (isOpaque()) { if (isOpaque()) {
sb.append(builder.schemeSpecificPart); sb.append(builder.schemeSpecificPart);
} else { } else {
appendHostInfo(sb, false, true); appendHostInfo(sb, false, true);
appendPath(sb, false); appendPath(sb, false);
if (!isNullOrEmpty(query)) { appendQuery(sb, false, true);
sb.append(QUESTION_CHAR).append(query); if (withFragment) {
} appendFragment(sb, false, true);
if (!isNullOrEmpty(fragment) && withFragment) {
sb.append(NUMBER_SIGN_CHAR).append(fragment);
} }
} }
return sb.toString(); return sb.toString();
@ -478,7 +507,7 @@ public class URL implements Comparable<URL> {
private String writeExternalForm() { private String writeExternalForm() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (!isNullOrEmpty(builder.scheme)) { if (!isNullOrEmpty(builder.scheme)) {
sb.append(builder.scheme).append(':'); sb.append(builder.scheme).append(COLON_CHAR);
} }
if (isOpaque()) { if (isOpaque()) {
sb.append(builder.schemeSpecificPart); sb.append(builder.schemeSpecificPart);
@ -521,17 +550,16 @@ public class URL implements Comparable<URL> {
if (s != null && !s.equals(builder.hostAddress)) { if (s != null && !s.equals(builder.hostAddress)) {
sb.append(s); sb.append(s);
} else if (builder.hostAddress != null) { } else if (builder.hostAddress != null) {
sb.append("[").append(builder.hostAddress).append("]"); sb.append(LEFT_BRACKET_CHAR).append(builder.hostAddress).append(RIGHT_BRACKET_CHAR);
} }
break; break;
case IPV4: case IPV4:
case IPV46:
sb.append(builder.host); sb.append(builder.host);
break; break;
default: default:
if (encoded) { if (encoded) {
try { try {
String encodedHostName = PercentEncoders.getRegNameEncoder(charset).encode(builder.host); String encodedHostName = regNameEncoder.encode(builder.host);
validateHostnameCharacters(encodedHostName); validateHostnameCharacters(encodedHostName);
sb.append(encodedHostName); sb.append(encodedHostName);
} catch (CharacterCodingException e) { } catch (CharacterCodingException e) {
@ -545,7 +573,7 @@ public class URL implements Comparable<URL> {
} else { } else {
if (encoded) { if (encoded) {
try { try {
String encodedHostName = PercentEncoders.getRegNameEncoder(charset).encode(builder.host); String encodedHostName = regNameEncoder.encode(builder.host);
validateHostnameCharacters(encodedHostName); validateHostnameCharacters(encodedHostName);
sb.append(encodedHostName); sb.append(encodedHostName);
} catch (CharacterCodingException e) { } catch (CharacterCodingException e) {
@ -556,7 +584,7 @@ public class URL implements Comparable<URL> {
} }
} }
if (scheme != null && builder.port != null && builder.port != scheme.getDefaultPort()) { if (scheme != null && builder.port != null && builder.port != scheme.getDefaultPort()) {
sb.append(':'); sb.append(COLON_CHAR);
if (builder.port != -1) { if (builder.port != -1) {
sb.append(builder.port); sb.append(builder.port);
} }
@ -587,8 +615,6 @@ public class URL implements Comparable<URL> {
} }
private void appendPath(StringBuilder sb, boolean encoded) { private void appendPath(StringBuilder sb, boolean encoded) {
PercentEncoder pathEncoder = PercentEncoders.getPathEncoder(charset);
PercentEncoder matrixEncoder = PercentEncoders.getMatrixEncoder(charset);
Iterator<PathSegment> it = builder.pathSegments.iterator(); Iterator<PathSegment> it = builder.pathSegments.iterator();
while (it.hasNext()) { while (it.hasNext()) {
PathSegment pathSegment = it.next(); PathSegment pathSegment = it.next();
@ -623,7 +649,6 @@ public class URL implements Comparable<URL> {
sb.append(QUESTION_CHAR); sb.append(QUESTION_CHAR);
} }
Iterator<QueryParameters.Pair<String, String>> it = builder.queryParams.iterator(); Iterator<QueryParameters.Pair<String, String>> it = builder.queryParams.iterator();
PercentEncoder queryParamEncoder = PercentEncoders.getQueryParamEncoder(charset);
while (it.hasNext()) { while (it.hasNext()) {
QueryParameters.Pair<String, String> queryParam = it.next(); QueryParameters.Pair<String, String> queryParam = it.next();
try { try {
@ -645,7 +670,7 @@ public class URL implements Comparable<URL> {
} }
if (encoded) { if (encoded) {
try { try {
sb.append(PercentEncoders.getQueryEncoder(charset).encode(builder.query)); sb.append(queryEncoder.encode(builder.query));
} catch (CharacterCodingException e) { } catch (CharacterCodingException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
@ -668,7 +693,7 @@ public class URL implements Comparable<URL> {
} }
if (encoded) { if (encoded) {
try { try {
sb.append(PercentEncoders.getFragmentEncoder(charset).encode(builder.fragment)); sb.append(fragmentEncoder.encode(builder.fragment));
} catch (CharacterCodingException e) { } catch (CharacterCodingException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
@ -713,10 +738,13 @@ public class URL implements Comparable<URL> {
} }
/** /**
* The URL Builder embedded class is for building an URL by fluent API methods. * The URL builder class is required for building an URL. It uses fluent API methods
* and pre-processes paralameter accordingly.
*/ */
public static class Builder { public static class Builder {
private PercentEncoder regNameEncoder;
private final PercentDecoder percentDecoder; private final PercentDecoder percentDecoder;
private final QueryParameters queryParams; private final QueryParameters queryParams;
@ -746,20 +774,26 @@ public class URL implements Comparable<URL> {
private boolean fatalResolveErrorsEnabled; private boolean fatalResolveErrorsEnabled;
private Builder() { private Builder() {
charset(StandardCharsets.UTF_8);
this.percentDecoder = new PercentDecoder(); this.percentDecoder = new PercentDecoder();
this.queryParams = new QueryParameters(); this.queryParams = new QueryParameters();
this.pathSegments = new ArrayList<>(); this.pathSegments = new ArrayList<>();
this.charset = StandardCharsets.UTF_8;
} }
/**
* Set the character set of the URL. Default is UTF-8.
* @param charset the chaarcter set
* @return this builder
*/
public Builder charset(Charset charset) { public Builder charset(Charset charset) {
this.charset = charset; this.charset = charset;
this.regNameEncoder = PercentEncoders.getRegNameEncoder(charset);
return this; return this;
} }
public Builder scheme(String scheme) { public Builder scheme(String scheme) {
if (!isNullOrEmpty(scheme)) { if (!isNullOrEmpty(scheme)) {
validateSchemeCharacters(scheme.toLowerCase()); validateSchemeCharacters(scheme.toLowerCase(Locale.ROOT));
this.scheme = scheme; this.scheme = scheme;
} }
return this; return this;
@ -777,8 +811,8 @@ public class URL implements Comparable<URL> {
public Builder userInfo(String user, String pass) { public Builder userInfo(String user, String pass) {
try { try {
this.userInfo = PercentEncoders.getRegNameEncoder(charset).encode(user) + ':' + // allow colons in usernames and passwords by percent-encoding them here
PercentEncoders.getRegNameEncoder(charset).encode(pass); this.userInfo = regNameEncoder.encode(user) + COLON_CHAR + regNameEncoder.encode(pass);
} catch (MalformedInputException | UnmappableCharacterException e) { } catch (MalformedInputException | UnmappableCharacterException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
@ -827,8 +861,8 @@ public class URL implements Comparable<URL> {
} }
logger.log(Level.WARNING, e.getMessage(), e); logger.log(Level.WARNING, e.getMessage(), e);
if (e.getMessage() != null && !e.getMessage().endsWith("invalid IPv6 address") && if (e.getMessage() != null && !e.getMessage().endsWith("invalid IPv6 address") &&
hostname.charAt(0) != '[' && hostname.charAt(0) != LEFT_BRACKET_CHAR &&
hostname.charAt(hostname.length() - 1) != ']') { hostname.charAt(hostname.length() - 1) != RIGHT_BRACKET_CHAR) {
try { try {
String idna = IDN.toASCII(percentDecoder.decode(hostname)); String idna = IDN.toASCII(percentDecoder.decode(hostname));
host(idna, ProtocolVersion.NONE); host(idna, ProtocolVersion.NONE);
@ -969,7 +1003,7 @@ public class URL implements Comparable<URL> {
} }
/** /**
* * A URL parser class.
*/ */
public static class Parser { public static class Parser {
@ -987,13 +1021,13 @@ public class URL implements Comparable<URL> {
public URL parse(String input, boolean resolve) public URL parse(String input, boolean resolve)
throws URLSyntaxException, MalformedInputException, UnmappableCharacterException { throws URLSyntaxException, MalformedInputException, UnmappableCharacterException {
if (isNullOrEmpty(input)) { if (isNullOrEmpty(input)) {
return INVALID; return NULL_URL;
} }
if (input.indexOf('\n') >= 0) { if (input.indexOf('\n') >= 0) {
return INVALID; return NULL_URL;
} }
if (input.indexOf('\t') >= 0) { if (input.indexOf('\t') >= 0) {
return INVALID; return NULL_URL;
} }
String remaining = parseScheme(builder, input); String remaining = parseScheme(builder, input);
if (remaining != null) { if (remaining != null) {
@ -1011,7 +1045,7 @@ public class URL implements Comparable<URL> {
String host = (pos >= 0 ? remaining.substring(0, pos) : remaining); String host = (pos >= 0 ? remaining.substring(0, pos) : remaining);
parseHostAndPort(builder, parseUserInfo(builder, host), resolve); parseHostAndPort(builder, parseUserInfo(builder, host), resolve);
if (builder.host == null) { if (builder.host == null) {
return INVALID; return NULL_URL;
} }
remaining = pos >= 0 ? remaining.substring(pos) : EMPTY; remaining = pos >= 0 ? remaining.substring(pos) : EMPTY;
} }
@ -1053,14 +1087,14 @@ public class URL implements Comparable<URL> {
private void parseHostAndPort(Builder builder, String rawHost, boolean resolve) private void parseHostAndPort(Builder builder, String rawHost, boolean resolve)
throws URLSyntaxException { throws URLSyntaxException {
String host = rawHost; String host = rawHost;
if (host.indexOf('[') == 0) { if (host.indexOf(LEFT_BRACKET_CHAR) == 0) {
int i = host.lastIndexOf(']'); int i = host.lastIndexOf(RIGHT_BRACKET_CHAR);
if (i >= 0) { if (i >= 0) {
builder.port(parsePort(host.substring(i + 1))); builder.port(parsePort(host.substring(i + 1)));
host = host.substring(1, i); host = host.substring(1, i);
} }
} else { } else {
int i = host.indexOf(':'); int i = host.indexOf(COLON_CHAR);
if (i >= 0) { if (i >= 0) {
builder.port(parsePort(host.substring(i))); builder.port(parsePort(host.substring(i)));
host = host.substring(0, i); host = host.substring(0, i);
@ -1077,7 +1111,7 @@ public class URL implements Comparable<URL> {
if (portStr == null || portStr.isEmpty()) { if (portStr == null || portStr.isEmpty()) {
return null; return null;
} }
int i = portStr.indexOf(":"); int i = portStr.indexOf(COLON_CHAR);
if (i >= 0) { if (i >= 0) {
portStr = portStr.substring(i + 1); portStr = portStr.substring(i + 1);
if (portStr.isEmpty()) { if (portStr.isEmpty()) {
@ -1179,8 +1213,7 @@ public class URL implements Comparable<URL> {
} }
/** /**
* The URL resolver class is an embedded class for resolving a relative URL specification to * The URL resolver class is a class for resolving a relative URL specification to a base URL.
* a base URL.
*/ */
public static class Resolver { public static class Resolver {
@ -1204,7 +1237,7 @@ public class URL implements Comparable<URL> {
public URL resolve(URL relative) public URL resolve(URL relative)
throws URLSyntaxException { throws URLSyntaxException {
if (relative == null || relative == INVALID) { if (relative == null || relative.equals(NULL_URL)) {
throw new URLSyntaxException("relative URL is invalid"); throw new URLSyntaxException("relative URL is invalid");
} }
if (!base.isAbsolute()) { if (!base.isAbsolute()) {

View file

@ -4,7 +4,8 @@ import java.util.Arrays;
import java.util.BitSet; import java.util.BitSet;
/** /**
* * The character matcher class is a fast table-based matcher class, able to match whitespace, control,
* literals, persent or hexdigit character groups.
*/ */
public abstract class CharMatcher { public abstract class CharMatcher {

View file

@ -3,6 +3,8 @@ package org.xbib.net.scheme;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.net.path.PathNormalizer; import org.xbib.net.path.PathNormalizer;
import java.util.Locale;
/** /**
* HTTP scheme. * HTTP scheme.
*/ */
@ -20,7 +22,7 @@ class HttpScheme extends AbstractScheme {
public URL normalize(URL url) { public URL normalize(URL url) {
String host = url.getHost(); String host = url.getHost();
if (host != null) { if (host != null) {
host = host.toLowerCase(); host = host.toLowerCase(Locale.ROOT);
} }
return URL.builder() return URL.builder()
.scheme(url.getScheme()) .scheme(url.getScheme())

View file

@ -1,6 +1,7 @@
package org.xbib.net.scheme; package org.xbib.net.scheme;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader; import java.util.ServiceLoader;
@ -62,7 +63,7 @@ public final class SchemeRegistry {
return false; return false;
} }
if (!schemes.containsKey(name)) { if (!schemes.containsKey(name)) {
schemes.put(name.toLowerCase(), scheme); schemes.put(name.toLowerCase(Locale.ROOT), scheme);
return true; return true;
} else { } else {
return false; return false;
@ -73,7 +74,7 @@ public final class SchemeRegistry {
if (scheme == null) { if (scheme == null) {
return null; return null;
} }
Scheme s = schemes.get(scheme.toLowerCase()); Scheme s = schemes.get(scheme.toLowerCase(Locale.ROOT));
return s != null ? s : new DefaultScheme(scheme); return s != null ? s : new DefaultScheme(scheme);
} }
} }

View file

@ -22,14 +22,14 @@ public class IRITest {
@Test @Test
public void testIpv6() { public void testIpv6() {
URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]"); URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]");
assertTrue(iri.getProtocolVersion().equals(ProtocolVersion.IPV6)); assertEquals(iri.getProtocolVersion(), ProtocolVersion.IPV6);
assertEquals("http://[2001:db8:85a3:8d3:1319:8a2e:370:7344]", iri.toString()); assertEquals("http://[2001:db8:85a3:8d3:1319:8a2e:370:7344]", iri.toString());
} }
@Test @Test
public void testIpv6Invalid() { public void testIpv6Invalid() {
URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:734o]"); URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:734o]");
assertEquals(URL.getInvalid(), iri); assertEquals(URL.nullUrl(), iri);
} }
@Test @Test

View file

@ -14,22 +14,22 @@ public class URLParserTest {
@Test @Test
public void testNull() { public void testNull() {
assertEquals(URL.getInvalid(), URL.from(null)); assertEquals(URL.nullUrl(), URL.from(null));
} }
@Test @Test
public void testEmpty() { public void testEmpty() {
assertEquals(URL.getInvalid(), URL.from("")); assertEquals(URL.nullUrl(), URL.from(""));
} }
@Test @Test
public void testNewline() { public void testNewline() {
assertEquals(URL.getInvalid(), URL.from("\n")); assertEquals(URL.nullUrl(), URL.from("\n"));
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void testInvalidScheme() throws Exception { public void testInvalidScheme() {
URL url = URL.from("/:23"); URL.from("/:23");
} }
@Test @Test
@ -62,7 +62,7 @@ public class URLParserTest {
} }
@Test @Test
public void testGopher() throws Exception { public void testGopher() {
URL url = URL.from("gopher:/example.com/"); URL url = URL.from("gopher:/example.com/");
assertEquals("gopher:/example.com/", url.toExternalForm()); assertEquals("gopher:/example.com/", url.toExternalForm());
} }
@ -76,7 +76,7 @@ public class URLParserTest {
} }
@Test @Test
public void testSlashAfterScheme() throws Exception { public void testSlashAfterScheme() {
URL url = URL.from("http:/example.com/"); URL url = URL.from("http:/example.com/");
assertEquals("http:/example.com/", url.toExternalForm()); assertEquals("http:/example.com/", url.toExternalForm());
} }
@ -96,7 +96,7 @@ public class URLParserTest {
} }
@Test @Test
public void testNetworkLocation() throws Exception { public void testNetworkLocation() {
URL url = URL.from("//foo.bar"); URL url = URL.from("//foo.bar");
assertEquals("//foo.bar", url.toExternalForm()); assertEquals("//foo.bar", url.toExternalForm());
assertEquals("//foo.bar", url.toString()); assertEquals("//foo.bar", url.toString());
@ -131,7 +131,7 @@ public class URLParserTest {
} }
@Test @Test
public void testBackslash() throws Exception { public void testBackslash() {
URL url = URL.from("http://foo.com/\\@"); URL url = URL.from("http://foo.com/\\@");
assertEquals("http://foo.com/@", url.toExternalForm()); assertEquals("http://foo.com/@", url.toExternalForm());
} }
@ -153,7 +153,6 @@ public class URLParserTest {
@Test @Test
public void testReservedChar() throws Exception { public void testReservedChar() throws Exception {
URL url = URL.from("http://www.google.com/ig/calculator?q=1USD=?EUR"); URL url = URL.from("http://www.google.com/ig/calculator?q=1USD=?EUR");
//assertEquals("http://www.google.com/ig/calculator?q=1USD=?EUR", url.toString());
if ("false".equals(System.getProperty("java.net.preferIPv6Addresses"))) { if ("false".equals(System.getProperty("java.net.preferIPv6Addresses"))) {
assertEquals("http://www.google.com/ig/calculator?q=1USD%3D?EUR", url.toString()); assertEquals("http://www.google.com/ig/calculator?q=1USD%3D?EUR", url.toString());
} }
@ -163,7 +162,7 @@ public class URLParserTest {
@Test @Test
public void testPlus() throws Exception { public void testPlus() throws Exception {
URL url = URL.from("http://foobar:8080/test/print?value=%EA%B0%80+%EB%82%98"); URL url = URL.from("http://foobar:8080/test/print?value=%EA%B0%80+%EB%82%98");
assertEquals("http://foobar:8080/test/print?value=%EA%B0%80%2B%EB%82%98", url.toString()); assertEquals("http://foobar:8080/test/print?value=%EA%B0%80%2B%EB%82%98", url.toExternalForm());
assertRoundTrip(url.toExternalForm()); assertRoundTrip(url.toExternalForm());
} }