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.util.BitSet;
import static java.nio.charset.CodingErrorAction.REPORT;
/**

View file

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

View file

@ -4,7 +4,8 @@ import java.util.Arrays;
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 {

View file

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

View file

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

View file

@ -22,14 +22,14 @@ public class IRITest {
@Test
public void testIpv6() {
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());
}
@Test
public void testIpv6Invalid() {
URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:734o]");
assertEquals(URL.getInvalid(), iri);
assertEquals(URL.nullUrl(), iri);
}
@Test

View file

@ -14,22 +14,22 @@ public class URLParserTest {
@Test
public void testNull() {
assertEquals(URL.getInvalid(), URL.from(null));
assertEquals(URL.nullUrl(), URL.from(null));
}
@Test
public void testEmpty() {
assertEquals(URL.getInvalid(), URL.from(""));
assertEquals(URL.nullUrl(), URL.from(""));
}
@Test
public void testNewline() {
assertEquals(URL.getInvalid(), URL.from("\n"));
assertEquals(URL.nullUrl(), URL.from("\n"));
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidScheme() throws Exception {
URL url = URL.from("/:23");
public void testInvalidScheme() {
URL.from("/:23");
}
@Test
@ -62,7 +62,7 @@ public class URLParserTest {
}
@Test
public void testGopher() throws Exception {
public void testGopher() {
URL url = URL.from("gopher:/example.com/");
assertEquals("gopher:/example.com/", url.toExternalForm());
}
@ -76,7 +76,7 @@ public class URLParserTest {
}
@Test
public void testSlashAfterScheme() throws Exception {
public void testSlashAfterScheme() {
URL url = URL.from("http:/example.com/");
assertEquals("http:/example.com/", url.toExternalForm());
}
@ -96,7 +96,7 @@ public class URLParserTest {
}
@Test
public void testNetworkLocation() throws Exception {
public void testNetworkLocation() {
URL url = URL.from("//foo.bar");
assertEquals("//foo.bar", url.toExternalForm());
assertEquals("//foo.bar", url.toString());
@ -131,7 +131,7 @@ public class URLParserTest {
}
@Test
public void testBackslash() throws Exception {
public void testBackslash() {
URL url = URL.from("http://foo.com/\\@");
assertEquals("http://foo.com/@", url.toExternalForm());
}
@ -153,7 +153,6 @@ public class URLParserTest {
@Test
public void testReservedChar() throws Exception {
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"))) {
assertEquals("http://www.google.com/ig/calculator?q=1USD%3D?EUR", url.toString());
}
@ -163,7 +162,7 @@ public class URLParserTest {
@Test
public void testPlus() throws Exception {
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());
}