UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,004 lines (859 loc) 27.4 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2011 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Tristan Koch (tristankoch) ************************************************************************ */ /** * AbstractRequest serves as a base class for {@link qx.io.request.Xhr} * and {@link qx.io.request.Jsonp}. It contains methods to conveniently * communicate with transports found in {@link qx.bom.request}. * * The general procedure to derive a new request is to choose a * transport (override {@link #_createTransport}) and link * the transport’s response (override {@link #_getParsedResponse}). * The transport must implement {@link qx.bom.request.IRequest}. * * To adjust the behavior of {@link #send} override * {@link #_getConfiguredUrl} and {@link #_getConfiguredRequestHeaders}. * * NOTE: Instances of this class must be disposed of after use * */ qx.Class.define("qx.io.request.AbstractRequest", { type : "abstract", extend : qx.core.Object, implement: [ qx.core.IDisposable ], /** * @param url {String?} The URL of the resource to request. */ construct : function(url) { this.base(arguments); if (url !== undefined) { this.setUrl(url); } this.__requestHeaders = {}; var transport = this._transport = this._createTransport(); this._setPhase("unsent"); this.__onReadyStateChangeBound = qx.lang.Function.bind(this._onReadyStateChange, this); this.__onLoadBound = qx.lang.Function.bind(this._onLoad, this); this.__onLoadEndBound = qx.lang.Function.bind(this._onLoadEnd, this); this.__onAbortBound = qx.lang.Function.bind(this._onAbort, this); this.__onTimeoutBound = qx.lang.Function.bind(this._onTimeout, this); this.__onErrorBound = qx.lang.Function.bind(this._onError, this); transport.onreadystatechange = this.__onReadyStateChangeBound; transport.onload = this.__onLoadBound; transport.onloadend = this.__onLoadEndBound; transport.onabort = this.__onAbortBound; transport.ontimeout = this.__onTimeoutBound; transport.onerror = this.__onErrorBound; }, events : { /** * Fired on every change of the transport’s readyState. */ "readyStateChange": "qx.event.type.Event", /** * Fired when request completes without error and transport’s status * indicates success. */ "success": "qx.event.type.Event", /** * Fired when request completes without error. */ "load": "qx.event.type.Event", /** * Fired when request completes with or without error. */ "loadEnd": "qx.event.type.Event", /** * Fired when request is aborted. */ "abort": "qx.event.type.Event", /** * Fired when request reaches timeout limit. */ "timeout": "qx.event.type.Event", /** * Fired when request completes with error. */ "error": "qx.event.type.Event", /** * Fired when request completes without error but erroneous HTTP status. */ "statusError": "qx.event.type.Event", /** * Fired when the configured parser runs into an unrecoverable error. */ "parseError": "qx.event.type.Data", /** * Fired on timeout, error or remote error. * * This event is fired for convenience. Usually, it is recommended * to handle error related events in a more fine-grained approach. */ "fail": "qx.event.type.Event", /** * Fired on change of the parsed response. * * This event allows to use data binding with the * parsed response as source. * * For example, to bind the response to the value of a label: * * <pre class="javascript"> * // req is an instance of qx.io.request.*, * // label an instance of qx.ui.basic.Label * req.bind("response", label, "value"); * </pre> * * The response is parsed (and therefore changed) only * after the request completes successfully. This means * that when a new request is made the initial empty value * is ignored, instead only the final value is bound. * */ "changeResponse": "qx.event.type.Data", /** * Fired on change of the phase. */ "changePhase": "qx.event.type.Data" }, properties : { /** * The URL of the resource to request. * * Note: Depending on the configuration of the request * and/or the transport chosen, query params may be appended * automatically. */ url: { check: "String" }, /** * Timeout limit in milliseconds. Default (0) means no limit. */ timeout: { check: "Number", nullable: true, init: 0 }, /** * Data to be sent as part of the request. * * Supported types: * * * String * * Map * * qooxdoo Object * * Blob * * ArrayBuffer * * FormData * * For maps, Arrays and qooxdoo objects, a URL encoded string * with unsafe characters escaped is internally generated and sent * as part of the request. * * Depending on the underlying transport and its configuration, the request * data is transparently included as URL query parameters or embedded in the * request body as form data. * * If a string is given the user must make sure it is properly formatted and * escaped. See {@link qx.util.Serializer#toUriParameter}. * */ requestData: { check: function(value) { return qx.lang.Type.isString(value) || qx.Class.isSubClassOf(value.constructor, qx.core.Object) || qx.lang.Type.isObject(value) || qx.lang.Type.isArray(value) || qx.Bootstrap.getClass(value) == "Blob" || qx.Bootstrap.getClass(value) == "ArrayBuffer" || qx.Bootstrap.getClass(value) == "FormData"; }, nullable: true }, /** * Authentication delegate. * * The delegate must implement {@link qx.io.request.authentication.IAuthentication}. */ authentication: { check: "qx.io.request.authentication.IAuthentication", nullable: true } }, members : { /** * Bound handlers. */ __onReadyStateChangeBound: null, __onLoadBound: null, __onLoadEndBound: null, __onAbortBound: null, __onTimeoutBound: null, __onErrorBound: null, /** * Parsed response. */ __response: null, /** * Abort flag. */ __abort: null, /** * Current phase. */ __phase: null, /** * Request headers. */ __requestHeaders: null, /** * Request headers (deprecated). */ __requestHeadersDeprecated: null, /** * Holds transport. */ _transport: null, /** * Holds information about the parser status for the last request. */ _parserFailed: false, /* --------------------------------------------------------------------------- CONFIGURE TRANSPORT --------------------------------------------------------------------------- */ /** * Create and return transport. * * This method MUST be overridden, unless the constructor is overridden as * well. It is called by the constructor and should return the transport that * is to be interfaced. * * @return {qx.bom.request} Transport. */ _createTransport: function() { throw new Error("Abstract method call"); }, /** * Get configured URL. * * A configured URL typically includes a query string that * encapsulates transport specific settings such as request * data or no-cache settings. * * This method MAY be overridden. It is called in {@link #send} * before the request is initialized. * * @return {String} The configured URL. */ _getConfiguredUrl: function() {}, /** * Get configuration related request headers. * * This method MAY be overridden to add request headers for features limited * to a certain transport. * * @return {Map} Map of request headers. */ _getConfiguredRequestHeaders: function() {}, /** * Get parsed response. * * Is called in the {@link #_onReadyStateChange} event handler * to parse and store the transport’s response. * * This method MUST be overridden. * * @return {String} The parsed response of the request. */ _getParsedResponse: function() { throw new Error("Abstract method call"); }, /** * Get method. * * This method MAY be overridden. It is called in {@link #send} * before the request is initialized. * * @return {String} The method. */ _getMethod: function() { return "GET"; }, /** * Whether async. * * This method MAY be overridden. It is called in {@link #send} * before the request is initialized. * * @return {Boolean} Whether to process asynchronously. */ _isAsync: function() { return true; }, /* --------------------------------------------------------------------------- INTERACT WITH TRANSPORT --------------------------------------------------------------------------- */ /** * Send request. */ send: function() { var transport = this._transport, url, method, async, requestData; // // Open request // url = this._getConfiguredUrl(); // Drop fragment (anchor) from URL as per // http://www.w3.org/TR/XMLHttpRequest/#the-open-method if (/\#/.test(url)) { url = url.replace(/\#.*/, ""); } transport.timeout = this.getTimeout(); // Support transports with enhanced feature set method = this._getMethod(); async = this._isAsync(); // Open if (qx.core.Environment.get("qx.debug.io")) { this.debug("Open low-level request with method: " + method + ", url: " + url + ", async: " + async); } transport.open(method, url, async); this._setPhase("opened"); // // Send request // requestData = this.getRequestData(); if (["ArrayBuffer", "Blob", "FormData"].indexOf(qx.Bootstrap.getClass(requestData)) == -1) { requestData = this._serializeData(requestData); } this._setRequestHeaders(); // Send if (qx.core.Environment.get("qx.debug.io")) { this.debug("Send low-level request"); } method == "GET" ? transport.send() : transport.send(requestData); this._setPhase("sent"); }, /** * The same as send() but also return a `qx.Promise` object. The promise * is resolved to this object if the request is successful. * * Calling `abort()` on the request object, rejects the promise. Calling * `cancel()` on the promise aborts the request if the request is not in a * final state. * If the promise has other listener paths, then cancelation of one path will * not have any effect on the request and consequently that call will not * affect the other paths. * * @param context {Object?} optional context to bind the qx.Promise. * @return {qx.Promise} The qx.Promise object * @throws {qx.type.BaseError} If the environment setting `qx.promise` is set to false */ sendWithPromise: function(context) { if (qx.core.Environment.get("qx.promise")) { context = context || this; // save this object's context var req = this; var promise = new qx.Promise(function(resolve, reject) { var listeners = []; var changeResponseListener = req.addListener("success", function(e) { listeners.forEach(req.removeListenerById.bind(req)); resolve(req); }, this); listeners.push(changeResponseListener); var statusErrorListener = req.addListener("statusError", function(e) { listeners.forEach(req.removeListenerById.bind(req)); var failMessage = qx.lang.String.format("%1: %2.", [req.getStatus(), req.getStatusText()]); var err = new qx.type.BaseError("statusError", failMessage); reject(err); }, this); listeners.push(statusErrorListener); var timeoutListener = req.addListener("timeout", function(e) { listeners.forEach(req.removeListenerById.bind(req)); var failMessage = qx.lang.String.format("Request failed with timeout after %1 ms.", [req.getTimeout()]); var err = new qx.type.BaseError("timeout", failMessage); reject(err); }, this); listeners.push(timeoutListener); var parseErrorListener = req.addListener("parseError", function(e) { listeners.forEach(req.removeListenerById.bind(req)); var failMessage = "Error parsing the response."; var err = new qx.type.BaseError("parseError", failMessage); reject(err); }, this); listeners.push(parseErrorListener); var abortListener = req.addListener("abort", function(e) { listeners.forEach(req.removeListenerById.bind(req)); var failMessage = "Request aborted."; var err = new qx.type.BaseError("abort", failMessage); reject(err); }, this); listeners.push(abortListener); var errorListener = req.addListener("error", function(e) { listeners.forEach(req.removeListenerById.bind(req)); var failMessage = "Request failed."; var err = new qx.type.BaseError("error", failMessage); reject(err); }, this); listeners.push(errorListener); req.send(); }, context) .finally(function() { if (req.getReadyState() !== 4) { req.abort(); } }); return promise; // eslint-disable-next-line no-else-return } else { // fail loudly throw new qx.type.BaseError("Error", "Environment setting qx.promise is set to false."); } }, /** * Abort request. */ abort: function() { if (qx.core.Environment.get("qx.debug.io")) { this.debug("Abort request"); } this.__abort = true; // Update phase to "abort" before user handler are invoked [BUG #5485] this.__phase = "abort"; this._transport.abort(); }, /* --------------------------------------------------------------------------- REQUEST HEADERS --------------------------------------------------------------------------- */ /** * Apply configured request headers to transport. * * This method MAY be overridden to customize application of request headers * to transport. */ _setRequestHeaders: function() { var transport = this._transport, requestHeaders = this._getAllRequestHeaders(); for (var key in requestHeaders) { transport.setRequestHeader(key, requestHeaders[key]); } }, /** * Get all request headers. * * @return {Map} All request headers. */ _getAllRequestHeaders: function() { var requestHeaders = {}; // Transport specific headers qx.lang.Object.mergeWith(requestHeaders, this._getConfiguredRequestHeaders()); // Authentication delegate qx.lang.Object.mergeWith(requestHeaders, this.__getAuthRequestHeaders()); // User-defined, requestHeaders property (deprecated) qx.lang.Object.mergeWith(requestHeaders, this.__requestHeadersDeprecated); // User-defined qx.lang.Object.mergeWith(requestHeaders, this.__requestHeaders); return requestHeaders; }, /** * Retrieve authentication headers from auth delegate. * * @return {Map} Authentication related request headers. */ __getAuthRequestHeaders: function() { var auth = this.getAuthentication(), headers = {}; if (auth) { auth.getAuthHeaders().forEach(function(header) { headers[header.key] = header.value; }); return headers; } }, /** * Set a request header. * * Note: Setting request headers has no effect after the request was send. * * @param key {String} Key of the header. * @param value {String} Value of the header. */ setRequestHeader: function(key, value) { this.__requestHeaders[key] = value; }, /** * Get a request header. * * @param key {String} Key of the header. * @return {String} The value of the header. */ getRequestHeader: function(key) { return this.__requestHeaders[key]; }, /** * Remove a request header. * * Note: Removing request headers has no effect after the request was send. * * @param key {String} Key of the header. */ removeRequestHeader: function(key) { if (this.__requestHeaders[key]) { delete this.__requestHeaders[key]; } }, /* --------------------------------------------------------------------------- QUERY TRANSPORT --------------------------------------------------------------------------- */ /** * Get low-level transport. * * Note: To be used with caution! * * This method can be used to query the transport directly, * but should be used with caution. Especially, it * is not advisable to call any destructive methods * such as <code>open</code> or <code>send</code>. * * @return {Object} An instance of a class found in * <code>qx.bom.request.*</code> */ // This method mainly exists so that some methods found in the // low-level transport can be deliberately omitted here, // but still be accessed should it be absolutely necessary. // // Valid use cases include to query the transport’s responseXML // property if performance is critical and any extra parsing // should be avoided at all costs. // getTransport: function() { return this._transport; }, /** * Get current ready state. * * States can be: * UNSENT: 0, * OPENED: 1, * HEADERS_RECEIVED: 2, * LOADING: 3, * DONE: 4 * * @return {Number} Ready state. */ getReadyState: function() { return this._transport.readyState; }, /** * Get current phase. * * A more elaborate version of {@link #getReadyState}, this method indicates * the current phase of the request. Maps to stateful (i.e. deterministic) * events (success, abort, timeout, statusError) and intermediate * readyStates (unsent, configured, loading, load). * * When the requests is successful, it progresses the states:<br> * 'unsent', 'opened', 'sent', 'loading', 'load', 'success' * * In case of failure, the final state is one of:<br> * 'abort', 'timeout', 'statusError' * * For each change of the phase, a {@link #changePhase} data event is fired. * * @return {String} Current phase. * */ getPhase: function() { return this.__phase; }, /** * Get status code. * * @return {Number} The transport’s status code. */ getStatus: function() { return this._transport.status; }, /** * Get status text. * * @return {String} The transport’s status text. */ getStatusText: function() { return this._transport.statusText; }, /** * Get raw (unprocessed) response. * * @return {String} The raw response of the request. */ getResponseText: function() { return this._transport.responseText; }, /** * Get all response headers from response. * * @return {String} All response headers. */ getAllResponseHeaders: function() { return this._transport.getAllResponseHeaders(); }, /** * Get a single response header from response. * * @param key {String} * Key of the header to get the value from. * @return {String} * Response header. */ getResponseHeader: function(key) { return this._transport.getResponseHeader(key); }, /** * Override the content type response header from response. * * @param contentType {String} * Content type for overriding. * @see qx.bom.request.Xhr#overrideMimeType */ overrideResponseContentType: function(contentType) { return this._transport.overrideMimeType(contentType); }, /** * Get the content type response header from response. * * @return {String} * Content type response header. */ getResponseContentType: function() { return this.getResponseHeader("Content-Type"); }, /** * Whether request completed (is done). */ isDone: function() { return this.getReadyState() === 4; }, /* --------------------------------------------------------------------------- RESPONSE --------------------------------------------------------------------------- */ /** * Get parsed response. * * @return {String} The parsed response of the request. */ getResponse: function() { return this.__response; }, /** * Set response. * * @param response {String} The parsed response of the request. */ _setResponse: function(response) { var oldResponse = response; if (this.__response !== response) { this.__response = response; this.fireEvent("changeResponse", qx.event.type.Data, [this.__response, oldResponse]); } }, /* --------------------------------------------------------------------------- EVENT HANDLING --------------------------------------------------------------------------- */ /** * Handle "readyStateChange" event. */ _onReadyStateChange: function() { var readyState = this.getReadyState(); if (qx.core.Environment.get("qx.debug.io")) { this.debug("Fire readyState: " + readyState); } this.fireEvent("readyStateChange"); // Transport switches to readyState DONE on abort and may already // have successful HTTP status when response is served from cache. // // Not fire custom event "loading" (or "success", when cached). if (this.__abort) { return; } if (readyState === 3) { this._setPhase("loading"); } if (this.isDone()) { this.__onReadyStateDone(); } }, /** * Called internally when readyState is DONE. */ __onReadyStateDone: function() { if (qx.core.Environment.get("qx.debug.io")) { this.debug("Request completed with HTTP status: " + this.getStatus()); } // Event "load" fired in onLoad this._setPhase("load"); // Successful HTTP status if (qx.util.Request.isSuccessful(this.getStatus())) { // Parse response if (qx.core.Environment.get("qx.debug.io")) { this.debug("Response is of type: '" + this.getResponseContentType() + "'"); } this._setResponse(this._getParsedResponse()); if (this._parserFailed) { this.fireEvent("fail"); } else { this._fireStatefulEvent("success"); } // Erroneous HTTP status } else { try { this._setResponse(this._getParsedResponse()); } catch (e) { // ignore if it does not work } // A remote error failure if (this.getStatus() !== 0) { this._fireStatefulEvent("statusError"); this.fireEvent("fail"); } } }, /** * Handle "load" event. */ _onLoad: function() { this.fireEvent("load"); }, /** * Handle "loadEnd" event. */ _onLoadEnd: function() { this.fireEvent("loadEnd"); }, /** * Handle "abort" event. */ _onAbort: function() { this._fireStatefulEvent("abort"); }, /** * Handle "timeout" event. */ _onTimeout: function() { this._fireStatefulEvent("timeout"); // A network error failure this.fireEvent("fail"); }, /** * Handle "error" event. */ _onError: function() { this.fireEvent("error"); // A network error failure this.fireEvent("fail"); }, /* --------------------------------------------------------------------------- INTERNAL / HELPERS --------------------------------------------------------------------------- */ /** * Fire stateful event. * * Fires event and sets phase to name of event. * * @param evt {String} Name of the event to fire. */ _fireStatefulEvent: function(evt) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertString(evt); } this._setPhase(evt); this.fireEvent(evt); }, /** * Set phase. * * @param phase {String} The phase to set. */ _setPhase: function(phase) { var previousPhase = this.__phase; if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertString(phase); qx.core.Assert.assertMatch(phase, /^(unsent)|(opened)|(sent)|(loading)|(load)|(success)|(abort)|(timeout)|(statusError)$/); } this.__phase = phase; this.fireDataEvent("changePhase", phase, previousPhase); }, /** * Serialize data. * * @param data {String|Map|qx.core.Object} Data to serialize. * @return {String|null} Serialized data. */ _serializeData: function(data) { var isPost = typeof this.getMethod !== "undefined" && this.getMethod() == "POST", isJson = (/application\/.*\+?json/).test(this.getRequestHeader("Content-Type")); if (!data) { return null; } if (qx.lang.Type.isString(data)) { return data; } if (qx.Class.isSubClassOf(data.constructor, qx.core.Object)) { return qx.util.Serializer.toUriParameter(data); } if (isJson && (qx.lang.Type.isObject(data) || qx.lang.Type.isArray(data))) { return qx.lang.Json.stringify(data); } if (qx.lang.Type.isObject(data)) { return qx.util.Uri.toParameter(data, isPost); } return null; } }, environment: { "qx.debug.io": false }, destruct: function() { var transport = this._transport, noop = function() {}; if (this._transport) { transport.onreadystatechange = transport.onload = transport.onloadend = transport.onabort = transport.ontimeout = transport.onerror = noop; // [BUG #8315] dispose asynchronously to work with Sinon.js fake server window.setTimeout(function() { transport.dispose(); }, 0); } } });