blob: 22283d67ac850a78de4d968f7fccd4cc6f6dd4a8 [file] [log] [blame]
// Copyright (c) 2014, 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.
import 'package:collection/collection.dart';
import 'package:string_scanner/string_scanner.dart';
import 'case_insensitive_map.dart';
import 'scan.dart';
import 'utils.dart';
/// A regular expression matching a character that needs to be backslash-escaped
/// in a quoted string.
final _escapedChar = new RegExp(r'["\x00-\x1F\x7F]');
/// A class representing an HTTP media type, as used in Accept and Content-Type
/// headers.
///
/// This is immutable; new instances can be created based on an old instance by
/// calling [change].
class MediaType {
/// The primary identifier of the MIME type.
///
/// This is always lowercase.
final String type;
/// The secondary identifier of the MIME type.
///
/// This is always lowercase.
final String subtype;
/// The parameters to the media type.
///
/// This map is immutable and the keys are case-insensitive.
final Map<String, String> parameters;
/// The media type's MIME type.
String get mimeType => "$type/$subtype";
/// Parses a media type.
///
/// This will throw a FormatError if the media type is invalid.
factory MediaType.parse(String mediaType) {
// This parsing is based on sections 3.6 and 3.7 of the HTTP spec:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html.
return wrapFormatException("media type", mediaType, () {
var scanner = new StringScanner(mediaType);
scanner.scan(whitespace);
scanner.expect(token);
var type = scanner.lastMatch[0];
scanner.expect('/');
scanner.expect(token);
var subtype = scanner.lastMatch[0];
scanner.scan(whitespace);
var parameters = <String, String>{};
while (scanner.scan(';')) {
scanner.scan(whitespace);
scanner.expect(token);
var attribute = scanner.lastMatch[0];
scanner.expect('=');
var value;
if (scanner.scan(token)) {
value = scanner.lastMatch[0];
} else {
value = expectQuotedString(scanner);
}
scanner.scan(whitespace);
parameters[attribute] = value;
}
scanner.expectDone();
return new MediaType(type, subtype, parameters);
});
}
MediaType(String type, String subtype, [Map<String, String> parameters])
: type = type.toLowerCase(),
subtype = subtype.toLowerCase(),
parameters = new UnmodifiableMapView(
parameters == null ? {} : new CaseInsensitiveMap.from(parameters));
/// Returns a copy of this [MediaType] with some fields altered.
///
/// [type] and [subtype] alter the corresponding fields. [mimeType] is parsed
/// and alters both the [type] and [subtype] fields; it cannot be passed along
/// with [type] or [subtype].
///
/// [parameters] overwrites and adds to the corresponding field. If
/// [clearParameters] is passed, it replaces the corresponding field entirely
/// instead.
MediaType change(
{String type,
String subtype,
String mimeType,
Map<String, String> parameters,
bool clearParameters: false}) {
if (mimeType != null) {
if (type != null) {
throw new ArgumentError("You may not pass both [type] and [mimeType].");
} else if (subtype != null) {
throw new ArgumentError("You may not pass both [subtype] and "
"[mimeType].");
}
var segments = mimeType.split('/');
if (segments.length != 2) {
throw new FormatException('Invalid mime type "$mimeType".');
}
type = segments[0];
subtype = segments[1];
}
if (type == null) type = this.type;
if (subtype == null) subtype = this.subtype;
if (parameters == null) parameters = {};
if (!clearParameters) {
var newParameters = parameters;
parameters = new Map.from(this.parameters);
parameters.addAll(newParameters);
}
return new MediaType(type, subtype, parameters);
}
/// Converts the media type to a string.
///
/// This will produce a valid HTTP media type.
String toString() {
var buffer = new StringBuffer()..write(type)..write("/")..write(subtype);
parameters.forEach((attribute, value) {
buffer.write("; $attribute=");
if (nonToken.hasMatch(value)) {
buffer
..write('"')
..write(
value.replaceAllMapped(_escapedChar, (match) => "\\" + match[0]))
..write('"');
} else {
buffer.write(value);
}
});
return buffer.toString();
}
}