/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander 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. * * muCommander 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 a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file; import com.mucommander.commons.file.compat.CompatURLStreamHandler; import com.mucommander.commons.file.protocol.FileProtocols; import com.mucommander.commons.file.protocol.local.LocalFile; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.util.StringUtils; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.Enumeration; import java.util.Hashtable; import java.util.NoSuchElementException; /** * This class represents a Uniform Resource Locator (URL). The general format of a URL is as follows: * <pre> * scheme://[login[:password]@]host[:port][/path][?query] * </pre> * * <h3>Instanciation</h3> * <p> * FileURL cannot be instantiated directly, instances can be created using {@link #getFileURL(String)}. * Unlike the <code>java.net.URL</code> and <code>java.net.URI</code> classes, FileURL instances are mutable -- * all URL parts can be freely modified. FileURL instances can also be cloned using the standard {@link #clone()} method. * </p> * * <h3>Handlers and Scheme-specific attributes</h3> * <p> * In addition to standard URL features, FileURL gives access to scheme-specific attributes: * <dl> * <dt>{@link #getStandardPort() standard port}</dt><dd>the standard port implied when no port is defined in the URL, * e.g. 21 for FTP</dd> * <dt>{@link #getPathSeparator() path separator}</dt><dd>the character(s) that separates path fragments, e.g. '/' for * most schemes, '\' for local paths under certain OSes like Windows.</dd> * <dt>{@link #getGuestCredentials() guest credentials}</dt><dd>credentials to authenticate as a guest, e.g. 'GUEST' * for SMB, 'anonymous' for FTP.</dd> * <dt>{@link #getRealm() authentication realm}</dt><dd>the base URL throughout which a set of credentials can be used. * </dd> * </dl> * These attribute values are provided by the {@link SchemeHandler} registered with the scheme, if any. * </p> * <p> * In addition to providing those attributes, a SchemeHandler provides a {@link SchemeParser} * instance which takes care of the actual parsing of URLs of a particular scheme when {@link #getFileURL(String)} is * invoked. This allows for scheme-specific parsing, like for example for the query part which should only be parsed * and considered as a separate part for certain schemes such as HTTP. * </p> * <p> * This class registers a number of handlers for the schemes/protocols supported by the muCommander file API. * Additional handlers can be registered dynamically using {@link #registerHandler(String, SchemeHandler)}. Likewise, * existing handlers can be unregistered or replaced at runtime using <code>registerHandler</code> and * <code>unregisterHandler</code>. * </p> * <p> * A {@link #getDefaultHandler() default handler} is used for schemes that do not have a specific handler registered. * It provides default values for the above-mentioned attributes and provides a parser that parses those scheme URLs. * The default handler's parser is also used for parsing locations passed to {@link #getFileURL(String)} that do not * contain a scheme (i.e. without the leading <code>scheme://</code>). Those locations can be system-dependent, * local and absolute paths, or UNC paths. These paths are turned by the parser into an equivalent, fully-qualified URL. * </p> * * <h3>Properties</h3> * <p> * This class provides methods to attach properties to a FileURL instance. These properties are not part of the URL * itself and are absent from its string representation. They allow protocol-specific properties like connection * settings to be passed along, to {@link AbstractFile} instances in particular. * </p> * * <h3>Limitations</h3> * <p> * This class has the several limitations that are worth noting: * <ul> * <li>URL syntax is not strictly enforced: some invalid URLs (as per RFC) will be parsed without throwing an exception</li> * <li>relative URLs are not supported</li> * <li>no proper percent encoding/decoding </li> * <li>no support for the fragment part</li> * </ul> * Some of these limitations will be addressed in upcoming revisions of this class. * </p> * * @see SchemeHandler * @see SchemeParser * @author Maxence Bernard */ public class FileURL implements Cloneable { // Todo: add support for the fragment part // Todo: add percent encoding/decoding /** Handler instance that provides the scheme-specific features of this FileURL */ private SchemeHandler handler; /** Scheme part */ private String scheme; /** Port part, -1 if this URL has none */ private int port = -1; /** Host part, null if this URL has none */ private String host; /** Path part */ private String path; /** Filename, extracted from the path, null if the path has none */ private String filename; /** Query part, null if this URL has none */ private String query; /** Properties, null if none have been set thus far */ private Hashtable<String, String> properties; /** Credentials (login and password parts), null if this URL has none */ private Credentials credentials; /** Caches the value returned by #hashCode() for as long as this instance is not modified */ private int hashCode; /** Default handler for schemes that do not have a specific handler */ private final static SchemeHandler DEFAULT_HANDLER = new DefaultSchemeHandler(); /** Maps schemes (String) onto SchemeHandler instances */ private final static Hashtable<String, SchemeHandler> handlers = new Hashtable<String, SchemeHandler>(); /** String designating the localhost */ public final static String LOCALHOST = "localhost"; static { // Register custom handlers for known schemes registerHandler(FileProtocols.FILE, new DefaultSchemeHandler(new DefaultSchemeParser(new DefaultPathCanonizer(LocalFile.SEPARATOR, System.getProperty("user.home")), false), -1, System.getProperty("file.separator"), AuthenticationType.NO_AUTHENTICATION, null)); registerHandler(FileProtocols.FTP, new DefaultSchemeHandler(new DefaultSchemeParser(), 21, "/", AuthenticationType.AUTHENTICATION_REQUIRED, new Credentials("anonymous", "anonymous_coward@mucommander.com"))); registerHandler(FileProtocols.SFTP, new DefaultSchemeHandler(new DefaultSchemeParser(), 22, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.HDFS, new DefaultSchemeHandler(new DefaultSchemeParser(true), 8020, "/", AuthenticationType.AUTHENTICATION_OPTIONAL, null)); registerHandler(FileProtocols.HTTP, new DefaultSchemeHandler(new DefaultSchemeParser(true), 80, "/", AuthenticationType.AUTHENTICATION_OPTIONAL, null)); registerHandler(FileProtocols.S3, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.WEBDAV, new DefaultSchemeHandler(new DefaultSchemeParser(true), 80, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.HTTPS, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_OPTIONAL, null)); registerHandler(FileProtocols.WEBDAVS, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.NFS, new DefaultSchemeHandler(new DefaultSchemeParser(), 2049, "/", AuthenticationType.NO_AUTHENTICATION, null)); registerHandler(FileProtocols.VSPHERE, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.SMB, new DefaultSchemeHandler(new DefaultSchemeParser(), -1, "/", AuthenticationType.AUTHENTICATION_REQUIRED, new Credentials("GUEST", "")) { @Override public FileURL getRealm(FileURL location) { FileURL realm = new FileURL(this); String newPath = location.getPath(); // Find first path token (share) int pos = newPath.indexOf('/', 1); newPath = newPath.substring(0, pos==-1?newPath.length():pos+1); realm.setPath(newPath); realm.setScheme(location.getScheme()); realm.setHost(location.getHost()); realm.setPort(location.getPort()); // Copy properties (if any) realm.importProperties(location); return realm; } }); } /** * Private constructor. Creates an empty FileURL that uses the given handler, all parts have to be manually set. * * @param handler the handler to have this FileURL use */ private FileURL(SchemeHandler handler) { this.handler = handler; } /** * This method is called whenever this instance is modified to invalidate caches. */ private void urlModified() { hashCode = 0; } /** * Creates and returns a new FileURL instance from the given location, throws a <code>MalformedURLException</code> * if the specified location is not a valid URL or path and cannot be resolved. The {@link SchemeParser parser} * of the {@link SchemeHandler handler} registered for the location's scheme is used to parse the given location. * If the scheme specified in the location does not have a specific handler, or if the location does not contain a * scheme (i.e. is local or UNC path, not a URL) then the default handler's parser is used. * * @param location the URL or path for which to get a <code>FileURL</code> instance * @throws MalformedURLException if the specified string isn't a valid URL, according to the scheme's parser used * @return a FileURL corresponding to the given location */ public static FileURL getFileURL(String location) throws MalformedURLException { int schemeDelimPos = location.indexOf("://"); SchemeHandler handler; if(schemeDelimPos==-1) { // No scheme: the location is a local or UNC path, not a URL handler = getDefaultHandler(); } else { handler = getSchemeHandler(location.substring(0, schemeDelimPos)); } FileURL fileURL = new FileURL(handler); try { handler.getParser().parse(location, fileURL); } catch(Exception e) { // Catch any unexpected exception thrown by the SchemeParser and turn it into a MalformedURLException // with a specific error message. if(e instanceof MalformedURLException) throw (MalformedURLException)e; throw new MalformedURLException("URL parser error"); } return fileURL; } /** * Returns the handler registered the specified scheme if there is one, the default handler otherwise. * * @param scheme the scheme for which to return a handler * @return a handler for the specified scheme */ private static SchemeHandler getSchemeHandler(String scheme) { SchemeHandler handler = getRegisteredHandler(scheme); if(handler==null) return getDefaultHandler(); return handler; } /** * Returns the <code>SchemeHandler</code> instance that provides the scheme-specific features of this FileURL. * * @return the <code>SchemeHandler</code> instance that provides the scheme-specific features of this FileURL */ public SchemeHandler getHandler() { return handler; } /** * Sets the <code>SchemeHandler</code> that provides the scheme-specific features of this FileURL. * <p> * <b>Important:</b> after calling this method, the scheme should also be changed to match the new handler -- * changing the handler without changing the scheme to an appropriate one will result in inconsistent * scheme-specific attributes to be returned. * </p> * * @param handler the <code>SchemeHandler</code> instance that provides the scheme-specific features of this FileURL */ public void setHandler(SchemeHandler handler) { this.handler = handler; } /** * Registers a handler for the specified scheme, replacing any handler previously registered * for the same scheme. * * @param scheme the scheme to associate the handler with (case-insensitive) * @param handler the new handler in charge of the scheme */ public static void registerHandler(String scheme, SchemeHandler handler) { handlers.put(scheme.toLowerCase(), handler); } /** * Removes any handler associated with the specified scheme, leaving the default handler in charge of the scheme. * This method has no effect if there is no handler registered for the scheme. * * @param scheme the scheme to remove the handler for */ public static void unregisterHandler(String scheme) { handlers.remove(scheme.toLowerCase()); } /** * Returns the handler registered for the specified scheme, <code>null</code> if there isn't any. * * @param scheme the scheme for which to return the handler * @return the handler registered for the specified scheme */ public static SchemeHandler getRegisteredHandler(String scheme) { return handlers.get(scheme.toLowerCase()); } /** * Returns the default handler, which handles schemes which do not have a specific handler. * The returned instance is a {@link DefaultSchemeHandler} created with the no-arg constructor. * * @return the default handler */ public static SchemeHandler getDefaultHandler() { return DEFAULT_HANDLER; } /** * Extracts the filename from the given path and returns it, or <code>null</null> if the path does not contain * a filename. * * @param path the path from which to extract a filename * @param separator the path separator * @return the filename extracted from the given path, <code>null</code> if the path doesn't contain any */ public static String getFilenameFromPath(String path, String separator) { if(path.equals("") || path.equals("/")) return null; // Remove any trailing separator path = PathUtils.removeTrailingSeparator(path, separator); if(!separator.equals("/")) path = PathUtils.removeLeadingSeparator(path, "/"); // Extract filename int pos = path.lastIndexOf(separator); if(pos==-1) return null; return path.substring(pos+1); } /** * Returns the scheme part of this URL. The returned scheme may never be <code>null</code>. * * @return the scheme part of this <code>FileURL</code>. * @see #setScheme(String) */ public String getScheme() { return scheme; } /** * Sets the scheme part of this URL. An <code>IllegalArgumentException</code> will be thrown if the specified scheme * is <code>null</code> or an empty string. * <p> * <b>Important:</b> after calling this method, the handler should also be changed to match the new scheme -- * changing the scheme without changing the handler to an appropriate one will result in inconsistent * scheme-specific attributes to be returned. * </p> * * @param scheme new scheme part of this URL. * @throws IllegalArgumentException if the specified is null or an empty string * @see #getScheme() */ public void setScheme(String scheme) { if(scheme==null) throw new IllegalArgumentException(); this.scheme = scheme; urlModified(); } /** * Returns the host part of this URL, <code>null</code> if it doesn't contain any. * * @return the host part of this URL. * @see #setHost(String) */ public String getHost() { return host; } /** * Sets the host part of this URL, <code>null</code> for no host. * * @param host new host part of this URL. * @see #getHost() */ public void setHost(String host) { this.host = host; urlModified(); } /** * Returns the port part of this URL, <code>-1</code> if none was specified in the URL. * * @return the port part of this URL, -1 if there isn't any. * @see #setPort(int) * @see #getDefaultHandler() */ public int getPort() { return port; } /** * Sets the port part of this URL, <code>-1</code> for no specific port. * * @param port new port part of this URL. * @see #getPort() * @see #getDefaultHandler() */ public void setPort(int port) { this.port = port; urlModified(); } /** * Returns this scheme's standard port, <code>-1</code> if the scheme doesn't have any. * If this URL doesn't have a specific port part, the return value should be considered as being this URL's port. * * <p>Some file protocols may not have a notion of standard port or even no use for the port part at all, for * example those that are not TCP or UDP based such as the local 'file' scheme.</p> * * <p>This method is just a shorthand for <code>getHandler().getStandardPort()</code>.</p> * * @return the scheme's standard port * @see #getPort() */ public int getStandardPort() { return handler.getStandardPort(); } /** * Returns the login part of this URL, <code>null</code> if there isn't any. * * @return the login part of this URL, <code>null</code> if there isn't any * @see #getCredentials() */ public String getLogin() { return credentials==null?null:credentials.getLogin(); } /** * Returns the password part of this URL, <code>null</code> if there isn't any. * * @return the password part of this URL, <code>null</code> if there isn't any * @see #getCredentials() */ public String getPassword() { return credentials==null?null:credentials.getPassword(); } /** * Returns the type of authentication used by the scheme's file protocol. The returned value is one of the constants * defined in the {@link AuthenticationType} enum. * * <p>This method is just a shorthand for <code>getHandler().getAuthenticationType()</code>.</p> * * @return the type of authentication used by the scheme's file protocol */ public AuthenticationType getAuthenticationType() { return handler.getAuthenticationType(); } /** * Returns true if this URL contains credentials, i.e. a login and/or password part. If <code>true</code> is * returned, {@link #getCredentials()} will return a non-null value. * * @return <code>true</code> if this URL contains credentials, <code>false</code> otherwise. */ public boolean containsCredentials() { return credentials!=null; } /** * Returns the credentials (login and password) contained by this URL, wrapped in an {@link Credentials} object. * Returns <code>null</code> if this URL doesn't have a login or password part. * * <p>The returned credentials may or may be of any use for the scheme's file protocol depending on the value * returned by {@link #getAuthenticationType()}.</p> * * @return the credentials contained by this URL, <code>null</code> if this URL doesn't have a login or password part. * @see #setCredentials(Credentials) * @see #getAuthenticationType() */ public Credentials getCredentials() { return credentials; } /** * Sets the login and password parts of this URL. Any credentials contained by this FileURL will be replaced. * <code>null</code> can be passed to discard existing credentials. * * <p>Credentials may or may not be of any use for the scheme's file protocol depending on the value * returned by {@link #getAuthenticationType()}.</p> * * @param credentials the new login and password parts, replacing any existing credentials. If null is passed, * existing credentials will be discarded. * @see #getCredentials() */ public void setCredentials(Credentials credentials) { if(credentials==null || credentials.isEmpty()) // Empty credentials are equivalent to null credentials this.credentials = null; else this.credentials = credentials; urlModified(); } /** * Returns this scheme's guest credentials, <code>null</code> if the scheme doesn't have any. * <p> * Guest credentials offer a way to authenticate a URL as a 'guest' on file protocols that require a set of * credentials to establish a connection. The returned credentials are provided with no guarantee that the fileystem * will actually accept them and allow the request/connection. The notion of 'guest' credentials may or may not * have a meaning depending on the underlying file protocol. * </p> * * <p>This method is just a shorthand for <code>getHandler().getGuestCredentials()</code>.</p> * * @return the scheme's guest credentials, <code>null</code> if the scheme doesn't have any */ public Credentials getGuestCredentials() { return handler.getGuestCredentials(); } /** * Returns the path part of this URL. The returned value will never be <code>null</code> and always start with a * leading '/' character. * * @return the path part of this URL. * @see #setPath(String) */ public String getPath() { return path; } /** * Sets the path part of this URL. The specified path cannot be <code>null</code> and must start with a leading * '/' character. If the specified path value is <code>null</code>, then the path will be set to "/". * If the path does not start with a leading separator, one will be added. * * @param path new path part of this URL * @see #getPath() */ public void setPath(String path) { if(path==null || path.equals("")) path = "/"; if(!path.startsWith("/")) path = "/"+path; this.path = path; // Extract new filename from path this.filename = getFilenameFromPath(path, getPathSeparator()); urlModified(); } /** * Returns this scheme's path separator, which serves as a delimiter for path fragments. For most schemes, this is * the forward slash character. * * <p>This method is just a shorthand for <code>getHandler().getPathSeparator()</code>.</p> * * @return this scheme's path separator */ public String getPathSeparator() { return handler.getPathSeparator(); } /** * Returns the parent of this URL according to its path, <code>null</code> if this URL has no parent (its path is "/"). * <p> * The returned FileURL will have the same handler, scheme, host, port, credentials and properties as this one. * The query part of the returned parent URL will always be <code>null</code>, even if this URL had one. * </p> * <p>Note: this method returns a new FileURL instance every time it is called, and all mutable fields of this FileURL * are cloned. Therefore, the returned URL can be safely modified without any risk of side effects.</p> * * @return this URL's parent, <code>null</code> if it doesn't have one. */ public FileURL getParent() { // If path equals '/', url has no parent if(!(path.equals("/") || path.equals(""))) { String separator = getPathSeparator(); // Remove any trailing separator String parentPath = path.endsWith(separator)?path.substring(0, path.length()-separator.length()):path; // Resolve parent folder's path and reconstruct parent URL int lastSeparatorPos = parentPath.lastIndexOf(separator); if(lastSeparatorPos!=-1) { FileURL parentURL = new FileURL(handler); parentURL.scheme = scheme; parentURL.host = host; parentURL.port = port; parentURL.path = parentPath.substring(0, lastSeparatorPos+1); // Keep trailing slash parentURL.filename = getFilenameFromPath(parentURL.path, separator); // Set same credentials for parent, (if any) // Note: Credentials are immutable. parentURL.credentials = credentials; // Copy properties to parent (if any) if(properties!=null) parentURL.properties = new Hashtable<String, String>(properties); return parentURL; } } return null; // URL has no parent } /** * Returns the authentication realm corresponding to this URL, i.e. the base location throughout which credentials * can be used. Any property contained by the specified FileURL will be carried over in the returned FileURL. * On the contrary, credentials will not be copied, the returned URL always has no credentials. * * <p>Note: this method returns a new FileURL instance every time it is called. Therefore the returned FileURL can * safely be modified without any risk of side effects.</p> * <p>This method is just a shorthand for <code>getHandler().getRealm(this)</code>.</p> * * @return this url's authentication realm */ public FileURL getRealm() { return handler.getRealm(this); } /** * Returns the filename of this URL , <code>null</code> if doesn't have one (e.g. if the path is "/"). * <p> * There is no <code>setFilename</code> as the filename is simply extrapolated from the path. * Use {@link #setPath(String)} to change the path and its filename. * </p> * * @return the filename of this URL, <code>null</code> if it doesn't have one. * @see #setPath(String) */ public String getFilename() { return filename; } /** * Returns the query part of this URL if it has one, <code>null</code> otherwise. * * @return the query part of this URL if it has one, <code>null</code> otherwise * @see #setQuery(String) */ public String getQuery() { return query; } /** * Sets the query part of this URL, <code>null</code> for no query part. * * @param query new query part of this URL, <code>null</code> for no query part * @see #getQuery() */ public void setQuery(String query) { this.query = query; urlModified(); } /** * Returns the value corresponding to the given property name, <code>null</code> if the property has no value. * * @param name name of the property whose value is to be retrieved * @return the value associated with the specified property name, <code>null</code> if it has no value * @see #setProperty(String,String) */ public String getProperty(String name) { return properties==null?null:properties.get(name); } /** * Sets the given property (name/value pair) in the FileURL instance. A <code>null</code> property value has the * effect of removing the property. * * @param name name of the property to set * @param value value of the property * @see #getProperty(String) */ public void setProperty(String name, String value) { // Create the property hashtable only when a property is set for the first time if(properties==null) properties = new Hashtable<String, String>(); if(value==null) properties.remove(name); else properties.put(name, value); urlModified(); } /** * Returns an <code>Enumeration</code> of all property names this FileURL contains. * * @return an <code>Enumeration</code> of all property names this FileURL contains */ public Enumeration<String> getPropertyNames() { // Return an empty enumeration if the property hashtable is null if(properties==null) { return new Enumeration<String>() { public boolean hasMoreElements() { return false; } public String nextElement() { throw new NoSuchElementException(); } }; } return properties.keys(); } /** * Copy the properties of the given FileURL into this FileURL. * * @param url FileURL instance whose properties should be imported into this one. */ public void importProperties(FileURL url) { // Slight optimization to avoid creating an enumeration if the FileURL doesn't have any property if(url.properties==null) return; Enumeration<String> propertyKeys = url.getPropertyNames(); String key; while(propertyKeys.hasMoreElements()) { key = propertyKeys.nextElement(); setProperty(key, url.getProperty(key)); } } /** * Returns a String representation of this FileURL, including the login and password parts (credentials) only if * specified, and masking the password as requested. * * @param includeCredentials if <code>true</code>, the login and password parts (if any) will be included in the * returned URL. * @param maskPassword if <code>true</code> and the includeCredentials parameter is also true, the password's * characters (if any) will be replaced by '*' characters. This allows a URL containing credentials to be displayed * to the end user without revealing the actual password. * @return a string representation of this <code>FileURL</code> */ public String toString(boolean includeCredentials, boolean maskPassword) { StringBuffer sb = new StringBuffer(scheme); sb.append("://"); if(includeCredentials && credentials!=null) { try { sb.append(URLEncoder.encode(credentials.getLogin(), "UTF-8")); } catch(UnsupportedEncodingException e) { // This can't happen in practice, UTF-8 is necessarily supported } String password = credentials.getPassword(); if(!"".equals(password)) { sb.append(':'); if(maskPassword) sb.append(credentials.getMaskedPassword()); else { try { sb.append(URLEncoder.encode(password, "UTF-8")); } catch(UnsupportedEncodingException e) { // This can't happen in practice, UTF-8 is necessarily supported } } } sb.append('@'); } if(host!=null) sb.append(host); // Set the port only if it has a value that is different from the standard port if(port!=-1 && port!=handler.getStandardPort()) { sb.append(':'); sb.append(port); } if(host!=null || !path.equals("/")) { // Test to avoid URLs like 'smb:///' if(path.startsWith("/")) { sb.append(path); } else { // Add a leading '/' if path doesn't already start with one, needed for scheme paths that are not // forward slash-separated sb.append('/'); sb.append(path); } } if(query!=null) { sb.append('?'); sb.append(query); } return sb.toString(); } /** * Returns a String representation of this FileURL, including the login and password parts (credentials) only if * requested. * * @param includeCredentials if <code>true</code>, the login and password parts (if any) will be included in the * returned URL. * @return a string representation of this <code>FileURL</code>. */ public String toString(boolean includeCredentials) { return toString(includeCredentials, false); } /** * Creates and returns a <code>java.net.URL</code> referring to the same location as this <code>FileURL</code>. * The <code>java.net.URL</code> is created from the string representation of this <code>FileURL</code>. * Thus, any credentials this <code>FileURL</code> contains are preserved, but properties are lost. * * <p>The returned <code>URL</code> uses an {@link AbstractFile} to access the associated resource. * An {@link AbstractFile} instance is created by the underlying <code>URLConnection</code> when the URL is * connected.</p> * * <p>It is important to note that this method is provided for interoperability purposes, for the sole purpose of * connecting to APIs that require a <code>java.net.URL</code>.</p> * * @return a <code>java.net.URL</code> referring to the same location as this <code>FileURL</code> * @throws MalformedURLException if the java.net.URL could not parse the location of this FileURL */ public URL getJavaNetURL() throws MalformedURLException { return new URL(null, toString(true), new CompatURLStreamHandler()); } /** * Returns <code>true</code> if the scheme part of this URL and the given URL are equal. * The comparison is case-sensitive. * * @param url the URL to test for scheme equality * @return <code>true</code> if the scheme part of this URL and the given URL are equal */ public boolean schemeEquals(FileURL url) { return this.scheme.equalsIgnoreCase(url.scheme); } /** * Returns <code>true</code> if the host part of this URL and the given URL are equal. * The comparison is case-insensitive. * * @param url the URL to test for host equality * @return <code>true</code> if the host part of this URL and the given URL are equal */ public boolean hostEquals(FileURL url) { // Note: StringUtils#equals is null-safe return StringUtils.equals(this.host, url.host, false); } /** * Returns <code>true</code> if the port of this URL and the given URL's are equal. Ports are said to be equal if * the values returned by {@link #getPort()} are equal, or if both URLs have the same standard port * (as returned by {@link #getStandardPort()} and one of the port value is <code>-1</code> (undefined) and the other * is the standard port. * * @param url the URL to test for port equality * @return <code>true</code> if the port of this URL and the given one are equal */ public boolean portEquals(FileURL url) { int port1 = this.port; int port2 = url.port; int standardPort = getStandardPort(); return port1==port2 || (standardPort==url.getStandardPort() && ((port1==-1 && port2==standardPort || (port2==-1 && port1==standardPort)))); } /** * Returns <code>true</code> if the path of this URL and the given URL are equal. The comparison is case-sensitive. * If the sole difference between two paths is a trailing path separator (and both URLs have the same path separator), * they will be considered as equal. * For example, <code>/path</code> and <code>/path/</code> are considered equal, assuming the path separator is '/'. * * <p>It is noteworthy that this method uses <code>java.lang.String#equals(Object)</code> to compare URL paths, * which in some rare cases may return <code>false</code> for non-ascii/Unicode paths that have the same written * representation but are not equal according to <code>java.lang.String#equals(Object)</code>. Handling such cases * would require a locale-aware String comparison which is not an option here.</p> * * @param url the URL to test for path equality * @return <code>true</code> if the path of this URL and the given URL are equal */ public boolean pathEquals(FileURL url) { boolean isCaseSensitiveOS = !(OsFamily.getCurrent().equals(OsFamily.WINDOWS) || OsFamily.getCurrent().equals(OsFamily.OS_2)); String path1 = isCaseSensitiveOS ? this.getPath() : this.getPath().toLowerCase(); String path2 = isCaseSensitiveOS ? url.getPath() : url.getPath().toLowerCase(); if(path1.equals(path2)) return true; String separator = getPathSeparator(); if(separator.equals(url.getPathSeparator())) { int separatorLen = separator.length(); int len1 = path1.length(); int len2 = path2.length(); // If the difference between the 2 strings is just a trailing path separator, we consider the paths as equal if(Math.abs(len1-len2)==separatorLen && (len1>len2 ? path1.startsWith(path2) : path2.startsWith(path1))) { String diff = len1>len2 ? path1.substring(len1-separatorLen) : path2.substring(len2-separatorLen); return separator.equals(diff); } } return false; } /** * Returns <code>true</code> if the query part of this URL and the given URL are equal. * The comparison is case-sensitive. * * @param url the URL to test for query equality * @return <code>true</code> if the query part of this URL and the given URL are equal */ public boolean queryEquals(FileURL url) { return StringUtils.equals(this.query, url.query, true); } /** * Returns <code>true</code> if the credentials (login and password) of this URL and the given URL are equal. * The comparison is case-sensitive. * * @param url the URL to test for credentials equality * @return <code>true</code> if the credentials of this URL and the given URL are equal */ public boolean credentialsEquals(FileURL url) { Credentials creds1 = this.credentials; Credentials creds2 = url.credentials; return (creds1==null && creds2==null) || (creds1!=null && creds1.equals(creds2, true)) || (creds2!=null && creds2.equals(creds1, true)); } /** * Returns <code>true</code> if the properties contained by this URL and the given URL are equal. * The comparison of each property is case-sensitive. * * @param url the URL to test for properties equality * @return <code>true</code> if the properties contained by this URL and the given URL are equal */ public boolean propertiesEquals(FileURL url) { return (this.properties==null && url.properties==null) || (this.properties!=null && this.properties.equals(url.properties)) || (url.properties!=null && url.properties.equals(this.properties)); } //////////////////////// // Overridden methods // //////////////////////// /** * Returns a String representation of this FileURL, without including the login and password parts it may have. */ public String toString() { return toString(false); } /** * Returns a clone of this FileURL. The returned instance can safely be modified without any impact on this FileURL * or any previously cloned URL. */ @Override public Object clone() { // Create a new FileURL return it, instead of using Object.clone() which is probably way slower; // most FileURL fields are immutable and as such reused in cloned instance FileURL clonedURL = new FileURL(handler); // Immutable fields clonedURL.scheme = scheme; clonedURL.host = host; clonedURL.port = port; clonedURL.path = path; clonedURL.filename = filename; clonedURL.query = query; clonedURL.credentials = credentials; // Note: Credentials are immutable. // Mutable fields if(properties!=null) // Copy properties (if any) clonedURL.properties = new Hashtable<String, String>(properties); // Caches clonedURL.hashCode = hashCode; return clonedURL; } /** * This method is equivalent to calling {@link #equals(Object, boolean, boolean)} with credentials and properties * comparisons enabled. * * @param o object to compare against this FileURL instance. * @return true if both FileURL instances are equal. */ public boolean equals(Object o) { return equals(o, true, true); } /** * Tests the specified FileURL for equality with this FileURL. <code>false</code> is systematically returned if the * specified object is not a FileURL instance or is <code>null</code>. * <p> * Two <code>FileURL</code> instances are said to be equal if: * <ul> * <li>schemes are equal (case-insensitive)</li> * <li>hosts are equal (case-insensitive)</li> * <li>ports are equal. The default port is taken into account when comparing ports: a non specified port part (-1) * is equivalent to the scheme's standard port. For instance, <code>http://mucommander.com:80/</code> * and <code>http://mucommander.com/</code> are considered equal.</li> * <li>paths are equal (case-sensitive). There can be a trailing separator difference in the two paths, they will * still be considered as equal. For example, <code>/path</code> and <code>/path/</code> are considered equal * (assuming the path separator is '/').</li> * <li>queries are equal (case-sensitive)</li> * </ul> * </p> * <p> * Credentials (login and password parts) are compared only if requested. The comparison for both the login and * password is case-sensitive.</br> * Likewise, properties are compared only if requested: the comparison of all properties is case-sensitive. * </p> * * @param o object to compare against this FileURL instance * @param compareCredentials if <code>true</code>, the login and password parts of both FileURL need to be * equal (case-sensitive) for the FileURL instances to be equal * @param compareProperties if <code>true</code>, all properties need to be equal (case-sensitive) in both * FileURL for them to be equal * @return true if both FileURL instances are equal */ public boolean equals(Object o, boolean compareCredentials, boolean compareProperties) { if(o==null || !(o instanceof FileURL)) return false; FileURL url = (FileURL)o; return pathEquals(url) // Compare the path first as it is the most likely to be different && schemeEquals(url) && hostEquals(url) && portEquals(url) && queryEquals(url) && (!compareCredentials || credentialsEquals(url)) && (!compareProperties || propertiesEquals(url)); } /** * This method is overridden to return a hash code that takes into account the behavior of {@link FileURL#equals(Object)}, * so that <code>url1.equals(url2)</code> implies <code>url1.hashCode()==url2.hashCode()</code>. */ public int hashCode() { if(hashCode==0) { String separator = handler.getPathSeparator(); // #equals(Object) is trailing separator insensitive, so the hashCode must be trailing separator invariant int h = PathUtils.getPathHashCode(path, separator); h = 31* h + scheme.toLowerCase().hashCode(); h = 31* h + (port==-1?handler.getStandardPort():port); if(host!=null) h = 31* h + host.toLowerCase().hashCode(); if(query!=null) h = 31* h + query.hashCode(); if(credentials!=null) h = 31* h + credentials.hashCode(); if(properties!=null) h = 31* h + properties.hashCode(); // Cache the value until for as long as this instance is not modified hashCode = h; } return hashCode; } }