
it.could.util.location.Location Maven / Gradle / Ivy
Show all versions of webdav Show documentation
/* ========================================================================== *
* Copyright (C) 2004-2006, Pier Fumagalli *
* All rights reserved. *
* ========================================================================== *
* *
* Licensed under the Apache License, Version 2.0 (the "License"). You may *
* not use this file except in compliance with the License. You may obtain a *
* copy of the License at . *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT *
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the *
* License for the specific language governing permissions and limitations *
* under the License. *
* *
* ========================================================================== */
package it.could.util.location;
import it.could.util.StringTools;
import it.could.util.encoding.Encodable;
import it.could.util.encoding.EncodingTools;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* An utility class representing an HTTP-like URL.
*
* This class can be used to represent any URL that roughly uses the HTTP
* format. Compared to the standard {@link java.net.URL} class, the scheme part
* of the a {@link Location} is never checked, and it's up to the application
* to verify its correctness, while compared to the {@link java.net.URI} class,
* its parsing mechanism is a lot more relaxed (be liberal in what you accept,
* be strict in what you send).
*
* For a bigger picture on how this class works, this is an easy-to-read
* representation of what the different parts of a {@link Location} are:
*
*
*
*
*
*
*
* One important difference between this implementation and the description
* of URLs and
* URIs is that parameter
* paths are represented only at the end of the entire path structure
* rather than for each path element. This over-simplification allows easy
* relativization of {@link Location}s when used with servlet containers, which
* normally use path parameters to encode the session id.
*
* @author Pier Fumagalli
*/
public class Location implements Encodable {
/** A {@link Map} of schemes and their default port number.
*/
private static final Map schemePorts = new HashMap();
static {
schemePorts.put("acap", new Integer( 674));
schemePorts.put("dav", new Integer( 80));
schemePorts.put("ftp", new Integer( 21));
schemePorts.put("gopher", new Integer( 70));
schemePorts.put("http", new Integer( 80));
schemePorts.put("https", new Integer( 443));
schemePorts.put("imap", new Integer( 143));
schemePorts.put("ldap", new Integer( 389));
schemePorts.put("mailto", new Integer( 25));
schemePorts.put("news", new Integer( 119));
schemePorts.put("nntp", new Integer( 119));
schemePorts.put("pop", new Integer( 110));
schemePorts.put("rtsp", new Integer( 554));
schemePorts.put("sip", new Integer(5060));
schemePorts.put("sips", new Integer(5061));
schemePorts.put("snmp", new Integer( 161));
schemePorts.put("telnet", new Integer( 23));
schemePorts.put("tftp", new Integer( 69));
}
/** The {@link List} of schemes of this {@link Location}.
*/
private final Schemes schemes;
/** The {@link Authority} of this {@link Location}.
*/
private final Authority authority;
/** The {@link Path} of this {@link Location}.
*/
private final Path path;
/** The {@link Parameters} of this {@link Location}.
*/
private final Parameters parameters;
/** The fragment part of this {@link Location}.
*/
private final String fragment;
/** The string representation of this {@link Location}.
*/
private final String string;
/**
* Create a new {@link Location} instance.
*/
public Location(Schemes schemes, Authority authority, Path path,
Parameters parameters, String fragment)
throws MalformedURLException {
if ((schemes == null) && (authority != null))
throw new MalformedURLException("No schemes specified");
if ((schemes != null) && (authority == null))
throw new MalformedURLException("No authority specified");
if (path == null) throw new MalformedURLException("No path specified");
this.schemes = schemes;
this.authority = authority;
this.path = path;
this.parameters = parameters;
this.fragment = fragment;
this.string = EncodingTools.toString(this);
}
/* ====================================================================== */
/* STATIC CONSTRUCTION METHODS */
/* ====================================================================== */
public static Location parse(String url)
throws MalformedURLException {
try {
return parse(url, DEFAULT_ENCODING);
} catch (UnsupportedEncodingException exception) {
final String message = "Unsupported encoding " + DEFAULT_ENCODING;
final InternalError error = new InternalError(message);
throw (InternalError) error.initCause(exception);
}
}
public static Location parse(String url, String encoding)
throws MalformedURLException, UnsupportedEncodingException {
if (url == null) return null;;
if (encoding == null) encoding = DEFAULT_ENCODING;
final String components[] = parseComponents(url);
final Schemes schemes = parseSchemes(components[0], encoding);
final int port = findPort(schemes, encoding);
final Authority auth = parseAuthority(components[1], port, encoding);
final Path path = Path.parse(components[2], encoding);
final Parameters params = Parameters.parse(components[3], '&', encoding);
final String fragment = components[4];
return new Location(schemes, auth, path, params, fragment);
}
/* ====================================================================== */
/* ACCESSOR METHODS */
/* ====================================================================== */
/**
* Return an unmodifiable {@link Schemes list of all schemes} for this
* {@link Location} instance or null.
*/
public Schemes getSchemes() {
return this.schemes;
}
/**
* Return the {@link Location.Authority Authority} part for this
* {@link Location} or null.
*/
public Authority getAuthority() {
return this.authority;
}
/**
* Return the non-null {@link Path Path} structure
* associated with this {@link Location} instance.
*/
public Path getPath() {
return this.path;
}
/**
* Return an unmodifiable {@link Parameters list of all parameters}
* parsed from this {@link Location}'s query string or null.
*/
public Parameters getParameters() {
return this.parameters;
}
/**
* Return the fragment of this {@link Location} unencoded.
*/
public String getFragment() {
return this.fragment;
}
/* ====================================================================== */
/* OBJECT METHODS */
/* ====================================================================== */
/**
* Check if the specified {@link Object} is equal to this instance.
*
* The specified {@link Object} must be a non-null
* {@link Location} instance whose {@link #toString() string value} equals
* this one's.
*/
public boolean equals(Object object) {
if ((object != null) && (object instanceof Location)) {
return this.string.equals(((Location)object).string);
} else {
return false;
}
}
/**
* Return the hash code value for this {@link Location} instance.
*/
public int hashCode() {
return this.string.hashCode();
}
/**
* Return the {@link String} representation of this {@link Location}
* instance.
*/
public String toString() {
return this.string;
}
/**
* Return the {@link String} representation of this {@link Location}
* instance using the specified character encoding.
*/
public String toString(String encoding)
throws UnsupportedEncodingException {
final StringBuffer buffer = new StringBuffer();
/* Render the schemes */
if (this.schemes != null)
buffer.append(this.schemes.toString(encoding)).append("://");
/* Render the authority part */
if (this.authority != null)
buffer.append(this.authority.toString(encoding));
/* Render the paths */
buffer.append(this.path.toString(encoding));
/* Render the query string */
if (this.parameters != null)
buffer.append('?').append(this.parameters.toString(encoding));
/* Render the fragment */
if (this.fragment != null) {
buffer.append('#');
buffer.append(EncodingTools.urlEncode(this.fragment, encoding));
}
/* Return the string */
return buffer.toString();
}
/* ====================================================================== */
/* PUBLIC METHODS */
/* ====================================================================== */
/**
* Checks whether this {@link Location} is absolute or not.
*
* This method must not be confused with the similarly named
* {@link Path#isAbsolute() Path.isAbsolute()} method.
* This method will check whether the full {@link Location} is absolute (it
* has a scheme), while the one exposed by the {@link Path Path}
* class will check if the path is absolute.
*/
public boolean isAbsolute() {
return this.schemes != null && this.authority != null;
}
public boolean isRelative() {
return ! (this.isAbsolute() || this.path.isAbsolute());
}
public boolean isAuthoritative(Location location) {
if (! this.isAbsolute()) return false;
if (! location.isAbsolute()) return true;
return this.schemes.equals(location.schemes) &&
this.authority.equals(location.authority);
}
/* ====================================================================== */
/* RESOLUTION METHODS */
/* ====================================================================== */
public Location resolve(String url)
throws MalformedURLException {
try {
return this.resolve(parse(url, DEFAULT_ENCODING));
} catch (UnsupportedEncodingException exception) {
final String message = "Unsupported encoding " + DEFAULT_ENCODING;
final InternalError error = new InternalError(message);
throw (InternalError) error.initCause(exception);
}
}
public Location resolve(String url, String encoding)
throws MalformedURLException, UnsupportedEncodingException {
if (encoding == null) encoding = DEFAULT_ENCODING;
return this.resolve(parse(url, encoding));
}
public Location resolve(Location location) {
if (! this.isAuthoritative(location)) return location;
/* Schemes are the same */
final Schemes schemes = this.schemes;
/* Authority needs to be merged (for username and password) */
final Authority auth;
if (location.authority != null) {
final String username = location.authority.username != null ?
location.authority.username :
this.authority.username;
final String password = location.authority.password != null ?
location.authority.password :
this.authority.password;
final String host = location.authority.host;
final int port = location.authority.port;
auth = new Authority(username, password, host, port);
} else {
auth = this.authority;
}
/* Path can be resolved */
final Path path = this.path.resolve(location.path);
/* Parametrs and fragment are the ones of the target */
final Parameters params = location.parameters;
final String fragment = location.fragment;
/* Create a new {@link Location} instance */
try {
return new Location(schemes, auth, path, params, fragment);
} catch (MalformedURLException exception) {
/* Should really never happen */
Error error = new InternalError("Can't instantiate Location");
throw (Error) error.initCause(exception);
}
}
/* ====================================================================== */
/* RELATIVIZATION METHODS */
/* ====================================================================== */
public Location relativize(String url)
throws MalformedURLException {
try {
return this.relativize(parse(url, DEFAULT_ENCODING));
} catch (UnsupportedEncodingException exception) {
final String message = "Unsupported encoding " + DEFAULT_ENCODING;
final InternalError error = new InternalError(message);
throw (InternalError) error.initCause(exception);
}
}
public Location relativize(String url, String encoding)
throws MalformedURLException, UnsupportedEncodingException {
if (encoding == null) encoding = DEFAULT_ENCODING;
return this.relativize(parse(url, encoding));
}
public Location relativize(Location location) {
final Path path;
if (!location.isAbsolute()) {
/* Target location is not absolute, its path might */
path = this.path.relativize(location.path);
} else {
if (this.isAuthoritative(location)) {
/* Target location is not on the same authority, process path */
path = this.path.relativize(location.path);
} else {
/* Not authoritative for a non-relative location, yah! */
return location;
}
}
try {
return new Location(null, null, path, location.parameters,
location.fragment);
} catch (MalformedURLException exception) {
/* Should really never happen */
Error error = new InternalError("Can't instantiate Location");
throw (Error) error.initCause(exception);
}
}
/* ====================================================================== */
/* INTERNAL PARSING ROUTINES */
/* ====================================================================== */
/**
* Return the port number associated with the specified schemes.
*/
public static int findPort(List schemes, String encoding)
throws UnsupportedEncodingException {
if (schemes == null) return -1;
if (schemes.size() < 1) return -1;
Integer p = (Integer) schemePorts.get(schemes.get(schemes.size() - 1));
return p == null ? -1 : p.intValue();
}
/**
* Parse scheme://authority/path?query#fragment
.
*
* @return an array of five {@link String}s: scheme (0), authority (1),
* path (2), query (3) and fragment (4).
*/
private static String[] parseComponents(String url)
throws MalformedURLException {
/* Scheme, easy and simple */
final String scheme;
final String afterScheme;
final int schemeEnd = url.indexOf(":/");
if (schemeEnd > 0) {
scheme = url.substring(0, schemeEnd).toLowerCase();
afterScheme = url.substring(schemeEnd + 2);
} else if (schemeEnd == 0) {
throw new MalformedURLException("Missing scheme");
} else {
scheme = null;
afterScheme = url;
}
/* Authority (can be tricky because it can be emtpy) */
final String auth;
final String afterAuth;
if (scheme == null) {
// --> /path... or path...
afterAuth = afterScheme;
auth = null;
} else if (afterScheme.length() > 0 && afterScheme.charAt(0) == '/') {
// --> scheme://...
final int pathStart = afterScheme.indexOf('/', 1);
if (pathStart == 1) {
// --> scheme:///path...
afterAuth = afterScheme.substring(pathStart);
auth = null;
} else if (pathStart > 1) {
// --> scheme://authority/path...
afterAuth = afterScheme.substring(pathStart);
auth = afterScheme.substring(1, pathStart);
} else {
// --> scheme://authority (but no slashes for the path)
final int authEnds = StringTools.findFirst(afterScheme, "?#");
if (authEnds < 0) {
// --> scheme://authority (that's it, return)
auth = afterScheme.substring(1);
return new String[] { scheme, auth, "/", null, null };
}
// --> scheme://authority?... or scheme://authority#...
auth = afterScheme.substring(1, authEnds);
afterAuth = "/" + afterScheme.substring(authEnds);
}
} else {
// --> scheme:/path...
afterAuth = url.substring(schemeEnd + 1);
auth = null;
}
/* Path, can be terminated by '?' or '#' whichever is first */
final int pathEnds = StringTools.findFirst(afterAuth, "?#");
if (pathEnds < 0) {
// --> ...path... (no fragment or query, return now)
return new String[] { scheme, auth, afterAuth, null, null };
}
/* We have either a query, a fragment or both after the path */
final String path = afterAuth.substring(0, pathEnds);
final String afterPath = afterAuth.substring(pathEnds + 1);
/* Query? The query can contain a "#" and has an extra fragment */
if (afterAuth.charAt(pathEnds) == '?') {
final int fragmPos = afterPath.indexOf('#');
if (fragmPos < 0) {
// --> ...path...?... (no fragment)
return new String[] { scheme, auth, path, afterPath, null };
}
// --> ...path...?...#... (has also a fragment)
final String query = afterPath.substring(1, fragmPos);
final String fragm = afterPath.substring(fragmPos + 1);
return new String[] { scheme, auth, path, query, fragm };
}
// --> ...path...#... (a path followed by a fragment but no query)
return new String[] { scheme, auth, path, null, afterPath };
}
/**
* Parse scheme:scheme:scheme...
.
*/
private static Schemes parseSchemes(String scheme, String encoding)
throws MalformedURLException, UnsupportedEncodingException {
if (scheme == null) return null;
final String split[] = StringTools.splitAll(scheme, ':');
List list = new ArrayList();
for (int x = 0; x < split.length; x++) {
if (split[x] == null) continue;
list.add(EncodingTools.urlDecode(split[x], encoding));
}
if (list.size() != 0) return new Schemes(list);
throw new MalformedURLException("Empty scheme detected");
}
/**
* Parse username:password@hostname:port
.
*/
private static Authority parseAuthority(String auth, int defaultPort,
String encoding)
throws MalformedURLException, UnsupportedEncodingException {
if (auth == null) return null;
final String split[] = StringTools.splitOnce(auth, '@', true);
final String uinfo[] = StringTools.splitOnce(split[0], ':', false);
final String hinfo[] = StringTools.splitOnce(split[1], ':', false);
final int port;
if ((split[0] != null) && (split[1] == null))
throw new MalformedURLException("Missing required host info part");
if ((uinfo[0] == null) && (uinfo[1] != null))
throw new MalformedURLException("Password specified without user");
if ((hinfo[0] == null) && (hinfo[1] != null))
throw new MalformedURLException("Port specified without host");
try {
if (hinfo[1] != null) {
final int parsedPort = Integer.parseInt(hinfo[1]);
if ((parsedPort < 1) || (parsedPort > 65535)) {
final String message = "Invalid port number " + parsedPort;
throw new MalformedURLException(message);
}
/* If the specified port is the default one, ignore it! */
if (defaultPort == parsedPort) port = -1;
else port = parsedPort;
} else {
port = -1;
}
} catch (NumberFormatException exception) {
throw new MalformedURLException("Specified port is not a number");
}
return new Authority(EncodingTools.urlDecode(uinfo[0], encoding),
EncodingTools.urlDecode(uinfo[1], encoding),
EncodingTools.urlDecode(hinfo[0], encoding),
port);
}
/* ====================================================================== */
/* PUBLIC INNER CLASSES */
/* ====================================================================== */
/**
* The {@link Location.Schemes Schemes} class represents an unmodifiable
* ordered collection of {@link String} schemes for a {@link Location}.
*
* @author Pier Fumagalli
*/
public static class Schemes extends AbstractList implements Encodable {
/** All the {@link String} schemes in order.
*/
private final String schemes[];
/** The {@link String} representation of this instance.
*/
private final String string;
/**
* Create a new {@link Schemes} instance.
*/
private Schemes(List schemes) {
final int size = schemes.size();
this.schemes = (String []) schemes.toArray(new String[size]);
this.string = EncodingTools.toString(this);
}
/**
* Return the {@link String} scheme at the specified index.
*/
public Object get(int index) {
return this.schemes[index];
}
/**
* Return the number of {@link String} schemes contained by this
* {@link Location.Schemes Schemes} instance.
*/
public int size() {
return this.schemes.length;
}
/**
* Return the URL-encoded {@link String} representation of this
* {@link Location.Schemes Schemes} instance.
*/
public String toString() {
return this.string;
}
/**
* Return the URL-encoded {@link String} representation of this
* {@link Location.Schemes Schemes} instance using the specified
* character encoding.
*/
public String toString(String encoding)
throws UnsupportedEncodingException {
final StringBuffer buffer = new StringBuffer();
for (int x = 0; x < this.schemes.length; x ++) {
buffer.append(':');
buffer.append(EncodingTools.urlEncode(this.schemes[x], encoding));
}
return buffer.substring(1);
}
/**
* Return the hash code value for this
* {@link Location.Schemes Schemes} instance.
*/
public int hashCode() {
return this.string.hashCode();
}
/**
* Check if the specified {@link Object} is equal to this
* {@link Location.Schemes Schemes} instance.
*
* The specified {@link Object} is considered equal to this one if
* it is non-null, it is a {@link Location.Schemes Schemes}
* instance, and its {@link #toString() string representation} equals
* this one's.
*/
public boolean equals(Object object) {
if ((object != null) && (object instanceof Schemes)) {
return this.string.equals(((Schemes) object).string);
} else {
return false;
}
}
}
/* ====================================================================== */
/**
* The {@link Location.Authority Authority} class represents the autority
* and user information for a {@link Location}.
*
* @author Pier Fumagalli
*/
public static class Authority implements Encodable {
/** The username of this instance (decoded).
*/
private final String username;
/** The password of this instance (decoded).
*/
private final String password;
/** The host name of this instance (decoded).
*/
private final String host;
/** The port number of this instance.
*/
private final int port;
/** The encoded host and port representation.
*/
private final String hostinfo;
/** The encoded string representation of this instance.
*/
private final String string;
/**
* Create a new {@link Location.Authority Authority} instance.
*/
private Authority(String user, String pass, String host, int port) {
this.username = user;
this.password = pass;
this.host = host;
this.port = port;
try {
this.hostinfo = this.getHostInfo(DEFAULT_ENCODING);
this.string = this.toString(DEFAULT_ENCODING);
} catch (UnsupportedEncodingException exception) {
final String message = "Default encoding \"" + DEFAULT_ENCODING
+ "\" not supported by the platform";
final InternalError error = new InternalError(message);
throw (InternalError) error.initCause(exception);
}
}
/**
* Returns the decoded user name.
*/
public String getUsername() {
return this.username;
}
/**
* Returns the decoded password.
*/
public String getPassword() {
return this.password;
}
/**
* Returns the "user info" field.
*
* This method will concatenate the username and password using the
* colon character and return a non-null {@link String} only if
* both of them are non-null.
*/
public String getUserInfo() {
if ((this.username == null) || (this.password == null)) return null;
return this.username + ':' + this.password;
}
/**
* Returns the decoded host name.
*/
public String getHost() {
return this.host;
}
/**
* Returns the port number.
*/
public int getPort() {
return this.port;
}
/**
* Returns the host info part of the
* {@link Location.Authority Authority}.
*
* This is the encoded representation of the
* {@link #getUsername() user name} optionally follwed by the colon (:)
* character and the encoded {@link #getPassword() password}.
*/
public String getHostInfo() {
return this.hostinfo;
}
/**
* Returns the host info part of the
* {@link Location.Authority Authority} using the specified character
* encoding.
*
* This is the encoded representation of the
* {@link #getUsername() user name} optionally follwed by the colon (:)
* character and the encoded {@link #getPassword() password}.
*/
public String getHostInfo(String encoding)
throws UnsupportedEncodingException {
final StringBuffer hostinfo = new StringBuffer();
hostinfo.append(EncodingTools.urlEncode(this.host, encoding));
if (port >= 0) hostinfo.append(':').append(port);
return hostinfo.toString();
}
/**
* Return the URL-encoded {@link String} representation of this
* {@link Location.Authority Authority} instance.
*/
public String toString() {
return this.string;
}
/**
* Return the URL-encoded {@link String} representation of this
* {@link Location.Authority Authority} instance using the specified
* character encoding.
*/
public String toString(String encoding)
throws UnsupportedEncodingException {
final StringBuffer buffer;
if (this.username != null) {
buffer = new StringBuffer();
buffer.append(EncodingTools.urlEncode(this.username, encoding));
if (this.password != null) {
buffer.append(':');
buffer.append(EncodingTools.urlEncode(this.password, encoding));
}
} else {
buffer = null;
}
if (buffer == null) return this.getHostInfo(encoding);
buffer.append('@').append(this.getHostInfo(encoding));
return buffer.toString();
}
/**
* Return the hash code value for this
* {@link Location.Authority Authority} instance.
*/
public int hashCode() {
return this.hostinfo.hashCode();
}
/**
* Check if the specified {@link Object} is equal to this
* {@link Location.Authority Authority} instance.
*
* The specified {@link Object} is considered equal to this one if
* it is non-null, it is a {@link Location.Authority Authority}
* instance, and its {@link #getHostInfo() host info} equals
* this one's.
*/
public boolean equals(Object object) {
if ((object != null) && (object instanceof Authority)) {
return this.hostinfo.equals(((Authority) object).hostinfo);
} else {
return false;
}
}
}
}