@qooxdoo/framework
Version:
The JS Framework for Coders
1,004 lines (859 loc) • 27.4 kB
JavaScript
/* ************************************************************************
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);
}
}
});