better exception method signatures, avoid null URLs as return value, remove internal package with single class

This commit is contained in:
Jörg Prante 2017-07-25 15:10:26 +02:00
parent 286ab07793
commit 93e201db8b
13 changed files with 149 additions and 231 deletions

View file

@ -1,6 +1,6 @@
group = org.xbib
name = net
version = 1.0.1
version = 1.0.2
junit.version = 4.12
asciidoclet.version = 1.5.4

View file

@ -1,118 +0,0 @@
package org.xbib.net;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Contains a simple context for namespaces.
*/
public class SimpleNamespaceContext {
private static final Logger logger = Logger.getLogger(SimpleNamespaceContext.class.getName());
private static final String DEFAULT_RESOURCE =
SimpleNamespaceContext.class.getPackage().getName().replace('.', '/') + '/' + "namespace";
private static final SimpleNamespaceContext DEFAULT_CONTEXT = newDefaultInstance();
// sort namespace by length in descending order, useful for compacting prefix
protected final SortedMap<String, String> namespaces = new TreeMap<>();
private final SortedMap<String, Set<String>> prefixes = new TreeMap<>();
protected SimpleNamespaceContext() {
}
protected SimpleNamespaceContext(ResourceBundle bundle) {
Enumeration<String> en = bundle.getKeys();
while (en.hasMoreElements()) {
String prefix = en.nextElement();
String namespace = bundle.getString(prefix);
addNamespace(prefix, namespace);
}
}
public static SimpleNamespaceContext getInstance() {
return DEFAULT_CONTEXT;
}
/**
* Empty namespace context.
*
* @return an XML namespace context
*/
public static SimpleNamespaceContext newInstance() {
return new SimpleNamespaceContext();
}
public static SimpleNamespaceContext newDefaultInstance() {
return newInstance(DEFAULT_RESOURCE);
}
/**
* Use thread context class laoder to instantiate a namespace context.
* @param bundleName the resource bundle name
* @return XML namespace context
*/
public static SimpleNamespaceContext newInstance(String bundleName) {
return newInstance(bundleName, Locale.getDefault(), Thread.currentThread().getContextClassLoader());
}
public static SimpleNamespaceContext newInstance(String bundleName, Locale locale, ClassLoader classLoader) {
try {
return new SimpleNamespaceContext(ResourceBundle.getBundle(bundleName, locale, classLoader));
} catch (MissingResourceException e) {
logger.log(Level.WARNING, e.getMessage(), e);
return new SimpleNamespaceContext();
}
}
public void addNamespace(String prefix, String namespace) {
namespaces.put(prefix, namespace);
if (prefixes.containsKey(namespace)) {
prefixes.get(namespace).add(prefix);
} else {
Set<String> set = new HashSet<>();
set.add(prefix);
prefixes.put(namespace, set);
}
}
public SortedMap<String, String> getNamespaces() {
return namespaces;
}
public String getNamespaceURI(String prefix) {
if (prefix == null) {
return null;
}
return namespaces.getOrDefault(prefix, null);
}
public String getPrefix(String namespaceURI) {
Iterator<String> it = getPrefixes(namespaceURI);
return it != null && it.hasNext() ? it.next() : null;
}
public Iterator<String> getPrefixes(String namespace) {
if (namespace == null) {
throw new IllegalArgumentException("namespace URI cannot be null");
}
return prefixes.containsKey(namespace) ?
prefixes.get(namespace).iterator() : null;
}
@Override
public String toString() {
return namespaces.toString();
}
}

View file

