Line data Source code
1 : // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'package:collection/collection.dart';
6 : import 'package:string_scanner/string_scanner.dart';
7 :
8 : import 'case_insensitive_map.dart';
9 : import 'scan.dart';
10 : import 'utils.dart';
11 :
12 : /// A regular expression matching a character that needs to be backslash-escaped
13 : /// in a quoted string.
14 : final _escapedChar = new RegExp(r'["\x00-\x1F\x7F]');
15 :
16 : /// A class representing an HTTP media type, as used in Accept and Content-Type
17 : /// headers.
18 : ///
19 : /// This is immutable; new instances can be created based on an old instance by
20 : /// calling [change].
21 : class MediaType {
22 : /// The primary identifier of the MIME type.
23 : ///
24 : /// This is always lowercase.
25 : final String type;
26 :
27 : /// The secondary identifier of the MIME type.
28 : ///
29 : /// This is always lowercase.
30 : final String subtype;
31 :
32 : /// The parameters to the media type.
33 : ///
34 : /// This map is immutable and the keys are case-insensitive.
35 : final Map<String, String> parameters;
36 :
37 : /// The media type's MIME type.
38 0 : String get mimeType => "$type/$subtype";
39 :
40 : /// Parses a media type.
41 : ///
42 : /// This will throw a FormatError if the media type is invalid.
43 : factory MediaType.parse(String mediaType) {
44 : // This parsing is based on sections 3.6 and 3.7 of the HTTP spec:
45 : // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html.
46 0 : return wrapFormatException("media type", mediaType, () {
47 0 : var scanner = new StringScanner(mediaType);
48 0 : scanner.scan(whitespace);
49 0 : scanner.expect(token);
50 0 : var type = scanner.lastMatch[0];
51 0 : scanner.expect('/');
52 0 : scanner.expect(token);
53 0 : var subtype = scanner.lastMatch[0];
54 0 : scanner.scan(whitespace);
55 :
56 0 : var parameters = <String, String>{};
57 0 : while (scanner.scan(';')) {
58 0 : scanner.scan(whitespace);
59 0 : scanner.expect(token);
60 0 : var attribute = scanner.lastMatch[0];
61 0 : scanner.expect('=');
62 :
63 : var value;
64 0 : if (scanner.scan(token)) {
65 0 : value = scanner.lastMatch[0];
66 : } else {
67 0 : value = expectQuotedString(scanner);
68 : }
69 :
70 0 : scanner.scan(whitespace);
71 0 : parameters[attribute] = value;
72 : }
73 :
74 0 : scanner.expectDone();
75 0 : return new MediaType(type, subtype, parameters);
76 : });
77 : }
78 :
79 : MediaType(String type, String subtype, [Map<String, String> parameters])
80 0 : : type = type.toLowerCase(),
81 0 : subtype = subtype.toLowerCase(),
82 0 : parameters = new UnmodifiableMapView(
83 0 : parameters == null ? {} : new CaseInsensitiveMap.from(parameters));
84 :
85 : /// Returns a copy of this [MediaType] with some fields altered.
86 : ///
87 : /// [type] and [subtype] alter the corresponding fields. [mimeType] is parsed
88 : /// and alters both the [type] and [subtype] fields; it cannot be passed along
89 : /// with [type] or [subtype].
90 : ///
91 : /// [parameters] overwrites and adds to the corresponding field. If
92 : /// [clearParameters] is passed, it replaces the corresponding field entirely
93 : /// instead.
94 : MediaType change({String type, String subtype, String mimeType,
95 : Map<String, String> parameters, bool clearParameters: false}) {
96 : if (mimeType != null) {
97 : if (type != null) {
98 0 : throw new ArgumentError("You may not pass both [type] and [mimeType].");
99 : } else if (subtype != null) {
100 0 : throw new ArgumentError("You may not pass both [subtype] and "
101 : "[mimeType].");
102 : }
103 :
104 0 : var segments = mimeType.split('/');
105 0 : if (segments.length != 2) {
106 0 : throw new FormatException('Invalid mime type "$mimeType".');
107 : }
108 :
109 0 : type = segments[0];
110 0 : subtype = segments[1];
111 : }
112 :
113 0 : if (type == null) type = this.type;
114 0 : if (subtype == null) subtype = this.subtype;
115 0 : if (parameters == null) parameters = {};
116 :
117 : if (!clearParameters) {
118 : var newParameters = parameters;
119 0 : parameters = new Map.from(this.parameters);
120 0 : parameters.addAll(newParameters);
121 : }
122 :
123 0 : return new MediaType(type, subtype, parameters);
124 : }
125 :
126 : /// Converts the media type to a string.
127 : ///
128 : /// This will produce a valid HTTP media type.
129 : String toString() {
130 0 : var buffer = new StringBuffer()
131 0 : ..write(type)
132 0 : ..write("/")
133 0 : ..write(subtype);
134 :
135 0 : parameters.forEach((attribute, value) {
136 0 : buffer.write("; $attribute=");
137 0 : if (nonToken.hasMatch(value)) {
138 : buffer
139 0 : ..write('"')
140 0 : ..write(
141 0 : value.replaceAllMapped(_escapedChar, (match) => "\\" + match[0]))
142 0 : ..write('"');
143 : } else {
144 0 : buffer.write(value);
145 : }
146 : });
147 :
148 0 : return buffer.toString();
149 : }
150 : }
|