diff --git a/modules/@angular/http/http.ts b/modules/@angular/http/http.ts index 5dcd3bb082..61c3312ea0 100644 --- a/modules/@angular/http/http.ts +++ b/modules/@angular/http/http.ts @@ -28,7 +28,7 @@ export {JSONPBackend, JSONPConnection} from './src/backends/jsonp_backend'; export {CookieXSRFStrategy, XHRBackend, XHRConnection} from './src/backends/xhr_backend'; export {BaseRequestOptions, RequestOptions} from './src/base_request_options'; export {BaseResponseOptions, ResponseOptions} from './src/base_response_options'; -export {ReadyState, RequestMethod, ResponseType} from './src/enums'; +export {ReadyState, RequestMethod, ResponseBuffer, ResponseType} from './src/enums'; export {Headers} from './src/headers'; export {Http, Jsonp} from './src/http'; export {Connection, ConnectionBackend, RequestOptionsArgs, ResponseOptionsArgs, XSRFStrategy} from './src/interfaces'; diff --git a/modules/@angular/http/src/backends/xhr_backend.ts b/modules/@angular/http/src/backends/xhr_backend.ts index d3a8c5184e..fbb379dcc7 100644 --- a/modules/@angular/http/src/backends/xhr_backend.ts +++ b/modules/@angular/http/src/backends/xhr_backend.ts @@ -12,7 +12,7 @@ import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; import {ResponseOptions} from '../base_response_options'; -import {ContentType, ReadyState, RequestMethod, ResponseType} from '../enums'; +import {ContentType, ReadyState, RequestMethod, ResponseType, ResponseBuffer} from '../enums'; import {isPresent, isString} from '../facade/lang'; import {Headers} from '../headers'; import {getResponseURL, isSuccess} from '../http_utils'; @@ -24,6 +24,7 @@ import {BrowserXhr} from './browser_xhr'; const XSSI_PREFIX = /^\)\]\}',?\n/; + /** * Creates connections using `XMLHttpRequest`. Given a fully-qualified * request, an `XHRConnection` will immediately create an `XMLHttpRequest` object and send the @@ -51,7 +52,7 @@ export class XHRConnection implements Connection { _xhr.withCredentials = req.withCredentials; } // load event handler - let onLoad = () => { + let onLoad = () => { // responseText is the old-school way of retrieving response (supported by IE8 & 9) // response/responseType properties were introduced in XHR Level2 spec (supported by // IE10) @@ -108,6 +109,20 @@ export class XHRConnection implements Connection { req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(','))); } + // Select the correct buffer type to store the response + if (isPresent(req.buffer) && isPresent(_xhr.responseType)) switch (req.buffer) { + case ResponseBuffer.ArrayBuffer: + _xhr.responseType = "arraybuffer"; + break; + case ResponseBuffer.Json: + _xhr.responseType = "json"; + break; + default: + case ResponseBuffer.Text: + _xhr.responseType = "text"; + break; + } + _xhr.addEventListener('load', onLoad); _xhr.addEventListener('error', onError); diff --git a/modules/@angular/http/src/base_request_options.ts b/modules/@angular/http/src/base_request_options.ts index fd1cb7018f..54e274eefe 100644 --- a/modules/@angular/http/src/base_request_options.ts +++ b/modules/@angular/http/src/base_request_options.ts @@ -10,7 +10,7 @@ import {Injectable} from '@angular/core'; import {isPresent, isString} from '../src/facade/lang'; -import {RequestMethod} from './enums'; +import {RequestMethod, ResponseBuffer} from './enums'; import {Headers} from './headers'; import {normalizeMethodName} from './http_utils'; import {RequestOptionsArgs} from './interfaces'; @@ -69,7 +69,13 @@ export class RequestOptions { * Enable use credentials for a {@link Request}. */ withCredentials: boolean; - constructor({method, headers, body, url, search, withCredentials}: RequestOptionsArgs = {}) { + /* + * Select a buffer to store the response, such as ArrayBuffer, Blob, Json (or Document) + */ + buffer: ResponseBuffer; + + constructor({method, headers, body, url, search, withCredentials, buffer}: RequestOptionsArgs = {}) { + this.method = isPresent(method) ? normalizeMethodName(method) : null; this.headers = isPresent(headers) ? headers : null; this.body = isPresent(body) ? body : null; @@ -78,6 +84,7 @@ export class RequestOptions { (isString(search) ? new URLSearchParams((search)) : (search)) : null; this.withCredentials = isPresent(withCredentials) ? withCredentials : null; + this.buffer = isPresent(buffer) ? buffer : null; } /** @@ -117,7 +124,8 @@ export class RequestOptions { this.search, withCredentials: isPresent(options) && isPresent(options.withCredentials) ? options.withCredentials : - this.withCredentials + this.withCredentials, + buffer: isPresent(options) && isPresent(options.buffer) ? options.buffer : this.buffer }); } } diff --git a/modules/@angular/http/src/base_response_options.ts b/modules/@angular/http/src/base_response_options.ts index 8de2d456e7..689bb89a32 100644 --- a/modules/@angular/http/src/base_response_options.ts +++ b/modules/@angular/http/src/base_response_options.ts @@ -44,11 +44,11 @@ import {ResponseOptionsArgs} from './interfaces'; * @experimental */ export class ResponseOptions { - // TODO: ArrayBuffer | FormData | Blob + // TODO: FormData | Blob /** - * String or Object representing the body of the {@link Response}. + * String, Object, ArrayBuffer representing the body of the {@link Response}. */ - body: string|Object; + body: string | Object | ArrayBuffer; /** * Http {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html status code} * associated with the response. diff --git a/modules/@angular/http/src/enums.ts b/modules/@angular/http/src/enums.ts index 31b25bd95b..1506cf50b5 100644 --- a/modules/@angular/http/src/enums.ts +++ b/modules/@angular/http/src/enums.ts @@ -61,3 +61,12 @@ export enum ContentType { BLOB, ARRAY_BUFFER } + +/* + * Define which buffer to use to store the response + */ +export enum ResponseBuffer { + ArrayBuffer, + Json, + Text +} diff --git a/modules/@angular/http/src/http.ts b/modules/@angular/http/src/http.ts index 24554eb9da..d6cdaca4c8 100644 --- a/modules/@angular/http/src/http.ts +++ b/modules/@angular/http/src/http.ts @@ -35,7 +35,8 @@ function mergeOptions( search: providedOpts.search, headers: providedOpts.headers, body: providedOpts.body, - withCredentials: providedOpts.withCredentials + withCredentials: providedOpts.withCredentials, + buffer: providedOpts.buffer })); } if (isPresent(method)) { diff --git a/modules/@angular/http/src/http_utils.ts b/modules/@angular/http/src/http_utils.ts index b0e1605731..67fbcc4f33 100644 --- a/modules/@angular/http/src/http_utils.ts +++ b/modules/@angular/http/src/http_utils.ts @@ -38,4 +38,12 @@ export function getResponseURL(xhr: any): string { return; } +export function stringToArrayBuffer(string: String): ArrayBuffer { + let view = new Uint16Array(string.length); + for (var i = 0, strLen = string.length; i < strLen; i++) { + view[i] = string.charCodeAt(i); + } + return view.buffer; +} + export {isJsObject} from '../src/facade/lang'; diff --git a/modules/@angular/http/src/interfaces.ts b/modules/@angular/http/src/interfaces.ts index 796764ac96..50c30c030d 100644 --- a/modules/@angular/http/src/interfaces.ts +++ b/modules/@angular/http/src/interfaces.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ReadyState, RequestMethod, ResponseType} from './enums'; +import {ReadyState, RequestMethod, ResponseType, ResponseBuffer} from './enums'; import {Headers} from './headers'; import {Request} from './static_request'; import {URLSearchParams} from './url_search_params'; @@ -52,6 +52,7 @@ export interface RequestOptionsArgs { headers?: Headers; body?: any; withCredentials?: boolean; + buffer?: ResponseBuffer; } /** @@ -66,8 +67,8 @@ export interface RequestArgs extends RequestOptionsArgs { url: string; } * @experimental */ export type ResponseOptionsArgs = { - // TODO: Support Blob, ArrayBuffer, JSON - body?: string | Object | FormData; status?: number; statusText?: string; headers?: Headers; + // TODO: Support Blob, JSON + body?: string | Object | FormData | ArrayBuffer; status?: number; statusText?: string; headers?: Headers; type?: ResponseType; url?: string; } diff --git a/modules/@angular/http/src/static_request.ts b/modules/@angular/http/src/static_request.ts index fe449cc9f9..08f30bd178 100644 --- a/modules/@angular/http/src/static_request.ts +++ b/modules/@angular/http/src/static_request.ts @@ -8,7 +8,7 @@ import {StringWrapper, isPresent} from '../src/facade/lang'; -import {ContentType, RequestMethod} from './enums'; +import {ContentType, RequestMethod, ResponseBuffer} from './enums'; import {Headers} from './headers'; import {normalizeMethodName} from './http_utils'; import {RequestArgs} from './interfaces'; @@ -72,6 +72,8 @@ export class Request { private contentType: ContentType; /** Enable use credentials */ withCredentials: boolean; + /* Select a buffer to store the response */ + buffer: ResponseBuffer; constructor(requestOptions: RequestArgs) { // TODO: assert that url is present let url = requestOptions.url; @@ -95,6 +97,7 @@ export class Request { // TODO(jeffbcross): implement behavior this.headers = new Headers(requestOptions.headers); this.withCredentials = requestOptions.withCredentials; + this.buffer = requestOptions.buffer; } diff --git a/modules/@angular/http/src/static_response.ts b/modules/@angular/http/src/static_response.ts index 4c341ba655..25b24d86c1 100644 --- a/modules/@angular/http/src/static_response.ts +++ b/modules/@angular/http/src/static_response.ts @@ -12,7 +12,7 @@ import {Json, isString} from '../src/facade/lang'; import {ResponseOptions} from './base_response_options'; import {ResponseType} from './enums'; import {Headers} from './headers'; -import {isJsObject} from './http_utils'; +import {isJsObject, stringToArrayBuffer} from './http_utils'; /** @@ -83,8 +83,10 @@ export class Response { * Spec](https://fetch.spec.whatwg.org/#headers-class). */ headers: Headers; - // TODO: Support ArrayBuffer, JSON, FormData, Blob - private _body: string|Object; + + // TODO: Support FormData, Blob + private _body: string | Object | ArrayBuffer; + constructor(responseOptions: ResponseOptions) { this._body = responseOptions.body; this.status = responseOptions.status; @@ -95,12 +97,6 @@ export class Response { this.url = responseOptions.url; } - /** - * Not yet implemented - */ - // TODO: Blob return type - blob(): any { throw new BaseException('"blob()" method not implemented on Response superclass'); } - /** * Attempts to return body as parsed `JSON` object, or raises an exception. */ @@ -110,6 +106,10 @@ export class Response { jsonResponse = this._body; } else if (isString(this._body)) { jsonResponse = Json.parse(this._body); + } else if (this._body instanceof ArrayBuffer) { + jsonResponse = Json.parse(this.text()); + } else { + jsonResponse = this._body; } return jsonResponse; } @@ -117,17 +117,40 @@ export class Response { /** * Returns the body as a string, presuming `toString()` can be called on the response body. */ - text(): string { return this._body.toString(); } + text(): string { + var textResponse: string; + if (this._body instanceof ArrayBuffer) { + textResponse = String.fromCharCode.apply(null, new Uint16Array(this._body)); + } else if (isJsObject(this._body)) { + textResponse = Json.stringify(this._body); + } else { + textResponse = this._body.toString(); + } + return textResponse; + } /** - * Not yet implemented + * Return the body as an ArrayBuffer */ - // TODO: ArrayBuffer return type - arrayBuffer(): any { - throw new BaseException('"arrayBuffer()" method not implemented on Response superclass'); + arrayBuffer(): ArrayBuffer { + var bufferResponse: ArrayBuffer; + if (this._body instanceof ArrayBuffer) { + bufferResponse = this._body; + } else { + bufferResponse = stringToArrayBuffer(this.text()); + } + return bufferResponse; } + toString(): string { return `Response with status: ${this.status} ${this.statusText} for URL: ${this.url}`; } + + /** + * Not yet implemented + */ + // TODO: Blob return type + blob(): any { throw new BaseException('"blob()" method not implemented on Response superclass'); } + } diff --git a/modules/@angular/http/test/backends/xhr_backend_spec.ts b/modules/@angular/http/test/backends/xhr_backend_spec.ts index 4072fd1fde..f1c75890fa 100644 --- a/modules/@angular/http/test/backends/xhr_backend_spec.ts +++ b/modules/@angular/http/test/backends/xhr_backend_spec.ts @@ -35,6 +35,7 @@ class MockBrowserXHR extends BrowserXhr { send: any; open: any; response: any; + responseType: string; responseText: string; setRequestHeader: any; callbacks = new Map(); diff --git a/modules/@angular/http/test/http_spec.ts b/modules/@angular/http/test/http_spec.ts index 761aa2d456..05cf0c6f59 100644 --- a/modules/@angular/http/test/http_spec.ts +++ b/modules/@angular/http/test/http_spec.ts @@ -10,9 +10,12 @@ import {Injector, ReflectiveInjector, provide} from '@angular/core'; import {afterEach, beforeEach, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal'; import {AsyncTestCompleter} from '@angular/core/testing/testing_internal'; import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/observable/zip'; import {Subject} from 'rxjs/Subject'; +import {stringToArrayBuffer} from "../src/http_utils"; +import {Json} from "../src/facade/lang"; -import {BaseRequestOptions, ConnectionBackend, HTTP_PROVIDERS, Http, JSONPBackend, JSONP_PROVIDERS, Jsonp, Request, RequestMethod, RequestOptions, Response, ResponseOptions, URLSearchParams, XHRBackend} from '../http'; +import {BaseRequestOptions, ConnectionBackend, HTTP_PROVIDERS, Http, JSONPBackend, JSONP_PROVIDERS, Jsonp, Request, RequestMethod, RequestOptions, Response, ResponseBuffer, ResponseOptions, URLSearchParams, XHRBackend} from '../http'; import {MockBackend, MockConnection} from '../testing/mock_backend'; export function main() { @@ -379,5 +382,122 @@ export function main() { }); }); }); + + describe('response buffer', () => { + + it('should attach the provided buffer to the response', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + backend.connections.subscribe((c: MockConnection) => { + expect(c.request.buffer).toBe(ResponseBuffer.ArrayBuffer); + c.mockRespond(new Response(new ResponseOptions({body: new ArrayBuffer(32)}))); + async.done(); + }); + http.get('https://www.google.com', + new RequestOptions({buffer: ResponseBuffer.ArrayBuffer})) + .subscribe((res: Response) => {}); + })); + + it('should be able to consume a buffer containing a String as any response type', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + backend.connections.subscribe((c: MockConnection) => c.mockRespond(baseResponse)); + http.get('https://www.google.com') + .subscribe((res: Response) => { + expect(res.arrayBuffer()).toBeAnInstanceOf(ArrayBuffer); + expect(res.text()).toBe("base response"); + async.done(); + }); + })); + + + it('should be able to consume a buffer containing an ArrayBuffer as any response type', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + let arrayBuffer = stringToArrayBuffer('{"response": "ok"}'); + backend.connections.subscribe((c: MockConnection) => c.mockRespond(new Response( + new ResponseOptions({body: arrayBuffer})))); + http.get('https://www.google.com') + .subscribe((res: Response) => { + expect(res.arrayBuffer()).toBe(arrayBuffer); + expect(res.text()).toEqual('{"response": "ok"}'); + expect(res.json()).toEqual({response: "ok"}); + async.done(); + }); + })); + + it('should be able to consume a buffer containing an Object as any response type', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + let simpleObject = {"content": "ok"}; + backend.connections.subscribe((c: MockConnection) => c.mockRespond(new Response( + new ResponseOptions({body: simpleObject})))); + http.get('https://www.google.com') + .subscribe((res: Response) => { + expect(res.arrayBuffer()).toBeAnInstanceOf(ArrayBuffer); + expect(res.text()).toEqual(Json.stringify(simpleObject)); + expect(res.json()).toBe(simpleObject); + async.done(); + }); + })); + + it('should preserve encoding of ArrayBuffer response', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + let message = "é@θЂ"; + let arrayBuffer = stringToArrayBuffer(message); + backend.connections.subscribe((c: MockConnection) => c.mockRespond(new Response( + new ResponseOptions({body: arrayBuffer})))); + http.get('https://www.google.com') + .subscribe((res: Response) => { + expect(res.arrayBuffer()).toBeAnInstanceOf(ArrayBuffer); + expect(res.text()).toEqual(message); + async.done(); + }); + })); + + it('should preserve encoding of String response', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + let message = "é@θЂ"; + backend.connections.subscribe((c: MockConnection) => c.mockRespond( + new Response(new ResponseOptions({body: message})))); + http.get('https://www.google.com') + .subscribe((res: Response) => { + expect(res.arrayBuffer()).toEqual(stringToArrayBuffer(message)); + async.done(); + }); + })); + + it('should have an equivalent response independently of the buffer used', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + let message = {"param": "content"}; + + backend.connections.subscribe((c: MockConnection) => { + let body = (): any => { + switch (c.request.buffer) { + case ResponseBuffer.Text: + return Json.stringify(message); + case ResponseBuffer.Json: + return message; + case ResponseBuffer.ArrayBuffer: + return stringToArrayBuffer(Json.stringify(message)); + } + }; + c.mockRespond(new Response(new ResponseOptions({body: body()}))) + }); + + Observable.zip(http.get('https://www.google.com', + new RequestOptions({buffer: ResponseBuffer.Text})), + http.get('https://www.google.com', + new RequestOptions({buffer: ResponseBuffer.Json})), + http.get('https://www.google.com', + new RequestOptions({buffer: ResponseBuffer.ArrayBuffer})), + (x, y, z) => [x, y, z]) + .subscribe((res: Array) => { + expect(res[0].text()).toEqual(res[1].text()); + expect(res[1].text()).toEqual(res[2].text()); + async.done(); + }); + })); + + + }); + + }); }