/* * Copyright 2005 by Oracle USA * 500 Oracle Parkway, Redwood Shores, California, 94065, U.S.A. * All rights reserved. */ package javax.ide.net; import java.io.File; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.net.URI; import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Iterator; /** * This class contains methods which create new instances of * {@link URI}. In order for {@link URI}s to be used effectively as * keys in a hashtable, the URI must be created in a very consistent * manner. Therefore, when creating a new instance of {@link URI}, it * is strongly recommended that you use one of the methods in this class * rather than calling the {@link URI} constructor directly. This will * help prevent subtle bugs that can come up when two {@link URI} * instances that should be equal aren't equal (resulting in caching * bugs) because of a difference in some detail of the URI that affects * the result of the {@link Object#equals(Object)} method but doesn't * affect the location pointed to by the {@link URI} (which means that * code other than the caching will actually continue to work).
*
* Additionally, by using the methods in this class to create instances
* of {@link URI}, dependencies on {@link URI} can be tracked more
* easily.
*
*/
public final class URIFactory
{
public static final String JAR_URI_SEPARATOR = "!/"; // NOTRANS
/**
* This is a callback interface used by the {@link URIFactory}
* while it is in the process of producing a new unique
* {@link java.net.URI URI}.
*/
public interface NameGenerator
{
/**
* This method is called by {@link URIFactory} from one of its
* newUniqueURI()
methods to obtain a relative
* file name. The name is usually just a simple file name without any
* preceeding directory names, that will be used in the process of
* generating a unique {@link java.net.URI}.
*
* The {@link URIFactory} is responsible for assembling a complete
* URI
by combining a base URI
with the
* relative file name returned by nextName()
. The
* {@link URIFactory} is also responsible for checking that the
* newly created URI
is unique among files on disk
* and within the IDE's caches. If the new URI
is not
* unique, then {@link URIFactory} will issue another call to
* nextName()
to get a new name. This process is
* repeated until either a unique URI
is produced, or
* the {@link URIFactory} "times out" on the
* NameGenerator
implementation after a very large
* number of iterations fails to produce a unique URI
.
*
*
* Therefore to interact properly with {@link URIFactory}, the
* nextName()
implementation must return a different
* name each time it is invoked. More specifically, a particular
* instance of NameGenerator
should not return any
* name more than once from its nextName()
method.
* Of course, this restriction does not apply across different
* instances of NameGenerator
.
*
* The exact means by which a new name is produced is not specified
* and is left to the specific NameGenerator
classes.
* However, the nextName()
method should not attempt to
* create an URI
and check for its uniqueness, as this
* may lead to problems in the future when the {@link URIFactory} and
* {@link VirtualFileSystem} classes are enhanced. For example, if
* individual NameGenerator
implementations are
* attempting to determine uniqueness, bugs may surface later if the
* IDE's algorithm for determining uniqueness changes. How the IDE
* determines URI
uniqueness is not documented and should
* be considered an implementation detail of the IDE framework.
*/
public String nextName();
}
//--------------------------------------------------------------------------
// non-sanitizing factory methods...
//--------------------------------------------------------------------------
/**
* Creates a new {@link URI} that is the combination of the
* specified base {@link URI} and the relative spec string. The
* base {@link URI} is treated as a directory, whether or not the
* {@link URI} ends with the "/" character, and the relative spec
* is always treated as relative, even if it begins with a "/".
* * Non-sanitizing. */ public static URI newURI( URI baseURI, String relativeSpec ) { final String newPath = resolveRelative( baseURI.getPath(), relativeSpec ); if ( isJarURI( baseURI ) ) { if ( newPath.indexOf( JAR_URI_SEPARATOR ) < 0 ) { // Then this means that the relative spec must have contined // some ".." sequences and cause the URI to ascend into the // jar file's path itself. return newJarFileURIImpl( newPath ); } } return URIFactory.replacePathPart( baseURI, newPath ); } /** * Creates a new {@link URI} that is the combination of the * specified base {@link URI} and the relative spec string. The * base {@link URI} is treated as a directory whether or not it * ends with the "/" character. The returned {@link URI} will * return with the "/" character in the path part.
* * Non-sanitizing. */ public static URI newDirURI( URI baseURI, String relativeSpec ) { return relativeSpec.endsWith( "/" ) ? //NOTRANS newURI( baseURI, relativeSpec ) : newURI( baseURI, relativeSpec + "/" ); //NOTRANS } /** * Standard way of specifying a scheme with a file path. The file * path is used in the {@link URI} verbatim, without any changes to * the file separator character or any other characters. For an {@link URI} whose scheme is "file", the * {@link #newFileURI(String)} factory method should be used instead. *
*
* Non-sanitizing.
*/
public static URI newURI( String scheme, String path )
{
return newURI( scheme, null, null, -1, path, null, null );
}
/**
* Creates a new {@link URI} that is the combination of the
* specified scheme
and the directory path. The directory
* path is used in the {@link URI} verbatim, without any changes to
* the file separator character or any other characters.
* The returned {@link URI} will return with the "/" character in the
* path part.
*
* Non-sanitizing.
*/
public static URI newDirURI( String scheme, String dirPath )
{
return dirPath.endsWith( "/" ) ? //NOTRANS
newURI( scheme, dirPath ) :
newURI( scheme, dirPath + "/" ); //NOTRANS
}
/**
* Creates a new unique URI using the scheme of the specified
* baseURI
. The nameGen
object is called to
* generate a unique name that will be appended to the base uri.
*
* Non-sanitizing.
*/
public static URI newUniqueURI( URI baseURI, NameGenerator nameGen )
{
while ( true )
{
final String name = nameGen.nextName();
final URI uri = newURI( baseURI, name );
if ( uri == null )
{
return null;
}
// This will get expensive if many iterations are required to get
// a unique URI, especially if the URI points to a resource that
// requires network access.
if ( !VirtualFileSystem.getVirtualFileSystem().isBound( uri ) )
{
return uri;
}
}
}
/**
* Returns a new {@link URI} that is identical to the specified
* {@link URI} except that the scheme part of the {@link URI}
* has been replaced with the specified newscheme
.
*
* Non-sanitizing.
*/
public static URI replaceSchemePart( URI uri, String newScheme )
{
final String userinfo = uri.getUserInfo();
final String host = uri.getHost();
final int port = uri.getPort();
final String path = uri.getPath();
final String query = uri.getQuery();
final String fragment = uri.getFragment();
return newURI( newScheme, userinfo, host, port, path, query, fragment );
}
/**
* Returns a new {@link URI} that is identical to the specified
* {@link URI} except that the port part of the {@link URI}
* has been replaced with the specified newPort
.
*
* Non-sanitizing.
*/
public static URI replacePortPart( URI uri, int newPort )
{
final String scheme = uri.getScheme();
final String userinfo = uri.getUserInfo();
final String host = uri.getHost();
final String path = uri.getPath();
final String query = uri.getQuery();
final String fragment = uri.getFragment();
return newURI( scheme, userinfo, host, newPort, path, query, fragment );
}
/**
* Returns a new {@link URI} that is identical to the specified
* {@link URI} except that the host part of the {@link URI}
* has been replaced with the specified newHost
.
*
* Non-sanitizing.
*/
public static URI replaceHostPart( URI uri, String newHost )
{
final String scheme = uri.getScheme();
final String userinfo = uri.getUserInfo();
final int port = uri.getPort();
final String path = uri.getPath();
final String query = uri.getQuery();
final String fragment = uri.getFragment();
return newURI( scheme, userinfo, newHost, port, path, query, fragment );
}
/**
* Returns a new {@link URI} that is identical to the specified
* {@link URI} except that the path part of {@link URI} has been
* replaced with the specified newPath
.
*
* Non-sanitizing.
*/
public static URI replacePathPart( URI uri, String newPath )
{
final String scheme = uri.getScheme();
final String userinfo = uri.getUserInfo();
final String host = uri.getHost();
final int port = uri.getPort();
final String query = uri.getQuery();
final String fragment = uri.getFragment();
return newURI( scheme, userinfo, host, port, newPath, query, fragment );
}
/**
* Returns a new {@link URI} that is identical to the specified
* {@link URI} except that the fragment part of the {@link URI}
* has been replaced with the specified newRef
.
*
* Non-sanitizing.
*/
public static URI replaceFragmentPart( URI uri, String newFragment )
{
final String scheme = uri.getScheme();
final String userinfo = uri.getUserInfo();
final String host = uri.getHost();
final int port = uri.getPort();
final String path = uri.getPath();
final String query = uri.getQuery();
return newURI( scheme, userinfo, host, port, path, query, newFragment );
}
/**
* Returns a new {@link URI} that is identical to the specified
* {@link URI} except that the query part of the {@link URI}
* has been replaced with the specified newQuery
.
*
* Non-sanitizing.
*/
public static URI replaceQueryPart( URI uri, String newQuery )
{
final String scheme = uri.getScheme();
final String userinfo = uri.getUserInfo();
final String host = uri.getHost();
final int port = uri.getPort();
final String path = uri.getPath();
final String fragment = uri.getFragment();
return newURI( scheme, userinfo, host, port, path, newQuery, fragment );
}
//--------------------------------------------------------------------------
// factory methods taking string specifications...
//--------------------------------------------------------------------------
/*-
* NOTE: This implementation has known bugs. To avoid these bugs,
* you should strongly consider using any of the other URIFactory
* methods that do not require parsing a uriSpec string.
*
* Non-sanitizing.
*/
public static URI newURI( String uriSpec )
{
return newURI( uriSpec, false, false );
}
/*-
* Do not make this public yet.
*/
static URI newURI( String uriSpec, boolean forceDir, boolean assumeFile )
{
if ( uriSpec == null )
{
return null;
}
if ( forceDir && !uriSpec.endsWith( "/" ) ) //NOTRANS
{
uriSpec += "/"; //NOTRANS
}
final int uriSpecLen = uriSpec.length();
if ( uriSpecLen > 0 )
{
if ( uriSpec.charAt( 0 ) == File.separatorChar ||
( uriSpecLen > 1 && uriSpec.charAt( 1 ) == ':' ) ||
( uriSpec.indexOf( ':' ) < 0 && assumeFile ) )
{
return newFileURI( uriSpec );
}
}
if ( uriSpec.toLowerCase().startsWith( "file:" ) ) // NOTRANS
{
return newFileURI( uriSpec.substring( 5 ) );
}
else
{
try
{
// -- Bugs here if # or ? appear in the URI string.
// -- Need to use a customized URI parser to convert
// -- the string into an URI. This will do for now,
// -- since # and ? are infrequently used in URIs.
//return new URI( URLEncoder.encode( uriSpec, "UTF-8" ) );
return new URI( uriSpec );
}
// catch ( UnsupportedEncodingException ue )
// {
//
// }
catch ( URISyntaxException e )
{
// Silent. The return value will be null, which the caller
// should interpret to mean that no valid URI was specified.
}
}
return null;
}
//--------------------------------------------------------------------------
// sanitizing factory methods...
//--------------------------------------------------------------------------
/**
* Creates a new {@link URI} using the "file
" scheme.
* The specified filePath
can be expressed in the
* notation of the platform that the Java VM is currently running on,
* or it can be expressed using the forward slash character ("/") as
* its file separator character, which is the standard file separator
* for {@link URI}s. Note that technically, the forward slash
* character is the only officially recognized hierarchy separator
* character for an URI.
*
* Sanitizing.
*/
public static URI newFileURI( String filePath )
{
if ( filePath == null )
{
return null;
}
return newFileURI( new File( filePath ) );
}
/**
* This method converts a {@link File} instance into an {@link URI}
* instance using an algorithm that is consistent with the other
* factory methods in URIFactory
.
*
* Sanitizing.
*
* @return An {@link URI} corresponding to the given {@link File}.
* The {@link URI} is produced using a mechanism that ensures
* uniformity of the {@link URI} format across platforms.
*/
public static URI newFileURI( File file )
{
if ( file == null )
{
return null;
}
URI uri = file.toURI();
if ( file.isDirectory() )
{
String path = uri.getPath();
if ( !path.endsWith( "/" ) )
{
path = path + "/";
try
{
uri = new URI( uri.getScheme(), uri.getAuthority(), path, uri.getQuery(), uri.getFragment() );
}
catch ( URISyntaxException e )
{
throw new AssertionError( e );
}
}
}
return uri;
}
/**
* Creates a new {@link URI} with the "file" scheme that is for the
* specified directory. Leading and trailing "/" characters are
* added if they are missing. If the specified dirPath
* is null
, then the returned {@link URI} is
* null
.
*
* Sanitizing.
*/
public static URI newDirURI( String dirPath )
{
if ( dirPath == null )
{
return null;
}
String path = sanitizePath( dirPath );
if ( !path.endsWith( "/" ) ) //NOTRANS
{
path = path + "/"; //NOTRANS
}
return newURI( VirtualFileSystem.FILE_SCHEME, path );
}
/**
* Creates a new {@link URI} with the "file" scheme that is for the
* specified directory. Leading and trailing "/" characters are
* added if they are missing. This method does not check whether the
* specified {@link File} is actually a directory on disk; it just
* assumes that it is. If the specified dirPath
is
* null
, then the returned {@link URI} is
* null
.
*
* Sanitizing.
*/
public static URI newDirURI( File dir )
{
return dir != null ? newDirURI( dir.getAbsolutePath() ) : null;
}
//--------------------------------------------------------------------------
// jar URI factory methods...
//--------------------------------------------------------------------------
/**
* Builds an {@link URI} using the "jar
" scheme based
* on the specified archive {@link File} and the entry name passed
* in. The entry name is relative to the root of the jar file, so
* it should not begin with a slash. The entry name may be the
* empty string or null, which means that the returned {@link URI}
* should represent the jar file itself.
*
* Sanitizing for archiveFile; non-sanitizing for entryName.
*/
public static URI newJarURI( File archiveFile, String entryName )
{
final URI archiveURI = newFileURI( archiveFile );
return newJarURI( archiveURI, entryName );
}
/**
* Returns true
if the specified {@link URI} has
* the "jar" scheme. Returns false
if the specified
* {@link URI} is null
or has a scheme other than
* "jar".
*/
public static boolean isJarURI( URI jarURI )
{
return jarURI != null
? jarURI.getScheme().equals( "jar" )
: false; // NOTRANS
}
/**
* Determine if a given URL represents an jar or zip file. The method
* does a simple check to determine if the pathname ends with
* .jar or .zip.
*/
public static boolean isArchive( String pathname )
{
final int lastDot = pathname.lastIndexOf( '.' );
if ( lastDot < 0 )
{
return false;
}
final String ext = pathname.substring( lastDot ).toLowerCase();
return ext.equals( ".jar" ) || ext.equals( ".zip" ); // NOTRANS
}
/**
* Builds an {@link URI} using the "jar
" scheme based
* on the specified archive {@link URI} and the entry name passed in.
* The entry name is relative to the root of the jar file, so it
* should not begin with a slash. The entry name may be the empty
* string or null, which means that the returned {@link URI}
* should represent the jar file itself.
* * Non-sanitizing for both archiveURI and entryName. */ public static URI newJarURI( URI archiveURI, String entryName ) { if ( isJarURI( archiveURI ) ) { // If the specified URI is already a "jar" URI, we don't want to // put another "jar:" scheme in front of it. Instead, we just // want to append the entry name to the existing jar URI. First // we need to verify that the "!/" delimiter is already there. // Then we force the ending "/" onto the base URI, if it didn't // already have it (it really should). Then the entry name is // appended. final String path = archiveURI.getPath(); final int bangSlash = path.indexOf( JAR_URI_SEPARATOR ); if ( bangSlash < 0 ) { throw new IllegalArgumentException( "Bad jar uri: " + archiveURI ); // NOTRANS } final StringBuffer newPath = new StringBuffer( path ); if ( !path.endsWith( "/" ) ) //NOTRANS { newPath.append( '/' ); //NOTRANS } if ( entryName != null ) { newPath.append( entryName ); } return replacePathPart( archiveURI, newPath.toString() ); } else { try { final StringBuffer path = new StringBuffer( URLDecoder.decode( archiveURI.toString(), "UTF-8" ) ); path.insert( 0, '/' ); path.append( JAR_URI_SEPARATOR ); if ( entryName != null ) { path.append( entryName ); } return newURI( VirtualFileSystem.JAR_SCHEME, path.toString() ); } catch ( UnsupportedEncodingException uee ) { uee.printStackTrace(); assert false; } return null; } } //-------------------------------------------------------------------------- // direct access factory methods... //-------------------------------------------------------------------------- /** * Creates a new {@link URI} whose parts have the exact values that * are specified. In general, you should avoid calling this * method directly.
*
* This method is the ultimate place where all of the other
* URIFactory
methods end up when creating an
* {@link URI}.
*
* Non-sanitizing.
*/
public static URI newURI( String scheme, String userinfo,
String host, int port,
String path, String query, String fragment )
{
try
{
return new URI( scheme, userinfo, host, port, path, query, fragment );
}
catch ( Exception e )
{
e.printStackTrace();
return null;
}
}
/**
* Creates a new {@link URI} form an {@link URL}.
* @param url The URL from which URI is derived.
* @return New URI.
*/
public static URI newURI( URL url )
{
// the URI() constructor does not work well with jar: URLs. Basically,
// URI likes jar:/file:/path/to/file.jar!/entry/file.txt
// URL likes jar:file:/path/to/file.jar!/entry/file.txt
String path = url.getPath();
if ( "jar".equalsIgnoreCase( url.getProtocol() ) )
{
path = "/" + path;
}
return newURI( url.getProtocol(),
url.getUserInfo(),
url.getHost(),
url.getPort(),
path,
url.getQuery(),
url.getRef() );
}
//--------------------------------------------------------------------------
// implementation details...
//--------------------------------------------------------------------------
/**
* This "sanitizes" the specified string path by converting all
* {@link File#separatorChar} characters to forward slash ('/').
* Also, a leading forward slash is prepended if the path does
* not begin with one.
*/
private static String sanitizePath( String path )
{
if ( File.separatorChar != '/' )
{
path = path.replace( File.separatorChar, '/' );
}
if ( !path.startsWith( "/" ) ) //NOTRANS
{
path = "/" + path; //NOTRANS
}
// bug 6994276 - replicates logic in java.io.File.toURI()
if ( path.startsWith( "//" ) )
{
path = "//" + path;
}
return path;
}
private static String resolveRelative( String basePath, String relPath )
{
// Concatenate the base path and relative spec together,
// eliminating any occurrences of "." or ".." in the relative
// spec. If "." or ".." occur in the baseURI, they are treated
// as literal names of directories.
//
// Note that the tokenization is done here directly for performance
// reasons rather than through a StringTokenizer.
// The basePath tokenization is set to return the "/" tokens
// as well. This is needed to detect a UNC path, which begins
// with two slashes.
final ArrayList pathElems = new ArrayList();
for ( int pos = 0 ;; )
{
final int newPos = basePath.indexOf( '/', pos );
final boolean done = newPos == -1;
final String substr = done
? basePath.substring( pos )
: basePath.substring( pos, newPos );
if ( substr.length() > 0 || newPos == pos + 1 )
{
pathElems.add( substr );
}
if ( done )
{
break;
}
pos = newPos + 1;
}
// The relPath tokenization is not currently configured to return
// the "/" tokens, so there is an asymmetry here wrt basePath.
// There isn't a problem with this right now because file systems
// treat multiple slashes in the middle of the path as being a
// single delimiter, but there may be a very special-case problem
// with some schemes if there is a dependency on the occurrence
// of multiple slashes in the path part of the URI.
boolean lastElemIsRelativeDir = false;
for ( int pos = 0 ;; )
{
final int newPos = relPath.indexOf( '/', pos );
final boolean done = newPos == -1;
final String substr = done
? relPath.substring( pos )
: relPath.substring( pos, newPos );
if ( substr.length() > 0 )
{
if ( substr.equals( ".." ) ) // NOTRANS
{
final int n = pathElems.size();
if ( n > 0 )
{
pathElems.remove( n - 1 );
}
lastElemIsRelativeDir = true;
}
else if ( substr.equals( "." ) ) //NOTRANS
{
lastElemIsRelativeDir = true;
}
else
{
pathElems.add( substr );
lastElemIsRelativeDir = false;
}
}
if ( done )
{
break;
}
pos = newPos + 1;
}
// Put the path elements together to form the new path specification.
final StringBuffer newPath = new StringBuffer();
if ( basePath.startsWith( "//" ) ) //NOTRANS
{
newPath.append( "//" ); //NOTRANS
}
else if ( basePath.startsWith( "/" ) ) //NOTRANS
{
newPath.append( '/' ); //NOTRANS
}
for ( Iterator iter = pathElems.iterator(); iter.hasNext(); )
{
newPath.append( iter.next().toString() ).append( '/' ); //NOTRANS
}
if ( !lastElemIsRelativeDir &&
!relPath.endsWith( "/" ) && //NOTRANS
relPath.length() != 0 )
{
final int length = newPath.length();
if ( length > 0 )
{
newPath.setLength( length - 1 );
}
}
return newPath.toString();
}
private static URI newJarFileURIImpl( String uriStr )
{
try
{
// No choice here -- must use the URI constructor directly
// (instead of using the preferred approach of going through
// URIFactory) because we have no way of knowing what's in
// the URI string. There may be problems on some JDK
// implementations if the jar/zip file resides on a path that
// has '#', '?', or whitespace in the name. That's because
// Sun's JDK has a number of bugs in the way it handles URIs.
return new URI( uriStr );
}
catch ( URISyntaxException e )
{
e.printStackTrace();
return null;
}
}
/**
* Private constructor prevents instantiation.
*/
private URIFactory()
{
// NOP.
}
}