diff --git a/CHANGES.md b/CHANGES.md index 0eea3f775bc5..31522154fcbc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,12 +3,20 @@ Change Log ### 1.43 - 2018-03-01 +##### Deprecated :hourglass_flowing_sand: +* In the `Resource` class, `addQueryParameters` and `addTemplateValues` have been deprecated and will be removed in Cesium 1.45. Please use `setQueryParameters` and `setTemplateValues` instead. + ##### Additions :tada: * Added support for a promise to a resource for `CesiumTerrainProvider`, `createTileMapServiceImageryProvider` and `Cesium3DTileset` [#6204](https://github.com/AnalyticalGraphicsInc/cesium/pull/6204) +* `Resource` class [#6205](https://github.com/AnalyticalGraphicsInc/cesium/issues/6205) + * Added `put`, `patch`, `delete`, `options` and `head` methods, so it can be used for all XHR requests. + * Added `preserveQueryParameters` parameter to `getDerivedResource`, to allow us to append query parameters instead of always replacing them. + * Added `setQueryParameters` and `appendQueryParameters` to allow for better handling of query strings. ##### Fixes :wrench: * Fixed bug where AxisAlignedBoundingBox did not copy over center value when cloning an undefined result. [#6183](https://github.com/AnalyticalGraphicsInc/cesium/pull/6183) * Fixed `Resource.fetch` when called with no arguments [#6206](https://github.com/AnalyticalGraphicsInc/cesium/issues/6206) +* Fixed `Resource.clone` to clone the `Request` object, so resource can be used in parallel. [#6208](https://github.com/AnalyticalGraphicsInc/cesium/issues/6208) * Fixed bug where 3D Tiles Point Clouds would fail in Internet Explorer. [#6220](https://github.com/AnalyticalGraphicsInc/cesium/pull/6220) ##### Additions :tada: diff --git a/Source/Core/Resource.js b/Source/Core/Resource.js index f620576d6fea..1ae03c402f6b 100644 --- a/Source/Core/Resource.js +++ b/Source/Core/Resource.js @@ -65,9 +65,16 @@ define([ })(); /** + * Parses a query string and returns the object equivalent. + * + * @param {Uri} uri The Uri with a query object. + * @param {Resource} resource The Resource that will be assigned queryParameters. + * @param {Boolean} merge If true, we'll merge with the resource's existing queryParameters. Otherwise they will be replaced. + * @param {Boolean} preserveQueryParameters If true duplicate parameters will be concatenated into an array. If false, keys in uri will take precedence. + * * @private */ - function parseQuery(uri, resource) { + function parseQuery(uri, resource, merge, preserveQueryParameters) { var queryString = uri.query; if (!defined(queryString) || (queryString.length === 0)) { return {}; @@ -83,11 +90,20 @@ define([ query = queryToObject(queryString); } - resource._queryParameters = combine(resource._queryParameters, query); + if (merge) { + resource._queryParameters = combineQueryParameters(query, resource._queryParameters, preserveQueryParameters); + } else { + resource._queryParameters = query; + } uri.query = undefined; } /** + * Converts a query object into a string. + * + * @param {Uri} uri The Uri object that will have the query object set. + * @param {Resource} resource The resource that has queryParameters + * * @private */ function stringifyQuery(uri, resource) { @@ -104,17 +120,28 @@ define([ } /** + * Clones a value if it is defined, otherwise returns the default value + * + * @param {*} [val] The value to clone. + * @param {*} [defaultVal] The default value. + * + * @returns {*} A clone of val or the defaultVal. + * * @private */ - function defaultClone(obj, defaultVal) { - if (!defined(obj)) { + function defaultClone(val, defaultVal) { + if (!defined(val)) { return defaultVal; } - return defined(obj.clone) ? obj.clone() : clone(obj); + return defined(val.clone) ? val.clone() : clone(val); } /** + * Checks to make sure the Resource isn't already being requested. + * + * @param {Request} request The request to check. + * * @private */ function checkAndResetRequest(request) { @@ -126,6 +153,88 @@ define([ request.deferred = undefined; } + /** + * This combines a map of query parameters. + * + * @param {Object} q1 The first map of query parameters. Values in this map will take precedence if preserveQueryParameters is false. + * @param {Object} q2 The second map of query parameters. + * @param {Boolean} preserveQueryParameters If true duplicate parameters will be concatenated into an array. If false, keys in q1 will take precedence. + * + * @returns {Object} The combined map of query parameters. + * + * @example + * var q1 = { + * a: 1, + * b: 2 + * }; + * var q2 = { + * a: 3, + * c: 4 + * }; + * var q3 = { + * b: [5, 6], + * d: 7 + * } + * + * // Returns + * // { + * // a: [1, 3], + * // b: 2, + * // c: 4 + * // }; + * combineQueryParameters(q1, q2, true); + * + * // Returns + * // { + * // a: 1, + * // b: 2, + * // c: 4 + * // }; + * combineQueryParameters(q1, q2, false); + * + * // Returns + * // { + * // a: 1, + * // b: [2, 5, 6], + * // d: 7 + * // }; + * combineQueryParameters(q1, q3, true); + * + * // Returns + * // { + * // a: 1, + * // b: 2, + * // d: 7 + * // }; + * combineQueryParameters(q1, q3, false); + * + * @private + */ + function combineQueryParameters(q1, q2, preserveQueryParameters) { + if (!preserveQueryParameters) { + return combine(q1, q2); + } + + var result = clone(q1, true); + for (var param in q2) { + if (q2.hasOwnProperty(param)) { + var value = result[param]; + var q2Value = q2[param]; + if (defined(value)) { + if (!Array.isArray(value)) { + value = result[param] = [value]; + } + + result[param] = value.concat(q2Value); + } else { + result[param] = Array.isArray(q2Value) ? q2Value.slice() : q2Value; + } + } + } + + return result; + } + /** * A resource that includes the location and any other parameters we need to retrieve it or create derived resources. It also provides the ability to retry requests. * @@ -224,7 +333,14 @@ define([ this.retryAttempts = defaultValue(options.retryAttempts, 0); this._retryCount = 0; - this.url = options.url; + + var uri = new Uri(options.url); + parseQuery(uri, this, true, true); + + // Remove the fragment as it's not sent with a request + uri.fragment = undefined; + + this._url = uri.toString(); } /** @@ -239,7 +355,13 @@ define([ */ Resource.createIfNeeded = function(resource, options) { if (resource instanceof Resource) { - return resource.clone(); + // Keep existing request object. This function is used internally to duplicate a Resource, so that it can't + // be modified outside of a class that holds it (eg. an imagery or terrain provider). Since the Request objects + // are managed outside of the providers, by the tile loading code, we want to keep the request property the same so if it is changed + // in the underlying tiling code the requests for this resource will use it. + return resource.getDerivedResource({ + request: resource.request + }); } if (typeof resource !== 'string') { @@ -309,7 +431,7 @@ define([ set: function(value) { var uri = new Uri(value); - parseQuery(uri, this); + parseQuery(uri, this, false); // Remove the fragment as it's not sent with a request uri.fragment = undefined; @@ -420,27 +542,52 @@ define([ /** * Combines the specified object and the existing query parameters. This allows you to add many parameters at once, - * as opposed to adding them one at a time to the queryParameters property. + * as opposed to adding them one at a time to the queryParameters property. If a value is already set, it will be replaced with the new value. * * @param {Object} params The query parameters * @param {Boolean} [useAsDefault=false] If true the params will be used as the default values, so they will only be set if they are undefined. */ - Resource.prototype.addQueryParameters = function(params, useAsDefault) { + Resource.prototype.setQueryParameters = function(params, useAsDefault) { if (useAsDefault) { - this._queryParameters = combine(this._queryParameters, params); + this._queryParameters = combineQueryParameters(this._queryParameters, params, false); } else { - this._queryParameters = combine(params, this._queryParameters); + this._queryParameters = combineQueryParameters(params, this._queryParameters, false); } }; + /** + * Combines the specified object and the existing query parameters. This allows you to add many parameters at once, + * as opposed to adding them one at a time to the queryParameters property. If a value is already set, it will be replaced with the new value. + * + * @param {Object} params The query parameters + * @param {Boolean} [useAsDefault=false] If true the params will be used as the default values, so they will only be set if they are undefined. + * + * @deprecated + */ + Resource.prototype.addQueryParameters = function(params, useAsDefault) { + deprecationWarning('Resource.addQueryParameters', 'addQueryParameters has been deprecated and will be removed 1.45. Use setQueryParameters or appendQueryParameters instead.'); + + return this.setQueryParameters(params, useAsDefault); + }; + + /** + * Combines the specified object and the existing query parameters. This allows you to add many parameters at once, + * as opposed to adding them one at a time to the queryParameters property. + * + * @param {Object} params The query parameters + */ + Resource.prototype.appendQueryParameters = function(params) { + this._queryParameters = combineQueryParameters(params, this._queryParameters, true); + }; + /** * Combines the specified object and the existing template values. This allows you to add many values at once, - * as opposed to adding them one at a time to the templateValues property. + * as opposed to adding them one at a time to the templateValues property. If a value is already set, it will become an array and the new value will be appended. * - * @param {Object} params The template values + * @param {Object} template The template values * @param {Boolean} [useAsDefault=false] If true the values will be used as the default values, so they will only be set if they are undefined. */ - Resource.prototype.addTemplateValues = function(template, useAsDefault) { + Resource.prototype.setTemplateValues = function(template, useAsDefault) { if (useAsDefault) { this._templateValues = combine(this._templateValues, template); } else { @@ -448,6 +595,21 @@ define([ } }; + /** + * Combines the specified object and the existing template values. This allows you to add many values at once, + * as opposed to adding them one at a time to the templateValues property. If a value is already set, it will become an array and the new value will be appended. + * + * @param {Object} template The template values + * @param {Boolean} [useAsDefault=false] If true the values will be used as the default values, so they will only be set if they are undefined. + * + * @deprecated + */ + Resource.prototype.addTemplateValues = function(template, useAsDefault) { + deprecationWarning('Resource.addTemplateValues', 'addTemplateValues has been deprecated and will be removed 1.45. Use setTemplateValues.'); + + return this.setTemplateValues(template, useAsDefault); + }; + /** * Returns a resource relative to the current instance. All properties remain the same as the current instance unless overridden in options. * @@ -460,6 +622,7 @@ define([ * @param {Resource~RetryCallback} [options.retryCallback] The function to call when loading the resource fails. * @param {Number} [options.retryAttempts] The number of times the retryCallback should be called before giving up. * @param {Request} [options.request] A Request object that will be used. Intended for internal use only. + * @param {Boolean} [options.preserveQueryParameters=false] If true, this will keep all query parameters from the current resource and derived resource. If false, derived parameters will replace those of the current resource. * * @returns {Resource} The resource derived from the current one. */ @@ -470,7 +633,8 @@ define([ if (defined(options.url)) { var uri = new Uri(options.url); - parseQuery(uri, resource); + var preserveQueryParameters = defaultValue(options.preserveQueryParameters, false); + parseQuery(uri, resource, true, preserveQueryParameters); // Remove the fragment as it's not sent with a request uri.fragment = undefined; @@ -492,9 +656,6 @@ define([ } if (defined(options.request)) { resource.request = options.request; - } else { - // Clone the request so we keep all the throttle settings - resource.request = this.request.clone(); } if (defined(options.retryCallback)) { resource.retryCallback = options.retryCallback; @@ -512,6 +673,8 @@ define([ * @param {Error} [error] The error that was encountered. * * @returns {Promise<Boolean>} A promise to a boolean, that if true will cause the resource request to be retried. + * + * @private */ Resource.prototype.retryOnError = function(error) { var retryCallback = this.retryCallback; @@ -550,10 +713,7 @@ define([ result.retryCallback = this.retryCallback; result.retryAttempts = this.retryAttempts; result._retryCount = 0; - - // In practice, we don't want this cloned. It usually not set, unless we purposely set it internally and not - // using the request will break the request scheduler. - result.request = this.request; + result.request = this.request.clone(); return result; }; @@ -1078,38 +1238,9 @@ define([ }; /** - * Asynchronously loads the given resource. Returns a promise that will resolve to - * the result once loaded, or reject if the resource failed to load. The data is loaded - * using XMLHttpRequest, which means that in order to make requests to another origin, - * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. - * - * @param {Object} [options] Object with the following properties: - * @param {String} [options.responseType] The type of response. This controls the type of item returned. - * @param {Object} [options.headers] Additional HTTP headers to send with the request, if any. - * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. - * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. - * - * - * @example - * // Load a single resource asynchronously. In real code, you should use loadBlob instead. - * resource.fetch() - * .then(function(blob) { - * // use the data - * }).otherwise(function(error) { - * // an error occurred - * }); - * - * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} - * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} + * @private */ - Resource.prototype.fetch = function(options) { - options = defaultClone(options, {}); - options.method = 'GET'; - - return makeRequest(this, options); - }; - - function makeRequest(resource, options) { + Resource._makeRequest = function(resource, options) { checkAndResetRequest(resource.request); var request = resource.request; @@ -1158,7 +1289,7 @@ define([ return when.reject(e); }); }); - } + }; var dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/; @@ -1209,6 +1340,38 @@ define([ } } + /** + * Asynchronously loads the given resource. Returns a promise that will resolve to + * the result once loaded, or reject if the resource failed to load. The data is loaded + * using XMLHttpRequest, which means that in order to make requests to another origin, + * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. It's recommended that you use + * the more specific functions eg. fetchJson, fetchBlob, etc. + * + * @param {Object} [options] Object with the following properties: + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {Object} [options.headers] Additional HTTP headers to send with the request, if any. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + * + * + * @example + * resource.fetch() + * .then(function(body) { + * // use the data + * }).otherwise(function(error) { + * // an error occurred + * }); + * + * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} + * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} + */ + Resource.prototype.fetch = function(options) { + options = defaultClone(options, {}); + options.method = 'GET'; + + return Resource._makeRequest(this, options); + }; + /** * Creates a Resource from a URL and calls fetch() on it. * @@ -1235,7 +1398,175 @@ define([ }; /** - * Asynchronously posts data the given resource. Returns a promise that will resolve to + * Asynchronously deletes the given resource. Returns a promise that will resolve to + * the result once loaded, or reject if the resource failed to load. The data is loaded + * using XMLHttpRequest, which means that in order to make requests to another origin, + * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. + * + * @param {Object} [options] Object with the following properties: + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {Object} [options.headers] Additional HTTP headers to send with the request, if any. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + * + * + * @example + * resource.delete() + * .then(function(body) { + * // use the data + * }).otherwise(function(error) { + * // an error occurred + * }); + * + * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} + * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} + */ + Resource.prototype.delete = function(options) { + options = defaultClone(options, {}); + options.method = 'DELETE'; + + return Resource._makeRequest(this, options); + }; + + /** + * Creates a Resource from a URL and calls delete() on it. + * + * @param {String|Object} options A url or an object with the following properties + * @param {String} options.url The url of the resource. + * @param {Object} [options.queryParameters] An object containing query parameters that will be sent when retrieving the resource. + * @param {Object} [options.templateValues] Key/Value pairs that are used to replace template values (eg. {x}). + * @param {Object} [options.headers={}] Additional HTTP headers that will be sent. + * @param {DefaultProxy} [options.proxy] A proxy to be used when loading the resource. + * @param {Resource~RetryCallback} [options.retryCallback] The Function to call when a request for this resource fails. If it returns true, the request will be retried. + * @param {Number} [options.retryAttempts=0] The number of times the retryCallback should be called before giving up. + * @param {Request} [options.request] A Request object that will be used. Intended for internal use only. + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + */ + Resource.delete = function (options) { + var resource = new Resource(options); + return resource.delete({ + // Make copy of just the needed fields because headers can be passed to both the constructor and to fetch + responseType: options.responseType, + overrideMimeType: options.overrideMimeType + }); + }; + + /** + * Asynchronously gets headers the given resource. Returns a promise that will resolve to + * the result once loaded, or reject if the resource failed to load. The data is loaded + * using XMLHttpRequest, which means that in order to make requests to another origin, + * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. + * + * @param {Object} [options] Object with the following properties: + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {Object} [options.headers] Additional HTTP headers to send with the request, if any. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + * + * + * @example + * resource.head() + * .then(function(headers) { + * // use the data + * }).otherwise(function(error) { + * // an error occurred + * }); + * + * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} + * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} + */ + Resource.prototype.head = function(options) { + options = defaultClone(options, {}); + options.method = 'HEAD'; + + return Resource._makeRequest(this, options); + }; + + /** + * Creates a Resource from a URL and calls head() on it. + * + * @param {String|Object} options A url or an object with the following properties + * @param {String} options.url The url of the resource. + * @param {Object} [options.queryParameters] An object containing query parameters that will be sent when retrieving the resource. + * @param {Object} [options.templateValues] Key/Value pairs that are used to replace template values (eg. {x}). + * @param {Object} [options.headers={}] Additional HTTP headers that will be sent. + * @param {DefaultProxy} [options.proxy] A proxy to be used when loading the resource. + * @param {Resource~RetryCallback} [options.retryCallback] The Function to call when a request for this resource fails. If it returns true, the request will be retried. + * @param {Number} [options.retryAttempts=0] The number of times the retryCallback should be called before giving up. + * @param {Request} [options.request] A Request object that will be used. Intended for internal use only. + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + */ + Resource.head = function (options) { + var resource = new Resource(options); + return resource.head({ + // Make copy of just the needed fields because headers can be passed to both the constructor and to fetch + responseType: options.responseType, + overrideMimeType: options.overrideMimeType + }); + }; + + /** + * Asynchronously gets options the given resource. Returns a promise that will resolve to + * the result once loaded, or reject if the resource failed to load. The data is loaded + * using XMLHttpRequest, which means that in order to make requests to another origin, + * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. + * + * @param {Object} [options] Object with the following properties: + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {Object} [options.headers] Additional HTTP headers to send with the request, if any. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + * + * + * @example + * resource.options() + * .then(function(headers) { + * // use the data + * }).otherwise(function(error) { + * // an error occurred + * }); + * + * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} + * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} + */ + Resource.prototype.options = function(options) { + options = defaultClone(options, {}); + options.method = 'OPTIONS'; + + return Resource._makeRequest(this, options); + }; + + /** + * Creates a Resource from a URL and calls options() on it. + * + * @param {String|Object} options A url or an object with the following properties + * @param {String} options.url The url of the resource. + * @param {Object} [options.queryParameters] An object containing query parameters that will be sent when retrieving the resource. + * @param {Object} [options.templateValues] Key/Value pairs that are used to replace template values (eg. {x}). + * @param {Object} [options.headers={}] Additional HTTP headers that will be sent. + * @param {DefaultProxy} [options.proxy] A proxy to be used when loading the resource. + * @param {Resource~RetryCallback} [options.retryCallback] The Function to call when a request for this resource fails. If it returns true, the request will be retried. + * @param {Number} [options.retryAttempts=0] The number of times the retryCallback should be called before giving up. + * @param {Request} [options.request] A Request object that will be used. Intended for internal use only. + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + */ + Resource.options = function (options) { + var resource = new Resource(options); + return resource.options({ + // Make copy of just the needed fields because headers can be passed to both the constructor and to fetch + responseType: options.responseType, + overrideMimeType: options.overrideMimeType + }); + }; + + /** + * Asynchronously posts data to the given resource. Returns a promise that will resolve to * the result once loaded, or reject if the resource failed to load. The data is loaded * using XMLHttpRequest, which means that in order to make requests to another origin, * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. @@ -1249,7 +1580,6 @@ define([ * * * @example - * // Load a single resource asynchronously. In real code, you should use loadBlob instead. * resource.post(data) * .then(function(result) { * // use the result @@ -1267,13 +1597,13 @@ define([ options.method = 'POST'; options.data = data; - return makeRequest(this, options); + return Resource._makeRequest(this, options); }; /** - * Creates a Resource from a URL and calls fetch() on it. + * Creates a Resource from a URL and calls post() on it. * - * @param {String|Object} options A url or an object with the following properties + * @param {Object} options A url or an object with the following properties * @param {String} options.url The url of the resource. * @param {Object} options.data Data that is posted with the resource. * @param {Object} [options.queryParameters] An object containing query parameters that will be sent when retrieving the resource. @@ -1296,6 +1626,128 @@ define([ }); }; + /** + * Asynchronously puts data to the given resource. Returns a promise that will resolve to + * the result once loaded, or reject if the resource failed to load. The data is loaded + * using XMLHttpRequest, which means that in order to make requests to another origin, + * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. + * + * @param {Object} data Data that is posted with the resource. + * @param {Object} [options] Object with the following properties: + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {Object} [options.headers] Additional HTTP headers to send with the request, if any. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + * + * + * @example + * resource.put(data) + * .then(function(result) { + * // use the result + * }).otherwise(function(error) { + * // an error occurred + * }); + * + * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} + * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} + */ + Resource.prototype.put = function(data, options) { + Check.defined('data', data); + + options = defaultClone(options, {}); + options.method = 'PUT'; + options.data = data; + + return Resource._makeRequest(this, options); + }; + + /** + * Creates a Resource from a URL and calls put() on it. + * + * @param {Object} options A url or an object with the following properties + * @param {String} options.url The url of the resource. + * @param {Object} options.data Data that is posted with the resource. + * @param {Object} [options.queryParameters] An object containing query parameters that will be sent when retrieving the resource. + * @param {Object} [options.templateValues] Key/Value pairs that are used to replace template values (eg. {x}). + * @param {Object} [options.headers={}] Additional HTTP headers that will be sent. + * @param {DefaultProxy} [options.proxy] A proxy to be used when loading the resource. + * @param {Resource~RetryCallback} [options.retryCallback] The Function to call when a request for this resource fails. If it returns true, the request will be retried. + * @param {Number} [options.retryAttempts=0] The number of times the retryCallback should be called before giving up. + * @param {Request} [options.request] A Request object that will be used. Intended for internal use only. + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + */ + Resource.put = function (options) { + var resource = new Resource(options); + return resource.put(options.data, { + // Make copy of just the needed fields because headers can be passed to both the constructor and to post + responseType: options.responseType, + overrideMimeType: options.overrideMimeType + }); + }; + + /** + * Asynchronously patches data to the given resource. Returns a promise that will resolve to + * the result once loaded, or reject if the resource failed to load. The data is loaded + * using XMLHttpRequest, which means that in order to make requests to another origin, + * the server must have Cross-Origin Resource Sharing (CORS) headers enabled. + * + * @param {Object} data Data that is posted with the resource. + * @param {Object} [options] Object with the following properties: + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {Object} [options.headers] Additional HTTP headers to send with the request, if any. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + * + * + * @example + * resource.patch(data) + * .then(function(result) { + * // use the result + * }).otherwise(function(error) { + * // an error occurred + * }); + * + * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} + * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} + */ + Resource.prototype.patch = function(data, options) { + Check.defined('data', data); + + options = defaultClone(options, {}); + options.method = 'PATCH'; + options.data = data; + + return Resource._makeRequest(this, options); + }; + + /** + * Creates a Resource from a URL and calls patch() on it. + * + * @param {Object} options A url or an object with the following properties + * @param {String} options.url The url of the resource. + * @param {Object} options.data Data that is posted with the resource. + * @param {Object} [options.queryParameters] An object containing query parameters that will be sent when retrieving the resource. + * @param {Object} [options.templateValues] Key/Value pairs that are used to replace template values (eg. {x}). + * @param {Object} [options.headers={}] Additional HTTP headers that will be sent. + * @param {DefaultProxy} [options.proxy] A proxy to be used when loading the resource. + * @param {Resource~RetryCallback} [options.retryCallback] The Function to call when a request for this resource fails. If it returns true, the request will be retried. + * @param {Number} [options.retryAttempts=0] The number of times the retryCallback should be called before giving up. + * @param {Request} [options.request] A Request object that will be used. Intended for internal use only. + * @param {String} [options.responseType] The type of response. This controls the type of item returned. + * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. + * @returns {Promise.<Object>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority. + */ + Resource.patch = function (options) { + var resource = new Resource(options); + return resource.patch(options.data, { + // Make copy of just the needed fields because headers can be passed to both the constructor and to post + responseType: options.responseType, + overrideMimeType: options.overrideMimeType + }); + }; + /** * Contains implementations of functions that can be replaced for testing * @@ -1371,6 +1823,21 @@ define([ var response = xhr.response; var browserResponseType = xhr.responseType; + if (method === 'HEAD' || method === 'OPTIONS') { + var responseHeaderString = xhr.getAllResponseHeaders(); + var splitHeaders = responseHeaderString.trim().split(/[\r\n]+/); + + var responseHeaders = {}; + splitHeaders.forEach(function (line) { + var parts = line.split(': '); + var header = parts.shift(); + responseHeaders[header] = parts.join(': '); + }); + + deferred.resolve(responseHeaders); + return; + } + //All modern browsers will go into either the first or second if block or last else block. //Other code paths support older browsers that either do not support the supplied responseType //or do not support the xhr.response property. diff --git a/Source/Core/loadWithXhr.js b/Source/Core/loadWithXhr.js index c7cd6f2491fe..ad3c6a17ffdf 100644 --- a/Source/Core/loadWithXhr.js +++ b/Source/Core/loadWithXhr.js @@ -1,12 +1,14 @@ define([ '../ThirdParty/when', './Check', + './defaultValue', './defineProperties', './deprecationWarning', './Resource' ], function( when, Check, + defaultValue, defineProperties, deprecationWarning, Resource) { @@ -61,9 +63,11 @@ define([ // Take advantage that most parameters are the same var resource = new Resource(options); - return resource.fetch({ + return Resource._makeRequest(resource, { responseType: options.responseType, - overrideMimeType: options.overrideMimeType + overrideMimeType: options.overrideMimeType, + method: defaultValue(options.method, 'GET'), + data: options.data }); } diff --git a/Source/DataSources/KmlDataSource.js b/Source/DataSources/KmlDataSource.js index 203fb879a002..4e57b55dd85f 100644 --- a/Source/DataSources/KmlDataSource.js +++ b/Source/DataSources/KmlDataSource.js @@ -728,10 +728,10 @@ define([ var viewFormat = defaultValue(queryStringValue(iconNode, 'viewFormat', namespaces.kml), defaultViewFormat); var httpQuery = queryStringValue(iconNode, 'httpQuery', namespaces.kml); if (defined(viewFormat)) { - hrefResource.addQueryParameters(queryToObject(cleanupString(viewFormat))); + hrefResource.setQueryParameters(queryToObject(cleanupString(viewFormat))); } if (defined(httpQuery)) { - hrefResource.addQueryParameters(queryToObject(cleanupString(httpQuery))); + hrefResource.setQueryParameters(queryToObject(cleanupString(httpQuery))); } processNetworkLinkQueryString(hrefResource, dataSource._camera, dataSource._canvas, viewBoundScale, dataSource._lastCameraView.bbox); @@ -2108,7 +2108,7 @@ define([ queryString = queryString.replace('[clientName]', 'Cesium'); queryString = queryString.replace('[language]', 'English'); - resource.addQueryParameters(queryToObject(queryString)); + resource.setQueryParameters(queryToObject(queryString)); } function processNetworkLink(dataSource, parent, node, entityCollection, styleCollection, sourceResource, uriResolver, promises, context) { @@ -2145,10 +2145,10 @@ define([ var viewFormat = defaultValue(queryStringValue(link, 'viewFormat', namespaces.kml), defaultViewFormat); var httpQuery = queryStringValue(link, 'httpQuery', namespaces.kml); if (defined(viewFormat)) { - href.addQueryParameters(queryToObject(cleanupString(viewFormat))); + href.setQueryParameters(queryToObject(cleanupString(viewFormat))); } if (defined(httpQuery)) { - href.addQueryParameters(queryToObject(cleanupString(httpQuery))); + href.setQueryParameters(queryToObject(cleanupString(httpQuery))); } processNetworkLinkQueryString(href, dataSource._camera, dataSource._canvas, viewBoundScale, dataSource._lastCameraView.bbox); @@ -2391,7 +2391,7 @@ define([ } if (defined(query)) { - sourceUri.addQueryParameters(query); + sourceUri.setQueryParameters(query); } return when(promise) @@ -2525,7 +2525,7 @@ define([ /** * Creates a Promise to a new instance loaded with the provided KML data. * - * @param {String|Document|Blob} data A url, parsed KML document, or Blob containing binary KMZ data or a parsed KML document. + * @param {Resource|String|Document|Blob} data A url, parsed KML document, or Blob containing binary KMZ data or a parsed KML document. * @param {Object} options An object with the following properties: * @param {Camera} options.camera The camera that is used for viewRefreshModes and sending camera properties to network links. * @param {Canvas} options.canvas The canvas that is used for sending viewer properties to network links. @@ -2681,9 +2681,9 @@ define([ * @param {Resource|String|Document|Blob} data A url, parsed KML document, or Blob containing binary KMZ data or a parsed KML document. * @param {Object} [options] An object with the following properties: * @param {Resource|String} [options.sourceUri] Overrides the url to use for resolving relative links and other KML network features. - * @returns {Promise.<KmlDataSource>} A promise that will resolve to this instances once the KML is loaded. * @param {Boolean} [options.clampToGround=false] true if we want the geometry features (Polygons, LineStrings and LinearRings) clamped to the ground. If true, lines will use corridors so use Entity.corridor instead of Entity.polyline. - * @param {Object} [options.query] Key-value pairs which are appended to all URIs in the CZML. + * + * @returns {Promise.<KmlDataSource>} A promise that will resolve to this instances once the KML is loaded. */ KmlDataSource.prototype.load = function(data, options) { //>>includeStart('debug', pragmas.debug); @@ -2976,7 +2976,7 @@ define([ networkLink.updating = true; var newEntityCollection = new EntityCollection(); var href = networkLink.href.clone(); - href.addQueryParameters(networkLink.cookie); + href.setQueryParameters(networkLink.cookie); processNetworkLinkQueryString(href, that._camera, that._canvas, networkLink.viewBoundScale, lastCameraView.bbox); load(that, newEntityCollection, href, {context : entity.id}) .then(getNetworkLinkUpdateCallback(that, networkLink, newEntityCollection, newNetworkLinks, href)) diff --git a/Source/Scene/ArcGisMapServerImageryProvider.js b/Source/Scene/ArcGisMapServerImageryProvider.js index c6a6cf8d06b5..2b727c69a20b 100644 --- a/Source/Scene/ArcGisMapServerImageryProvider.js +++ b/Source/Scene/ArcGisMapServerImageryProvider.js @@ -125,7 +125,7 @@ define([ resource.appendForwardSlash(); if (defined(options.token)) { - resource.addQueryParameters({ + resource.setQueryParameters({ token: options.token }); } diff --git a/Source/Scene/BingMapsImageryProvider.js b/Source/Scene/BingMapsImageryProvider.js index 14a284d1645e..c3c43838b70d 100644 --- a/Source/Scene/BingMapsImageryProvider.js +++ b/Source/Scene/BingMapsImageryProvider.js @@ -113,7 +113,7 @@ define([ proxy: options.proxy }); - urlResource.addQueryParameters({ + urlResource.setQueryParameters({ key: this._key }); diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index be37ce87d547..5ffb0114e887 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -619,7 +619,7 @@ define([ var expired = this.contentExpired; if (expired) { // Append a query parameter of the tile expiration date to prevent caching - resource.addQueryParameters({ + resource.setQueryParameters({ expired: this.expireDate.toString() }); } diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 200e480cc7f1..237e51888708 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1261,7 +1261,7 @@ define([ v: defaultValue(asset.tilesetVersion, '0.0') }; this._basePath += '?v=' + versionQuery.v; - tilesetResource.addQueryParameters(versionQuery); + tilesetResource.setQueryParameters(versionQuery); } // A tileset.json referenced from a tile may exist in a different directory than the root tileset. diff --git a/Source/Scene/GoogleEarthEnterpriseMapsProvider.js b/Source/Scene/GoogleEarthEnterpriseMapsProvider.js index a1dbe9a1bd35..7e48643fae7b 100644 --- a/Source/Scene/GoogleEarthEnterpriseMapsProvider.js +++ b/Source/Scene/GoogleEarthEnterpriseMapsProvider.js @@ -118,9 +118,14 @@ define([ var url = options.url; var path = defaultValue(options.path, '/default_map'); - var resource = Resource.createIfNeeded(url + path, { - proxy: options.proxy + + var resource = Resource.createIfNeeded(url, { + proxy : options.proxy + }).getDerivedResource({ + // We used to just append path to url, so now that we do proper URI resolution, removed the / + url : (path[0] === '/') ? path.substring(1) : path }); + resource.appendForwardSlash(); this._resource = resource; diff --git a/Source/Scene/MapboxImageryProvider.js b/Source/Scene/MapboxImageryProvider.js index f6c62254e47e..ac46c2693cd6 100644 --- a/Source/Scene/MapboxImageryProvider.js +++ b/Source/Scene/MapboxImageryProvider.js @@ -100,7 +100,7 @@ define([ templateUrl += mapId + '/{z}/{x}/{y}' + this._format; resource.url = templateUrl; - resource.addQueryParameters({ + resource.setQueryParameters({ access_token: accessToken }); diff --git a/Source/Scene/WebMapServiceImageryProvider.js b/Source/Scene/WebMapServiceImageryProvider.js index 6e9865cf3a8c..e62309b3443a 100644 --- a/Source/Scene/WebMapServiceImageryProvider.js +++ b/Source/Scene/WebMapServiceImageryProvider.js @@ -111,15 +111,15 @@ define([ var pickFeatureResource = resource.clone(); - resource.addQueryParameters(WebMapServiceImageryProvider.DefaultParameters, true); - pickFeatureResource.addQueryParameters(WebMapServiceImageryProvider.GetFeatureInfoDefaultParameters, true); + resource.setQueryParameters(WebMapServiceImageryProvider.DefaultParameters, true); + pickFeatureResource.setQueryParameters(WebMapServiceImageryProvider.GetFeatureInfoDefaultParameters, true); if (defined(options.parameters)) { - resource.addQueryParameters(objectToLowercase(options.parameters)); + resource.setQueryParameters(objectToLowercase(options.parameters)); } if (defined(options.getFeatureInfoParameters)) { - pickFeatureResource.addQueryParameters(objectToLowercase(options.getFeatureInfoParameters)); + pickFeatureResource.setQueryParameters(objectToLowercase(options.getFeatureInfoParameters)); } var parameters = {}; @@ -139,8 +139,8 @@ define([ parameters.srs = options.tilingScheme instanceof WebMercatorTilingScheme ? 'EPSG:3857' : 'EPSG:4326'; } - resource.addQueryParameters(parameters, true); - pickFeatureResource.addQueryParameters(parameters, true); + resource.setQueryParameters(parameters, true); + pickFeatureResource.setQueryParameters(parameters, true); var pickFeatureParams = { query_layers: options.layers, @@ -148,7 +148,7 @@ define([ y: '{j}', info_format: '{format}' }; - pickFeatureResource.addQueryParameters(pickFeatureParams, true); + pickFeatureResource.setQueryParameters(pickFeatureParams, true); this._resource = resource; this._pickFeaturesResource = pickFeatureResource; diff --git a/Source/Scene/WebMapTileServiceImageryProvider.js b/Source/Scene/WebMapTileServiceImageryProvider.js index 997fd2ed4bb8..0ec60896e918 100644 --- a/Source/Scene/WebMapTileServiceImageryProvider.js +++ b/Source/Scene/WebMapTileServiceImageryProvider.js @@ -175,10 +175,10 @@ define([ TileMatrixSet : tileMatrixSetID }; - resource.addTemplateValues(templateValues); + resource.setTemplateValues(templateValues); this._useKvp = false; } else { - resource.addQueryParameters(defaultParameters); + resource.setQueryParameters(defaultParameters); this._useKvp = true; } @@ -265,14 +265,14 @@ define([ resource = imageryProvider._resource.getDerivedResource({ request: request }); - resource.addTemplateValues(templateValues); + resource.setTemplateValues(templateValues); if (defined(staticDimensions)) { - resource.addTemplateValues(staticDimensions); + resource.setTemplateValues(staticDimensions); } if (defined(dynamicIntervalData)) { - resource.addTemplateValues(dynamicIntervalData); + resource.setTemplateValues(dynamicIntervalData); } } else { // build KVP request diff --git a/Specs/Core/ResourceSpec.js b/Specs/Core/ResourceSpec.js index c947355d786f..4e8cab60a41f 100644 --- a/Specs/Core/ResourceSpec.js +++ b/Specs/Core/ResourceSpec.js @@ -108,6 +108,49 @@ defineSuite([ expect(resource.url).toEqual('http://test.com/tileset'); }); + it('multiple values for query parameters are allowed', function() { + var resource = new Resource('http://test.com/tileset/endpoint?a=1&a=2&b=3&a=4'); + expect(resource.queryParameters.a).toEqual(['1', '2', '4']); + expect(resource.queryParameters.b).toEqual('3'); + + expect(resource.url).toEqual('http://test.com/tileset/endpoint?a=1&a=2&a=4&b=3'); + }); + + it('multiple values for query parameters works with getDerivedResource without preserverQueryParameters', function() { + var resource = new Resource('http://test.com/tileset/endpoint?a=1&a=2&b=3&a=4'); + expect(resource.queryParameters.a).toEqual(['1', '2', '4']); + expect(resource.queryParameters.b).toEqual('3'); + + expect(resource.url).toEqual('http://test.com/tileset/endpoint?a=1&a=2&a=4&b=3'); + + var derived = resource.getDerivedResource({ + url: 'other_endpoint?a=5&b=6&a=7' + }); + + expect(derived.queryParameters.a).toEqual(['5', '7']); + expect(derived.queryParameters.b).toEqual('6'); + + expect(derived.url).toEqual('http://test.com/tileset/other_endpoint?a=5&a=7&b=6'); + }); + + it('multiple values for query parameters works with getDerivedResource with preserveQueryParameters', function() { + var resource = new Resource('http://test.com/tileset/endpoint?a=1&a=2&b=3&a=4'); + expect(resource.queryParameters.a).toEqual(['1', '2', '4']); + expect(resource.queryParameters.b).toEqual('3'); + + expect(resource.url).toEqual('http://test.com/tileset/endpoint?a=1&a=2&a=4&b=3'); + + var derived = resource.getDerivedResource({ + url: 'other_endpoint?a=5&b=6&a=7', + preserveQueryParameters: true + }); + + expect(derived.queryParameters.a).toEqual(['5', '7', '1', '2', '4']); + expect(derived.queryParameters.b).toEqual(['6', '3']); + + expect(derived.url).toEqual('http://test.com/tileset/other_endpoint?a=5&a=7&a=1&a=2&a=4&b=6&b=3'); + }); + it('templateValues are respected', function() { var resource = new Resource({ url: 'http://test.com/tileset/{foo}/{bar}', @@ -158,10 +201,10 @@ defineSuite([ }); expect(resource.getUrlComponent(false, false)).toEqual('http://test.com/tileset/tileset.json'); - expect(resource.getUrlComponent(true, false)).toEqual('http://test.com/tileset/tileset.json?key1=value1&key2=value2&foo=bar&key=value'); + expect(resource.getUrlComponent(true, false)).toEqual('http://test.com/tileset/tileset.json?key1=value1&key2=value2&key=value&foo=bar'); expect(resource.getUrlComponent(false, true)).toEqual(proxy.getURL('http://test.com/tileset/tileset.json')); - expect(resource.getUrlComponent(true, true)).toEqual(proxy.getURL('http://test.com/tileset/tileset.json?key1=value1&key2=value2&foo=bar&key=value')); - expect(resource.url).toEqual(proxy.getURL('http://test.com/tileset/tileset.json?key1=value1&key2=value2&foo=bar&key=value')); + expect(resource.getUrlComponent(true, true)).toEqual(proxy.getURL('http://test.com/tileset/tileset.json?key1=value1&key2=value2&key=value&foo=bar')); + expect(resource.url).toEqual(proxy.getURL('http://test.com/tileset/tileset.json?key1=value1&key2=value2&key=value&foo=bar')); expect(resource.queryParameters).toEqual({ foo: 'bar', key: 'value', @@ -248,7 +291,7 @@ defineSuite([ expect(resource.url).toEqual('http://test.com/terrain?x=1&y=2&z=0'); }); - it('addQueryParameters with useAsDefault set to true', function() { + it('setQueryParameters with useAsDefault set to true', function() { var resource = new Resource({ url: 'http://test.com/terrain', queryParameters: { @@ -262,7 +305,7 @@ defineSuite([ y: 2 }); - resource.addQueryParameters({ + resource.setQueryParameters({ x: 3, y: 4, z: 0 @@ -275,7 +318,7 @@ defineSuite([ }); }); - it('addQueryParameters with useAsDefault set to false', function() { + it('setQueryParameters with useAsDefault set to false', function() { var resource = new Resource({ url: 'http://test.com/terrain', queryParameters: { @@ -289,7 +332,7 @@ defineSuite([ y: 2 }); - resource.addQueryParameters({ + resource.setQueryParameters({ x: 3, y: 4, z: 0 @@ -302,7 +345,64 @@ defineSuite([ }); }); - it('addTemplateValues with useAsDefault set to true', function() { + it('appendQueryParameters works with non-arrays', function() { + var resource = new Resource({ + url: 'http://test.com/terrain', + queryParameters: { + x: 1, + y: 2 + } + }); + + expect(resource.queryParameters).toEqual({ + x: 1, + y: 2 + }); + + resource.appendQueryParameters({ + x: 3, + y: 4, + z: 0 + }); + + expect(resource.queryParameters).toEqual({ + x: [3, 1], + y: [4, 2], + z: 0 + }); + }); + + it('appendQueryParameters works with arrays/non-arrays', function() { + var resource = new Resource({ + url: 'http://test.com/terrain', + queryParameters: { + x: [1, 2], + y: 2, + z: [-1, -2] + } + }); + + expect(resource.queryParameters).toEqual({ + x: [1, 2], + y: 2, + z: [-1, -2] + }); + + resource.appendQueryParameters({ + x: 3, + y: [4, 5], + z: [-3, -4] + }); + + expect(resource.queryParameters).toEqual({ + x: [3, 1, 2], + y: [4, 5, 2], + z: [-3, -4, -1, -2] + }); + }); + + + it('setTemplateValues with useAsDefault set to true', function() { var resource = new Resource({ url: 'http://test.com/terrain/{z}/{x}/{y}.terrain', templateValues: { @@ -318,7 +418,7 @@ defineSuite([ map: 'my map' }); - resource.addTemplateValues({ + resource.setTemplateValues({ x: 3, y: 4, z: 0, @@ -334,7 +434,7 @@ defineSuite([ }); }); - it('addTemplateValues with useAsDefault set to false', function() { + it('setTemplateValues with useAsDefault set to false', function() { var resource = new Resource({ url: 'http://test.com/terrain/{z}/{x}/{y}.terrain', templateValues: { @@ -350,7 +450,7 @@ defineSuite([ map: 'my map' }); - resource.addTemplateValues({ + resource.setTemplateValues({ x: 3, y: 4, z: 0, @@ -518,6 +618,160 @@ defineSuite([ }); }); + it('put calls with correct method', function() { + var expectedUrl = 'http://test.com/endpoint'; + var expectedResponseType = 'json'; + var expectedData = { + stuff: 'myStuff' + }; + var expectedHeaders = { + 'X-My-Header': 'My-Value' + }; + var expectedResult = { + status: 'success' + }; + var expectedMimeType = 'application/test-data'; + var resource = new Resource({ + url: expectedUrl, + headers: expectedHeaders + }); + + spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + expect(url).toEqual(expectedUrl); + expect(responseType).toEqual(expectedResponseType); + expect(method).toEqual('PUT'); + expect(data).toEqual(expectedData); + expect(headers['X-My-Header']).toEqual('My-Value'); + expect(headers['X-My-Other-Header']).toEqual('My-Other-Value'); + expect(overrideMimeType).toBe(expectedMimeType); + deferred.resolve(expectedResult); + }); + + return resource.put(expectedData, { + responseType: expectedResponseType, + headers: { + 'X-My-Other-Header': 'My-Other-Value' + }, + overrideMimeType: expectedMimeType + }) + .then(function(result) { + expect(result).toEqual(expectedResult); + }); + }); + + it('static put calls with correct method', function() { + var expectedUrl = 'http://test.com/endpoint'; + var expectedResponseType = 'json'; + var expectedData = { + stuff: 'myStuff' + }; + var expectedHeaders = { + 'X-My-Header': 'My-Value' + }; + var expectedResult = { + status: 'success' + }; + var expectedMimeType = 'application/test-data'; + + spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + expect(url).toEqual(expectedUrl); + expect(responseType).toEqual(expectedResponseType); + expect(method).toEqual('PUT'); + expect(data).toEqual(expectedData); + expect(headers).toEqual(expectedHeaders); + expect(overrideMimeType).toBe(expectedMimeType); + deferred.resolve(expectedResult); + }); + + return Resource.put({ + url: expectedUrl, + data: expectedData, + responseType: expectedResponseType, + headers: expectedHeaders, + overrideMimeType: expectedMimeType + }) + .then(function(result) { + expect(result).toEqual(expectedResult); + }); + }); + + it('patch calls with correct method', function() { + var expectedUrl = 'http://test.com/endpoint'; + var expectedResponseType = 'json'; + var expectedData = { + stuff: 'myStuff' + }; + var expectedHeaders = { + 'X-My-Header': 'My-Value' + }; + var expectedResult = { + status: 'success' + }; + var expectedMimeType = 'application/test-data'; + var resource = new Resource({ + url: expectedUrl, + headers: expectedHeaders + }); + + spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + expect(url).toEqual(expectedUrl); + expect(responseType).toEqual(expectedResponseType); + expect(method).toEqual('PATCH'); + expect(data).toEqual(expectedData); + expect(headers['X-My-Header']).toEqual('My-Value'); + expect(headers['X-My-Other-Header']).toEqual('My-Other-Value'); + expect(overrideMimeType).toBe(expectedMimeType); + deferred.resolve(expectedResult); + }); + + return resource.patch(expectedData, { + responseType: expectedResponseType, + headers: { + 'X-My-Other-Header': 'My-Other-Value' + }, + overrideMimeType: expectedMimeType + }) + .then(function(result) { + expect(result).toEqual(expectedResult); + }); + }); + + it('static patch calls with correct method', function() { + var expectedUrl = 'http://test.com/endpoint'; + var expectedResponseType = 'json'; + var expectedData = { + stuff: 'myStuff' + }; + var expectedHeaders = { + 'X-My-Header': 'My-Value' + }; + var expectedResult = { + status: 'success' + }; + var expectedMimeType = 'application/test-data'; + + spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + expect(url).toEqual(expectedUrl); + expect(responseType).toEqual(expectedResponseType); + expect(method).toEqual('PATCH'); + expect(data).toEqual(expectedData); + expect(headers).toEqual(expectedHeaders); + expect(overrideMimeType).toBe(expectedMimeType); + deferred.resolve(expectedResult); + }); + + return Resource.patch({ + url: expectedUrl, + data: expectedData, + responseType: expectedResponseType, + headers: expectedHeaders, + overrideMimeType: expectedMimeType + }) + .then(function(result) { + expect(result).toEqual(expectedResult); + }); + }); + it('static fetchArrayBuffer calls correct method', function() { var url = 'http://test.com/data'; spyOn(Resource.prototype, 'fetchArrayBuffer').and.returnValue(when.resolve()); @@ -608,4 +862,157 @@ defineSuite([ expect(result).toEqual(expectedResult); }); }); + + it('static delete calls correct method', function() { + var url = 'http://test.com/data'; + spyOn(Resource.prototype, 'delete').and.returnValue(when.resolve()); + return Resource.delete(url) + .then(function() { + expect(Resource.prototype.delete).toHaveBeenCalled(); + }); + }); + + it('delete calls correct method', function() { + var expectedUrl = 'http://test.com/endpoint'; + var expectedResult = { + status: 'success' + }; + + spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + expect(url).toEqual(expectedUrl); + expect(method).toEqual('DELETE'); + deferred.resolve(expectedResult); + }); + + var resource = new Resource({url: expectedUrl}); + return resource.delete() + .then(function(result) { + expect(result).toEqual(expectedResult); + }); + }); + + it('static head calls correct method', function() { + var url = 'http://test.com/data'; + spyOn(Resource.prototype, 'head').and.returnValue(when.resolve({})); + return Resource.head(url) + .then(function() { + expect(Resource.prototype.head).toHaveBeenCalled(); + }); + }); + + it('head calls correct method', function() { + var expectedUrl = 'http://test.com/endpoint'; + var expectedResult = { + 'accept-ranges': 'bytes', + 'access-control-allow-headers' : 'Origin, X-Requested-With, Content-Type, Accept', + 'access-control-allow-origin' : '*', + 'cache-control': 'public, max-age=0', + 'connection' : 'keep-alive', + 'content-length' : '883', + 'content-type' : 'image/png', + 'date' : 'Tue, 13 Feb 2018 03:38:55 GMT', + 'etag' : 'W/"373-15e34d146a1"', + 'vary' : 'Accept-Encoding', + 'x-powered-vy' : 'Express' + }; + var headerString = ''; + for (var key in expectedResult) { + if (expectedResult.hasOwnProperty(key)) { + headerString += key + ': ' + expectedResult[key] + '\r\n'; + } + } + var fakeXHR = { + status: 200, + send : function() { + this.onload(); + }, + open : function() {}, + getAllResponseHeaders : function() { + return headerString; + } + }; + spyOn(window, 'XMLHttpRequest').and.returnValue(fakeXHR); + + spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + expect(url).toEqual(expectedUrl); + expect(method).toEqual('HEAD'); + Resource._DefaultImplementations.loadWithXhr(url, responseType, method, data, headers, deferred, overrideMimeType); + }); + + var resource = new Resource({url: expectedUrl}); + return resource.head() + .then(function(result) { + expect(result.date).toEqual(expectedResult.date); + expect(result['last-modified']).toEqual(expectedResult['last-modified']); + expect(result['x-powered-by']).toEqual(expectedResult['x-powered-by']); + expect(result.etag).toEqual(expectedResult.etag); + expect(result['content-type']).toEqual(expectedResult['content-type']); + expect(result['access-control-allow-origin']).toEqual(expectedResult['access-control-allow-origin']); + expect(result['cache-control']).toEqual(expectedResult['cache-control']); + expect(result['accept-ranges']).toEqual(expectedResult['accept-ranges']); + expect(result['access-control-allow-headers']).toEqual(expectedResult['access-control-allow-headers']); + expect(result['content-length']).toEqual(expectedResult['content-length']); + }); + }); + + it('static options calls correct method', function() { + var url = 'http://test.com/data'; + spyOn(Resource.prototype, 'options').and.returnValue(when.resolve({})); + return Resource.options(url) + .then(function() { + expect(Resource.prototype.options).toHaveBeenCalled(); + }); + }); + + it('options calls correct method', function() { + var expectedUrl = 'http://test.com/endpoint'; + var expectedResult = { + 'access-control-allow-headers' : 'Origin, X-Requested-With, Content-Type, Accept', + 'access-control-allow-methods' : 'GET, PUT, POST, DELETE, OPTIONS', + 'access-control-allow-origin' : '*', + 'connection' : 'keep-alive', + 'content-length' : '2', + 'content-type' : 'text/plain; charset=utf-8', + 'date' : 'Tue, 13 Feb 2018 03:38:55 GMT', + 'etag' : 'W/"2-nOO9QiTIwXgNtWtBJezz8kv3SLc"', + 'vary' : 'Accept-Encoding', + 'x-powered-vy' : 'Express' + }; + var headerString = ''; + for (var key in expectedResult) { + if (expectedResult.hasOwnProperty(key)) { + headerString += key + ': ' + expectedResult[key] + '\r\n'; + } + } + var fakeXHR = { + status: 200, + send : function() { + this.onload(); + }, + open : function() {}, + getAllResponseHeaders : function() { + return headerString; + } + }; + spyOn(window, 'XMLHttpRequest').and.returnValue(fakeXHR); + + spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + expect(url).toEqual(expectedUrl); + expect(method).toEqual('OPTIONS'); + Resource._DefaultImplementations.loadWithXhr(url, responseType, method, data, headers, deferred, overrideMimeType); + }); + + var resource = new Resource({url: expectedUrl}); + return resource.options() + .then(function(result) { + expect(result.date).toEqual(expectedResult.date); + expect(result['x-powered-by']).toEqual(expectedResult['x-powered-by']); + expect(result.etag).toEqual(expectedResult.etag); + expect(result['content-type']).toEqual(expectedResult['content-type']); + expect(result['access-control-allow-origin']).toEqual(expectedResult['access-control-allow-origin']); + expect(result['access-control-allow-methods']).toEqual(expectedResult['access-control-allow-methods']); + expect(result['access-control-allow-headers']).toEqual(expectedResult['access-control-allow-headers']); + expect(result['content-length']).toEqual(expectedResult['content-length']); + }); + }); }); diff --git a/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js b/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js index 693a2c27a511..705c57df5011 100644 --- a/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js +++ b/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js @@ -102,7 +102,7 @@ defineSuite([ }); it('rejects readyPromise on error', function() { - var url = 'invalid.localhost'; + var url = 'http://invalid.localhost'; var provider = new GoogleEarthEnterpriseMapsProvider({ url : url, channel : 1234 @@ -292,7 +292,7 @@ defineSuite([ }); it('raises error on invalid url', function() { - var url = 'invalid.localhost'; + var url = 'http://invalid.localhost'; var provider = new GoogleEarthEnterpriseMapsProvider({ url : url, channel : 1234