UNPKG

datajs

Version:

Microsoft data.js for OData

1,247 lines (1,038 loc) 308 kB
// Copyright (c) Microsoft. All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, // modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // datajs.js (function(global){ if (typeof window === "undefined") { window = this; } })(this); (function (window, undefined) { if (!window.datajs) { window.datajs = {}; } if (!window.OData) { window.OData = {}; } var datajs = window.datajs; var odata = window.OData; // Provides an enumeration of possible kinds of payloads. var PAYLOADTYPE_BATCH = "b"; var PAYLOADTYPE_COMPLEXTYPE = "c"; var PAYLOADTYPE_ENTRY = "entry"; // This is used when building the payload. var PAYLOADTYPE_FEEDORLINKS = "f"; var PAYLOADTYPE_PRIMITIVETYPE = "p"; var PAYLOADTYPE_SVCDOC = "s"; var PAYLOADTYPE_UNKNOWN = "u"; var PAYLOADTYPE_NONE = "n"; // Provides an enumeration of possible kinds of properties. var PROPERTYKIND_COMPLEX = "c"; var PROPERTYKIND_DEFERRED = "d"; var PROPERTYKIND_INLINE = "i"; var PROPERTYKIND_PRIMITIVE = "p"; var PROPERTYKIND_NONE = "n"; var assigned = function (value) { /// <summary>Checks whether the specified value is different from null and undefined.</summary> /// <param name="value" mayBeNull="true" optional="true">Value to check.</param> /// <returns type="Boolean">true if the value is assigned; false otherwise.</returns> return value !== null && value !== undefined; }; var contains = function (arr, item) { /// <summary>Checks whether the specified item is in the array.</summary> /// <param name="arr" type="Array" optional="false" mayBeNull="false">Array to check in.</param> /// <param name="item">Item to look for.</param> /// <returns type="Boolean">true if the item is contained, false otherwise.</returns> var i, len; for (i = 0, len = arr.length; i < len; i++) { if (arr[i] === item) { return true; } } return false; }; var defined = function (a, b) { /// <summary>Given two values, picks the first one that is not undefined.</summary> /// <param name="a">First value.</param> /// <param name="b">Second value.</param> /// <returns>a if it's a defined value; else b.</returns> return (a !== undefined) ? a : b; }; var delay = function (callback) { /// <summary>Delays the invocation of the specified function until execution unwinds.</summary> /// <param name="callback" type="Function">Callback function.</param> if (arguments.length === 1) { window.setTimeout(callback, 0); return; } var args = Array.prototype.slice.call(arguments, 1); window.setTimeout(function () { callback.apply(this, args); }, 0); }; var forEachSchema = function (metadata, callback) { /// <summary>Invokes a function once per schema in metadata.</summary> /// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param> /// <param name="callback" type="Function">Callback function to invoke once per schema.</param> /// <returns> /// The first truthy value to be returned from the callback; null or the last falsy value otherwise. /// </returns> if (!metadata) { return null; } if (isArray(metadata)) { var i, len, result; for (i = 0, len = metadata.length; i < len; i++) { result = forEachSchema(metadata[i], callback); if (result) { return result; } } return null; } else { if (metadata.dataServices) { return forEachSchema(metadata.dataServices.schema, callback); } return callback(metadata); } }; var isDateTimeOffset = function (value) { /// <summary>Checks whether a Date object is DateTimeOffset value</summary> /// <param name="value" type="Date" mayBeNull="false">Value to check.</param> /// <returns type="Boolean">true if the value is a DateTimeOffset, false otherwise.</returns> return (value.__edmType === "Edm.DateTimeOffset" || (!value.__edmType && value.__offset)); }; var formatMilliseconds = function (ms, ns) { /// <summary>Formats a millisecond and a nanosecond value into a single string.</summary> /// <param name="ms" type="Number" mayBeNull="false">Number of milliseconds to format.</param> /// <param name="ns" type="Number" mayBeNull="false">Number of nanoseconds to format.</param> /// <returns type="String">Formatted text.</returns> /// <remarks>If the value is already as string it's returned as-is.</remarks> // Avoid generating milliseconds if not necessary. if (ms === 0) { ms = ""; } else { ms = "." + formatNumberWidth(ms.toString(), 3); } if (ns > 0) { if (ms === "") { ms = ".000"; } ms += formatNumberWidth(ns.toString(), 4); } return ms; }; var formatDateTimeOffset = function (value) { /// <summary>Formats a DateTime or DateTimeOffset value a string.</summary> /// <param name="value" type="Date" mayBeNull="false">Value to format.</param> /// <returns type="String">Formatted text.</returns> /// <remarks>If the value is already as string it's returned as-is.</remarks> if (typeof value === "string") { return value; } var hasOffset = isDateTimeOffset(value); var offset = getCanonicalTimezone(value.__offset); if (hasOffset && offset !== "Z") { // We're about to change the value, so make a copy. value = new Date(value.valueOf()); var timezone = parseTimezone(offset); var hours = value.getUTCHours() + (timezone.d * timezone.h); var minutes = value.getMinutes() + (timezone.d * timezone.m); value.setUTCHours(hours, minutes); } else if (!hasOffset) { // Don't suffix a 'Z' for Edm.DateTime values. offset = ""; } var year = value.getUTCFullYear(); var month = value.getUTCMonth() + 1; var sign = ""; if (year <= 0) { year = -(year - 1); sign = "-"; } var ms = formatMilliseconds(value.getUTCMilliseconds(), value.__ns); return sign + formatNumberWidth(year, 4) + "-" + formatNumberWidth(month, 2) + "-" + formatNumberWidth(value.getUTCDate(), 2) + "T" + formatNumberWidth(value.getUTCHours(), 2) + ":" + formatNumberWidth(value.getUTCMinutes(), 2) + ":" + formatNumberWidth(value.getUTCSeconds(), 2) + ms + offset; }; var formatDuration = function (value) { /// <summary>Converts a duration to a string in xsd:duration format.</summary> /// <param name="value" type="Object">Object with ms and __edmType properties.</param> /// <returns type="String">String representation of the time object in xsd:duration format.</returns> var ms = value.ms; var sign = ""; if (ms < 0) { sign = "-"; ms = -ms; } var days = Math.floor(ms / 86400000); ms -= 86400000 * days; var hours = Math.floor(ms / 3600000); ms -= 3600000 * hours; var minutes = Math.floor(ms / 60000); ms -= 60000 * minutes; var seconds = Math.floor(ms / 1000); ms -= seconds * 1000; return sign + "P" + formatNumberWidth(days, 2) + "DT" + formatNumberWidth(hours, 2) + "H" + formatNumberWidth(minutes, 2) + "M" + formatNumberWidth(seconds, 2) + formatMilliseconds(ms, value.ns) + "S"; }; var formatNumberWidth = function (value, width, append) { /// <summary>Formats the specified value to the given width.</summary> /// <param name="value" type="Number">Number to format (non-negative).</param> /// <param name="width" type="Number">Minimum width for number.</param> /// <param name="append" type="Boolean">Flag indicating if the value is padded at the beginning (false) or at the end (true).</param> /// <returns type="String">Text representation.</returns> var result = value.toString(10); while (result.length < width) { if (append) { result += "0"; } else { result = "0" + result; } } return result; }; var getCanonicalTimezone = function (timezone) { /// <summary>Gets the canonical timezone representation.</summary> /// <param name="timezone" type="String">Timezone representation.</param> /// <returns type="String">An 'Z' string if the timezone is absent or 0; the timezone otherwise.</returns> return (!timezone || timezone === "Z" || timezone === "+00:00" || timezone === "-00:00") ? "Z" : timezone; }; var invokeRequest = function (request, success, error, handler, httpClient, context) { /// <summary>Sends a request containing OData payload to a server.</summary> /// <param name="request">Object that represents the request to be sent..</param> /// <param name="success">Callback for a successful read operation.</param> /// <param name="error">Callback for handling errors.</param> /// <param name="handler">Handler for data serialization.</param> /// <param name="httpClient">HTTP client layer.</param> /// <param name="context">Context used for processing the request</param> return httpClient.request(request, function (response) { try { if (response.headers) { normalizeHeaders(response.headers); } if (response.data === undefined) { handler.read(response, context); } } catch (err) { if (err.request === undefined) { err.request = request; } if (err.response === undefined) { err.response = response; } error(err); return; } success(response.data, response); }, error); }; var isArray = function (value) { /// <summary>Checks whether the specified value is an array object.</summary> /// <param name="value">Value to check.</param> /// <returns type="Boolean">true if the value is an array object; false otherwise.</returns> return Object.prototype.toString.call(value) === "[object Array]"; }; var isDate = function (value) { /// <summary>Checks whether the specified value is a Date object.</summary> /// <param name="value">Value to check.</param> /// <returns type="Boolean">true if the value is a Date object; false otherwise.</returns> return Object.prototype.toString.call(value) === "[object Date]"; }; var lookupProperty = function (properties, name) { /// <summary>Looks up a property by name.</summary> /// <param name="properties" type="Array" mayBeNull="true">Array of property objects as per EDM metadata.</param> /// <param name="name" type="String">Name to look for.</param> /// <returns type="Object">The property object; null if not found.</returns> if (properties) { var i, len; for (i = 0, len = properties.length; i < len; i++) { if (properties[i].name === name) { return properties[i]; } } } return null; }; var lookupTypeInMetadata = function (name, metadata, kind) { /// <summary>Looks up a type object by name.</summary> /// <param name="name" type="String">Name, possibly null or empty.</param> /// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param> /// <param name="kind" type="String">Kind of type to look for; one of 'entityType' or 'complexType'.</param> /// <returns>An type description if the name is found; null otherwise.</returns> return (name) ? forEachSchema(metadata, function (schema) { return lookupTypeInSchema(name, schema, kind); }) : null; }; var lookupComplexType = function (name, metadata) { /// <summary>Looks up a complex type object by name.</summary> /// <param name="name" type="String">Name, possibly null or empty.</param> /// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param> /// <returns>A complex type description if the name is found; null otherwise.</returns> return lookupTypeInMetadata(name, metadata, "complexType"); }; var lookupEntityType = function (name, metadata) { /// <summary>Looks up an entity type object by name.</summary> /// <param name="name" type="String">Name, possibly null or empty.</param> /// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param> /// <returns>An entity type description if the name is found; null otherwise.</returns> return lookupTypeInMetadata(name, metadata, "entityType"); }; //// Commented out - metadata is largely optional and we don't rely on it to this extent. //// var lookupEntityTypeForNavigation = function (navigationProperty, metadata) { //// /// <summary>Looks up the target entity type for a navigation property.</summary> //// /// <param name="navigationProperty" type="Object"></param> //// /// <param name="metadata" type="Object"></param> //// /// <returns type="Object">The entity type metadata for the specified property, null if not found.</returns> //// var rel = navigationProperty.relationship; //// var association = forEachSchema(metadata, function (schema) { //// // The name should be the namespace qualified name in 'ns'.'type' format. //// var nameOnly = removeNamespace(schema["namespace"], rel); //// var associations = schema.association; //// if (nameOnly && associations) { //// var i, len; //// for (i = 0, len = associations.length; i < len; i++) { //// if (associations[i].name === nameOnly) { //// return associations[i]; //// } //// } //// } //// }); //// var result = null; //// if (association) { //// var end = association.end[0]; //// if (end.role !== navigationProperty.toRole) { //// end = association.end[1]; //// // For metadata to be valid, end.role === navigationProperty.toRole now. //// } //// result = lookupEntityType(end.type, metadata); //// } //// return result; //// }; var removeNamespace = function (ns, fullName) { /// <summary>Given an expected namespace prefix, removes it from a full name.</summary> /// <param name="ns" type="String">Expected namespace.</param> /// <param name="fullName" type="String">Full name in 'ns'.'name' form.</param> /// <returns type="String">The local name, null if it isn't found in the expected namespace.</returns> if (fullName.indexOf(ns) === 0 && fullName.charAt(ns.length) === ".") { return fullName.substr(ns.length + 1); } return null; }; var lookupTypeInSchema = function (name, metadata, kind) { /// <summary>Looks up an entity type object by name.</summary> /// <param name="name" type="String">Name (assigned).</param> /// <param name="metadata">Metadata store; one of edmx, schema.</param> /// <param name="kind" type="String">Kind of type to look for; one of 'entityType' or 'complexType'.</param> /// <returns>An entity type description if the name is found; null otherwise.</returns> /// <remarks> /// metadata is considered an edmx object if it contains a dataServices object. /// </remarks> if (metadata) { // The name should be the namespace qualified name in 'ns'.'type' format. var nameOnly = removeNamespace(metadata["namespace"], name); var types = metadata[kind]; if (nameOnly && types) { var i, len; for (i = 0, len = types.length; i < len; i++) { if (types[i].name === nameOnly) { return types[i]; } } } } return null; }; var normalHeaders = { "accept": "Accept", "content-type": "Content-Type", "dataserviceversion": "DataServiceVersion", "maxdataserviceversion": "MaxDataServiceVersion" }; var normalizeHeaders = function (headers) { /// <summary>Normalizes headers so they can be found with consistent casing.</summary> /// <param name="headers" type="Object">Dictionary of name/value pairs.</param> for (var name in headers) { var lowerName = name.toLowerCase(); var normalName = normalHeaders[lowerName]; if (normalName && name !== normalName) { var val = headers[name]; delete headers[name]; headers[normalName] = val; } } }; var undefinedDefault = function (value, defaultValue) { /// <summary>Returns a default value in place of undefined.</summary> /// <param name="value" mayBeNull="true" optional="true">Value to check.</param> /// <param name="defaultValue">Value to return if value is undefined.</param> /// <returns>value if it's defined; defaultValue otherwise.</returns> /// <remarks> /// This should only be used for cases where falsy values are valid; /// otherwise the pattern should be 'x = (value) ? value : defaultValue;'. /// </remarks> return (value !== undefined) ? value : defaultValue; }; var parseInt10 = function (value) { /// <summary>Parses a value in base 10.</summary> /// <param name="value" type="String">String value to parse.</param> /// <returns type="Number">The parsed value, NaN if not a valid value.</returns> return parseInt(value, 10); }; // The captured indices for this expression are: // 0 - complete input // 1 - direction // 2,3,4 - years, months, days // 5,6,7,8 - hours, minutes, seconds, miliseconds var parseTimeRE = /^([+-])?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.(\d+))?S)?)?/; var parseDuration = function (duration) { /// <summary>Parses a string in xsd:duration format.</summary> /// <param name="duration" type="String">Duration value.</param> /// <remarks> /// This method will throw an exception if the input string has a year or a month component. /// </remarks> /// <returns type="Object">Object representing the time</returns> var parts = parseTimeRE.exec(duration); if (parts === null) { throw { message: "Invalid duration value." }; } var years = parts[2] || "0"; var months = parts[3] || "0"; var days = parseInt10(parts[4] || 0); var hours = parseInt10(parts[5] || 0); var minutes = parseInt10(parts[6] || 0); var seconds = parseFloat(parts[7] || 0); if (years !== "0" || months !== "0") { throw { message: "Unsupported duration value." }; } var ms = parts[8]; var ns = 0; if (!ms) { ms = 0; } else { if (ms.length > 7) { throw { message: "Cannot parse duration value to given precision." }; } ns = formatNumberWidth(ms.substring(3), 4, true); ms = formatNumberWidth(ms.substring(0, 3), 3, true); ms = parseInt10(ms); ns = parseInt10(ns); } ms += seconds * 1000 + minutes * 60000 + hours * 3600000 + days * 86400000; if (parts[1] === "-") { ms = -ms; } var result = { ms: ms, __edmType: "Edm.Time" }; if (ns) { result.ns = ns; } return result; }; var parseTimezone = function (timezone) { /// <summary>Parses a timezone description in (+|-)nn:nn format.</summary> /// <param name="timezone" type="String">Timezone offset.</param> /// <returns type="Object"> /// An object with a (d)irection property of 1 for + and -1 for -, /// offset (h)ours and offset (m)inutes. /// </returns> var direction = timezone.substring(0, 1); direction = (direction === "+") ? 1 : -1; var offsetHours = parseInt10(timezone.substring(1)); var offsetMinutes = parseInt10(timezone.substring(timezone.indexOf(":") + 1)); return { d: direction, h: offsetHours, m: offsetMinutes }; }; var payloadTypeOf = function (data) { /// <summary>Determines the kind of payload applicable for the specified value.</summary> /// <param name="data">Value to check.</param> /// <returns type="String">One of the values declared on the payloadType object.</returns> switch (typeof (data)) { case "object": if (!data) { return PAYLOADTYPE_NONE; } if (isArray(data) || isArray(data.results)) { return PAYLOADTYPE_FEEDORLINKS; } if (data.__metadata && data.__metadata.uri !== undefined) { return PAYLOADTYPE_ENTRY; } if (isArray(data.EntitySets)) { return PAYLOADTYPE_SVCDOC; } if (isArray(data.__batchRequests)) { return PAYLOADTYPE_BATCH; } if (isDate(data)) { return PAYLOADTYPE_PRIMITIVETYPE; } return PAYLOADTYPE_COMPLEXTYPE; case "string": case "number": case "boolean": return PAYLOADTYPE_PRIMITIVETYPE; } return PAYLOADTYPE_UNKNOWN; }; var prepareRequest = function (request, handler, context) { /// <summary>Prepares a request object so that it can be sent through the network.</summary> /// <param name="request">Object that represents the request to be sent.</param> /// <param name="handler">Handler for data serialization</param> /// <param name="context">Context used for preparing the request</param> // Default to GET if no method has been specified. if (!request.method) { request.method = "GET"; } if (!request.headers) { request.headers = {}; } else { normalizeHeaders(request.headers); } if (request.headers.Accept === undefined) { request.headers.Accept = handler.accept; } if (assigned(request.data) && request.body === undefined) { handler.write(request, context); } if (!assigned(request.headers.MaxDataServiceVersion)) { request.headers.MaxDataServiceVersion = handler.maxDataServiceVersion || "1.0"; } }; var propertyKindOf = function (value) { /// <summary>Determines the kind of property for the specified value.</summary> /// <param name="value">Value to check.</param> /// <returns type="String">One of the values declared on the propertyKind object.</returns> switch (payloadTypeOf(value)) { case PAYLOADTYPE_COMPLEXTYPE: if (value.__deferred && value.__deferred.uri) { return PROPERTYKIND_DEFERRED; } return PROPERTYKIND_COMPLEX; case PAYLOADTYPE_FEEDORLINKS: case PAYLOADTYPE_ENTRY: return PROPERTYKIND_INLINE; case PAYLOADTYPE_PRIMITIVETYPE: return PROPERTYKIND_PRIMITIVE; } return PROPERTYKIND_NONE; }; var throwErrorCallback = function (error) { /// <summary>Default error handler.</summary> /// <param name="error" type="Object">Error to handle.</param> throw error; }; var trimString = function (str) { /// <summary>Removes leading and trailing whitespaces from a string.</summary> /// <param name="str" type="String" optional="false" mayBeNull="false">String to trim</param> /// <returns type="String">The string with no leading or trailing whitespace.</returns> if (str.trim) { return str.trim(); } return str.replace(/^\s+|\s+$/g, ''); }; // Regular expression that splits a uri into its components: // 0 - is the matched string. // 1 - is the scheme. // 2 - is the authority. // 3 - is the path. // 4 - is the query. // 5 - is the fragment. var uriRegEx = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#:]+)?(\?[^#]*)?(#.*)?/; var uriPartNames = ["scheme", "authority", "path", "query", "fragment"]; var getURIInfo = function (uri) { /// <summary>Gets information about the components of the specified URI.</summary> /// <param name="uri" type="String">URI to get information from.</param> /// <returns type="Object"> /// An object with an isAbsolute flag and part names (scheme, authority, etc.) if available. /// </returns> var result = { isAbsolute: false }; if (uri) { var matches = uriRegEx.exec(uri); if (matches) { var i, len; for (i = 0, len = uriPartNames.length; i < len; i++) { if (matches[i + 1]) { result[uriPartNames[i]] = matches[i + 1]; } } } if (result.scheme) { result.isAbsolute = true; } } return result; }; var getURIFromInfo = function (uriInfo) { /// <summary>Builds a URI string from its components.</summary> /// <param name="uriInfo" type="Object"> An object with uri parts (scheme, authority, etc.).</param> /// <returns type="String">URI string.</returns> return "".concat( uriInfo.scheme || "", uriInfo.authority || "", uriInfo.path || "", uriInfo.query || "", uriInfo.fragment || ""); }; // Regular expression that splits a uri authority into its subcomponents: // 0 - is the matched string. // 1 - is the userinfo subcomponent. // 2 - is the host subcomponent. // 3 - is the port component. var uriAuthorityRegEx = /^\/{0,2}(?:([^@]*)@)?([^:]+)(?::{1}(\d+))?/; // Regular expression that matches percentage enconded octects (i.e %20 or %3A); var pctEncodingRegEx = /%[0-9A-F]{2}/ig; var normalizeURICase = function (uri) { /// <summary>Normalizes the casing of a URI.</summary> /// <param name="uri" type="String">URI to normalize, absolute or relative.</param> /// <returns type="String">The URI normalized to lower case.</returns> var uriInfo = getURIInfo(uri); var scheme = uriInfo.scheme; var authority = uriInfo.authority; if (scheme) { uriInfo.scheme = scheme.toLowerCase(); if (authority) { var matches = uriAuthorityRegEx.exec(authority); if (matches) { uriInfo.authority = "//" + (matches[1] ? matches[1] + "@" : "") + (matches[2].toLowerCase()) + (matches[3] ? ":" + matches[3] : ""); } } } uri = getURIFromInfo(uriInfo); return uri.replace(pctEncodingRegEx, function (str) { return str.toLowerCase(); }); }; var normalizeURI = function (uri, base) { /// <summary>Normalizes a possibly relative URI with a base URI.</summary> /// <param name="uri" type="String">URI to normalize, absolute or relative.</param> /// <param name="base" type="String" mayBeNull="true">Base URI to compose with.</param> /// <returns type="String">The composed URI if relative; the original one if absolute.</returns> if (!base) { return uri; } var uriInfo = getURIInfo(uri); if (uriInfo.isAbsolute) { return uri; } var baseInfo = getURIInfo(base); var normInfo = {}; var path; if (uriInfo.authority) { normInfo.authority = uriInfo.authority; path = uriInfo.path; normInfo.query = uriInfo.query; } else { if (!uriInfo.path) { path = baseInfo.path; normInfo.query = uriInfo.query || baseInfo.query; } else { if (uriInfo.path.charAt(0) === '/') { path = uriInfo.path; } else { path = mergeUriPathWithBase(uriInfo.path, baseInfo.path); } normInfo.query = uriInfo.query; } normInfo.authority = baseInfo.authority; } normInfo.path = removeDotsFromPath(path); normInfo.scheme = baseInfo.scheme; normInfo.fragment = uriInfo.fragment; return getURIFromInfo(normInfo); }; var mergeUriPathWithBase = function (uriPath, basePath) { /// <summary>Merges the path of a relative URI and a base URI.</summary> /// <param name="uriPath" type="String>Relative URI path.</param> /// <param name="basePath" type="String">Base URI path.</param> /// <returns type="String">A string with the merged path.</returns> var path = "/"; var end; if (basePath) { end = basePath.lastIndexOf("/"); path = basePath.substring(0, end); if (path.charAt(path.length - 1) !== "/") { path = path + "/"; } } return path + uriPath; }; var removeDotsFromPath = function (path) { /// <summary>Removes the special folders . and .. from a URI's path.</summary> /// <param name="path" type="string">URI path component.</param> /// <returns type="String">Path without any . and .. folders.</returns> var result = ""; var segment = ""; var end; while (path) { if (path.indexOf("..") === 0 || path.indexOf(".") === 0) { path = path.replace(/^\.\.?\/?/g, ""); } else if (path.indexOf("/..") === 0) { path = path.replace(/^\/\..\/?/g, "/"); end = result.lastIndexOf("/"); if (end === -1) { result = ""; } else { result = result.substring(0, end); } } else if (path.indexOf("/.") === 0) { path = path.replace(/^\/\.\/?/g, "/"); } else { segment = path; end = path.indexOf("/", 1); if (end !== -1) { segment = path.substring(0, end); } result = result + segment; path = path.replace(segment, ""); } } return result; }; var ticks = 0; var canUseJSONP = function (request) { /// <summary> /// Checks whether the specified request can be satisfied with a JSONP request. /// </summary> /// <param name="request">Request object to check.</param> /// <returns type="Boolean">true if the request can be satisfied; false otherwise.</returns> // Requests that 'degrade' without changing their meaning by going through JSONP // are considered usable. // // We allow data to come in a different format, as the servers SHOULD honor the Accept // request but may in practice return content with a different MIME type. if (request.method && request.method !== "GET") { return false; } return true; }; var createIFrame = function (url) { /// <summary>Creates an IFRAME tag for loading the JSONP script</summary> /// <param name="url" type="String">The source URL of the script</param> /// <returns type="HTMLElement">The IFRAME tag</returns> var iframe = window.document.createElement("IFRAME"); iframe.style.display = "none"; var attributeEncodedUrl = url.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/\</g, "&lt;"); var html = "<html><head><script type=\"text/javascript\" src=\"" + attributeEncodedUrl + "\"><\/script><\/head><body><\/body><\/html>"; var body = window.document.getElementsByTagName("BODY")[0]; body.appendChild(iframe); writeHtmlToIFrame(iframe, html); return iframe; }; var createXmlHttpRequest = function () { /// <summary>Creates a XmlHttpRequest object.</summary> /// <returns type="XmlHttpRequest">XmlHttpRequest object.</returns> if (window.XMLHttpRequest) { return new window.XMLHttpRequest(); } var exception; if (window.ActiveXObject) { try { return new window.ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (_) { try { return new window.ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e) { exception = e; } } } else { exception = { message: "XMLHttpRequest not supported" }; } throw exception; }; var isAbsoluteUrl = function (url) { /// <summary>Checks whether the specified URL is an absolute URL.</summary> /// <param name="url" type="String">URL to check.</param> /// <returns type="Boolean">true if the url is an absolute URL; false otherwise.</returns> return url.indexOf("http://") === 0 || url.indexOf("https://") === 0 || url.indexOf("file://") === 0; }; var isLocalUrl = function (url) { /// <summary>Checks whether the specified URL is local to the current context.</summary> /// <param name="url" type="String">URL to check.</param> /// <returns type="Boolean">true if the url is a local URL; false otherwise.</returns> if (!isAbsoluteUrl(url)) { return true; } // URL-embedded username and password will not be recognized as same-origin URLs. var location = window.location; var locationDomain = location.protocol + "//" + location.host + "/"; return (url.indexOf(locationDomain) === 0); }; var removeCallback = function (name, tick) { /// <summary>Removes a callback used for a JSONP request.</summary> /// <param name="name" type="String">Function name to remove.</param> /// <param name="tick" type="Number">Tick count used on the callback.</param> try { delete window[name]; } catch (err) { window[name] = undefined; if (tick === ticks - 1) { ticks -= 1; } } }; var removeIFrame = function (iframe) { /// <summary>Removes an iframe.</summary> /// <param name="iframe" type="Object">The iframe to remove.</param> /// <returns type="Object">Null value to be assigned to iframe reference.</returns> if (iframe) { writeHtmlToIFrame(iframe, ""); iframe.parentNode.removeChild(iframe); } return null; }; var readResponseHeaders = function (xhr, headers) { /// <summary>Reads response headers into array.</summary> /// <param name="xhr" type="XMLHttpRequest">HTTP request with response available.</param> /// <param name="headers" type="Array">Target array to fill with name/value pairs.</param> var responseHeaders = xhr.getAllResponseHeaders().split(/\r?\n/); var i, len; for (i = 0, len = responseHeaders.length; i < len; i++) { if (responseHeaders[i]) { var header = responseHeaders[i].split(": "); headers[header[0]] = header[1]; } } }; var writeHtmlToIFrame = function (iframe, html) { /// <summary>Writes HTML to an IFRAME document.</summary> /// <param name="iframe" type="HTMLElement">The IFRAME element to write to.</param> /// <param name="html" type="String">The HTML to write.</param> var frameDocument = (iframe.contentWindow) ? iframe.contentWindow.document : iframe.contentDocument.document; frameDocument.open(); frameDocument.write(html); frameDocument.close(); }; odata.defaultHttpClient = { callbackParameterName: "$callback", formatQueryString: "$format=json", enableJsonpCallback: false, request: function (request, success, error) { /// <summary>Performs a network request.</summary> /// <param name="request" type="Object">Request description.</request> /// <param name="success" type="Function">Success callback with the response object.</param> /// <param name="error" type="Function">Error callback with an error object.</param> /// <returns type="Object">Object with an 'abort' method for the operation.</returns> var result = {}; var xhr = null; var done = false; var iframe; result.abort = function () { iframe = removeIFrame(iframe); if (done) { return; } done = true; if (xhr) { xhr.abort(); xhr = null; } error({ message: "Request aborted" }); }; var handleTimeout = function () { iframe = removeIFrame(iframe); if (!done) { done = true; xhr = null; error({ message: "Request timed out" }); } }; var name; var url = request.requestUri; var enableJsonpCallback = defined(request.enableJsonpCallback, this.enableJsonpCallback); var callbackParameterName = defined(request.callbackParameterName, this.callbackParameterName); var formatQueryString = defined(request.formatQueryString, this.formatQueryString); if (!enableJsonpCallback || isLocalUrl(url)) { xhr = createXmlHttpRequest(); xhr.onreadystatechange = function () { if (done || xhr === null || xhr.readyState !== 4) { return; } // Workaround for XHR behavior on IE. var statusText = xhr.statusText; var statusCode = xhr.status; if (statusCode === 1223) { statusCode = 204; statusText = "No Content"; } var headers = []; readResponseHeaders(xhr, headers); var response = { requestUri: url, statusCode: statusCode, statusText: statusText, headers: headers, body: xhr.responseText }; done = true; xhr = null; if (statusCode >= 200 && statusCode <= 299) { success(response); } else { error({ message: "HTTP request failed", request: request, response: response }); } }; xhr.open(request.method || "GET", url, true, request.user, request.password); // Set the name/value pairs. if (request.headers) { for (name in request.headers) { xhr.setRequestHeader(name, request.headers[name]); } } // Set the timeout if available. if (request.timeoutMS) { xhr.timeout = request.timeoutMS; xhr.ontimeout = handleTimeout; } xhr.send(request.body); } else { if (!canUseJSONP(request)) { throw { message: "Request is not local and cannot be done through JSONP." }; } var tick = ticks; ticks += 1; var tickText = tick.toString(); var succeeded = false; var timeoutId; name = "handleJSONP_" + tickText; window[name] = function (data) { iframe = removeIFrame(iframe); if (!done) { succeeded = true; window.clearTimeout(timeoutId); removeCallback(name, tick); // Workaround for IE8 and below where trying to access data.constructor after the IFRAME has been removed // throws an "unknown exception" if (window.ActiveXObject && !window.DOMParser) { data = window.JSON.parse(window.JSON.stringify(data)); } // Call the success callback in the context of the parent window, instead of the IFRAME delay(success, { body: data, statusCode: 200, headers: { "Content-Type": "application/json"} }); } }; // Default to two minutes before timing out, 1000 ms * 60 * 2 = 120000. var timeoutMS = (request.timeoutMS) ? request.timeoutMS : 120000; timeoutId = window.setTimeout(handleTimeout, timeoutMS); var queryStringParams = callbackParameterName + "=parent." + name; if (this.formatQueryString) { queryStringParams += "&" + formatQueryString; } var qIndex = url.indexOf("?"); if (qIndex === -1) { url = url + "?" + queryStringParams; } else if (qIndex === url.length - 1) { url = url + queryStringParams; } else { url = url + "&" + queryStringParams; } iframe = createIFrame(url); } return result; } }; var MAX_DATA_SERVICE_VERSION = "2.0"; var contentType = function (str) { /// <summary>Parses a string into an object with media type and properties.</summary> /// <param name="str" type="String">String with media type to parse.</param> /// <returns>null if the string is empty; an object with 'mediaType' and a 'properties' dictionary otherwise.</returns> if (!str) { return null; } var contentTypeParts = str.split(";"); var properties = {}; var i, len; for (i = 1, len = contentTypeParts.length; i < len; i++) { var contentTypeParams = contentTypeParts[i].split("="); properties[trimString(contentTypeParams[0])] = contentTypeParams[1]; } return { mediaType: trimString(contentTypeParts[0]), properties: properties }; }; var contentTypeToString = function (contentType) { /// <summary>Serializes an object with media type and properties dictionary into a string.</summary> /// <param name="contentType">Object with media type and properties dictionary to serialize.</param> /// <returns>String representation of the media type object; undefined if contentType is null or undefined.</returns> if (!contentType) { return undefined; } var result = contentType.mediaType; var property; for (property in contentType.properties) { result += ";" + property + "=" + contentType.properties[property]; } return result; }; var createReadWriteContext = function (contentType, dataServiceVersion, context, handler) { /// <summary>Creates an object that is going to be used as the context for the handler's parser and serializer.</summary> /// <param name="contentType">Object with media type and properties dictionary.</param> /// <param name="dataServiceVersion" type="String">String indicating the version of the protocol to use.</param> /// <param name="context">Operation context.</param> /// <param name="handler">Handler object that is processing a resquest or response.</param> /// <returns>Context object.</returns> return { contentType: contentType, dataServiceVersion: dataServiceVersion, metadata: context ? context.metadata : null, context: context, handler: handler }; }; var fixRequestHeader = function (request, name, value) { /// <summary>Sets a request header's value. If the header has already a value other than undefined, null or empty string, then this method does nothing.</summary> /// <param name="request">Request object on which the header will be set.</param> /// <param name="name" type="String">Header name.</param> /// <param name="value" type="String">Header value.</param> if (!request) { return; } var headers = request.headers; if (!headers[name]) { headers[name] = value; } }; var fixDataServiceVersion = function (context, version) { /// <summary>Sets the dataServiceVersion component of the context.</summary> /// <param name="context">Context object used for serialization.</param> /// <param name="version" type="String">Version value.</param> /// <remarks> /// If the component has already a value other than undefined, null or /// empty string, then this method does nothing. /// </remarks> if (!context.dataServiceVersion) { context.dataServiceVersion = version; } }; var getRequestOrResponseHeader = function (requestOrResponse, name) { /// <summary>Gets the value of a request or response header.</summary> /// <param name="requestOrResponse">Object representing a request or a response.</param> /// <param name="name" type="String">Name of the header to retrieve.</param> /// <returns type="String">String value of the header; undefined if the header cannot be found.</returns> var headers = requestOrResponse.headers; return (headers && headers[name]) || undefined; }; var getContentType = function (requestOrResponse) { /// <summary>Gets the value of the Content-Type header from a request or response.</summary> /// <param name="requestOrResponse">Object representing a request or a response.</param> /// <returns type="Object">Object with 'mediaType' and a 'properties' dictionary; null in case that the header is not found or doesn't have a value.</returns> return contentType(getRequestOrResponseHeader(requestOrResponse, "Content-Type")); }; var versionRE = /^\s?(\d+\.\d+);?.*$/; var getDataServiceVersion = function (requestOrResponse) { /// <summary>Gets the value of the DataServiceVersion header from a request or response.</summary> /// <param name="requestOrResponse">Object representing a request or a response.</param> /// <returns type="String">Data service version; undefined if the header cannot be found.</returns> var value = getRequestOrResponseHeader(requestOrResponse, "D