UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

693 lines (608 loc) 19.3 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2013 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: * Richard Sternagel (rsternagel) ************************************************************************ */ /** * This class is internal because it's tailored to {@link qx.io.rest.Resource} * which needs more functionality than {@link qx.bom.request.Xhr} provides. * The usage of {@link qx.io.request.Xhr} isn't possible either due to it's qx.Class nature. * * For alternatives to this class have a look at: * * * "qx.bom.request.Xhr" (low level, cross-browser XHR abstraction compatible with spec) * * "qx.io.request.Xhr" (high level XHR abstraction) * * A wrapper of {@link qx.bom.request.Xhr} which offers: * * * set/get HTTP method, URL, request data and headers * * retrieve the parsed response as object (content-type recognition) * * more fine-grained events such as success, fail, ... * * supports hash code for request identification * * It does *not* comply the interface defined by {@link qx.bom.request.IRequest}. * * <div class="desktop"> * Example: * * <pre class="javascript"> * var req = new qx.bom.request.SimpleXhr("/some/path/file.json"); * req.setRequestData({"a":"b"}); * req.once("success", function successHandler() { * var response = req.getResponse(); * }, this); * req.once("fail", function successHandler() { * var response = req.getResponse(); * }, this); * req.send(); * </pre> * </div> * * @internal */ qx.Bootstrap.define("qx.bom.request.SimpleXhr", { extend: qx.event.Emitter, implement: [ qx.core.IDisposable ], /** * @param url {String?} The URL of the resource to request. * @param method {String?"GET"} The HTTP method. */ construct: function(url, method) { if (url !== undefined) { this.setUrl(url); } this.useCaching(true); this.setMethod((method !== undefined) ? method : "GET"); this._transport = this._registerTransportListener(this._createTransport()); qx.core.ObjectRegistry.register(this); this.__requestHeaders = {}; this.__parser = this._createResponseParser(); }, members : { /* --------------------------------------------------------------------------- PUBLIC --------------------------------------------------------------------------- */ /** * Sets a request header. * * @param key {String} Key of the header. * @param value {String} Value of the header. * @return {qx.bom.request.SimpleXhr} Self for chaining. */ setRequestHeader: function(key, value) { this.__requestHeaders[key] = value; return this; }, /** * Gets a request header. * * @param key {String} Key of the header. * @return {String} The value of the header. */ getRequestHeader: function(key) { return this.__requestHeaders[key]; }, /** * Returns a single response header * * @param header {String} Name of the header to get. * @return {String} Response header */ getResponseHeader: function(header) { return this._transport.getResponseHeader(header); }, /** * Returns all response headers * @return {String} String of response headers */ getAllResponseHeaders: function() { return this._transport.getAllResponseHeaders(); }, /** * Sets the URL. * * @param url {String} URL to be requested. * @return {qx.bom.request.SimpleXhr} Self for chaining. */ setUrl: function(url) { if (qx.lang.Type.isString(url)) { this.__url = url; } return this; }, /** * Gets the URL. * * @return {String} URL to be requested. */ getUrl: function() { return this.__url; }, /** * Sets the HTTP-Method. * * @param method {String} The method. * @return {qx.bom.request.SimpleXhr} Self for chaining. */ setMethod: function(method) { if (qx.util.Request.isMethod(method)) { this.__method = method; } return this; }, /** * Gets the HTTP-Method. * * @return {String} The method. */ getMethod: function() { return this.__method; }, /** * Sets the request data to be send as part of the request. * * The request data is transparently included as URL query parameters or embedded in the * request body as form data. * * @param data {String|Object} The request data. * @return {qx.bom.request.SimpleXhr} Self for chaining. */ setRequestData: function(data) { if (qx.lang.Type.isString(data) || qx.lang.Type.isObject(data) || ["ArrayBuffer", "Blob", "FormData"].indexOf(qx.lang.Type.getClass(data)) !== -1) { this.__requestData = data; } return this; }, /** * Gets the request data. * * @return {String} The request data. */ getRequestData: function() { return this.__requestData; }, /** * Gets parsed response. * * If problems occurred an empty string ("") is more likely to be returned (instead of null). * * @return {String|null} The parsed response of the request. */ getResponse: function() { if (this.__response !== null) { return this.__response; } else { return (this._transport.responseXML !== null) ? this._transport.responseXML : this._transport.responseText; } return null; }, /** * Gets 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; }, /** * Sets (i.e. override) the parser for the response parsing. * * @see qx.util.ResponseParser#setParser * * @param parser {String|Function} * @return {Function} The parser function */ setParser: function(parser) { return this.__parser.setParser(parser); }, /** * Sets the timout limit in milliseconds. * * @param millis {Number} limit in milliseconds. * @return {qx.bom.request.SimpleXhr} Self for chaining. */ setTimeout: function(millis) { if (qx.lang.Type.isNumber(millis)) { this.__timeout = millis; } return this; }, /** * The current timeout in milliseconds. * * @return {Number} The current timeout in milliseconds. */ getTimeout: function() { return this.__timeout; }, /** * Whether to allow request to be answered from cache. * * Allowed values: * * * <code>true</code>: Allow caching (Default) * * <code>false</code>: Prohibit caching. Appends 'nocache' parameter to URL. * * Consider setting a Cache-Control header instead. A request’s Cache-Control * header may contain a number of directives controlling the behavior of * any caches in between client and origin server and allows therefore a more * fine grained control over caching. If such a header is provided, the setting * of setCache() will be ignored. * * * <code>"no-cache"</code>: Force caches to submit request in order to * validate the freshness of the representation. Note that the requested * resource may still be served from cache if the representation is * considered fresh. Use this directive to ensure freshness but save * bandwidth when possible. * * <code>"no-store"</code>: Do not keep a copy of the representation under * any conditions. * * See <a href="http://www.mnot.net/cache_docs/#CACHE-CONTROL"> * Caching tutorial</a> for an excellent introduction to Caching in general. * Refer to the corresponding section in the * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9"> * HTTP 1.1 specification</a> for more details and advanced directives. * * It is recommended to choose an appropriate Cache-Control directive rather * than prohibit caching using the nocache parameter. * * @param value {Boolean} * @return {qx.bom.request.SimpleXhr} Self for chaining. */ useCaching: function(value) { if (qx.lang.Type.isBoolean(value)) { this.__cache = value; } return this; }, /** * Whether requests are cached. * * @return {Boolean} Whether requests are cached. */ isCaching: function() { return this.__cache; }, /** * Whether request completed (is done). * @return {Boolean} Whether request is completed. */ isDone: function() { return (this._transport.readyState === qx.bom.request.Xhr.DONE); }, /** * Returns unique hash code of object. * * @return {Integer} unique hash code of the object */ toHashCode : function() { return this.$$hash; }, /** * Returns true if the object is disposed. * * @return {Boolean} Whether the object has been disposed */ isDisposed: function() { return !!this.__disposed; }, /** * Sends request. * * Relies on set before: * * a HTTP method * * an URL * * optional request headers * * optional request data */ send: function() { var curTimeout = this.getTimeout(), hasRequestData = (this.getRequestData() !== null), hasCacheControlHeader = this.__requestHeaders.hasOwnProperty("Cache-Control"), isBodyForMethodAllowed = qx.util.Request.methodAllowsRequestBody(this.getMethod()), curContentType = this.getRequestHeader("Content-Type"), serializedData = this._serializeData(this.getRequestData(), curContentType); // add GET params if needed if (this.getMethod() === "GET" && hasRequestData) { this.setUrl(qx.util.Uri.appendParamsToUrl(this.getUrl(), serializedData)); } // cache prevention if (this.isCaching() === false && !hasCacheControlHeader) { // Make sure URL cannot be served from cache and new request is made this.setUrl(qx.util.Uri.appendParamsToUrl(this.getUrl(), {nocache: new Date().valueOf()})); } // set timeout if (curTimeout) { this._transport.timeout = curTimeout; } // initialize request this._transport.open(this.getMethod(), this.getUrl(), true); // set all previously stored headers on initialized request for (var key in this.__requestHeaders) { this._transport.setRequestHeader(key, this.__requestHeaders[key]); } // send if (!isBodyForMethodAllowed) { // GET & HEAD this._transport.send(); } else { // POST & PUT ... if (typeof curContentType === "undefined" && ["ArrayBuffer", "Blob", "FormData"].indexOf(qx.Bootstrap.getClass(serializedData)) === -1) { // by default, set content-type urlencoded for requests with body this._transport.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); } this._transport.send(serializedData); } }, /** * Aborts request. * * Cancels any network activity. * @return {qx.bom.request.SimpleXhr} Self for chaining. */ abort: function() { this._transport.abort(); return this; }, /** * Disposes object and wrapped transport. * @return {Boolean} <code>true</code> if the object was successfully disposed */ dispose: function() { if (this._transport.dispose()) { this.__parser = null; this.__disposed = true; return true; } return false; }, /* --------------------------------------------------------------------------- PROTECTED --------------------------------------------------------------------------- */ /** * Holds transport. */ _transport: null, /** * Creates XHR transport. * * May be overridden to change type of resource. * @return {qx.bom.request.IRequest} Transport. */ _createTransport: function() { return new qx.bom.request.Xhr(); }, /** * Registers common listeners on given transport. * * @param transport {qx.bom.request.IRequest} Transport. * @return {qx.bom.request.IRequest} Transport. */ _registerTransportListener: function(transport) { transport.onreadystatechange = qx.lang.Function.bind(this._onReadyStateChange, this); transport.onloadend = qx.lang.Function.bind(this._onLoadEnd, this); transport.ontimeout = qx.lang.Function.bind(this._onTimeout, this); transport.onerror = qx.lang.Function.bind(this._onError, this); transport.onabort = qx.lang.Function.bind(this._onAbort, this); transport.onprogress = qx.lang.Function.bind(this._onProgress, this); return transport; }, /** * Creates response parser. * * @return {qx.util.ResponseParser} parser. */ _createResponseParser: function() { return new qx.util.ResponseParser(); }, /** * Sets the response. * * @param response {String} The parsed response of the request. */ _setResponse: function(response) { this.__response = response; }, /** * Serializes data. * * @param data {String|Map} Data to serialize. * @param contentType {String?} Content-Type which influences the serialization. * @return {String|null} Serialized data. */ _serializeData: function(data, contentType) { var isPost = this.getMethod() === "POST", isJson = (/application\/.*\+?json/).test(contentType); if (!data) { return null; } if (qx.lang.Type.isString(data)) { return 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); } if (["ArrayBuffer", "Blob", "FormData"].indexOf(qx.Bootstrap.getClass(data)) !== -1) { return data; } return null; }, /* --------------------------------------------------------------------------- PRIVATE --------------------------------------------------------------------------- */ /** * {Array} Request headers. */ __requestHeaders: null, /** * {Object} Request data (i.e. body). */ __requestData: null, /** * {String} HTTP method to use for request. */ __method: "", /** * {String} Requested URL. */ __url: "", /** * {Object} Response data. */ __response: null, /** * {Function} Parser. */ __parser: null, /** * {Boolean} Whether caching will be enabled. */ __cache: null, /** * {Number} The current timeout in milliseconds. */ __timeout: null, /** * {Boolean} Whether object has been disposed. */ __disposed: null, /* --------------------------------------------------------------------------- EVENT HANDLING --------------------------------------------------------------------------- */ /** * Adds an event listener for the given event name which is executed only once. * * @param name {String} The name of the event to listen to. * @param listener {Function} The function to execute when the event is fired * @param ctx {var?} The context of the listener. * @return {qx.bom.request.Xhr} Self for chaining. */ addListenerOnce: function(name, listener, ctx) { this.once(name, listener, ctx); return this; }, /** * Adds an event listener for the given event name. * * @param name {String} The name of the event to listen to. * @param listener {Function} The function to execute when the event is fired * @param ctx {var?} The context of the listener. * @return {qx.bom.request.Xhr} Self for chaining. */ addListener: function(name, listener, ctx) { this._transport._emitter.on(name, listener, ctx); return this; }, /** * Handles "readyStateChange" event. */ _onReadyStateChange: function() { if (qx.core.Environment.get("qx.debug.io")) { qx.Bootstrap.debug("Fire readyState: " + this._transport.readyState); } if (this.isDone()) { this.__onReadyStateDone(); } }, /** * Called internally when readyState is DONE. */ __onReadyStateDone: function() { if (qx.core.Environment.get("qx.debug.io")) { qx.Bootstrap.debug("Request completed with HTTP status: " + this._transport.status); } var response = this._transport.responseText; var contentType = this._transport.getResponseHeader("Content-Type"); // Successful HTTP status if (qx.util.Request.isSuccessful(this._transport.status)) { // Parse response if (qx.core.Environment.get("qx.debug.io")) { qx.Bootstrap.debug("Response is of type: '" + contentType + "'"); } this._setResponse(this.__parser.parse(response, contentType)); this.emit("success"); // Erroneous HTTP status } else { try { this._setResponse(this.__parser.parse(response, contentType)); } catch (e) { // ignore if it does not work } // A remote error failure if (this._transport.status !== 0) { this.emit("fail"); } } }, /** * Handles "loadEnd" event. */ _onLoadEnd: function() { this.emit("loadEnd"); }, /** * Handles "abort" event. */ _onAbort: function() { this.emit("abort"); }, /** * Handles "timeout" event. */ _onTimeout: function() { this.emit("timeout"); // A network error failure this.emit("fail"); }, /** * Handles "error" event. */ _onError: function() { this.emit("error"); // A network error failure this.emit("fail"); }, /** * Handles "error" event. */ _onProgress: function() { this.emit("progress"); } } });