Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(111)

Side by Side Diff: generated/googleapis_beta/lib/src/common_internal.dart

Issue 1078053002: Roll of googleapis as of 4/7/2015. (Closed) Base URL: https://github.com/dart-lang/googleapis.git@master
Patch Set: Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 library googleapis_beta.common_internal;
2
3 import "dart:async";
4 import "dart:convert";
5 import "dart:collection" as collection;
6
7 import "package:crypto/crypto.dart" as crypto;
8 import "../common/common.dart" as common_external;
9 import "package:http/http.dart" as http;
10
11 const String USER_AGENT_STRING =
12 'google-api-dart-client googleapis_beta/0.10.0';
13
14 const CONTENT_TYPE_JSON_UTF8 = 'application/json; charset=utf-8';
15
16 /**
17 * Base class for all API clients, offering generic methods for
18 * HTTP Requests to the API
19 */
20 class ApiRequester {
21 final http.Client _httpClient;
22 final String _rootUrl;
23 final String _basePath;
24
25 ApiRequester(this._httpClient, this._rootUrl, this._basePath) {
26 assert(_rootUrl.endsWith('/'));
27 }
28
29
30 /**
31 * Sends a HTTPRequest using [method] (usually GET or POST) to [requestUrl]
32 * using the specified [urlParams] and [queryParams]. Optionally include a
33 * [body] and/or [uploadMedia] in the request.
34 *
35 * If [uploadMedia] was specified [downloadOptions] must be
36 * [DownloadOptions.Metadata] or `null`.
37 *
38 * If [downloadOptions] is [DownloadOptions.Metadata] the result will be
39 * decoded as JSON.
40 *
41 * If [downloadOptions] is `null` the result will be a Future completing with
42 * `null`.
43 *
44 * Otherwise the result will be downloaded as a [common_external.Media]
45 */
46 Future request(String requestUrl, String method,
47 {String body, Map queryParams,
48 common_external.Media uploadMedia,
49 common_external.UploadOptions uploadOptions,
50 common_external.DownloadOptions downloadOptions:
51 common_external.DownloadOptions.Metadata}) {
52 if (uploadMedia != null &&
53 downloadOptions != common_external.DownloadOptions.Metadata) {
54 throw new ArgumentError('When uploading a [Media] you cannot download a '
55 '[Media] at the same time!');
56 }
57 common_external.ByteRange downloadRange;
58 if (downloadOptions is common_external.PartialDownloadOptions &&
59 !downloadOptions.isFullDownload) {
60 downloadRange = downloadOptions.range;
61 }
62
63 return _request(requestUrl, method, body, queryParams,
64 uploadMedia, uploadOptions,
65 downloadOptions,
66 downloadRange)
67 .then(_validateResponse).then((http.StreamedResponse response) {
68 if (downloadOptions == null) {
69 // If no download options are given, the response is of no interest
70 // and we will drain the stream.
71 return response.stream.drain();
72 } else if (downloadOptions == common_external.DownloadOptions.Metadata) {
73 // Downloading JSON Metadata
74 var stringStream = _decodeStreamAsText(response);
75 if (stringStream != null) {
76 return stringStream.join('').then((String bodyString) {
77 if (bodyString == '') return null;
78 return JSON.decode(bodyString);
79 });
80 } else {
81 throw new common_external.ApiRequestError(
82 "Unable to read response with content-type "
83 "${response.headers['content-type']}.");
84 }
85 } else {
86 // Downloading Media.
87 var contentType = response.headers['content-type'];
88 if (contentType == null) {
89 throw new common_external.ApiRequestError(
90 "No 'content-type' header in media response.");
91 }
92 var contentLength;
93 try {
94 contentLength = int.parse(response.headers['content-length']);
95 } catch (_) {
96 // We silently ignore errors here. If no content-length was specified
97 // we use `null`.
98 // Please note that the code below still asserts the content-length
99 // is correct for range downloads.
100 }
101
102 if (downloadRange != null) {
103 if (contentLength != downloadRange.length) {
104 throw new common_external.ApiRequestError(
105 "Content length of response does not match requested range "
106 "length.");
107 }
108 var contentRange = response.headers['content-range'];
109 var expected = 'bytes ${downloadRange.start}-${downloadRange.end}/';
110 if (contentRange == null || !contentRange.startsWith(expected)) {
111 throw new common_external.ApiRequestError("Attempting partial "
112 "download but got invalid 'Content-Range' header "
113 "(was: $contentRange, expected: $expected).");
114 }
115 }
116
117 return new common_external.Media(
118 response.stream, contentLength, contentType: contentType);
119 }
120 });
121 }
122
123 Future _request(String requestUrl, String method,
124 String body, Map queryParams,
125 common_external.Media uploadMedia,
126 common_external.UploadOptions uploadOptions,
127 common_external.DownloadOptions downloadOptions,
128 common_external.ByteRange downloadRange) {
129 bool downloadAsMedia =
130 downloadOptions != null &&
131 downloadOptions != common_external.DownloadOptions.Metadata;
132
133 if (queryParams == null) queryParams = {};
134
135 if (uploadMedia != null) {
136 if (uploadOptions is common_external.ResumableUploadOptions) {
137 queryParams['uploadType'] = const ['resumable'];
138 } else if (body == null) {
139 queryParams['uploadType'] = const ['media'];
140 } else {
141 queryParams['uploadType'] = const ['multipart'];
142 }
143 }
144
145 if (downloadAsMedia) {
146 queryParams['alt'] = const ['media'];
147 } else if (downloadOptions != null) {
148 queryParams['alt'] = const ['json'];
149 }
150
151 var path;
152 if (requestUrl.startsWith('/')) {
153 path ="$_rootUrl${requestUrl.substring(1)}";
154 } else {
155 path ="$_rootUrl${_basePath}$requestUrl";
156 }
157
158 bool containsQueryParameter = path.contains('?');
159 addQueryParameter(String name, String value) {
160 name = Escaper.escapeQueryComponent(name);
161 value = Escaper.escapeQueryComponent(value);
162 if (containsQueryParameter) {
163 path = '$path&$name=$value';
164 } else {
165 path = '$path?$name=$value';
166 }
167 containsQueryParameter = true;
168 }
169 queryParams.forEach((String key, List<String> values) {
170 for (var value in values) {
171 addQueryParameter(key, value);
172 }
173 });
174
175 var uri = Uri.parse(path);
176
177 Future simpleUpload() {
178 var bodyStream = uploadMedia.stream;
179 var request = new RequestImpl(method, uri, bodyStream);
180 request.headers.addAll({
181 'user-agent' : USER_AGENT_STRING,
182 'content-type' : uploadMedia.contentType,
183 'content-length' : '${uploadMedia.length}'
184 });
185 return _httpClient.send(request);
186 }
187
188 Future simpleRequest() {
189 var length = 0;
190 var bodyController = new StreamController<List<int>>();
191 if (body != null) {
192 var bytes = UTF8.encode(body);
193 bodyController.add(bytes);
194 length = bytes.length;
195 }
196 bodyController.close();
197
198 var headers;
199 if (downloadRange != null) {
200 headers = {
201 'user-agent' : USER_AGENT_STRING,
202 'content-type' : CONTENT_TYPE_JSON_UTF8,
203 'content-length' : '$length',
204 'range' : 'bytes=${downloadRange.start}-${downloadRange.end}',
205 };
206 } else {
207 headers = {
208 'user-agent' : USER_AGENT_STRING,
209 'content-type' : CONTENT_TYPE_JSON_UTF8,
210 'content-length' : '$length',
211 };
212 }
213
214 var request = new RequestImpl(method, uri, bodyController.stream);
215 request.headers.addAll(headers);
216 return _httpClient.send(request);
217 }
218
219 if (uploadMedia != null) {
220 // Three upload types:
221 // 1. Resumable: Upload of data + metdata with multiple requests.
222 // 2. Simple: Upload of media.
223 // 3. Multipart: Upload of data + metadata.
224
225 if (uploadOptions is common_external.ResumableUploadOptions) {
226 var helper = new ResumableMediaUploader(
227 _httpClient, uploadMedia, body, uri, method, uploadOptions);
228 return helper.upload();
229 }
230
231 if (uploadMedia.length == null) {
232 throw new ArgumentError(
233 'For non-resumable uploads you need to specify the length of the '
234 'media to upload.');
235 }
236
237 if (body == null) {
238 return simpleUpload();
239 } else {
240 var uploader = new MultipartMediaUploader(
241 _httpClient, uploadMedia, body, uri, method);
242 return uploader.upload();
243 }
244 }
245 return simpleRequest();
246 }
247 }
248
249
250 /**
251 * Does media uploads using the multipart upload protocol.
252 */
253 class MultipartMediaUploader {
254 static final _boundary = '314159265358979323846';
255 static final _base64Encoder = new Base64Encoder();
256
257 final http.Client _httpClient;
258 final common_external.Media _uploadMedia;
259 final Uri _uri;
260 final String _body;
261 final String _method;
262
263 MultipartMediaUploader(
264 this._httpClient, this._uploadMedia, this._body, this._uri, this._method);
265
266 Future<http.StreamedResponse> upload() {
267 var base64MediaStream =
268 _uploadMedia.stream.transform(_base64Encoder).transform(ASCII.encoder);
269 var base64MediaStreamLength =
270 Base64Encoder.lengthOfBase64Stream(_uploadMedia.length);
271
272 // NOTE: We assume that [_body] is encoded JSON without any \r or \n in it.
273 // This guarantees us that [_body] cannot contain a valid multipart
274 // boundary.
275 var bodyHead =
276 '--$_boundary\r\n'
277 "Content-Type: $CONTENT_TYPE_JSON_UTF8\r\n\r\n"
278 + _body +
279 '\r\n--$_boundary\r\n'
280 "Content-Type: ${_uploadMedia.contentType}\r\n"
281 "Content-Transfer-Encoding: base64\r\n\r\n";
282 var bodyTail = '\r\n--$_boundary--';
283
284 var totalLength =
285 bodyHead.length + base64MediaStreamLength + bodyTail.length;
286
287 var bodyController = new StreamController<List<int>>();
288 bodyController.add(UTF8.encode(bodyHead));
289 bodyController.addStream(base64MediaStream).then((_) {
290 bodyController.add(UTF8.encode(bodyTail));
291 }).catchError((error, stack) {
292 bodyController.addError(error, stack);
293 }).then((_) {
294 bodyController.close();
295 });
296
297 var headers = {
298 'user-agent' : USER_AGENT_STRING,
299 'content-type' : "multipart/related; boundary=\"$_boundary\"",
300 'content-length' : '$totalLength'
301 };
302 var bodyStream = bodyController.stream;
303 var request = new RequestImpl(_method, _uri, bodyStream);
304 request.headers.addAll(headers);
305 return _httpClient.send(request);
306 }
307 }
308
309
310 /**
311 * Base64 encodes a stream of bytes.
312 */
313 class Base64Encoder implements StreamTransformer<List<int>, String> {
314 static int lengthOfBase64Stream(int lengthOfByteStream) {
315 return ((lengthOfByteStream + 2) ~/ 3) * 4;
316 }
317
318 Stream<String> bind(Stream<List<int>> stream) {
319 StreamController<String> controller;
320
321 // Holds between 0 and 3 bytes and is used as a buffer.
322 List<int> remainingBytes = [];
323
324 void onData(List<int> bytes) {
325 if ((remainingBytes.length + bytes.length) < 3) {
326 remainingBytes.addAll(bytes);
327 return;
328 }
329 int start;
330 if (remainingBytes.length == 0) {
331 start = 0;
332 } else if (remainingBytes.length == 1) {
333 remainingBytes.add(bytes[0]);
334 remainingBytes.add(bytes[1]);
335 start = 2;
336 } else if (remainingBytes.length == 2) {
337 remainingBytes.add(bytes[0]);
338 start = 1;
339 }
340
341 // Convert & Send bytes from buffer (if necessary).
342 if (remainingBytes.length > 0) {
343 controller.add(crypto.CryptoUtils.bytesToBase64(remainingBytes));
344 remainingBytes.clear();
345 }
346
347 int chunksOf3 = (bytes.length - start) ~/ 3;
348 int end = start + 3 * chunksOf3;
349 int remaining = bytes.length - end;
350
351 // Convert & Send main bytes.
352 if (start == 0 && end == bytes.length) {
353 // Fast path if [bytes] are devisible by 3.
354 controller.add(crypto.CryptoUtils.bytesToBase64(bytes));
355 } else {
356 controller.add(
357 crypto.CryptoUtils.bytesToBase64(bytes.sublist(start, end)));
358
359 // Buffer remaining bytes if necessary.
360 if (end < bytes.length) {
361 remainingBytes.addAll(bytes.sublist(end));
362 }
363 }
364 }
365
366 void onError(error, stack) {
367 controller.addError(error, stack);
368 }
369
370 void onDone() {
371 if (remainingBytes.length > 0) {
372 controller.add(crypto.CryptoUtils.bytesToBase64(remainingBytes));
373 remainingBytes.clear();
374 }
375 controller.close();
376 }
377
378 var subscription;
379 controller = new StreamController<String>(
380 onListen: () {
381 subscription = stream.listen(
382 onData, onError: onError, onDone: onDone);
383 },
384 onPause: () {
385 subscription.pause();
386 },
387 onResume: () {
388 subscription.resume();
389 },
390 onCancel: () {
391 subscription.cancel();
392 });
393 return controller.stream;
394 }
395 }
396
397
398 // TODO: Buffer less if we know the content length in advance.
399 /**
400 * Does media uploads using the resumable upload protocol.
401 */
402 class ResumableMediaUploader {
403 final http.Client _httpClient;
404 final common_external.Media _uploadMedia;
405 final Uri _uri;
406 final String _body;
407 final String _method;
408 final common_external.ResumableUploadOptions _options;
409
410 ResumableMediaUploader(
411 this._httpClient, this._uploadMedia, this._body, this._uri, this._method,
412 this._options);
413
414 /**
415 * Returns the final [http.StreamedResponse] if the upload succeded and
416 * completes with an error otherwise.
417 *
418 * The returned response stream has not been listened to.
419 */
420 Future<http.StreamedResponse> upload() {
421 return _startSession().then((Uri uploadUri) {
422 StreamSubscription subscription;
423
424 var completer = new Completer<http.StreamedResponse>();
425 bool completed = false;
426
427 var chunkStack = new ChunkStack(_options.chunkSize);
428 subscription = _uploadMedia.stream.listen((List<int> bytes) {
429 chunkStack.addBytes(bytes);
430
431 // Upload all but the last chunk.
432 // The final send will be done in the [onDone] handler.
433 bool hasPartialChunk = chunkStack.hasPartialChunk;
434 if (chunkStack.length > 1 ||
435 (chunkStack.length == 1 && hasPartialChunk)) {
436 // Pause the input stream.
437 subscription.pause();
438
439 // Upload all chunks except the last one.
440 var fullChunks;
441 if (hasPartialChunk) {
442 fullChunks = chunkStack.removeSublist(0, chunkStack.length);
443 } else {
444 fullChunks = chunkStack.removeSublist(0, chunkStack.length - 1);
445 }
446 Future.forEach(fullChunks,
447 (c) => _uploadChunkDrained(uploadUri, c)).then((_) {
448 // All chunks uploaded, we can continue consuming data.
449 subscription.resume();
450 }).catchError((error, stack) {
451 subscription.cancel();
452 completed = true;
453 completer.completeError(error, stack);
454 });
455 }
456 }, onError: (error, stack) {
457 subscription.cancel();
458 if (!completed) {
459 completed = true;
460 completer.completeError(error, stack);
461 }
462 }, onDone: () {
463 if (!completed) {
464 chunkStack.finalize();
465
466 var lastChunk;
467 if (chunkStack.length == 1) {
468 lastChunk = chunkStack.removeSublist(0, chunkStack.length).first;
469 } else {
470 completer.completeError(new StateError(
471 'Resumable uploads need to result in at least one non-empty '
472 'chunk at the end.'));
473 return;
474 }
475 var end = lastChunk.endOfChunk;
476
477 // Validate that we have the correct number of bytes if length was
478 // specified.
479 if (_uploadMedia.length != null) {
480 if (end < _uploadMedia.length) {
481 completer.completeError(new common_external.ApiRequestError(
482 'Received less bytes than indicated by [Media.length].'));
483 return;
484 } else if (end > _uploadMedia.length) {
485 completer.completeError(new common_external.ApiRequestError(
486 'Received more bytes than indicated by [Media.length].'));
487 return;
488 }
489 }
490
491 // Upload last chunk and *do not drain the response* but complete
492 // with it.
493 _uploadChunkResumable(uploadUri, lastChunk, lastChunk: true)
494 .then((response) {
495 completer.complete(response);
496 }).catchError((error, stack) {
497 completer.completeError(error, stack);
498 });
499 }
500 });
501
502 return completer.future;
503 });
504 }
505
506 /**
507 * Starts a resumable upload.
508 *
509 * Returns the [Uri] which should be used for uploading all content.
510 */
511 Future<Uri> _startSession() {
512 var length = 0;
513 var bytes;
514 if (_body != null) {
515 bytes = UTF8.encode(_body);
516 length = bytes.length;
517 }
518 var bodyStream = _bytes2Stream(bytes);
519
520 var request = new RequestImpl(_method, _uri, bodyStream);
521 request.headers.addAll({
522 'user-agent' : USER_AGENT_STRING,
523 'content-type' : CONTENT_TYPE_JSON_UTF8,
524 'content-length' : '$length',
525 'x-upload-content-type' : _uploadMedia.contentType,
526 'x-upload-content-length' : '${_uploadMedia.length}',
527 });
528
529 return _httpClient.send(request).then((http.StreamedResponse response) {
530 return response.stream.drain().then((_) {
531 var uploadUri = response.headers['location'];
532 if (response.statusCode != 200 || uploadUri == null) {
533 throw new common_external.ApiRequestError(
534 'Invalid response for resumable upload attempt '
535 '(status was: ${response.statusCode})');
536 }
537 return Uri.parse(uploadUri);
538 });
539 });
540 }
541
542 /**
543 * Uploads [chunk], retries upon server errors. The response stream will be
544 * drained.
545 */
546 Future _uploadChunkDrained(Uri uri, ResumableChunk chunk) {
547 return _uploadChunkResumable(uri, chunk).then((response) {
548 return response.stream.drain();
549 });
550 }
551
552 /**
553 * Does repeated attempts to upload [chunk].
554 */
555 Future _uploadChunkResumable(Uri uri,
556 ResumableChunk chunk,
557 {bool lastChunk: false}) {
558 tryUpload(int attemptsLeft) {
559 return _uploadChunk(uri, chunk, lastChunk: lastChunk)
560 .then((http.StreamedResponse response) {
561 var status = response.statusCode;
562 if (attemptsLeft > 0 &&
563 (status == 500 || (502 <= status && status < 504))) {
564 return response.stream.drain().then((_) {
565 // Delay the next attempt. Default backoff function is exponential.
566 int failedAttemts = _options.numberOfAttempts - attemptsLeft;
567 var duration = _options.backoffFunction(failedAttemts);
568 if (duration == null) {
569 throw new common_external.DetailedApiRequestError(
570 status,
571 'Resumable upload: Uploading a chunk resulted in status '
572 '$status. Maximum number of retries reached.');
573 }
574
575 return new Future.delayed(duration).then((_) {
576 return tryUpload(attemptsLeft - 1);
577 });
578 });
579 } else if (!lastChunk && status != 308) {
580 return response.stream.drain().then((_) {
581 throw new common_external.DetailedApiRequestError(
582 status,
583 'Resumable upload: Uploading a chunk resulted in status '
584 '$status instead of 308.');
585 });
586 } else if (lastChunk && status != 201 && status != 200) {
587 return response.stream.drain().then((_) {
588 throw new common_external.DetailedApiRequestError(
589 status,
590 'Resumable upload: Uploading a chunk resulted in status '
591 '$status instead of 200 or 201.');
592 });
593 } else {
594 return response;
595 }
596 });
597 }
598
599 return tryUpload(_options.numberOfAttempts - 1);
600 }
601
602 /**
603 * Uploads [length] bytes in [byteArrays] and ensures the upload was
604 * successful.
605 *
606 * Content-Range: [start ... (start + length)[
607 *
608 * Returns the returned [http.StreamedResponse] or completes with an error if
609 * the upload did not succeed. The response stream will not be listened to.
610 */
611 Future _uploadChunk(Uri uri, ResumableChunk chunk, {bool lastChunk: false}) {
612 // If [uploadMedia.length] is null, we do not know the length.
613 var mediaTotalLength = _uploadMedia.length;
614 if (mediaTotalLength == null || lastChunk) {
615 if (lastChunk) {
616 mediaTotalLength = '${chunk.endOfChunk}';
617 } else {
618 mediaTotalLength = '*';
619 }
620 }
621
622 var headers = {
623 'user-agent' : USER_AGENT_STRING,
624 'content-type' : _uploadMedia.contentType,
625 'content-length' : '${chunk.length}',
626 'content-range' :
627 'bytes ${chunk.offset}-${chunk.endOfChunk - 1}/$mediaTotalLength',
628 };
629
630 var stream = _listOfBytes2Stream(chunk.byteArrays);
631 var request = new RequestImpl('PUT', uri, stream);
632 request.headers.addAll(headers);
633 return _httpClient.send(request);
634 }
635
636 Stream<List<int>> _bytes2Stream(List<int> bytes) {
637 var bodyController = new StreamController<List<int>>();
638 if (bytes != null) {
639 bodyController.add(bytes);
640 }
641 bodyController.close();
642 return bodyController.stream;
643 }
644
645 Stream<List<int>> _listOfBytes2Stream(List<List<int>> listOfBytes) {
646 var controller = new StreamController();
647 for (var array in listOfBytes) {
648 controller.add(array);
649 }
650 controller.close();
651 return controller.stream;
652 }
653 }
654
655
656 /**
657 * Represents a stack of [ResumableChunk]s.
658 */
659 class ChunkStack {
660 final int _chunkSize;
661 final List<ResumableChunk> _chunkStack = [];
662
663 // Currently accumulated data.
664 List<List<int>> _byteArrays = [];
665 int _length = 0;
666 int _offset = 0;
667
668 bool _finalized = false;
669
670 ChunkStack(this._chunkSize);
671
672 /**
673 * Whether data for a not-yet-finished [ResumableChunk] is present. A call to
674 * `finalize` will create a [ResumableChunk] of this data.
675 */
676 bool get hasPartialChunk => _length > 0;
677
678 /**
679 * The number of chunks in this [ChunkStack].
680 */
681 int get length => _chunkStack.length;
682
683 /**
684 * The total number of bytes which have been converted to [ResumableChunk]s.
685 * Can only be called once this [ChunkStack] has been finalized.
686 */
687 int get totalByteLength {
688 if (!_finalized) {
689 throw new StateError('ChunkStack has not been finalized yet.');
690 }
691
692 return _offset;
693 }
694
695 /**
696 * Returns the chunks [from] ... [to] and deletes it from the stack.
697 */
698 List<ResumableChunk> removeSublist(int from, int to) {
699 var sublist = _chunkStack.sublist(from, to);
700 _chunkStack.removeRange(from, to);
701 return sublist;
702 }
703
704 /**
705 * Adds [bytes] to the buffer. If the buffer is larger than the given chunk
706 * size a new [ResumableChunk] will be created.
707 */
708 void addBytes(List<int> bytes) {
709 if (_finalized) {
710 throw new StateError('ChunkStack has already been finalized.');
711 }
712
713 var remaining = _chunkSize - _length;
714
715 if (bytes.length >= remaining) {
716 var left = bytes.sublist(0, remaining);
717 var right = bytes.sublist(remaining);
718
719 _byteArrays.add(left);
720 _length += left.length;
721
722 _chunkStack.add(new ResumableChunk(_byteArrays, _offset, _length));
723
724 _byteArrays = [];
725 _offset += _length;
726 _length = 0;
727
728 addBytes(right);
729 } else if (bytes.length > 0) {
730 _byteArrays.add(bytes);
731 _length += bytes.length;
732 }
733 }
734
735 /**
736 * Finalizes this [ChunkStack] and creates the last chunk (may have less bytes
737 * than the chunk size, but not zero).
738 */
739 void finalize() {
740 if (_finalized) {
741 throw new StateError('ChunkStack has already been finalized.');
742 }
743 _finalized = true;
744
745 if (_length > 0) {
746 _chunkStack.add(new ResumableChunk(_byteArrays, _offset, _length));
747 _offset += _length;
748 }
749 }
750 }
751
752
753 /**
754 * Represents a chunk of data that will be transferred in one http request.
755 */
756 class ResumableChunk {
757 final List<List<int>> byteArrays;
758 final int offset;
759 final int length;
760
761 /**
762 * Index of the next byte after this chunk.
763 */
764 int get endOfChunk => offset + length;
765
766 ResumableChunk(this.byteArrays, this.offset, this.length);
767 }
768
769 class RequestImpl extends http.BaseRequest {
770 final Stream<List<int>> _stream;
771
772 RequestImpl(String method, Uri url, [Stream<List<int>> stream])
773 : _stream = stream == null ? new Stream.fromIterable([]) : stream,
774 super(method, url);
775
776 http.ByteStream finalize() {
777 super.finalize();
778 return new http.ByteStream(_stream);
779 }
780 }
781
782
783 class Escaper {
784 // Character class definitions from RFC 6570
785 // (see http://tools.ietf.org/html/rfc6570)
786 // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
787 // DIGIT = %x30-39 ; 0
788 // HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
789 // pct-encoded = "%" HEXDIG HEXDIG
790 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
791 // reserved = gen-delims / sub-delims
792 // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
793 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
794 // / "*" / "+" / "," / ";" / "="
795
796 // NOTE: Uri.encodeQueryComponent() does the following:
797 // ...
798 // Then the resulting bytes are "percent-encoded". This transforms spaces
799 // (U+0020) to a plus sign ('+') and all bytes that are not the ASCII decimal
800 // digits, letters or one of '-._~' are written as a percent sign '%'
801 // followed by the two-digit hexadecimal representation of the byte.
802 // ...
803
804 // NOTE: Uri.encodeFull() does the following:
805 // ...
806 // All characters except uppercase and lowercase letters, digits and the
807 // characters !#$&'()*+,-./:;=?@_~ are percent-encoded.
808 // ...
809
810 static String ecapeVariableReserved(String name) {
811 // ... perform variable expansion, as defined in Section 3.2.1, with the
812 // allowed characters being those in the set
813 // (unreserved / reserved / pct-encoded)
814
815 // NOTE: The chracters [ and ] need (according to URI Template spec) not be
816 // percent encoded. The dart implementation does percent-encode [ and ].
817 // This gives us in effect a conservative encoding, since the server side
818 // must interpret percent-encoded parts anyway due to arbitrary unicode.
819
820 // NOTE: This is broken in the discovery protocol. It allows ? and & to be
821 // expanded via URI Templates which may generate completely bogus URIs.
822 // TODO/FIXME: Should we change this to _encodeUnreserved() as well
823 // (disadvantage, slashes get encoded at this point)?
824 return Uri.encodeFull(name);
825 }
826
827 static String ecapePathComponent(String name) {
828 // For each defined variable in the variable-list, append "/" to the
829 // result string and then perform variable expansion, as defined in
830 // Section 3.2.1, with the allowed characters being those in the
831 // *unreserved set*.
832 return _encodeUnreserved(name);
833 }
834
835 static String ecapeVariable(String name) {
836 // ... perform variable expansion, as defined in Section 3.2.1, with the
837 // allowed characters being those in the *unreserved set*.
838 return _encodeUnreserved(name);
839 }
840
841 static String escapeQueryComponent(String name) {
842 // This method will not be used by UriTemplate, but rather for encoding
843 // normal query name/value pairs.
844
845 // NOTE: For safety reasons we use '%20' instead of '+' here as well.
846 // TODO/FIXME: Should we do this?
847 return _encodeUnreserved(name);
848 }
849
850 static String _encodeUnreserved(String name) {
851 // The only difference between dart's [Uri.encodeQueryComponent] and the
852 // encoding defined by RFC 6570 for the above-defined unreserved character
853 // set is the encoding of space.
854 // Dart's Uri class will convert spaces to '+' which we replace by '%20'.
855 return Uri.encodeQueryComponent(name).replaceAll('+', '%20');
856 }
857 }
858
859
860 Future<http.StreamedResponse> _validateResponse(
861 http.StreamedResponse response) {
862 var statusCode = response.statusCode;
863
864 // TODO: We assume that status codes between [200..400[ are OK.
865 // Can we assume this?
866 if (statusCode < 200 || statusCode >= 400) {
867 throwGeneralError() {
868 throw new common_external.DetailedApiRequestError(
869 statusCode, 'No error details. HTTP status was: ${statusCode}.');
870 }
871
872 // Some error happened, try to decode the response and fetch the error.
873 Stream<String> stringStream = _decodeStreamAsText(response);
874 if (stringStream != null) {
875 return stringStream.transform(JSON.decoder).first.then((json) {
876 if (json is Map && json['error'] is Map) {
877 var error = json['error'];
878 var code = error['code'];
879 var message = error['message'];
880 throw new common_external.DetailedApiRequestError(code, message);
881 } else {
882 throwGeneralError();
883 }
884 });
885 } else {
886 throwGeneralError();
887 }
888 }
889
890 return new Future.value(response);
891 }
892
893
894 Stream<String> _decodeStreamAsText(http.StreamedResponse response) {
895 // TODO: Correctly handle the response content-types, using correct
896 // decoder.
897 // Currently we assume that the api endpoint is responding with json
898 // encoded in UTF8.
899 String contentType = response.headers['content-type'];
900 if (contentType != null &&
901 contentType.toLowerCase().startsWith('application/json')) {
902 return response.stream.transform(new Utf8Decoder(allowMalformed: true));
903 } else {
904 return null;
905 }
906 }
907
908 Map mapMap(Map source, [Object convert(Object source) = null]) {
909 assert(source != null);
910 var result = new collection.LinkedHashMap();
911 source.forEach((String key, value) {
912 assert(key != null);
913 if(convert == null) {
914 result[key] = value;
915 } else {
916 result[key] = convert(value);
917 }
918 });
919 return result;
920 }
921
OLDNEW
« no previous file with comments | « generated/googleapis_beta/lib/sqladmin/v1beta4.dart ('k') | generated/googleapis_beta/lib/taskqueue/v1beta2.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698