datajs
Version:
Microsoft data.js for OData
1,247 lines (1,038 loc) • 308 kB
JavaScript
// 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, "&").replace(/"/g, """).replace(/\</g, "<");
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