@ -22,9 +22,21 @@ import java.util.logging.Level;
import java.util.logging.Logger;
/**
* {@link URL} is a Java implementation of the Uniform Resource Identifier ({@code RFC 3986})
*
* A Uniform Resource Locator (URL) is a compact representation of the
* location and access method for a resource available via the Internet.
*
* Historically, there are many different forms of internet resource representations, for example,
* the URL (RFC 1738 as of 1994), the URI (RFC 2396 as of 1998), and IRI (RFC 3987 as of 2005),
* and most of them have updated specifications.
*
* {@link URL} is a Java implementation that serves as a universal point of handling all
* different forms. It follows the syntax of the Uniform Resource Identifier ({@code RFC 3986})
* in accordance with the link:https://url.spec.whatwg.org/[{@code WHATWG} URL standard].
*
* The reason for the name {@code URL} is merely because of the popularity of the name, which
* overweighs the URI or IRI popularity.
*
* [source,java]
* --
* URL url = URL.http().resolveFromHost("google.com").build();
@ -87,6 +99,9 @@ public class URL implements Serializable {
this.fragment = encodeFragment();
}
/**
* A special, scheme-less URL denoting the fact that this URL should be considered as invalid.
*/
public static final URL INVALID = URL.builder().build();
public static Builder file() {
@ -224,7 +239,7 @@ public class URL implements Serializable {
public static URL from(String input) {
try {
return parser().parse(input, true);
} catch (CharacterCodingException e) {
} catch (URLSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
@ -232,13 +247,25 @@ public class URL implements Serializable {
public static URL create(String input) {
try {
return parser().parse(input, false);
} catch (CharacterCodingException e) {
} catch (URLSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
public URL resolve(String spec) {
return new Resolver(this).resolve(spec);
try {
return new Resolver(this).resolve(spec);
} catch (URLSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
public URL resolve(URL spec) {
try {
return new Resolver(this).resolve(spec);
} catch (URLSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
public String decode(String input) {
@ -627,7 +654,7 @@ public class URL implements Serializable {
}
/**
*
* The URL Builder embedded class is for building an URL by fluent API methods.
*/
public static class Builder {
@ -706,7 +733,7 @@ public class URL implements Serializable {
return this;
}
public Builder resolveFromHost(String hostname) throws CharacterCodingException {
public Builder resolveFromHost(String hostname) {
if (hostname == null) {
return this;
}
@ -729,8 +756,12 @@ public class URL implements Serializable {
if (e.getMessage() != null && !e.getMessage().endsWith("invalid IPv6 address") &&
hostname.charAt(0) != '[' &&
hostname.charAt(hostname.length() - 1) != ']') {
String idna = IDN.toASCII(percentDecoder.decode(hostname));
host(idna, ProtocolVersion.NONE);
try {
String idna = IDN.toASCII(percentDecoder.decode(hostname));
host(idna, ProtocolVersion.NONE);
} catch (CharacterCodingException e2) {
throw new IllegalArgumentException(e2);
}
}
}
return this;
@ -863,27 +894,26 @@ public class URL implements Serializable {
*/
public static class Parser {
private final PercentDecoder percentDecoder;
private final Builder builder;
private Parser() {
percentDecoder = new PercentDecoder();
builder = new Builder();
}
public URL parse(String input) throws CharacterCodingException {
public URL parse(String input) throws URLSyntaxException {
return parse(input, true);
}
public URL parse(String input, boolean resolve) throws CharacterCodingException {
public URL parse(String input, boolean resolve) throws URLSyntaxException {
if (isNullOrEmpty(input)) {
return null;
return INVALID;
}
if (input.indexOf('\n') >= 0) {
return null;
return INVALID;
}
if (input.indexOf('\t') >= 0) {
return null;
return INVALID;
}
Builder builder = new Builder();
String remaining = parseScheme(builder, input);
if (remaining != null) {
remaining = remaining.replace('\\', SEPARATOR_CHAR);
@ -906,7 +936,11 @@ public class URL implements Serializable {
}
}
if (!isNullOrEmpty(remaining)) {
parsePathWithQueryAndFragment(builder, remaining);
try {
parsePathWithQueryAndFragment(builder, remaining);
} catch (CharacterCodingException e) {
throw new URLSyntaxException(e);
}
}
}
return builder.build();
@ -933,7 +967,7 @@ public class URL implements Serializable {
return remaining;
}
private void parseHostAndPort(Builder builder, String host, boolean resolve) throws CharacterCodingException {
private void parseHostAndPort(Builder builder, String host, boolean resolve) throws URLSyntaxException {
if (host.indexOf('[') == 0) {
int i = host.lastIndexOf(']');
if (i >= 0) {
@ -954,7 +988,7 @@ public class URL implements Serializable {
}
}
private Integer parsePort(String portStr) {
private Integer parsePort(String portStr) throws URLSyntaxException {
if (portStr == null || portStr.isEmpty()) {
return null;
}
@ -970,20 +1004,21 @@ public class URL implements Serializable {
if (port > 0 && port < 65536) {
return port;
} else {
throw new IllegalArgumentException("invalid port");
throw new URLSyntaxException("invalid port");
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("no numeric port: " + portStr);
throw new URLSyntaxException("no numeric port: " + portStr);
}
}
void parsePathWithQueryAndFragment(Builder builder, String input) throws CharacterCodingException {
private void parsePathWithQueryAndFragment(Builder builder, String input)
throws MalformedInputException, UnmappableCharacterException {
if (input == null) {
return;
}
int i = input.lastIndexOf(NUMBER_SIGN_CHAR);
if (i >= 0) {
builder.fragment(percentDecoder.decode(input.substring(i + 1)));
builder.fragment(builder.percentDecoder.decode(input.substring(i + 1)));
input = input.substring(0, i);
}
i = input.indexOf(QUESTION_CHAR);
@ -1006,7 +1041,8 @@ public class URL implements Serializable {
Pair<String, String> pathWithMatrixElem = indexOf(SEMICOLON_CHAR, t);
String matrixElem = pathWithMatrixElem.getFirst();
Pair<String, String> p = indexOf(EQUAL_CHAR, matrixElem);
builder.matrixParam(percentDecoder.decode(p.getFirst()), percentDecoder.decode(p.getSecond()));
builder.matrixParam(builder.percentDecoder.decode(p.getFirst()),
builder.percentDecoder.decode(p.getSecond()));
t = pathWithMatrixElem.getSecond();
}
} else {
@ -1016,10 +1052,11 @@ public class URL implements Serializable {
Pair<String, String> pathWithMatrixElem = indexOf(SEMICOLON_CHAR, t);
String segment = pathWithMatrixElem.getFirst();
if (i == 0) {
builder.pathSegment(percentDecoder.decode(segment));
builder.pathSegment(builder.percentDecoder.decode(segment));
} else {
Pair<String, String> p = indexOf(EQUAL_CHAR, segment);
builder.matrixParam(percentDecoder.decode(p.getFirst()), percentDecoder.decode(p.getSecond()));
builder.matrixParam(builder.percentDecoder.decode(p.getFirst()),
builder.percentDecoder.decode(p.getSecond()));
}
t = pathWithMatrixElem.getSecond();
i++;
@ -1033,7 +1070,8 @@ public class URL implements Serializable {
}
}
private void parseQuery(Builder builder, String query) throws CharacterCodingException {
private void parseQuery(Builder builder, String query)
throws MalformedInputException, UnmappableCharacterException {
if (query == null) {
return;
}
@ -1042,18 +1080,19 @@ public class URL implements Serializable {
Pair<String, String> p = indexOf(AMPERSAND_CHAR, s);
Pair<String, String> param = indexOf(EQUAL_CHAR, p.getFirst());
if (!isNullOrEmpty(param.getFirst())) {
builder.queryParam(percentDecoder.decode(param.getFirst()), percentDecoder.decode(param.getSecond()));
builder.queryParam(builder.percentDecoder.decode(param.getFirst()),
builder.percentDecoder.decode(param.getSecond()));
}
s = p.getSecond();
}
if (builder.queryParams.isEmpty()) {
builder.query(percentDecoder.decode(query));
builder.query(builder.percentDecoder.decode(query));
} else {
builder.query(query);
}
}
Pair<String, String> indexOf(char ch, String input) {
private Pair<String, String> indexOf(char ch, String input) {
int i = input.indexOf(ch);
String k = i >= 0 ? input.substring(0, i) : input;
String v = i >= 0 ? input.substring(i + 1) : null;
@ -1062,7 +1101,8 @@ public class URL implements Serializable {
}
/**
*
* The URL resolver class is an embedded class for resolving a relative URL specification to
* a base URL.
*/
public static class Resolver {
@ -1072,27 +1112,23 @@ public class URL implements Serializable {
this.base = base;
}
public URL resolve(String relative) {
public URL resolve(String relative) throws URLSyntaxException {
if (relative == null) {
return null;
}
if (relative.isEmpty()) {
return base;
}
try {
URL url = parser().parse(relative);
return url != null ? resolve(url) : null;
} catch (CharacterCodingException e) {
throw new IllegalArgumentException(e);
}
URL url = parser().parse(relative);
return resolve(url);
}
public URL resolve(URL relative) throws CharacterCodingException {
if (relative == null) {
return null;
public URL resolve(URL relative) throws URLSyntaxException {
if (relative == null || relative == INVALID) {
throw new URLSyntaxException("relative URL is invalid");
}
if (!base.isAbsolute()) {
throw new IllegalArgumentException("base is not absolute");
throw new URLSyntaxException("base URL is not absolute");
}
Builder builder = new Builder();
if (relative.isOpaque()) {

View file

@ -3,15 +3,15 @@ package org.xbib.net;
/**
*
*/
public class IRISyntaxException extends RuntimeException {
public class URLSyntaxException extends Exception {
private static final long serialVersionUID = 1813084470937980392L;
IRISyntaxException(String message) {
URLSyntaxException(String message) {
super(message);
}
IRISyntaxException(Throwable cause) {
URLSyntaxException(Throwable cause) {
super(cause);
}

View file

@ -1,26 +0,0 @@
package org.xbib.net.internal;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A simple LRU cache, based on a {@link LinkedHashMap}.
*
* @param <K> the key type parameter
* @param <V> the vale type parameter
*/
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = -2795566703268944901L;
private final int cacheSize;
public LRUCache(int cacheSize) {
super(16, 0.75f, true);
this.cacheSize = cacheSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() >= cacheSize;
}
}

View file

@ -1,4 +0,0 @@
/**
* Classes for internal use in the {@code org.xbib.net} package.
*/
package org.xbib.net.internal;

View file

@ -1,11 +1,11 @@
package org.xbib.net.path;
import org.xbib.net.QueryParameters;
import org.xbib.net.internal.LRUCache;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
@ -16,9 +16,11 @@ public class PathMatcher {
private static final String DEFAULT_PATH_SEPARATOR = "/";
private final Map<String, List<String>> tokenizedPatternCache = Collections.synchronizedMap(new LRUCache<>(1024));
private final Map<String, List<String>> tokenizedPatternCache =
Collections.synchronizedMap(new LRUCache<>(1024));
private final Map<String, PathStringMatcher> stringMatcherCache = Collections.synchronizedMap(new LRUCache<>(1024));
private final Map<String, PathStringMatcher> stringMatcherCache =
Collections.synchronizedMap(new LRUCache<>(1024));
private String pathSeparator;
@ -28,8 +30,6 @@ public class PathMatcher {
private boolean trimTokens = true;
private volatile boolean cachePatterns = true;
public PathMatcher() {
this(DEFAULT_PATH_SEPARATOR);
}
@ -60,10 +60,6 @@ public class PathMatcher {
return queryParameters;
}
public void setCachePatterns(boolean cachePatterns) {
this.cachePatterns = cachePatterns;
}
public Map<String, PathStringMatcher> stringMatcherCache() {
return stringMatcherCache;
}
@ -274,9 +270,7 @@ public class PathMatcher {
}
private List<String> tokenizePattern(String pattern) {
return cachePatterns ?
tokenizedPatternCache.computeIfAbsent(pattern, this::tokenizePath) :
tokenizePath(pattern);
return tokenizedPatternCache.computeIfAbsent(pattern, this::tokenizePath);
}
private List<String> tokenizePath(String path) {
@ -306,8 +300,28 @@ public class PathMatcher {
}
private PathStringMatcher getStringMatcher(String pattern) {
return cachePatterns ?
stringMatcherCache.computeIfAbsent(pattern, p -> new PathStringMatcher(p, this.caseSensitive)) :
new PathStringMatcher(pattern, this.caseSensitive);
return stringMatcherCache.computeIfAbsent(pattern, p -> new PathStringMatcher(p, this.caseSensitive));
}
/**
* A simple LRU cache, based on a {@link LinkedHashMap}.
*
* @param <K> the key type parameter
* @param <V> the vale type parameter
*/
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = -2795566703268944901L;
private final int cacheSize;
LRUCache(int cacheSize) {
super(16, 0.75f, true);
this.cacheSize = cacheSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() >= cacheSize;
}
}
}

View file

@ -3,7 +3,6 @@ package org.xbib.net.scheme;
/**
* The mailto scheme.
*
* @see <a href="https://tools.ietf.org/html/rfc2368">mailto RFC</a>
*/
public class MailtoScheme extends AbstractScheme {

View file

@ -261,6 +261,13 @@ public class URLBuilderTest {
assertEquals("http://foo.com?q%23%2B", s);
}
@Test
public void testNewBuilder() {
URL.Builder builder = URL.from("http://google.com:8008/foobar").newBuilder();
builder.scheme("https");
assertEquals("https://google.com:8008/foobar", builder.build().toString());
}
private void assertUrl(String urlString, String expected) throws Exception {
assertEquals(expected, urlString);
assertEquals(expected, URL.from(urlString).toExternalForm());

View file

@ -15,17 +15,17 @@ public class URLParserTest {
@Test
public void testNull() {
assertNull(URL.from(null));
assertEquals(URL.INVALID, URL.from(null));
}
@Test
public void testEmpty() {
assertNull(URL.from(""));
assertEquals(URL.INVALID, URL.from(""));
}
@Test
public void testNewline() {
assertNull(URL.from("\n"));
assertEquals(URL.INVALID, URL.from("\n"));
}
@Test(expected = IllegalArgumentException.class)

View file

@ -4,17 +4,34 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals;
import java.net.URI;
/**
*/
public class URLResolverTest {
@Test
public void testResolve() throws Exception {
URL base = URL.create("http://example.org/foo/");
public void testResolveURI() throws Exception {
URI base = URI.create("http://example.org/foo");
assertEquals("http://example.org/", base.resolve("/").toString());
resolve("http://foo.bar", "foobar", "http://foo.bar/foobar");
resolve("http://foo.bar/", "foobar", "http://foo.bar/foobar");
resolve("http://foo.bar/foobar", "foobar", "http://foo.bar/foobar");
assertEquals("http://example.org/foobar", base.resolve("/foobar").toString());
assertEquals("http://example.org/foobar", base.resolve("foobar").toString());
base = URI.create("http://example.org/foo/");
assertEquals("http://example.org/", base.resolve("/").toString());
assertEquals("http://example.org/foobar", base.resolve("/foobar").toString());
assertEquals("http://example.org/foo/foobar", base.resolve("foobar").toString());
}
@Test
public void testResolveURL() throws Exception {
URL base = URL.create("http://example.org/foo");
assertEquals("http://example.org/", base.resolve("/").toString());
assertEquals("http://example.org/foobar", base.resolve("/foobar").toString());
assertEquals("http://example.org/foobar", base.resolve("foobar").toString());
base = URL.create("http://example.org/foo/");
assertEquals("http://example.org/", base.resolve("/").toString());
assertEquals("http://example.org/foobar", base.resolve("/foobar").toString());
assertEquals("http://example.org/foo/foobar", base.resolve("foobar").toString());
}
@Test
@ -69,7 +86,7 @@ public class URLResolverTest {
resolve("http://a/b/c/d;p?q", "http:", "http:");
}
private void resolve(String inputBase, String relative, String expected) {
private void resolve(String inputBase, String relative, String expected) throws URLSyntaxException {
assertEquals(expected, URL.base(inputBase).resolve(relative).toExternalForm());
}
}

View file

@ -37,8 +37,8 @@ public class URLTest {
}
} else {
if (base != null && input != null) {
URL url = URL.base(base).resolve(input);
if (url != null) {
try {
URL url = URL.base(base).resolve(input);
System.err.println("resolved: " + url.toString());
if (test.protocol != null) {
assertEquals(test.protocol, url.getScheme() + ":");
@ -54,8 +54,8 @@ public class URLTest {
// assertEquals(test.pathname, url.getPath());
//}
System.err.println("passed: " + base + " " + input);
} else {
System.err.println("unable to resolve: " + base + " " + input);
} catch (URLSyntaxException e) {
System.err.println("unable to resolve: " + base + " " + input + " reason: " + e.getMessage());
}
}
}

View file

@ -453,7 +453,7 @@ public class PathMatcherTest {
@Test
public void patternComparatorSort() {
Comparator<String> comparator = pathMatcher.getPatternComparator("/hotels/new");
List<String> paths = new ArrayList<String>(3);
List<String> paths = new ArrayList<>(3);
paths.add(null);
paths.add("/hotels/new");
paths.sort(comparator);
@ -561,13 +561,6 @@ public class PathMatcherTest {
assertTrue(pathMatcher.match("/Group/{groupName}/Members", "/group/Sales/members"));
}
@Test
public void cachePatternsSetToFalse() {
pathMatcher.setCachePatterns(false);
match();
assertTrue(pathMatcher.stringMatcherCache().isEmpty());
}
@Test
public void extensionMappingWithDotPathSeparator() {
pathMatcher.setPathSeparator(".");