blob: 2fef5f6325910a8cb9e4a06c720cbcabe5bc9000 [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library resoure.resource;
import "dart:async" show Future, Stream;
import "dart:convert" show Encoding;
import "dart:isolate" show Isolate;
import "package_resolver.dart";
import "io.dart" as io; // Loading strategy. TODO: Be configuration dependent.
/// A strategy for resolving package URIs
abstract class PackageResolver {
/// Cache of the current resolver, accessible directly after it has first
/// been found asynchronously.
static PackageResolver _current;
/// The package resolution strategy used by the current isolate.
static final Future<PackageResolver> current = _findIsolateResolution();
PackageResolver();
/// Creates a resolver using a map from package name to package location.
factory PackageResolver.fromMap(Map<String, Uri> packages) = MapResolver;
/// Creates a resolver using a package root.
factory PackageResolver.fromRoot(Uri packageRoot) = RootResolver;
/// Resolves a package URI to its location.
///
/// If [uri] does not have `package` as scheme, it is returned again.
/// Otherwise the package name is looked up, and if found, a location
/// for the package file is returned.
Future<Uri> resolve(Uri uri);
/// Returns a [Resource] for the [uri] as resolved by this resolver.
Resource resource(Uri uri) {
return new _UriResource(this, uri);
}
/// Finds the way the current isolate resolves package URIs.
///
/// Is only called once, and when it has been called, the [_current]
/// resolver is initialized, so [UriResource] will be initialized
/// with the resolver directly.
static Future<PackageResolver> _findIsolateResolution() async {
var pair = await Future.wait([Isolate.packageRoot, Isolate.packageMap]);
Uri root = pair[0];
if (root != null) {
_current = new RootResolver(root);
} else {
Map<String, Uri> map = pair[1];
_current = new MapResolver(map);
}
return _current;
}
}
/// A resource that can be read into the program.
///
/// A resource is data that can be located using a URI and read into
/// the program at runtime.
/// The URI may use the `package` scheme to read resources provided
/// along with package sources.
abstract class Resource {
/// Creates a resource object with the given [uri] as location.
///
/// The `uri` is a string containing a valid URI.
/// If the string is not a valid URI, using any of the functions on
/// the resource object will fail.
///
/// The URI may be relative, in which case it will be resolved
/// against [Uri.base] before being used.
///
/// The URI may use the `package` scheme, which is always supported.
/// Other schemes may also be supported where possible.
const factory Resource(String uri) = _StringResource;
/// Creates a resource object with the given [uri] as location.
///
/// The URI may be relative, in which case it will be resolved
/// against [Uri.base] before being used.
///
/// The URI may use the `package` scheme, which is always supported.
/// Other schemes may also be supported where possible.
factory Resource.forUri(Uri uri) =>
new _UriResource(PackageResolver._current, uri);
/// The location `uri` of this resource.
///
/// This is a [Uri] of the `uri` parameter given to the constructor.
/// If the parameter was not a valid URI, reading `uri` may fail.
Uri get uri;
Stream<List<int>> openRead();
Future<List<int>> readAsBytes();
/// Read the resource content as a string.
///
/// The content is decoded into a string using an [Encoding].
/// If no other encoding is provided, it defaults to UTF-8.
Future<String> readAsString({Encoding encoding});
}
class _StringResource implements Resource {
final String _uri;
const _StringResource(String uri) : _uri = uri;
Uri get uri => Uri.parse(_uri);
Stream<List<int>> openRead() {
return new _UriResource(PackageResolver._current, uri).openRead();
}
Future<List<int>> readAsBytes() {
return new _UriResource(PackageResolver._current, uri).readAsBytes();
}
Future<String> readAsString({Encoding encoding}) {
return new _UriResource(PackageResolver._current, uri)
.readAsString(encoding: encoding);
}
}
class _UriResource implements Resource {
/// The strategy for resolving package: URIs.
///
/// May be null intially. If so, the [PackageResolver.current] resolver is
/// used (and cached for later use).
PackageResolver _resolver;
/// The URI of the resource.
final Uri uri;
_UriResource(this.resolver, Uri uri);
Stream<List<int>> openRead() async* {
Uri uri = await _resolve(this.uri);
return io.readAsStream(uri);
}
Future<List<int>> readAsBytes() async {
Uri uri = await _resolve(this.uri);
return io.readAsBytes(uri);
}
Future<String> readAsString({Encoding encoding}) async {
Uri uri = await _resolve(this.uri);
return io.readAsString(uri, encoding);
}
static void _checkPackageUri(Uri uri) {
if (uri.scheme != "package") {
throw new ArgumentError.value(uri, "Not a package: URI");
}
if (uri.hasAuthority) {
throw new ArgumentError.value(uri,
"A package: URI must not have an authority part");
}
if (uri.path.isEmpty || uri.path.startsWith('/')) {
throw new ArgumentError.value(uri,
"A package: URI must have the form "
"'package:packageName/packagePath'");
}
if (uri.hasQuery) {
throw new ArgumentError.value(uri,
"A package: URI must not have a query part");
}
if (uri.hasFragment) {
throw new ArgumentError.value(uri,
"A package: URI must not have a fragment part");
}
}
Future<Uri> _resolve(Uri uri) async {
if (uri.scheme != "package") {
return Uri.base.resolveUri(uri);
}
_checkPackageUri(uri);
_resolver ??= await PackageResolver._current;
return _resolver.resolve(uri);
}
}
/// A [PackageResolver] based on a packags map.
class MapResolver extends PackageResolver {
Map<String, Uri> _mapping;
MapResolver(this._mapping);
Uri resolve(Uri uri) {
if (uri.scheme != "package") return uri;
var path = uri.path;
int slashIndex = path.indexOf('/');
if (slashIndex <= 0) {
throw new ArgumentError.value(uri, "Invalid package URI");
}
int packageName = path.substring(0, slashIndex);
var base = _mapping[packageName];
if (base != null) {
int packagePath = path.substring(slashIndex + 1);
return base.resolveUri(new Uri(path: packagePath));
}
throw new UnsupportedError("No package named '$packageName' found");
}
}
/// A [PackageResolver] based on a package root.
class RootResolver extends PackageResolver {
Uri _root;
RootResolver(this._root);
Uri resolve(Uri uri) {
if (uri.scheme != "package") return uri;
return _root.resolve(uri.path);
}
}