diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc index b9e50751ab1..2fda6212235 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc @@ -4,6 +4,79 @@ import IO; import DateTime; import String; +test bool testLogicalLocationResolution() { + str scheme = "test-logical"; + + value exceptionOf(value() f) { + try { + f(); + return ""; + } catch e: { + return e; + } + } + + bool throwsUnsupportedAuthority(value() f) { + return /Unsupported authority/ := ""; + } + + bool throwsExceptionDownstream(value() f) { + return str s := "" && "" != s && /Unsupported authority/ !:= s; + } + + try { + // Register authorities `foo` and `bar` + registerLocations(scheme, "foo", ( + |://foo/|: |file:///|, + |://foo/x/y/z|: |file:///x/y/z| + )); + registerLocations(scheme, "bar", ( + |://bar/|: |://foo/|, + |://bar/x/y/z|: |://foo/x/y/z| + )); + + assert lastModified(|://foo/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + assert lastModified(|://bar/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + + str cause = "Resolution either unexpectedly succeeded or failed with an unexpected exception"; + assert throwsExceptionDownstream(value() { return lastModified(|://foo/x/y/z|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|://bar/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://baz/|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://baz/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///x/y/z|); }) : cause; + + // Register default authority + registerLocations(scheme, "", ( + |://baz/|: |://bar/|, + |://baz/x/y/z|: |://bar/x/y/z| + )); + + assert lastModified(|://foo/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + assert lastModified(|://bar/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + assert lastModified(|://baz/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + + assert throwsExceptionDownstream(value() { return lastModified(|://foo/x/y/z|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|://bar/x/y/z|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|://baz/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///x/y/z|); }) : cause; + + return true; + } + catch false: // Catch block only to make finally block grammatical + throw false; + finally { + unregisterLocations(scheme, "foo"); + unregisterLocations(scheme, "bar"); + unregisterLocations(scheme, ""); + } +} + test bool testFileCopyCompletely() { writeFile(|tmp:///longFile|, "123456789"); writeFile(|tmp:///shortFile|, "321"); @@ -22,9 +95,9 @@ test bool testFileCopyRecursive() { test bool watchDoesNotCrashOnURIRewrites() { writeFile(|tmp:///watchDoesNotCrashOnURIRewrites/someFile.txt|, "123456789"); - watch(|tmp:///watchDoesNotCrashOnURIRewrites|, true, void (FileSystemChange event) { + watch(|tmp:///watchDoesNotCrashOnURIRewrites|, true, void (FileSystemChange event) { // this should trigger the failing test finally - remove(event.file); + remove(event.file); }); return true; } @@ -75,4 +148,4 @@ test bool renameDirectory() { writeFile(|tmp:///Folder/hello.txt|, "Hello World!"); rename(|tmp:///Folder|, |tmp:///RenamedFolder|); return readFile(|tmp:///RenamedFolder/hello.txt|) == "Hello World!"; -} \ No newline at end of file +} diff --git a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java index 05f17c91303..2f6fe715f7a 100644 --- a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java +++ b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java @@ -2,10 +2,32 @@ import java.io.IOException; +import org.checkerframework.checker.nullness.qual.Nullable; + import io.usethesource.vallang.ISourceLocation; +/** + * Resolver of logical locations with a certain {@link #scheme} and {@link #authority}. Implementations of this + * interface for non-empty authorities are authority-specific resolvers. Implementations for the distinguished + * empty authority are default resolvers. To resolve a logical location, first, an authority-specific resolver + * for the scheme is sought. If it doesn't exist, then the default resolver for the scheme is sought instead. If it + * doesn't exist either, then an exception is thrown. Note: If an authority-specific resolver does exist, but fails, + * then the default resolver will not be sought instead (i.e., the default resolver resembles the default case of a + * switch statement with non-fallthrough cases). + */ public interface ILogicalSourceLocationResolver { - ISourceLocation resolve(ISourceLocation input) throws IOException; + + /** + * @return {@code null} only if the authority of {@code input} is not supported/covered by this resolver. + * Non-{@code null} otherwise. + */ + @Nullable ISourceLocation resolve(ISourceLocation input) throws IOException; + String scheme(); + + /** + * @return A non-empty string if this is an authority-specific resolver. The empty string if this is a default + * resolver. + */ String authority(); } diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 79b5468115d..29cdf73cb85 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -4,7 +4,7 @@ * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: - * + * * * Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI * Paul Klint - Paul.Klint@cwi.nl - CWI * Mark Hills * - Mark.Hills@cwi.nl (CWI) * Arnold Lankamp - Arnold.Lankamp@cwi.nl *******************************************************************************/ @@ -58,7 +58,6 @@ import io.usethesource.vallang.type.TypeStore; public class URIResolverRegistry { - private static final int FILE_BUFFER_SIZE = 8 * 1024; private static final String RESOLVERS_CONFIG = "org/rascalmpl/uri/resolvers.config"; private static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); private final Map inputResolvers = new ConcurrentHashMap<>(); @@ -81,7 +80,7 @@ private URIResolverRegistry() { /** * Use with care! This (expensive) reinitialization method clears all caches of all resolvers by * reloading them from scratch. - * + * *

* This can be beneficial if the state of a system changes outside of the scope of the resolvers * themselves, for example when projects open or close inside a workspace or when plugins are loaded @@ -90,7 +89,7 @@ private URIResolverRegistry() { * from scratch. If such a URI re-defining event is detected, host environments (IDEs, app * containers, language servers) should call this method. *

- * + * *

* CAVEAT: after this reinitialization all location caches have been removed and so the first * locations to be used may require expensive indexing and probing operations, for example @@ -276,14 +275,14 @@ private OutputStream makeBuffered(ISourceLocation loc, boolean existed, OutputSt if (original instanceof BufferedOutputStream || original instanceof ByteArrayOutputStream) { return new NotifyingOutputStream( - original, - loc, + original, + loc, existed ? ISourceLocationWatcher.modified(loc) : ISourceLocationWatcher.created(loc) ); } - return new NotifyingOutputStream(new BufferedOutputStream(original), - loc, + return new NotifyingOutputStream(new BufferedOutputStream(original), + loc, existed ? ISourceLocationWatcher.modified(loc) : ISourceLocationWatcher.created(loc) ); } @@ -314,7 +313,7 @@ public void write(byte[] b, int off, int len) throws IOException { * this mapping the registered {@link ILogicalSourceLocationResolver} collection is used. These are * indexed first by scheme and then by authority. If the scheme is registered but the authority is * not, then the same lookup is tried again without authority. - * + * * @param loc logical source location * @return physical source location * @throws IOException when there is no registered resolver for the logical scheme provided @@ -327,7 +326,7 @@ public ISourceLocation logicalToPhysical(ISourceLocation loc) throws IOException return result; } - private static ISourceLocation resolveAndFixOffsets(ISourceLocation loc, ILogicalSourceLocationResolver resolver, Iterable backups) throws IOException { + private static ISourceLocation resolveAndFixOffsets(ISourceLocation loc, ILogicalSourceLocationResolver resolver) throws IOException { ISourceLocation prev = loc; boolean removedOffset = false; @@ -335,27 +334,11 @@ private static ISourceLocation resolveAndFixOffsets(ISourceLocation loc, ILogica loc = resolver.resolve(loc); } - if (loc == null && prev.hasOffsetLength()) { + if (loc == null && resolver != null && prev.hasOffsetLength()) { loc = resolver.resolve(URIUtil.removeOffset(prev)); removedOffset = true; } - if (loc == null || prev.equals(loc)) { - for (ILogicalSourceLocationResolver backup : backups) { - removedOffset = false; - loc = backup.resolve(prev); - - if (loc == null && prev.hasOffsetLength()) { - loc = backup.resolve(URIUtil.removeOffset(prev)); - removedOffset = true; - } - - if (loc != null && !prev.equals(loc)) { - break; // continue to offset/length handling below with found location - } - } - } - if (loc == null || prev.equals(loc)) { return null; } @@ -394,29 +377,54 @@ else if (loc.hasOffsetLength()) { // the logical location offsets relative to th return loc; } - private ISourceLocation physicalLocation(ISourceLocation loc) throws IOException { - ISourceLocation original = loc; - while (loc != null && logicalResolvers.containsKey(loc.getScheme())) { - Map map = logicalResolvers.get(loc.getScheme()); - String auth = loc.hasAuthority() ? loc.getAuthority() : ""; - ILogicalSourceLocationResolver resolver = map.get(auth); - loc = resolveAndFixOffsets(loc, resolver, map.values()); - } - - if (externalRegistry != null && original != null) { - try { - var externalResolve = loc == null ? original : loc; - if (externalRegistry.supportsLogical(externalResolve.getScheme())) { - var externalResult = resolveAndFixOffsets(externalResolve, externalRegistry, Collections.emptyList()); - return externalResult == null ? loc : externalResult; - } - } catch (IOException e) { - // Ignore remote IO errors + /** + * Computes the physical location for the provided location {@code loc}, according to the following rules: + *

    + *
  • If {@code loc} is known to be logical, and a resolver does exists for its authority, and it can be resolved, then return that physical location. + *
  • If {@code loc} is known to be logical, and a resolver does exists for its authority, but it cannot be resolved, then return null. + *
  • If {@code loc} is known to be logical, but a resolver doesn't exist for its authority, then throw an exception. + *
  • If {@code loc} is unknown to be logical, then return {@code loc}. + *
+ */ + private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IOException { + var scheme = loc.getScheme(); + var resolversByAuth = logicalResolvers.get(scheme); + + // Case: `loc` is known to be logical and has a local resolver + if (resolversByAuth != null) { + var auth = loc.getAuthority(); + + var resolver = resolversByAuth.get(auth); + if (resolver == null) { + resolver = resolversByAuth.get(""); + } + if (resolver == null) { + throw new UnsupportedAuthorityException(scheme, auth); + } + + var resolved = resolveAndFixOffsets(loc, resolver); + if (resolved == null) { + throw new UnsupportedAuthorityException(scheme, auth); } + + return resolved != null ? physicalLocation(resolved) : null; + } + + // Case: `loc` is known to be logical and has a remote resolver + if (externalRegistry != null && externalRegistry.supportsLogical(scheme)) { + var resolved = resolveAndFixOffsets(loc, externalRegistry); + return resolved != null ? physicalLocation(resolved) : null; } + + // Case: `loc` is unknown to be logical return loc; } + private @NonNull ISourceLocation tryResolve(@NonNull ISourceLocation loc) throws IOException { + var resolved = physicalLocation(loc); + return resolved != null ? resolved : loc; + } + private @NonNull ISourceLocation safeResolve(@NonNull ISourceLocation loc) { ISourceLocation resolved = null; @@ -561,12 +569,12 @@ public boolean exists(ISourceLocation uri) { /** * set the last modification date of a file - * + * * @param timestamp in millis since the epoch * @throws IOException */ public void setLastModified(ISourceLocation uri, long timestamp) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); @@ -588,7 +596,7 @@ public boolean isDirectory(ISourceLocation uri) { } public void mkDirectory(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); if (resolver == null) { @@ -606,7 +614,7 @@ private void notifyWatcher(ISourceLocation key, ISourceLocationChanged event) { } public void remove(ISourceLocation uri, boolean recursive) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput out = getOutputResolver(uri.getScheme()); if (out == null) { @@ -632,7 +640,7 @@ else if (listEntries(uri).length != 0) { /** * Moves a file from source name to target name. If the source is a folder, then it is moved recursively. - * + * * @param from existing name of file or folder * @param to new name of file or folder * @param overwrite if `false` and the target folder or file already exists, throw an exception @@ -640,8 +648,8 @@ else if (listEntries(uri).length != 0) { * exists and overwrite was `false`. */ public void rename(ISourceLocation from, ISourceLocation to, boolean overwrite) throws IOException { - from = safeResolve(from); - to = safeResolve(to); + from = tryResolve(from); + to = tryResolve(to); if (from.getScheme().equals(to.getScheme())) { ISourceLocationOutput out = getOutputResolver(from.getScheme()); @@ -669,7 +677,7 @@ public boolean isFile(ISourceLocation uri) { } public long lastModified(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -688,7 +696,7 @@ public long lastModified(ISourceLocation uri) throws IOException { } public long created(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -707,7 +715,7 @@ public long created(ISourceLocation uri) throws IOException { } public boolean isWritable(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); var resolver = getOutputResolver(uri.getScheme()); if (resolver != null) { return resolver.isWritable(uri); @@ -719,7 +727,7 @@ public boolean isWritable(ISourceLocation uri) throws IOException { return false; } public boolean isReadable(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); var resolver = getInputResolver(uri.getScheme()); if (resolver == null) { throw new UnsupportedSchemeException(uri.getScheme()); @@ -728,13 +736,13 @@ public boolean isReadable(ISourceLocation uri) throws IOException { } /** - * This is byte size, and should not be exposed to the rascal users. + * This is byte size, and should not be exposed to the rascal users. * @param uri * @return * @throws IOException */ public long size(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -749,7 +757,8 @@ private boolean isRootLogical(ISourceLocation uri) { && logicalResolvers.containsKey(uri.getScheme()); } - public String[] listEntries(ISourceLocation uri) throws IOException { uri = safeResolve(uri); + public String[] listEntries(ISourceLocation uri) throws IOException { + uri = tryResolve(uri); if (isRootLogical(uri)) { // if it's a location without any path and authority // we want to list possible authorities if it's a logical one @@ -783,8 +792,8 @@ private boolean isRootLogical(ISourceLocation uri) { * when a source folder or file can not be read */ public void copy(ISourceLocation source, ISourceLocation target, boolean recursive, boolean overwrite) throws IOException { - var sourceResolved = safeResolve(source); - var targetResolved = safeResolve(target); + var sourceResolved = tryResolve(source); + var targetResolved = tryResolve(target); if (sourceResolved.getScheme().equals(targetResolved.getScheme())) { var commonResolver = getOutputResolver(sourceResolved.getScheme()); if (commonResolver != null && commonResolver.supportsCopy()) { @@ -804,7 +813,7 @@ public void copy(ISourceLocation source, ISourceLocation target, boolean recursi throw new IOException("can not make directory because file exists: " + target); } } - + mkDirectory(targetResolved); for (String elem : URIResolverRegistry.getInstance().listEntries(sourceResolved)) { @@ -830,7 +839,7 @@ private void copyFile(ISourceLocation source, ISourceLocation target, boolean ov if (exists(target) && overwrite) { remove(target, false); } - + if (supportsReadableFileChannel(source) && supportsWritableFileChannel(target) && size(source) > 8*1024) { try (FileChannel from = getReadableFileChannel(source)) { try (FileChannel to = getWriteableFileChannel(target, false)) { @@ -875,7 +884,7 @@ public Reader getCharacterReader(ISourceLocation uri, String encoding) throws IO } public Reader getCharacterReader(ISourceLocation uri, Charset encoding) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); Reader res = new UnicodeInputStreamReader(getInputStream(uri), encoding); if (uri.hasOffsetLength()) { @@ -888,20 +897,20 @@ public Reader getCharacterReader(ISourceLocation uri, Charset encoding) throws I /** * Return a character Writer for the given uri, using the given character encoding. - * + * * @param uri file to write to or append to * @param encoding how to encode individual characters @see Charset * @param append whether to append or start at the beginning. * @return - * @throws IOException + * @throws IOException */ public Writer getCharacterWriter(ISourceLocation uri, String encoding, boolean append) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); return new UnicodeOutputStreamWriter(getOutputStream(uri, append), encoding); } public ClassLoader getClassLoader(ISourceLocation uri, ClassLoader parent) throws IOException { - IClassloaderLocationResolver resolver = getClassloaderResolver(safeResolve(uri).getScheme()); + IClassloaderLocationResolver resolver = getClassloaderResolver(tryResolve(uri).getScheme()); if (resolver != null) { // we always try the most specific implementation for efficiency's sake @@ -955,7 +964,7 @@ protected Class findClass(final String qualifiedClassName) throws ClassNotFou public InputStream getInputStream(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -966,7 +975,7 @@ public InputStream getInputStream(ISourceLocation uri) throws IOException { } public FileChannel getReadableFileChannel(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null || !resolver.supportsReadableFileChannel()) { @@ -978,7 +987,7 @@ public FileChannel getReadableFileChannel(ISourceLocation uri) throws IOExceptio public Charset detectCharset(ISourceLocation sloc) { URIResolverRegistry reg = URIResolverRegistry.getInstance(); - + // in case the file already has a encoding, we have to correctly append that. Charset detected = null; try (InputStream in = reg.getInputStream(sloc);) { @@ -992,14 +1001,14 @@ public Charset detectCharset(ISourceLocation sloc) { // we stick with the default if something happened above. // if the writing hereafter fails as well, the exception will // be just as descriptive - detected = null; - } + detected = null; + } return detected != null ? Charset.forName(detected.name()) : Charset.defaultCharset(); } public Charset getCharset(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null || (externalRegistry != null && resolver == externalRegistry && !externalRegistry.supportsGetCharset(uri.getScheme()))) { @@ -1010,7 +1019,7 @@ public Charset getCharset(ISourceLocation uri) throws IOException { } public OutputStream getOutputStream(ISourceLocation uri, boolean append) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); boolean existedBefore = exists(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); @@ -1028,7 +1037,7 @@ public OutputStream getOutputStream(ISourceLocation uri, boolean append) throws } public FileChannel getWriteableFileChannel(ISourceLocation uri, boolean append) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); if (resolver == null || !resolver.supportsWritableFileChannel()) { @@ -1048,7 +1057,7 @@ public FileChannel getWriteableFileChannel(ISourceLocation uri, boolean append) } private void mkParentDir(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocation parentURI = URIUtil.getParentLocation(uri); if (parentURI != null && !parentURI.equals(uri) && !exists(parentURI)) { @@ -1112,7 +1121,7 @@ public ISet capabilities(ISourceLocation loc) { if (watchers.hasNativeSupport(scheme)) { result.insert(vf.constructor(watchCap)); } - + return result.done(); } @@ -1137,7 +1146,7 @@ public boolean hasNativelyWatchableResolver(ISourceLocation loc) { } public FileAttributes stat(ISourceLocation loc) throws IOException { - loc = safeResolve(loc); + loc = tryResolve(loc); var resolver = getInputResolver(loc.getScheme()); if (resolver == null) { throw new IOException("Unsupported scheme: " + loc.getScheme()); diff --git a/src/org/rascalmpl/uri/UnsupportedAuthorityException.java b/src/org/rascalmpl/uri/UnsupportedAuthorityException.java new file mode 100644 index 00000000000..0d746754fc6 --- /dev/null +++ b/src/org/rascalmpl/uri/UnsupportedAuthorityException.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2026, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri; + +import java.io.IOException; + +public class UnsupportedAuthorityException extends IOException { + private static final long serialVersionUID = -3261531203667050736L; + + public UnsupportedAuthorityException(String scheme, String authority) { + super("Unsupported authority '" + authority + "' for scheme '" + scheme + "'"); + } +}