blob: 5040efc9c7a9c8161e78f9882049a6d2fad7a742 [file] [log] [blame]
// Copyright (c) Microsoft Open Technologies, Inc. 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.
// odatajs.js
(function (window, undefined) {
var datajs = window.odatajs || {};
var odata = window.OData || {};
// AMD support
if (typeof define === 'function' && define.amd) {
define('datajs', datajs);
define('OData', odata);
} else {
window.odatajs = datajs;
window.OData = odata;
}
odatajs.version = {
major: 1,
minor: 1,
build: 1
};
var activeXObject = function (progId) {
/// <summary>Creates a new ActiveXObject from the given progId.</summary>
/// <param name="progId" type="String" mayBeNull="false" optional="false">
/// ProgId string of the desired ActiveXObject.
/// </param>
/// <remarks>
/// This function throws whatever exception might occur during the creation
/// of the ActiveXObject.
/// </remarks>
/// <returns type="Object">
/// The ActiveXObject instance. Null if ActiveX is not supported by the
/// browser.
/// </returns>
if (window.ActiveXObject) {
return new window.ActiveXObject(progId);
}
return null;
};
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 extend = function (target, values) {
/// <summary>Extends the target with the specified values.</summary>
/// <param name="target" type="Object">Object to add properties to.</param>
/// <param name="values" type="Object">Object with properties to add into target.</param>
/// <returns type="Object">The target object.</returns>
for (var name in values) {
target[name] = values[name];
}
return target;
};
var find = function (arr, callback) {
/// <summary>Returns the first item in the array that makes the callback function true.</summary>
/// <param name="arr" type="Array" optional="false" mayBeNull="true">Array to check in.</param>
/// <param name="callback" type="Function">Callback function to invoke once per item in the array.</param>
/// <returns>The first item that makes the callback return true; null otherwise or if the array is null.</returns>
if (arr) {
var i, len;
for (i = 0, len = arr.length; i < len; i++) {
if (callback(arr[i])) {
return arr[i];
}
}
}
return null;
};
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 isObject = function (value) {
/// <summary>Tests whether a value is an object.</summary>
/// <param name="value">Value to test.</param>
/// <remarks>
/// Per javascript rules, null and array values are objects and will cause this function to return true.
/// </remarks>
/// <returns type="Boolean">True is the value is an object; false otherwise.</returns>
return typeof value === "object";
};
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);
};
var renameProperty = function (obj, oldName, newName) {
/// <summary>Renames a property in an object.</summary>
/// <param name="obj" type="Object">Object in which the property will be renamed.</param>
/// <param name="oldName" type="String">Name of the property that will be renamed.</param>
/// <param name="newName" type="String">New name of the property.</param>
/// <remarks>
/// This function will not do anything if the object doesn't own a property with the specified old name.
/// </remarks>
if (obj.hasOwnProperty(oldName)) {
obj[newName] = obj[oldName];
delete obj[oldName];
}
};
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, '');
};
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;
};
// 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 convertByteArrayToHexString = function (str) {
var arr = [];
if (window.atob === undefined) {
arr = decodeBase64(str);
} else {
var binaryStr = window.atob(str);
for (var i = 0; i < binaryStr.length; i++) {
arr.push(binaryStr.charCodeAt(i));
}
}
var hexValue = "";
var hexValues = "0123456789ABCDEF";
for (var j = 0; j < arr.length; j++) {
var t = arr[j];
hexValue += hexValues[t >> 4];
hexValue += hexValues[t & 0x0F];
}
return hexValue;
};
var decodeBase64 = function (str) {
var binaryString = "";
for (var i = 0; i < str.length; i++) {
var base65IndexValue = getBase64IndexValue(str[i]);
var binaryValue = "";
if (base65IndexValue !== null) {
binaryValue = base65IndexValue.toString(2);
binaryString += addBase64Padding(binaryValue);
}
}
var byteArray = [];
var numberOfBytes = parseInt(binaryString.length / 8, 10);
for (i = 0; i < numberOfBytes; i++) {
var intValue = parseInt(binaryString.substring(i * 8, (i + 1) * 8), 2);
byteArray.push(intValue);
}
return byteArray;
};
var getBase64IndexValue = function (character) {
var asciiCode = character.charCodeAt(0);
var asciiOfA = 65;
var differenceBetweenZanda = 6;
if (asciiCode >= 65 && asciiCode <= 90) { // between "A" and "Z" inclusive
return asciiCode - asciiOfA;
} else if (asciiCode >= 97 && asciiCode <= 122) { // between 'a' and 'z' inclusive
return asciiCode - asciiOfA - differenceBetweenZanda;
} else if (asciiCode >= 48 && asciiCode <= 57) { // between '0' and '9' inclusive
return asciiCode + 4;
} else if (character == "+") {
return 62;
} else if (character == "/") {
return 63;
} else {
return null;
}
};
var addBase64Padding = function (binaryString) {
while (binaryString.length < 6) {
binaryString = "0" + binaryString;
}
return binaryString;
};
// URI prefixes to generate smaller code.
var http = "http://";
var w3org = http + "www.w3.org/"; // http://www.w3.org/
var xhtmlNS = w3org + "1999/xhtml"; // http://www.w3.org/1999/xhtml
var xmlnsNS = w3org + "2000/xmlns/"; // http://www.w3.org/2000/xmlns/
var xmlNS = w3org + "XML/1998/namespace"; // http://www.w3.org/XML/1998/namespace
var mozillaParserErroNS = http + "www.mozilla.org/newlayout/xml/parsererror.xml";
var hasLeadingOrTrailingWhitespace = function (text) {
/// <summary>Checks whether the specified string has leading or trailing spaces.</summary>
/// <param name="text" type="String">String to check.</param>
/// <returns type="Boolean">true if text has any leading or trailing whitespace; false otherwise.</returns>
var re = /(^\s)|(\s$)/;
return re.test(text);
};
var isWhitespace = function (text) {
/// <summary>Determines whether the specified text is empty or whitespace.</summary>
/// <param name="text" type="String">Value to inspect.</param>
/// <returns type="Boolean">true if the text value is empty or all whitespace; false otherwise.</returns>
var ws = /^\s*$/;
return text === null || ws.test(text);
};
var isWhitespacePreserveContext = function (domElement) {
/// <summary>Determines whether the specified element has xml:space='preserve' applied.</summary>
/// <param name="domElement">Element to inspect.</param>
/// <returns type="Boolean">Whether xml:space='preserve' is in effect.</returns>
while (domElement !== null && domElement.nodeType === 1) {
var val = xmlAttributeValue(domElement, "space", xmlNS);
if (val === "preserve") {
return true;
} else if (val === "default") {
break;
} else {
domElement = domElement.parentNode;
}
}
return false;
};
var isXmlNSDeclaration = function (domAttribute) {
/// <summary>Determines whether the attribute is a XML namespace declaration.</summary>
/// <param name="domAttribute">Element to inspect.</param>
/// <returns type="Boolean">
/// True if the attribute is a namespace declaration (its name is 'xmlns' or starts with 'xmlns:'; false otherwise.
/// </returns>
var nodeName = domAttribute.nodeName;
return nodeName == "xmlns" || nodeName.indexOf("xmlns:") === 0;
};
var safeSetProperty = function (obj, name, value) {
/// <summary>Safely set as property in an object by invoking obj.setProperty.</summary>
/// <param name="obj">Object that exposes a setProperty method.</param>
/// <param name="name" type="String" mayBeNull="false">Property name.</param>
/// <param name="value">Property value.</param>
try {
obj.setProperty(name, value);
} catch (_) { }
};
var msXmlDom3 = function () {
/// <summary>Creates an configures new MSXML 3.0 ActiveX object.</summary>
/// <remakrs>
/// This function throws any exception that occurs during the creation
/// of the MSXML 3.0 ActiveX object.
/// <returns type="Object">New MSXML 3.0 ActiveX object.</returns>
var msxml3 = activeXObject("Msxml2.DOMDocument.3.0");
if (msxml3) {
safeSetProperty(msxml3, "ProhibitDTD", true);
safeSetProperty(msxml3, "MaxElementDepth", 256);
safeSetProperty(msxml3, "AllowDocumentFunction", false);
safeSetProperty(msxml3, "AllowXsltScript", false);
}
return msxml3;
};
var msXmlDom = function () {
/// <summary>Creates an configures new MSXML 6.0 or MSXML 3.0 ActiveX object.</summary>
/// <remakrs>
/// This function will try to create a new MSXML 6.0 ActiveX object. If it fails then
/// it will fallback to create a new MSXML 3.0 ActiveX object. Any exception that
/// happens during the creation of the MSXML 6.0 will be handled by the function while
/// the ones that happend during the creation of the MSXML 3.0 will be thrown.
/// <returns type="Object">New MSXML 3.0 ActiveX object.</returns>
try {
var msxml = activeXObject("Msxml2.DOMDocument.6.0");
if (msxml) {
msxml.async = true;
}
return msxml;
} catch (_) {
return msXmlDom3();
}
};
var msXmlParse = function (text) {
/// <summary>Parses an XML string using the MSXML DOM.</summary>
/// <remakrs>
/// This function throws any exception that occurs during the creation
/// of the MSXML ActiveX object. It also will throw an exception
/// in case of a parsing error.
/// <returns type="Object">New MSXML DOMDocument node representing the parsed XML string.</returns>
var dom = msXmlDom();
if (!dom) {
return null;
}
dom.loadXML(text);
var parseError = dom.parseError;
if (parseError.errorCode !== 0) {
xmlThrowParserError(parseError.reason, parseError.srcText, text);
}
return dom;
};
var xmlThrowParserError = function (exceptionOrReason, srcText, errorXmlText) {
/// <summary>Throws a new exception containing XML parsing error information.</summary>
/// <param name="exceptionOrReason">
/// String indicatin the reason of the parsing failure or
/// Object detailing the parsing error.
/// </param>
/// <param name="srcText" type="String">
/// String indicating the part of the XML string that caused the parsing error.
/// </param>
/// <param name="errorXmlText" type="String">XML string for wich the parsing failed.</param>
if (typeof exceptionOrReason === "string") {
exceptionOrReason = { message: exceptionOrReason };
}
throw extend(exceptionOrReason, { srcText: srcText || "", errorXmlText: errorXmlText || "" });
};
var xmlParse = function (text) {
/// <summary>Returns an XML DOM document from the specified text.</summary>
/// <param name="text" type="String">Document text.</param>
/// <returns>XML DOM document.</returns>
/// <remarks>This function will throw an exception in case of a parse error.</remarks>
var domParser = window.DOMParser && new window.DOMParser();
var dom;
if (!domParser) {
dom = msXmlParse(text);
if (!dom) {
xmlThrowParserError("XML DOM parser not supported");
}
return dom;
}
try {
dom = domParser.parseFromString(text, "text/xml");
} catch (e) {
xmlThrowParserError(e, "", text);
}
var element = dom.documentElement;
var nsURI = element.namespaceURI;
var localName = xmlLocalName(element);
// Firefox reports errors by returing the DOM for an xml document describing the problem.
if (localName === "parsererror" && nsURI === mozillaParserErroNS) {
var srcTextElement = xmlFirstChildElement(element, mozillaParserErroNS, "sourcetext");
var srcText = srcTextElement ? xmlNodeValue(srcTextElement) : "";
xmlThrowParserError(xmlInnerText(element) || "", srcText, text);
}
// Chrome (and maybe other webkit based browsers) report errors by injecting a header with an error message.
// The error may be localized, so instead we simply check for a header as the
// top element or descendant child of the document.
if (localName === "h3" && nsURI === xhtmlNS || xmlFirstDescendantElement(element, xhtmlNS, "h3")) {
var reason = "";
var siblings = [];
var cursor = element.firstChild;
while (cursor) {
if (cursor.nodeType === 1) {
reason += xmlInnerText(cursor) || "";
}
siblings.push(cursor.nextSibling);
cursor = cursor.firstChild || siblings.shift();
}
reason += xmlInnerText(element) || "";
xmlThrowParserError(reason, "", text);
}
return dom;
};
var xmlQualifiedName = function (prefix, name) {
/// <summary>Builds a XML qualified name string in the form of "prefix:name".</summary>
/// <param name="prefix" type="String" maybeNull="true">Prefix string.</param>
/// <param name="name" type="String">Name string to qualify with the prefix.</param>
/// <returns type="String">Qualified name.</returns>
return prefix ? prefix + ":" + name : name;
};
var xmlAppendText = function (domNode, textNode) {
/// <summary>Appends a text node into the specified DOM element node.</summary>
/// <param name="domNode">DOM node for the element.</param>
/// <param name="text" type="String" mayBeNull="false">Text to append as a child of element.</param>
if (hasLeadingOrTrailingWhitespace(textNode.data)) {
var attr = xmlAttributeNode(domNode, xmlNS, "space");
if (!attr) {
attr = xmlNewAttribute(domNode.ownerDocument, xmlNS, xmlQualifiedName("xml", "space"));
xmlAppendChild(domNode, attr);
}
attr.value = "preserve";
}
domNode.appendChild(textNode);
return domNode;
};
var xmlAttributes = function (element, onAttributeCallback) {
/// <summary>Iterates through the XML element's attributes and invokes the callback function for each one.</summary>
/// <param name="element">Wrapped element to iterate over.</param>
/// <param name="onAttributeCallback" type="Function">Callback function to invoke with wrapped attribute nodes.</param>
var attributes = element.attributes;
var i, len;
for (i = 0, len = attributes.length; i < len; i++) {
onAttributeCallback(attributes.item(i));
}
};
var xmlAttributeValue = function (domNode, localName, nsURI) {
/// <summary>Returns the value of a DOM element's attribute.</summary>
/// <param name="domNode">DOM node for the owning element.</param>
/// <param name="localName" type="String">Local name of the attribute.</param>
/// <param name="nsURI" type="String">Namespace URI of the attribute.</param>
/// <returns type="String" maybeNull="true">The attribute value, null if not found.</returns>
var attribute = xmlAttributeNode(domNode, localName, nsURI);
return attribute ? xmlNodeValue(attribute) : null;
};
var xmlAttributeNode = function (domNode, localName, nsURI) {
/// <summary>Gets an attribute node from a DOM element.</summary>
/// <param name="domNode">DOM node for the owning element.</param>
/// <param name="localName" type="String">Local name of the attribute.</param>
/// <param name="nsURI" type="String">Namespace URI of the attribute.</param>
/// <returns>The attribute node, null if not found.</returns>
var attributes = domNode.attributes;
if (attributes.getNamedItemNS) {
return attributes.getNamedItemNS(nsURI || null, localName);
}
return attributes.getQualifiedItem(localName, nsURI) || null;
};
var xmlBaseURI = function (domNode, baseURI) {
/// <summary>Gets the value of the xml:base attribute on the specified element.</summary>
/// <param name="domNode">Element to get xml:base attribute value from.</param>
/// <param name="baseURI" mayBeNull="true" optional="true">Base URI used to normalize the value of the xml:base attribute.</param>
/// <returns type="String">Value of the xml:base attribute if found; the baseURI or null otherwise.</returns>
var base = xmlAttributeNode(domNode, "base", xmlNS);
return (base ? normalizeURI(base.value, baseURI) : baseURI) || null;
};
var xmlChildElements = function (domNode, onElementCallback) {
/// <summary>Iterates through the XML element's child DOM elements and invokes the callback function for each one.</summary>
/// <param name="element">DOM Node containing the DOM elements to iterate over.</param>
/// <param name="onElementCallback" type="Function">Callback function to invoke for each child DOM element.</param>
xmlTraverse(domNode, /*recursive*/false, function (child) {
if (child.nodeType === 1) {
onElementCallback(child);
}
// continue traversing.
return true;
});
};
var xmlFindElementByPath = function (root, namespaceURI, path) {
/// <summary>Gets the descendant element under root that corresponds to the specified path and namespace URI.</summary>
/// <param name="root">DOM element node from which to get the descendant element.</param>
/// <param name="namespaceURI" type="String">The namespace URI of the element to match.</param>
/// <param name="path" type="String">Path to the desired descendant element.</param>
/// <returns>The element specified by path and namespace URI.</returns>
/// <remarks>
/// All the elements in the path are matched against namespaceURI.
/// The function will stop searching on the first element that doesn't match the namespace and the path.
/// </remarks>
var parts = path.split("/");
var i, len;
for (i = 0, len = parts.length; i < len; i++) {
root = root && xmlFirstChildElement(root, namespaceURI, parts[i]);
}
return root || null;
};
var xmlFindNodeByPath = function (root, namespaceURI, path) {
/// <summary>Gets the DOM element or DOM attribute node under root that corresponds to the specified path and namespace URI.</summary>
/// <param name="root">DOM element node from which to get the descendant node.</param>
/// <param name="namespaceURI" type="String">The namespace URI of the node to match.</param>
/// <param name="path" type="String">Path to the desired descendant node.</param>
/// <returns>The node specified by path and namespace URI.</returns>
/// <remarks>
/// This function will traverse the path and match each node associated to a path segement against the namespace URI.
/// The traversal stops when the whole path has been exahusted or a node that doesn't belogong the specified namespace is encountered.
///
/// The last segment of the path may be decorated with a starting @ character to indicate that the desired node is a DOM attribute.
/// </remarks>
var lastSegmentStart = path.lastIndexOf("/");
var nodePath = path.substring(lastSegmentStart + 1);
var parentPath = path.substring(0, lastSegmentStart);
var node = parentPath ? xmlFindElementByPath(root, namespaceURI, parentPath) : root;
if (node) {
if (nodePath.charAt(0) === "@") {
return xmlAttributeNode(node, nodePath.substring(1), namespaceURI);
}
return xmlFirstChildElement(node, namespaceURI, nodePath);
}
return null;
};
var xmlFirstChildElement = function (domNode, namespaceURI, localName) {
/// <summary>Returns the first child DOM element under the specified DOM node that matches the specified namespace URI and local name.</summary>
/// <param name="domNode">DOM node from which the child DOM element is going to be retrieved.</param>
/// <param name="namespaceURI" type="String" optional="true">The namespace URI of the element to match.</param>
/// <param name="localName" type="String" optional="true">Name of the element to match.</param>
/// <returns>The node's first child DOM element that matches the specified namespace URI and local name; null otherwise.</returns>
return xmlFirstElementMaybeRecursive(domNode, namespaceURI, localName, /*recursive*/false);
};
var xmlFirstDescendantElement = function (domNode, namespaceURI, localName) {
/// <summary>Returns the first descendant DOM element under the specified DOM node that matches the specified namespace URI and local name.</summary>
/// <param name="domNode">DOM node from which the descendant DOM element is going to be retrieved.</param>
/// <param name="namespaceURI" type="String" optional="true">The namespace URI of the element to match.</param>
/// <param name="localName" type="String" optional="true">Name of the element to match.</param>
/// <returns>The node's first descendant DOM element that matches the specified namespace URI and local name; null otherwise.</returns>
if (domNode.getElementsByTagNameNS) {
var result = domNode.getElementsByTagNameNS(namespaceURI, localName);
return result.length > 0 ? result[0] : null;
}
return xmlFirstElementMaybeRecursive(domNode, namespaceURI, localName, /*recursive*/true);
};
var xmlFirstElementMaybeRecursive = function (domNode, namespaceURI, localName, recursive) {
/// <summary>Returns the first descendant DOM element under the specified DOM node that matches the specified namespace URI and local name.</summary>
/// <param name="domNode">DOM node from which the descendant DOM element is going to be retrieved.</param>
/// <param name="namespaceURI" type="String" optional="true">The namespace URI of the element to match.</param>
/// <param name="localName" type="String" optional="true">Name of the element to match.</param>
/// <param name="recursive" type="Boolean">
/// True if the search should include all the descendants of the DOM node.
/// False if the search should be scoped only to the direct children of the DOM node.
/// </param>
/// <returns>The node's first descendant DOM element that matches the specified namespace URI and local name; null otherwise.</returns>
var firstElement = null;
xmlTraverse(domNode, recursive, function (child) {
if (child.nodeType === 1) {
var isExpectedNamespace = !namespaceURI || xmlNamespaceURI(child) === namespaceURI;
var isExpectedNodeName = !localName || xmlLocalName(child) === localName;
if (isExpectedNamespace && isExpectedNodeName) {
firstElement = child;
}
}
return firstElement === null;
});
return firstElement;
};
var xmlInnerText = function (xmlElement) {
/// <summary>Gets the concatenated value of all immediate child text and CDATA nodes for the specified element.</summary>
/// <param name="domElement">Element to get values for.</param>
/// <returns type="String">Text for all direct children.</returns>
var result = null;
var root = (xmlElement.nodeType === 9 && xmlElement.documentElement) ? xmlElement.documentElement : xmlElement;
var whitespaceAlreadyRemoved = root.ownerDocument.preserveWhiteSpace === false;
var whitespacePreserveContext;
xmlTraverse(root, false, function (child) {
if (child.nodeType === 3 || child.nodeType === 4) {
// isElementContentWhitespace indicates that this is 'ignorable whitespace',
// but it's not defined by all browsers, and does not honor xml:space='preserve'
// in some implementations.
//
// If we can't tell either way, we walk up the tree to figure out whether
// xml:space is set to preserve; otherwise we discard pure-whitespace.
//
// For example <a> <b>1</b></a>. The space between <a> and <b> is usually 'ignorable'.
var text = xmlNodeValue(child);
var shouldInclude = whitespaceAlreadyRemoved || !isWhitespace(text);
if (!shouldInclude) {
// Walk up the tree to figure out whether we are in xml:space='preserve' context
// for the cursor (needs to happen only once).
if (whitespacePreserveContext === undefined) {
whitespacePreserveContext = isWhitespacePreserveContext(root);
}
shouldInclude = whitespacePreserveContext;
}
if (shouldInclude) {
if (!result) {
result = text;
} else {
result += text;
}
}
}
// Continue traversing?
return true;
});
return result;
};
var xmlLocalName = function (domNode) {
/// <summary>Returns the localName of a XML node.</summary>
/// <param name="domNode">DOM node to get the value from.</param>
/// <returns type="String">localName of domNode.</returns>
return domNode.localName || domNode.baseName;
};
var xmlNamespaceURI = function (domNode) {
/// <summary>Returns the namespace URI of a XML node.</summary>
/// <param name="node">DOM node to get the value from.</param>
/// <returns type="String">Namespace URI of domNode.</returns>
return domNode.namespaceURI || null;
};
var xmlNodeValue = function (domNode) {
/// <summary>Returns the value or the inner text of a XML node.</summary>
/// <param name="node">DOM node to get the value from.</param>
/// <returns>Value of the domNode or the inner text if domNode represents a DOM element node.</returns>
if (domNode.nodeType === 1) {
return xmlInnerText(domNode);
}
return domNode.nodeValue;
};
var xmlTraverse = function (domNode, recursive, onChildCallback) {
/// <summary>Walks through the descendants of the domNode and invokes a callback for each node.</summary>
/// <param name="domNode">DOM node whose descendants are going to be traversed.</param>
/// <param name="recursive" type="Boolean">
/// True if the traversal should include all the descenants of the DOM node.
/// False if the traversal should be scoped only to the direct children of the DOM node.
/// </param>
/// <returns type="String">Namespace URI of node.</returns>
var subtrees = [];
var child = domNode.firstChild;
var proceed = true;
while (child && proceed) {
proceed = onChildCallback(child);
if (proceed) {
if (recursive && child.firstChild) {
subtrees.push(child.firstChild);
}
child = child.nextSibling || subtrees.shift();
}
}
};
var xmlSiblingElement = function (domNode, namespaceURI, localName) {
/// <summary>Returns the next sibling DOM element of the specified DOM node.</summary>
/// <param name="domNode">DOM node from which the next sibling is going to be retrieved.</param>
/// <param name="namespaceURI" type="String" optional="true">The namespace URI of the element to match.</param>
/// <param name="localName" type="String" optional="true">Name of the element to match.</param>
/// <returns>The node's next sibling DOM element, null if there is none.</returns>
var sibling = domNode.nextSibling;
while (sibling) {
if (sibling.nodeType === 1) {
var isExpectedNamespace = !namespaceURI || xmlNamespaceURI(sibling) === namespaceURI;
var isExpectedNodeName = !localName || xmlLocalName(sibling) === localName;
if (isExpectedNamespace && isExpectedNodeName) {
return sibling;
}
}
sibling = sibling.nextSibling;
}
return null;
};
var xmlDom = function () {
/// <summary>Creates a new empty DOM document node.</summary>
/// <returns>New DOM document node.</returns>
/// <remarks>
/// This function will first try to create a native DOM document using
/// the browsers createDocument function. If the browser doesn't
/// support this but supports ActiveXObject, then an attempt to create
/// an MSXML 6.0 DOM will be made. If this attempt fails too, then an attempt
/// for creating an MXSML 3.0 DOM will be made. If this last attemp fails or
/// the browser doesn't support ActiveXObject then an exception will be thrown.
/// </remarks>
var implementation = window.document.implementation;
return (implementation && implementation.createDocument) ?
implementation.createDocument(null, null, null) :
msXmlDom();
};
var xmlAppendChildren = function (parent, children) {
/// <summary>Appends a collection of child nodes or string values to a parent DOM node.</summary>
/// <param name="parent">DOM node to which the children will be appended.</param>
/// <param name="children" type="Array">Array containing DOM nodes or string values that will be appended to the parent.</param>
/// <returns>The parent with the appended children or string values.</returns>
/// <remarks>
/// If a value in the children collection is a string, then a new DOM text node is going to be created
/// for it and then appended to the parent.
/// </remarks>
if (!isArray(children)) {
return xmlAppendChild(parent, children);
}
var i, len;
for (i = 0, len = children.length; i < len; i++) {
children[i] && xmlAppendChild(parent, children[i]);
}
return parent;
};
var xmlAppendChild = function (parent, child) {
/// <summary>Appends a child node or a string value to a parent DOM node.</summary>
/// <param name="parent">DOM node to which the child will be appended.</param>
/// <param name="child">Child DOM node or string value to append to the parent.</param>
/// <returns>The parent with the appended child or string value.</returns>
/// <remarks>
/// If child is a string value, then a new DOM text node is going to be created
/// for it and then appended to the parent.
/// </remarks>
if (child) {
if (typeof child === "string") {
return xmlAppendText(parent, xmlNewText(parent.ownerDocument, child));
}
if (child.nodeType === 2) {
parent.setAttributeNodeNS ? parent.setAttributeNodeNS(child) : parent.setAttributeNode(child);
} else {
parent.appendChild(child);
}
}
return parent;
};
var xmlNewAttribute = function (dom, namespaceURI, qualifiedName, value) {
/// <summary>Creates a new DOM attribute node.</summary>
/// <param name="dom">DOM document used to create the attribute.</param>
/// <param name="prefix" type="String">Namespace prefix.</param>
/// <param name="namespaceURI" type="String">Namespace URI.</param>
/// <returns>DOM attribute node for the namespace declaration.</returns>
var attribute =
dom.createAttributeNS && dom.createAttributeNS(namespaceURI, qualifiedName) ||
dom.createNode(2, qualifiedName, namespaceURI || undefined);
attribute.value = value || "";
return attribute;
};
var xmlNewElement = function (dom, nampespaceURI, qualifiedName, children) {
/// <summary>Creates a new DOM element node.</summary>
/// <param name="dom">DOM document used to create the DOM element.</param>
/// <param name="namespaceURI" type="String">Namespace URI of the new DOM element.</param>
/// <param name="qualifiedName" type="String">Qualified name in the form of "prefix:name" of the new DOM element.</param>
/// <param name="children" type="Array" optional="true">
/// Collection of child DOM nodes or string values that are going to be appended to the new DOM element.
/// </param>
/// <returns>New DOM element.</returns>
/// <remarks>
/// If a value in the children collection is a string, then a new DOM text node is going to be created
/// for it and then appended to the new DOM element.
/// </remarks>
var element =
dom.createElementNS && dom.createElementNS(nampespaceURI, qualifiedName) ||
dom.createNode(1, qualifiedName, nampespaceURI || undefined);
return xmlAppendChildren(element, children || []);
};
var xmlNewNSDeclaration = function (dom, namespaceURI, prefix) {
/// <summary>Creates a namespace declaration attribute.</summary>
/// <param name="dom">DOM document used to create the attribute.</param>
/// <param name="namespaceURI" type="String">Namespace URI.</param>
/// <param name="prefix" type="String">Namespace prefix.</param>
/// <returns>DOM attribute node for the namespace declaration.</returns>
return xmlNewAttribute(dom, xmlnsNS, xmlQualifiedName("xmlns", prefix), namespaceURI);
};
var xmlNewFragment = function (dom, text) {
/// <summary>Creates a new DOM document fragment node for the specified xml text.</summary>
/// <param name="dom">DOM document from which the fragment node is going to be created.</param>
/// <param name="text" type="String" mayBeNull="false">XML text to be represented by the XmlFragment.</param>
/// <returns>New DOM document fragment object.</returns>
var value = "<c>" + text + "</c>";
var tempDom = xmlParse(value);
var tempRoot = tempDom.documentElement;
var imported = ("importNode" in dom) ? dom.importNode(tempRoot, true) : tempRoot;
var fragment = dom.createDocumentFragment();
var importedChild = imported.firstChild;
while (importedChild) {
fragment.appendChild(importedChild);
importedChild = importedChild.nextSibling;
}
return fragment;
};
var xmlNewText = function (dom, text) {
/// <summary>Creates new DOM text node.</summary>
/// <param name="dom">DOM document used to create the text node.</param>
/// <param name="text" type="String">Text value for the DOM text node.</param>
/// <returns>DOM text node.</returns>
return dom.createTextNode(text);
};
var xmlNewNodeByPath = function (dom, root, namespaceURI, prefix, path) {
/// <summary>Creates a new DOM element or DOM attribute node as specified by path and appends it to the DOM tree pointed by root.</summary>
/// <param name="dom">DOM document used to create the new node.</param>
/// <param name="root">DOM element node used as root of the subtree on which the new nodes are going to be created.</param>
/// <param name="namespaceURI" type="String">Namespace URI of the new DOM element or attribute.</param>
/// <param name="namespacePrefix" type="String">Prefix used to qualify the name of the new DOM element or attribute.</param>
/// <param name="Path" type="String">Path string describing the location of the new DOM element or attribute from the root element.</param>
/// <returns>DOM element or attribute node for the last segment of the path.</returns>
/// <remarks>
/// This function will traverse the path and will create a new DOM element with the specified namespace URI and prefix
/// for each segment that doesn't have a matching element under root.
///
/// The last segment of the path may be decorated with a starting @ character. In this case a new DOM attribute node
/// will be created.
/// </remarks>
var name = "";
var parts = path.split("/");
var xmlFindNode = xmlFirstChildElement;
var xmlNewNode = xmlNewElement;
var xmlNode = root;
var i, len;
for (i = 0, len = parts.length; i < len; i++) {
name = parts[i];
if (name.charAt(0) === "@") {
name = name.substring(1);
xmlFindNode = xmlAttributeNode;
xmlNewNode = xmlNewAttribute;
}
var childNode = xmlFindNode(xmlNode, namespaceURI, name);
if (!childNode) {
childNode = xmlNewNode(dom, namespaceURI, xmlQualifiedName(prefix, name));
xmlAppendChild(xmlNode, childNode);
}
xmlNode = childNode;
}
return xmlNode;
};
var xmlSerialize = function (domNode) {
/// <summary>
/// Returns the text representation of the document to which the specified node belongs.
/// </summary>
/// <param name="root">Wrapped element in the document to serialize.</param>
/// <returns type="String">Serialized document.</returns>
var xmlSerializer = window.XMLSerializer;
if (xmlSerializer) {
var serializer = new xmlSerializer();
return serializer.serializeToString(domNode);
}
if (domNode.xml) {
return domNode.xml;
}
throw { message: "XML serialization unsupported" };
};
var xmlSerializeDescendants = function (domNode) {
/// <summary>Returns the XML representation of the all the descendants of the node.</summary>
/// <param name="domNode" optional="false" mayBeNull="false">Node to serialize.</param>
/// <returns type="String">The XML representation of all the descendants of the node.</returns>
var children = domNode.childNodes;
var i, len = children.length;
if (len === 0) {
return "";
}
// Some implementations of the XMLSerializer don't deal very well with fragments that
// don't have a DOMElement as their first child. The work around is to wrap all the
// nodes in a dummy root node named "c", serialize it and then just extract the text between
// the <c> and the </c> substrings.
var dom = domNode.ownerDocument;
var fragment = dom.createDocumentFragment();
var fragmentRoot = dom.createElement("c");
fragment.appendChild(fragmentRoot);
// Move the children to the fragment tree.
for (i = 0; i < len; i++) {
fragmentRoot.appendChild(children[i]);
}
var xml = xmlSerialize(fragment);
xml = xml.substr(3, xml.length - 7);
// Move the children back to the original dom tree.
for (i = 0; i < len; i++) {
domNode.appendChild(fragmentRoot.childNodes[i]);
}
return xml;
};
var xmlSerializeNode = function (domNode) {
/// <summary>Returns the XML representation of the node and all its descendants.</summary>
/// <param name="domNode" optional="false" mayBeNull="false">Node to serialize.</param>
/// <returns type="String">The XML representation of the node and all its descendants.</returns>
var xml = domNode.xml;
if (xml !== undefined) {
return xml;
}
if (window.XMLSerializer) {
var serializer = new window.XMLSerializer();
return serializer.serializeToString(domNode);
}
throw { message: "XML serialization unsupported" };
};
var forwardCall = function (thisValue, name, returnValue) {
/// <summary>Creates a new function to forward a call.</summary>
/// <param name="thisValue" type="Object">Value to use as the 'this' object.</param>
/// <param name="name" type="String">Name of function to forward to.</param>
/// <param name="returnValue" type="Object">Return value for the forward call (helps keep identity when chaining calls).</param>
/// <returns type="Function">A new function that will forward a call.</returns>
return function () {
thisValue[name].apply(thisValue, arguments);
return returnValue;
};
};
var DjsDeferred = function () {
/// <summary>Initializes a new DjsDeferred object.</summary>
/// <remarks>
/// Compability Note A - Ordering of callbacks through chained 'then' invocations
///
/// The Wiki entry at http://wiki.commonjs.org/wiki/Promises/A
/// implies that .then() returns a distinct object.
////
/// For compatibility with http://api.jquery.com/category/deferred-object/
/// we return this same object. This affects ordering, as
/// the jQuery version will fire callbacks in registration
/// order regardless of whether they occur on the result
/// or the original object.
///
/// Compability Note B - Fulfillment value
///
/// The Wiki entry at http://wiki.commonjs.org/wiki/Promises/A
/// implies that the result of a success callback is the
/// fulfillment value of the object and is received by
/// other success callbacks that are chained.
///
/// For compatibility with http://api.jquery.com/category/deferred-object/
/// we disregard this value instead.
/// </remarks>
this._arguments = undefined;
this._done = undefined;
this._fail = undefined;
this._resolved = false;
this._rejected = false;
};
DjsDeferred.prototype = {
then: function (fulfilledHandler, errorHandler /*, progressHandler */) {
/// <summary>Adds success and error callbacks for this deferred object.</summary>
/// <param name="fulfilledHandler" type="Function" mayBeNull="true" optional="true">Success callback.</param>
/// <param name="errorHandler" type="Function" mayBeNull="true" optional="true">Error callback.</param>
/// <remarks>See Compatibility Note A.</remarks>
if (fulfilledHandler) {
if (!this._done) {
this._done = [fulfilledHandler];
} else {
this._done.push(fulfilledHandler);
}
}
if (errorHandler) {
if (!this._fail) {
this._fail = [errorHandler];
} else {
this._fail.push(errorHandler);
}
}
//// See Compatibility Note A in the DjsDeferred constructor.
//// if (!this._next) {
//// this._next = createDeferred();
//// }
//// return this._next.promise();
if (this._resolved) {
this.resolve.apply(this, this._arguments);
} else if (this._rejected) {
this.reject.apply(this, this._arguments);
}
return this;
},
resolve: function (/* args */) {
/// <summary>Invokes success callbacks for this deferred object.</summary>
/// <remarks>All arguments are forwarded to success callbacks.</remarks>
if (this._done) {
var i, len;
for (i = 0, len = this._done.length; i < len; i++) {
//// See Compability Note B - Fulfillment value.
//// var nextValue =
this._done[i].apply(null, arguments);
}
//// See Compatibility Note A in the DjsDeferred constructor.
//// this._next.resolve(nextValue);
//// delete this._next;
this._done = undefined;
this._resolved = false;
this._arguments = undefined;
} else {
this._resolved = true;
this._arguments = arguments;
}
},
reject: function (/* args */) {
/// <summary>Invokes error callbacks for this deferred object.</summary>
/// <remarks>All arguments are forwarded to error callbacks.</remarks>
if (this._fail) {
var i, len;
for (i = 0, len = this._fail.length; i < len; i++) {
this._fail[i].apply(null, arguments);
}
this._fail = undefined;
this._rejected = false;
this._arguments = undefined;
} else {
this._rejected = true;
this._arguments = arguments;
}
},
promise: function () {
/// <summary>Returns a version of this object that has only the read-only methods available.</summary>
/// <returns>An object with only the promise object.</returns>
var result = {};
result.then = forwardCall(this, "then", result);
return result;
}
};
var createDeferred = function () {
/// <summary>Creates a deferred object.</summary>
/// <returns type="DjsDeferred">
/// A new deferred object. If jQuery is installed, then a jQuery
/// Deferred object is returned, which provides a superset of features.
/// </returns>
if (window.jQuery && window.jQuery.Deferred) {
return new window.jQuery.Deferred();
} else {
return new DjsDeferred();
}
};
var dataItemTypeName = function (value, metadata) {
/// <summary>Gets the type name of a data item value that belongs to a feed, an entry, a complex type property, or a collection property.</summary>
/// <param name="value">Value of the data item from which the type name is going to be retrieved.</param>
/// <param name="metadata" type="object" optional="true">Object containing metadata about the data tiem.</param>
/// <remarks>
/// This function will first try to get the type name from the data item's value itself if it is an object with a __metadata property; otherwise
/// it will try to recover it from the metadata. If both attempts fail, it will return null.
/// </remarks>
/// <returns type="String">Data item type name; null if the type name cannot be found within the value or the metadata</returns>
var valueTypeName = ((value && value.__metadata) || {}).type;
return valueTypeName || (metadata ? metadata.type : null);
};
var EDM = "Edm.";
var EDM_BINARY = EDM + "Binary";
var EDM_BOOLEAN = EDM + "Boolean";
var EDM_BYTE = EDM + "Byte";
var EDM_DATETIME = EDM + "DateTime";
var EDM_DATETIMEOFFSET = EDM + "DateTimeOffset";
var EDM_DECIMAL = EDM + "Decimal";
var EDM_DOUBLE = EDM + "Double";
var EDM_GUID = EDM + "Guid";
var EDM_INT16 = EDM + "Int16";
var EDM_INT32 = EDM + "Int32";
var EDM_INT64 = EDM + "Int64";
var EDM_SBYTE = EDM + "SByte";
var EDM_SINGLE = EDM + "Single";
var EDM_STRING = EDM + "String";
var EDM_TIME = EDM + "Time";
var EDM_GEOGRAPHY = EDM + "Geography";
var EDM_GEOGRAPHY_POINT = EDM_GEOGRAPHY + "Point";
var EDM_GEOGRAPHY_LINESTRING = EDM_GEOGRAPHY + "LineString";
var EDM_GEOGRAPHY_POLYGON = EDM_GEOGRAPHY + "Polygon";
var EDM_GEOGRAPHY_COLLECTION = EDM_GEOGRAPHY + "Collection";
var EDM_GEOGRAPHY_MULTIPOLYGON = EDM_GEOGRAPHY + "MultiPolygon";
var EDM_GEOGRAPHY_MULTILINESTRING = EDM_GEOGRAPHY + "MultiLineString";
var EDM_GEOGRAPHY_MULTIPOINT = EDM_GEOGRAPHY + "MultiPoint";
var EDM_GEOMETRY = EDM + "Geometry";
var EDM_GEOMETRY_POINT = EDM_GEOMETRY + "Point";
var EDM_GEOMETRY_LINESTRING = EDM_GEOMETRY + "LineString";
var EDM_GEOMETRY_POLYGON = EDM_GEOMETRY + "Polygon";
var EDM_GEOMETRY_COLLECTION = EDM_GEOMETRY + "Collection";
var EDM_GEOMETRY_MULTIPOLYGON = EDM_GEOMETRY + "MultiPolygon";
var EDM_GEOMETRY_MULTILINESTRING = EDM_GEOMETRY + "MultiLineString";
var EDM_GEOMETRY_MULTIPOINT = EDM_GEOMETRY + "MultiPoint";
var GEOJSON_POINT = "Point";
var GEOJSON_LINESTRING = "LineString";
var GEOJSON_POLYGON = "Polygon";
var GEOJSON_MULTIPOINT = "MultiPoint";
var GEOJSON_MULTILINESTRING = "MultiLineString";
var GEOJSON_MULTIPOLYGON = "MultiPolygon";
var GEOJSON_GEOMETRYCOLLECTION = "GeometryCollection";
var primitiveEdmTypes = [
EDM_STRING,
EDM_INT32,
EDM_INT64,
EDM_BOOLEAN,
EDM_DOUBLE,
EDM_SINGLE,
EDM_DATETIME,
EDM_DATETIMEOFFSET,
EDM_TIME,
EDM_DECIMAL,
EDM_GUID,
EDM_BYTE,
EDM_INT16,
EDM_SBYTE,
EDM_BINARY
];
var geometryEdmTypes = [
EDM_GEOMETRY,
EDM_GEOMETRY_POINT,
EDM_GEOMETRY_LINESTRING,
EDM_GEOMETRY_POLYGON,
EDM_GEOMETRY_COLLECTION,
EDM_GEOMETRY_MULTIPOLYGON,
EDM_GEOMETRY_MULTILINESTRING,
EDM_GEOMETRY_MULTIPOINT
];
var geographyEdmTypes = [
EDM_GEOGRAPHY,
EDM_GEOGRAPHY_POINT,
EDM_GEOGRAPHY_LINESTRING,
EDM_GEOGRAPHY_POLYGON,
EDM_GEOGRAPHY_COLLECTION,
EDM_GEOGRAPHY_MULTIPOLYGON,
EDM_GEOGRAPHY_MULTILINESTRING,
EDM_GEOGRAPHY_MULTIPOINT
];
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 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.getUTCMinutes() + (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 getCollectionType = function (typeName) {
/// <summary>Gets the type of a collection type name.</summary>
/// <param name="typeName" type="String">Type name of the collection.</param>
/// <returns type="String">Type of the collection; null if the type name is not a collection type.</returns>
if (typeof typeName === "string") {
var end = typeName.indexOf(")", 10);
if (typeName.indexOf("Collection(") === 0 && end > 0) {
return typeName.substring(11, end);
}
}
return null;
};
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 && response.statusCode !== 204) {
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 isBatch = function (value) {
/// <summary>Tests whether a value is a batch object in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <returns type="Boolean">True is the value is a batch object; false otherwise.</returns>
return isComplex(value) && isArray(value.__batchRequests);
};
// Regular expression used for testing and parsing for a collection type.
var collectionTypeRE = /Collection\((.*)\)/;
var isCollection = function (value, typeName) {
/// <summary>Tests whether a value is a collection value in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <param name="typeName" type="Sting">Type name of the value. This is used to disambiguate from a collection property value.</param>
/// <returns type="Boolean">True is the value is a feed value; false otherwise.</returns>
var colData = value && value.results || value;
return !!colData &&
(isCollectionType(typeName)) ||
(!typeName && isArray(colData) && !isComplex(colData[0]));
};
var isCollectionType = function (typeName) {
/// <summary>Checks whether the specified type name is a collection type.</summary>
/// <param name="typeName" type="String">Name of type to check.</param>
/// <returns type="Boolean">True if the type is the name of a collection type; false otherwise.</returns>
return collectionTypeRE.test(typeName);
};
var isComplex = function (value) {
/// <summary>Tests whether a value is a complex type value in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <returns type="Boolean">True is the value is a complex type value; false otherwise.</returns>
return !!value &&
isObject(value) &&
!isArray(value) &&
!isDate(value);
};
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 isDeferred = function (value) {
/// <summary>Tests whether a value is a deferred navigation property in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <returns type="Boolean">True is the value is a deferred navigation property; false otherwise.</returns>
if (!value && !isComplex(value)) {
return false;
}
var metadata = value.__metadata || {};
var deferred = value.__deferred || {};
return !metadata.type && !!deferred.uri;
};
var isEntry = function (value) {
/// <summary>Tests whether a value is an entry object in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <returns type="Boolean">True is the value is an entry object; false otherwise.</returns>
return isComplex(value) && value.__metadata && "uri" in value.__metadata;
};
var isFeed = function (value, typeName) {
/// <summary>Tests whether a value is a feed value in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <param name="typeName" type="Sting">Type name of the value. This is used to disambiguate from a collection property value.</param>
/// <returns type="Boolean">True is the value is a feed value; false otherwise.</returns>
var feedData = value && value.results || value;
return isArray(feedData) && (
(!isCollectionType(typeName)) &&
(isComplex(feedData[0]))
);
};
var isGeographyEdmType = function (typeName) {
/// <summary>Checks whether the specified type name is a geography EDM type.</summary>
/// <param name="typeName" type="String">Name of type to check.</param>
/// <returns type="Boolean">True if the type is a geography EDM type; false otherwise.</returns>
return contains(geographyEdmTypes, typeName);
};
var isGeometryEdmType = function (typeName) {
/// <summary>Checks whether the specified type name is a geometry EDM type.</summary>
/// <param name="typeName" type="String">Name of type to check.</param>
/// <returns type="Boolean">True if the type is a geometry EDM type; false otherwise.</returns>
return contains(geometryEdmTypes, typeName);
};
var isNamedStream = function (value) {
/// <summary>Tests whether a value is a named stream value in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <returns type="Boolean">True is the value is a named stream; false otherwise.</returns>
if (!value && !isComplex(value)) {
return false;
}
var metadata = value.__metadata;
var mediaResource = value.__mediaresource;
return !metadata && !!mediaResource && !!mediaResource.media_src;
};
var isPrimitive = function (value) {
/// <summary>Tests whether a value is a primitive type value in the library's internal representation.</summary>
/// <param name="value">Value to test.</param>
/// <remarks>
/// Date objects are considered primitive types by the library.
/// </remarks>
/// <returns type="Boolean">True is the value is a primitive type value.</returns>
return isDate(value) ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean";
};
var isPrimitiveEdmType = function (typeName) {
/// <summary>Checks whether the specified type name is a primitive EDM type.</summary>
/// <param name="typeName" type="String">Name of type to check.</param>
/// <returns type="Boolean">True if the type is a primitive EDM type; false otherwise.</returns>
return contains(primitiveEdmTypes, typeName);
};
var navigationPropertyKind = function (value, propertyModel) {
/// <summary>Gets the kind of a navigation property value.</summary>
/// <param name="value">Value of the navigation property.</param>
/// <param name="propertyModel" type="Object" optional="true">
/// Object that describes the navigation property in an OData conceptual schema.
/// </param>
/// <remarks>
/// The returned string is as follows
/// </remarks>
/// <returns type="String">String value describing the kind of the navigation property; null if the kind cannot be determined.</returns>
if (isDeferred(value)) {
return "deferred";
}
if (isEntry(value)) {
return "entry";
}
if (isFeed(value)) {
return "feed";
}
if (propertyModel && propertyModel.relationship) {
if (value === null || value === undefined || !isFeed(value)) {
return "entry";
}
return "feed";
}
return null;
};
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>
return find(properties, function (property) {
return property.name === name;
});
};
var lookupInMetadata = 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 object to look for as per EDM metadata.</param>
/// <returns>An type description if the name is found; null otherwise.</returns>
return (name) ? forEachSchema(metadata, function (schema) {
return lookupInSchema(name, schema, kind);
}) : null;
};
var lookupEntitySet = function (entitySets, name) {
/// <summary>Looks up a entity set by name.</summary>
/// <param name="properties" type="Array" mayBeNull="true">Array of entity set objects as per EDM metadata.</param>
/// <param name="name" type="String">Name to look for.</param>
/// <returns type="Object">The entity set object; null if not found.</returns>
return find(entitySets, function (entitySet) {
return entitySet.name === name;
});
};
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 lookupInMetadata(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 lookupInMetadata(name, metadata, "entityType");
};
var lookupDefaultEntityContainer = function (metadata) {
/// <summary>Looks up an</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 container description if the name is found; null otherwise.</returns>
return forEachSchema(metadata, function (schema) {
return find(schema.entityContainer, function (container) {
return parseBool(container.isDefaultEntityContainer);
});
});
};
var lookupEntityContainer = function (name, metadata) {
/// <summary>Looks up an entity container 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 container description if the name is found; null otherwise.</returns>
return lookupInMetadata(name, metadata, "entityContainer");
};
var lookupFunctionImport = function (functionImports, name) {
/// <summary>Looks up a function import by name.</summary>
/// <param name="properties" type="Array" mayBeNull="true">Array of function import objects as per EDM metadata.</param>
/// <param name="name" type="String">Name to look for.</param>
/// <returns type="Object">The entity set object; null if not found.</returns>
return find(functionImports, function (functionImport) {
return functionImport.name === name;
});
};
var lookupNavigationPropertyType = 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="String">The entity type name for the specified property, null if not found.</returns>
var result = null;
if (navigationProperty) {
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];
}
}
}
return 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 = end.type;
}
}
return result;
};
var lookupNavigationPropertyEntitySet = function (navigationProperty, sourceEntitySetName, metadata) {
/// <summary>Looks up the target entityset name for a navigation property.</summary>
/// <param name="navigationProperty" type="Object"></param>
/// <param name="metadata" type="Object"></param>
/// <returns type="String">The entityset name for the specified property, null if not found.</returns>
if (navigationProperty) {
var rel = navigationProperty.relationship;
var associationSet = forEachSchema(metadata, function (schema) {
var containers = schema.entityContainer;
for (var i = 0; i < containers.length; i++) {
var associationSets = containers[i].associationSet;
if (associationSets) {
for (var j = 0; j < associationSets.length; j++) {
if (associationSets[j].association == rel) {
return associationSets[j];
}
}
}
}
return null;
});
if (associationSet && associationSet.end[0] && associationSet.end[1]) {
return (associationSet.end[0].entitySet == sourceEntitySetName) ? associationSet.end[1].entitySet : associationSet.end[0].entitySet;
}
}
return null;
};
var getEntitySetInfo = function (entitySetName, metadata) {
/// <summary>Gets the entitySet info, container name and functionImports for an entitySet</summary>
/// <param name="navigationProperty" type="Object"></param>
/// <param name="metadata" type="Object"></param>
/// <returns type="Object">The info about the entitySet.</returns>
var info = forEachSchema(metadata, function (schema) {
var containers = schema.entityContainer;
for (var i = 0; i < containers.length; i++) {
var entitySets = containers[i].entitySet;
if (entitySets) {
for (var j = 0; j < entitySets.length; j++) {
if (entitySets[j].name == entitySetName) {
return { entitySet: entitySets[j], containerName: containers[i].name, functionImport: containers[i].functionImport };
}
}
}
}
return null;
});
return info;
};
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 lookupInSchema = function (name, schema, kind) {
/// <summary>Looks up a schema object by name.</summary>
/// <param name="name" type="String">Name (assigned).</param>
/// <param name="schema">Schema object as per EDM metadata.</param>
/// <param name="kind" type="String">Kind of object to look for as per EDM metadata.</param>
/// <returns>An entity type description if the name is found; null otherwise.</returns>
if (name && schema) {
// The name should be the namespace qualified name in 'ns'.'type' format.
var nameOnly = removeNamespace(schema["namespace"], name);
if (nameOnly) {
return find(schema[kind], function (item) {
return item.name === nameOnly;
});
}
}
return null;
};
var maxVersion = function (left, right) {
/// <summary>Compares to version strings and returns the higher one.</summary>
/// <param name="left" type="String">Version string in the form "major.minor.rev"</param>
/// <param name="right" type="String">Version string in the form "major.minor.rev"</param>
/// <returns type="String">The higher version string.</returns>
if (left === right) {
return left;
}
var leftParts = left.split(".");
var rightParts = right.split(".");
var len = (leftParts.length >= rightParts.length) ?
leftParts.length :
rightParts.length;
for (var i = 0; i < len; i++) {
var leftVersion = leftParts[i] && parseInt10(leftParts[i]);
var rightVersion = rightParts[i] && parseInt10(rightParts[i]);
if (leftVersion > rightVersion) {
return left;
}
if (leftVersion < rightVersion) {
return right;
}
}
};
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 parseBool = function (propertyValue) {
/// <summary>Parses a string into a boolean value.</summary>
/// <param name="propertyValue">Value to parse.</param>
/// <returns type="Boolean">true if the property value is 'true'; false otherwise.</returns>
if (typeof propertyValue === "boolean") {
return propertyValue;
}
return typeof propertyValue === "string" && propertyValue.toLowerCase() === "true";
};
// The captured indices for this expression are:
// 0 - complete input
// 1,2,3 - year with optional minus sign, month, day
// 4,5,6 - hours, minutes, seconds
// 7 - optional milliseconds
// 8 - everything else (presumably offset information)
var parseDateTimeRE = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d+))?(.*)$/;
var parseDateTimeMaybeOffset = function (value, withOffset, nullOnError) {
/// <summary>Parses a string into a DateTime value.</summary>
/// <param name="value" type="String">Value to parse.</param>
/// <param name="withOffset" type="Boolean">Whether offset is expected.</param>
/// <returns type="Date">The parsed value.</returns>
// We cannot parse this in cases of failure to match or if offset information is specified.
var parts = parseDateTimeRE.exec(value);
var offset = (parts) ? getCanonicalTimezone(parts[8]) : null;
if (!parts || (!withOffset && offset !== "Z")) {
if (nullOnError) {
return null;
}
throw { message: "Invalid date/time value" };
}
// Pre-parse years, account for year '0' being invalid in dateTime.
var year = parseInt10(parts[1]);
if (year <= 0) {
year++;
}
// Pre-parse optional milliseconds, fill in default. Fail if value is too precise.
var ms = parts[7];
var ns = 0;
if (!ms) {
ms = 0;
} else {
if (ms.length > 7) {
if (nullOnError) {
return null;
}
throw { message: "Cannot parse date/time 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);
}
// Pre-parse other time components and offset them if necessary.
var hours = parseInt10(parts[4]);
var minutes = parseInt10(parts[5]);
var seconds = parseInt10(parts[6]) || 0;
if (offset !== "Z") {
// The offset is reversed to get back the UTC date, which is
// what the API will eventually have.
var timezone = parseTimezone(offset);
var direction = -(timezone.d);
hours += timezone.h * direction;
minutes += timezone.m * direction;
}
// Set the date and time separately with setFullYear, so years 0-99 aren't biased like in Date.UTC.
var result = new Date();
result.setUTCFullYear(
year, // Year.
parseInt10(parts[2]) - 1, // Month (zero-based for Date.UTC and setFullYear).
parseInt10(parts[3]) // Date.
);
result.setUTCHours(hours, minutes, seconds, ms);
if (isNaN(result.valueOf())) {
if (nullOnError) {
return null;
}
throw { message: "Invalid date/time value" };
}
if (withOffset) {
result.__edmType = "Edm.DateTimeOffset";
result.__offset = offset;
}
if (ns) {
result.__ns = ns;
}
return result;
};
var parseDateTime = function (propertyValue, nullOnError) {
/// <summary>Parses a string into a DateTime value.</summary>
/// <param name="propertyValue" type="String">Value to parse.</param>
/// <returns type="Date">The parsed value.</returns>
return parseDateTimeMaybeOffset(propertyValue, false, nullOnError);
};
var parseDateTimeOffset = function (propertyValue, nullOnError) {
/// <summary>Parses a string into a DateTimeOffset value.</summary>
/// <param name="propertyValue" type="String">Value to parse.</param>
/// <returns type="Date">The parsed value.</returns>
/// <remarks>
/// The resulting object is annotated with an __edmType property and
/// an __offset property reflecting the original intended offset of
/// the value. The time is adjusted for UTC time, as the current
/// timezone-aware Date APIs will only work with the local timezone.
/// </remarks>
return parseDateTimeMaybeOffset(propertyValue, true, nullOnError);
};
// 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 isEdmDurationValue = function(value) {
parseTimeRE.test(value);
};
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 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 traverseInternal = function (item, owner, callback) {
/// <summary>Traverses a tree of objects invoking callback for every value.</summary>
/// <param name="item" type="Object">Object or array to traverse.</param>
/// <param name="callback" type="Function">
/// Callback function with key and value, similar to JSON.parse reviver.
/// </param>
/// <returns type="Object">The object with traversed properties.</returns>
/// <remarks>Unlike the JSON reviver, this won't delete null members.</remarks>
if (item && typeof item === "object") {
for (var name in item) {
var value = item[name];
var result = traverseInternal(value, name, callback);
result = callback(name, result, owner);
if (result !== value) {
if (value === undefined) {
delete item[name];
} else {
item[name] = result;
}
}
}
}
return item;
};
var traverse = function (item, callback) {
/// <summary>Traverses a tree of objects invoking callback for every value.</summary>
/// <param name="item" type="Object">Object or array to traverse.</param>
/// <param name="callback" type="Function">
/// Callback function with key and value, similar to JSON.parse reviver.
/// </param>
/// <returns type="Object">The traversed object.</returns>
/// <remarks>Unlike the JSON reviver, this won't delete null members.</remarks>
return callback("", traverseInternal(item, "", callback));
};
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 IE10 below where trying to access data.constructor after the IFRAME has been removed
// throws an "unknown exception"
if (window.ActiveXObject) {
data = window.JSON.parse(window.JSON.stringify(data));
}
var headers;
// Adding dataServiceVersion in case of json light ( data.d doesn't exist )
if (data.d === undefined) {
headers = { "Content-Type": "application/json;odata=minimalmetadata", dataServiceVersion: "3.0" };
} else {
headers = { "Content-Type": "application/json" };
}
// Call the success callback in the context of the parent window, instead of the IFRAME
delay(function () {
removeIFrame(iframe);
success({ body: data, statusCode: 200, headers: headers });
});
}
};
// 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 = "3.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>
var rwContext = {};
extend(rwContext, context);
extend(rwContext, {
contentType: contentType,
dataServiceVersion: dataServiceVersion,
handler: handler
});
return rwContext;
};
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 fixDataServiceVersionHeader = function (request, version) {
/// <summary>Sets the DataServiceVersion header of the request if its value is not yet defined or of a lower version.</summary>
/// <param name="request">Request object on which the header will be set.</param>
/// <param name="version" type="String">Version value.</param>
/// <remarks>
/// If the request has already a version value higher than the one supplied the this function does nothing.
/// </remarks>
if (request) {
var headers = request.headers;
var dsv = headers["DataServiceVersion"];
headers["DataServiceVersion"] = dsv ? maxVersion(dsv, version) : 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, "DataServiceVersion");
if (value) {
var matches = versionRE.exec(value);
if (matches && matches.length) {
return matches[1];
}
}
// Fall through and return undefined.
};
var handlerAccepts = function (handler, cType) {
/// <summary>Checks that a handler can process a particular mime type.</summary>
/// <param name="handler">Handler object that is processing a resquest or response.</param>
/// <param name="cType">Object with 'mediaType' and a 'properties' dictionary.</param>
/// <returns type="Boolean">True if the handler can process the mime type; false otherwise.</returns>
// The following check isn't as strict because if cType.mediaType = application/; it will match an accept value of "application/xml";
// however in practice we don't not expect to see such "suffixed" mimeTypes for the handlers.
return handler.accept.indexOf(cType.mediaType) >= 0;
};
var handlerRead = function (handler, parseCallback, response, context) {
/// <summary>Invokes the parser associated with a handler for reading the payload of a HTTP response.</summary>
/// <param name="handler">Handler object that is processing the response.</param>
/// <param name="parseCallback" type="Function">Parser function that will process the response payload.</param>
/// <param name="response">HTTP response whose payload is going to be processed.</param>
/// <param name="context">Object used as the context for processing the response.</param>
/// <returns type="Boolean">True if the handler processed the response payload and the response.data property was set; false otherwise.</returns>
if (!response || !response.headers) {
return false;
}
var cType = getContentType(response);
var version = getDataServiceVersion(response) || "";
var body = response.body;
if (!assigned(body)) {
return false;
}
if (handlerAccepts(handler, cType)) {
var readContext = createReadWriteContext(cType, version, context, handler);
readContext.response = response;
response.data = parseCallback(handler, body, readContext);
return response.data !== undefined;
}
return false;
};
var handlerWrite = function (handler, serializeCallback, request, context) {
/// <summary>Invokes the serializer associated with a handler for generating the payload of a HTTP request.</summary>
/// <param name="handler">Handler object that is processing the request.</param>
/// <param name="serializeCallback" type="Function">Serializer function that will generate the request payload.</param>
/// <param name="response">HTTP request whose payload is going to be generated.</param>
/// <param name="context">Object used as the context for serializing the request.</param>
/// <returns type="Boolean">True if the handler serialized the request payload and the request.body property was set; false otherwise.</returns>
if (!request || !request.headers) {
return false;
}
var cType = getContentType(request);
var version = getDataServiceVersion(request);
if (!cType || handlerAccepts(handler, cType)) {
var writeContext = createReadWriteContext(cType, version, context, handler);
writeContext.request = request;
request.body = serializeCallback(handler, request.data, writeContext);
if (request.body !== undefined) {
fixDataServiceVersionHeader(request, writeContext.dataServiceVersion || "1.0");
fixRequestHeader(request, "Content-Type", contentTypeToString(writeContext.contentType));
fixRequestHeader(request, "MaxDataServiceVersion", handler.maxDataServiceVersion);
return true;
}
}
return false;
};
var handler = function (parseCallback, serializeCallback, accept, maxDataServiceVersion) {
/// <summary>Creates a handler object for processing HTTP requests and responses.</summary>
/// <param name="parseCallback" type="Function">Parser function that will process the response payload.</param>
/// <param name="serializeCallback" type="Function">Serializer function that will generate the request payload.</param>
/// <param name="accept" type="String">String containing a comma separated list of the mime types that this handler can work with.</param>
/// <param name="maxDataServiceVersion" type="String">String indicating the highest version of the protocol that this handler can work with.</param>
/// <returns type="Object">Handler object.</returns>
return {
accept: accept,
maxDataServiceVersion: maxDataServiceVersion,
read: function (response, context) {
return handlerRead(this, parseCallback, response, context);
},
write: function (request, context) {
return handlerWrite(this, serializeCallback, request, context);
}
};
};
var textParse = function (handler, body /*, context */) {
return body;
};
var textSerialize = function (handler, data /*, context */) {
if (assigned(data)) {
return data.toString();
} else {
return undefined;
}
};
odata.textHandler = handler(textParse, textSerialize, "text/plain", MAX_DATA_SERVICE_VERSION);
var gmlOpenGis = http + "www.opengis.net"; // http://www.opengis.net
var gmlXmlNs = gmlOpenGis + "/gml"; // http://www.opengis.net/gml
var gmlSrsPrefix = gmlOpenGis + "/def/crs/EPSG/0/"; // http://www.opengis.net/def/crs/EPSG/0/
var gmlPrefix = "gml";
var gmlCreateGeoJSONOBject = function (type, member, data) {
/// <summary>Creates a GeoJSON object with the specified type, member and value.</summary>
/// <param name="type" type="String">GeoJSON object type.</param>
/// <param name="member" type="String">Name for the data member in the GeoJSON object.</param>
/// <param name="data">Data to be contained by the GeoJSON object.</param>
/// <returns type="Object">GeoJSON object.</returns>
var result = { type: type };
result[member] = data;
return result;
};
var gmlSwapLatLong = function (coordinates) {
/// <summary>Swaps the longitude and latitude in the coordinates array.</summary>
/// <param name="coordinates" type="Array">Array of doubles descrbing a set of coordinates.</param>
/// <returns type="Array">Array of doubles with the latitude and longitude components swapped.</returns>
if (isArray(coordinates) && coordinates.length >= 2) {
var tmp = coordinates[0];
coordinates[0] = coordinates[1];
coordinates[1] = tmp;
}
return coordinates;
};
var gmlReadODataMultiItem = function (domElement, type, member, members, valueReader, isGeography) {
/// <summary>
/// Reads a GML DOM element that represents a composite structure like a multi-point or a
/// multi-geometry returnig its GeoJSON representation.
/// </summary>
/// <param name="domElement">GML DOM element.</param>
/// <param name="type" type="String">GeoJSON object type.</param>
/// <param name="member" type="String">Name for the child element representing a single item in the composite structure.</param>
/// <param name="members" type="String">Name for the child element representing a collection of items in the composite structure.</param>
/// <param name="valueReader" type="Function">Callback function invoked to get the coordinates of each item in the comoposite structure.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">GeoJSON object.</returns>
var coordinates = gmlReadODataMultiItemValue(domElement, member, members, valueReader, isGeography);
return gmlCreateGeoJSONOBject(type, "coordinates", coordinates);
};
var gmlReadODataMultiItemValue = function (domElement, member, members, valueReader, isGeography) {
/// <summary>
/// Reads the value of a GML DOM element that represents a composite structure like a multi-point or a
/// multi-geometry returnig its items.
/// </summary>
/// <param name="domElement">GML DOM element.</param>
/// <param name="type" type="String">GeoJSON object type.</param>
/// <param name="member" type="String">Name for the child element representing a single item in the composite structure.</param>
/// <param name="members" type="String">Name for the child element representing a collection of items in the composite structure.</param>
/// <param name="valueReader" type="Function">Callback function invoked to get the transformed value of each item in the comoposite structure.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Array">Array containing the transformed value of each item in the multi-item.</returns>
var items = [];
xmlChildElements(domElement, function (child) {
if (xmlNamespaceURI(child) !== gmlXmlNs) {
return;
}
var localName = xmlLocalName(child);
if (localName === member) {
var valueElement = xmlFirstChildElement(child, gmlXmlNs);
if (valueElement) {
var value = valueReader(valueElement, isGeography);
if (value) {
items.push(value);
}
}
return;
}
if (localName === members) {
xmlChildElements(child, function (valueElement) {
if (xmlNamespaceURI(valueElement) !== gmlXmlNs) {
return;
}
var value = valueReader(valueElement, isGeography);
if (value) {
items.push(value);
}
});
}
});
return items;
};
var gmlReadODataCollection = function (domElement, isGeography) {
/// <summary>Reads a GML DOM element representing a multi-geometry returning its GeoJSON representation.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">MultiGeometry object in GeoJSON format.</returns>
var geometries = gmlReadODataMultiItemValue(domElement, "geometryMember", "geometryMembers", gmlReadODataSpatialValue, isGeography);
return gmlCreateGeoJSONOBject(GEOJSON_GEOMETRYCOLLECTION, "geometries", geometries);
};
var gmlReadODataLineString = function (domElement, isGeography) {
/// <summary>Reads a GML DOM element representing a line string returning its GeoJSON representation.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">LineString object in GeoJSON format.</returns>
return gmlCreateGeoJSONOBject(GEOJSON_LINESTRING, "coordinates", gmlReadODataLineValue(domElement, isGeography));
};
var gmlReadODataMultiLineString = function (domElement, isGeography) {
/// <summary>Reads a GML DOM element representing a multi-line string returning its GeoJSON representation.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">MultiLineString object in GeoJSON format.</returns>
return gmlReadODataMultiItem(domElement, GEOJSON_MULTILINESTRING, "curveMember", "curveMembers", gmlReadODataLineValue, isGeography);
};
var gmlReadODataMultiPoint = function (domElement, isGeography) {
/// <summary>Reads a GML DOM element representing a multi-point returning its GeoJSON representation.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">MultiPoint object in GeoJSON format.</returns>
return gmlReadODataMultiItem(domElement, GEOJSON_MULTIPOINT, "pointMember", "pointMembers", gmlReadODataPointValue, isGeography);
};
var gmlReadODataMultiPolygon = function (domElement, isGeography) {
/// <summary>Reads a GML DOM element representing a multi-polygon returning its GeoJSON representation.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">MultiPolygon object in GeoJSON format.</returns>
return gmlReadODataMultiItem(domElement, GEOJSON_MULTIPOLYGON, "surfaceMember", "surfaceMembers", gmlReadODataPolygonValue, isGeography);
};
var gmlReadODataPoint = function (domElement, isGeography) {
/// <summary>Reads a GML DOM element representing a point returning its GeoJSON representation.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">Point object in GeoJSON format.</returns>
return gmlCreateGeoJSONOBject(GEOJSON_POINT, "coordinates", gmlReadODataPointValue(domElement, isGeography));
};
var gmlReadODataPolygon = function (domElement, isGeography) {
/// <summary>Reads a GML DOM element representing a polygon returning its GeoJSON representation.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Object">Polygon object in GeoJSON format.</returns>
return gmlCreateGeoJSONOBject(GEOJSON_POLYGON, "coordinates", gmlReadODataPolygonValue(domElement, isGeography));
};
var gmlReadODataLineValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML DOM element representing a line returning its set of coordinates.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Array">Array containing an array of doubles for each coordinate of the line.</returns>
var coordinates = [];
xmlChildElements(domElement, function (child) {
var nsURI = xmlNamespaceURI(child);
if (nsURI !== gmlXmlNs) {
return;
}
var localName = xmlLocalName(child);
if (localName === "posList") {
coordinates = gmlReadODataPosListValue(child, isGeography);
return;
}
if (localName === "pointProperty") {
coordinates.push(gmlReadODataPointWrapperValue(child, isGeography));
return;
}
if (localName === "pos") {
coordinates.push(gmlReadODataPosValue(child, isGeography));
return;
}
});
return coordinates;
};
var gmlReadODataPointValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML DOM element representing a point returning its coordinates.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Array">Array of doubles containing the point coordinates.</returns>
var pos = xmlFirstChildElement(domElement, gmlXmlNs, "pos");
return pos ? gmlReadODataPosValue(pos, isGeography) : [];
};
var gmlReadODataPointWrapperValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML DOM element wrapping an element representing a point returning its coordinates.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Array">Array of doubles containing the point coordinates.</returns>
var point = xmlFirstChildElement(domElement, gmlXmlNs, "Point");
return point ? gmlReadODataPointValue(point, isGeography) : [];
};
var gmlReadODataPolygonValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML DOM element representing a polygon returning its set of coordinates.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Array">Array containing an array of array of doubles for each ring of the polygon.</returns>
var coordinates = [];
var exteriorFound = false;
xmlChildElements(domElement, function (child) {
if (xmlNamespaceURI(child) !== gmlXmlNs) {
return;
}
// Only the exterior and the interior rings are interesting
var localName = xmlLocalName(child);
if (localName === "exterior") {
exteriorFound = true;
coordinates.unshift(gmlReadODataPolygonRingValue(child, isGeography));
return;
}
if (localName === "interior") {
coordinates.push(gmlReadODataPolygonRingValue(child, isGeography));
return;
}
});
if (!exteriorFound && coordinates.length > 0) {
// Push an empty exterior ring.
coordinates.unshift([[]]);
}
return coordinates;
};
var gmlReadODataPolygonRingValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML DOM element representing a linear ring in a GML Polygon element.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Array">Array containing an array of doubles for each coordinate of the linear ring.</returns>
var value = [];
xmlChildElements(domElement, function (child) {
if (xmlNamespaceURI(child) !== gmlXmlNs || xmlLocalName(child) !== "LinearRing") {
return;
}
value = gmlReadODataLineValue(child, isGeography);
});
return value;
};
var gmlReadODataPosListValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML DOM element representing a list of positions retruning its set of coordinates.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
///
/// The positions described by the list are assumed to be 2D, so
/// an exception will be thrown if the list has an odd number elements.
/// </remarks>
/// <returns type="Array">Array containing an array of doubles for each coordinate in the list.</returns>
var coordinates = gmlReadODataPosValue(domElement, false);
var len = coordinates.length;
if (len % 2 !== 0) {
throw { message: "GML posList element has an uneven number of numeric values" };
}
var value = [];
for (var i = 0; i < len; i += 2) {
var pos = coordinates.slice(i, i + 2);
value.push(isGeography ? gmlSwapLatLong(pos) : pos);
}
return value;
};
var gmlReadODataPosValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML element describing a position or a set of coordinates in an OData spatial property value.</summary>
/// <param name="property">DOM element for the GML element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns type="Array">Array of doubles containing the coordinates.</returns>
var value = [];
var delims = " \t\r\n";
var text = xmlInnerText(domElement);
if (text) {
var len = text.length;
var start = 0;
var end = 0;
while (end <= len) {
if (delims.indexOf(text.charAt(end)) !== -1) {
var coord = text.substring(start, end);
if (coord) {
value.push(parseFloat(coord));
}
start = end + 1;
}
end++;
}
}
return isGeography ? gmlSwapLatLong(value) : value;
};
var gmlReadODataSpatialValue = function (domElement, isGeography) {
/// <summary>Reads the value of a GML DOM element a spatial value in an OData XML document.</summary>
/// <param name="domElement">DOM element.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each position coordinates in the resulting GeoJSON object.
/// </remarks>
/// <returns type="Array">Array containing an array of doubles for each coordinate of the polygon.</returns>
var localName = xmlLocalName(domElement);
var reader;
switch (localName) {
case "Point":
reader = gmlReadODataPoint;
break;
case "Polygon":
reader = gmlReadODataPolygon;
break;
case "LineString":
reader = gmlReadODataLineString;
break;
case "MultiPoint":
reader = gmlReadODataMultiPoint;
break;
case "MultiCurve":
reader = gmlReadODataMultiLineString;
break;
case "MultiSurface":
reader = gmlReadODataMultiPolygon;
break;
case "MultiGeometry":
reader = gmlReadODataCollection;
break;
default:
throw { message: "Unsupported element: " + localName, element: domElement };
}
var value = reader(domElement, isGeography);
// Read the CRS
// WCF Data Services qualifies the srsName attribute withing the GML namespace; however
// other end points might no do this as per the standard.
var srsName = xmlAttributeValue(domElement, "srsName", gmlXmlNs) ||
xmlAttributeValue(domElement, "srsName");
if (srsName) {
if (srsName.indexOf(gmlSrsPrefix) !== 0) {
throw { message: "Unsupported srs name: " + srsName, element: domElement };
}
var crsId = srsName.substring(gmlSrsPrefix.length);
if (crsId) {
value.crs = {
type: "name",
properties: {
name: "EPSG:" + crsId
}
};
}
}
return value;
};
var gmlNewODataSpatialValue = function (dom, value, type, isGeography) {
/// <summary>Creates a new GML DOM element for the value of an OData spatial property or GeoJSON object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">Spatial property value in GeoJSON format.</param>
/// <param name="type" type="String">String indicating the GeoJSON type of the value to serialize.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the spatial value. </returns>
var gmlWriter;
switch (type) {
case GEOJSON_POINT:
gmlWriter = gmlNewODataPoint;
break;
case GEOJSON_LINESTRING:
gmlWriter = gmlNewODataLineString;
break;
case GEOJSON_POLYGON:
gmlWriter = gmlNewODataPolygon;
break;
case GEOJSON_MULTIPOINT:
gmlWriter = gmlNewODataMultiPoint;
break;
case GEOJSON_MULTILINESTRING:
gmlWriter = gmlNewODataMultiLineString;
break;
case GEOJSON_MULTIPOLYGON:
gmlWriter = gmlNewODataMultiPolygon;
break;
case GEOJSON_GEOMETRYCOLLECTION:
gmlWriter = gmlNewODataGeometryCollection;
break;
default:
return null;
}
var gml = gmlWriter(dom, value, isGeography);
// Set the srsName attribute if applicable.
var crs = value.crs;
if (crs) {
if (crs.type === "name") {
var properties = crs.properties;
var name = properties && properties.name;
if (name && name.indexOf("ESPG:") === 0 && name.length > 5) {
var crsId = name.substring(5);
var srsName = xmlNewAttribute(dom, null, "srsName", gmlPrefix + crsId);
xmlAppendChild(gml, srsName);
}
}
}
return gml;
};
var gmlNewODataElement = function (dom, name, children) {
/// <summary>Creates a new DOM element in the GML namespace.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Local name of the GML element to create.</param>
/// <param name="children" type="Array">Array containing DOM nodes or string values that will be added as children of the new DOM element.</param>
/// <returns>New DOM element in the GML namespace.</returns>
/// <remarks>
/// If a value in the children collection is a string, then a new DOM text node is going to be created
/// for it and then appended as a child of the new DOM Element.
/// </remarks>
return xmlNewElement(dom, gmlXmlNs, xmlQualifiedName(gmlPrefix, name), children);
};
var gmlNewODataPosElement = function (dom, coordinates, isGeography) {
/// <summary>Creates a new GML pos DOM element.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="coordinates" type="Array">Array of doubles describing the coordinates of the pos element.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the coordinates use a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first coordinate is the Longitude and
/// will be serialized as the second component of the <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New pos DOM element in the GML namespace.</returns>
var posValue = isArray(coordinates) ? coordinates : [];
// If using a geographic reference system, then the first coordinate is the longitude and it has to
// swapped with the latitude.
posValue = isGeography ? gmlSwapLatLong(posValue) : posValue;
return gmlNewODataElement(dom, "pos", posValue.join(" "));
};
var gmlNewODataLineElement = function (dom, name, coordinates, isGeography) {
/// <summary>Creates a new GML DOM element representing a line.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Name of the element to create.</param>
/// <param name="coordinates" type="Array">Array of array of doubles describing the coordinates of the line element.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the coordinates use a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace.</returns>
var element = gmlNewODataElement(dom, name);
if (isArray(coordinates)) {
var i, len;
for (i = 0, len = coordinates.length; i < len; i++) {
xmlAppendChild(element, gmlNewODataPosElement(dom, coordinates[i], isGeography));
}
if (len === 0) {
xmlAppendChild(element, gmlNewODataElement(dom, "posList"));
}
}
return element;
};
var gmlNewODataPointElement = function (dom, coordinates, isGeography) {
/// <summary>Creates a new GML Point DOM element.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON Point object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON Point.</returns>
return gmlNewODataElement(dom, "Point", gmlNewODataPosElement(dom, coordinates, isGeography));
};
var gmlNewODataLineStringElement = function (dom, coordinates, isGeography) {
/// <summary>Creates a new GML LineString DOM element.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="coordinates" type="Array">Array of array of doubles describing the coordinates of the line element.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON LineString.</returns>
return gmlNewODataLineElement(dom, "LineString", coordinates, isGeography);
};
var gmlNewODataPolygonRingElement = function (dom, name, coordinates, isGeography) {
/// <summary>Creates a new GML DOM element representing a polygon ring.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Name of the element to create.</param>
/// <param name="coordinates" type="Array">Array of array of doubles describing the coordinates of the polygon ring.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the coordinates use a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace.</returns>
var ringElement = gmlNewODataElement(dom, name);
if (isArray(coordinates) && coordinates.length > 0) {
var linearRing = gmlNewODataLineElement(dom, "LinearRing", coordinates, isGeography);
xmlAppendChild(ringElement, linearRing);
}
return ringElement;
};
var gmlNewODataPolygonElement = function (dom, coordinates, isGeography) {
/// <summary>Creates a new GML Polygon DOM element for a GeoJSON Polygon object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="coordinates" type="Array">Array of array of array of doubles describing the coordinates of the polygon.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace.</returns>
var len = coordinates && coordinates.length;
var element = gmlNewODataElement(dom, "Polygon");
if (isArray(coordinates) && len > 0) {
xmlAppendChild(element, gmlNewODataPolygonRingElement(dom, "exterior", coordinates[0], isGeography));
var i;
for (i = 1; i < len; i++) {
xmlAppendChild(element, gmlNewODataPolygonRingElement(dom, "interior", coordinates[i], isGeography));
}
}
return element;
};
var gmlNewODataPoint = function (dom, value, isGeography) {
/// <summary>Creates a new GML Point DOM element for a GeoJSON Point object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON Point object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON Point.</returns>
return gmlNewODataPointElement(dom, value.coordinates, isGeography);
};
var gmlNewODataLineString = function (dom, value, isGeography) {
/// <summary>Creates a new GML LineString DOM element for a GeoJSON LineString object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON LineString object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON LineString.</returns>
return gmlNewODataLineStringElement(dom, value.coordinates, isGeography);
};
var gmlNewODataPolygon = function (dom, value, isGeography) {
/// <summary>Creates a new GML Polygon DOM element for a GeoJSON Polygon object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON Polygon object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON Polygon.</returns>
return gmlNewODataPolygonElement(dom, value.coordinates, isGeography);
};
var gmlNewODataMultiItem = function (dom, name, members, items, itemWriter, isGeography) {
/// <summary>Creates a new GML DOM element for a composite structure like a multi-point or a multi-geometry.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Name of the element to create.</param>
/// <param name="items" type="Array">Array of items in the composite structure.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the multi-item uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each of the items is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace.</returns>
var len = items && items.length;
var element = gmlNewODataElement(dom, name);
if (isArray(items) && len > 0) {
var membersElement = gmlNewODataElement(dom, members);
var i;
for (i = 0; i < len; i++) {
xmlAppendChild(membersElement, itemWriter(dom, items[i], isGeography));
}
xmlAppendChild(element, membersElement);
}
return element;
};
var gmlNewODataMultiPoint = function (dom, value, isGeography) {
/// <summary>Creates a new GML MultiPoint DOM element for a GeoJSON MultiPoint object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON MultiPoint object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON MultiPoint.</returns>
return gmlNewODataMultiItem(dom, "MultiPoint", "pointMembers", value.coordinates, gmlNewODataPointElement, isGeography);
};
var gmlNewODataMultiLineString = function (dom, value, isGeography) {
/// <summary>Creates a new GML MultiCurve DOM element for a GeoJSON MultiLineString object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON MultiLineString object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON MultiLineString.</returns>
return gmlNewODataMultiItem(dom, "MultiCurve", "curveMembers", value.coordinates, gmlNewODataLineStringElement, isGeography);
};
var gmlNewODataMultiPolygon = function (dom, value, isGeography) {
/// <summary>Creates a new GML MultiSurface DOM element for a GeoJSON MultiPolygon object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON MultiPolygon object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON MultiPolygon.</returns>
return gmlNewODataMultiItem(dom, "MultiSurface", "surfaceMembers", value.coordinates, gmlNewODataPolygonElement, isGeography);
};
var gmlNewODataGeometryCollectionItem = function (dom, value, isGeography) {
/// <summary>Creates a new GML element for an item in a geometry collection object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="item" type="Object">GeoJSON object in the geometry collection.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace.</returns>
return gmlNewODataSpatialValue(dom, value, value.type, isGeography);
};
var gmlNewODataGeometryCollection = function (dom, value, isGeography) {
/// <summary>Creates a new GML MultiGeometry DOM element for a GeoJSON GeometryCollection object.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="value" type="Object">GeoJSON GeometryCollection object.</param>
/// <param name="isGeography" type="Boolean">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in the GeoJSON value is the Longitude and
/// will be serialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the GeoJSON GeometryCollection.</returns>
return gmlNewODataMultiItem(dom, "MultiGeometry", "geometryMembers", value.geometries, gmlNewODataGeometryCollectionItem, isGeography);
};
var xmlMediaType = "application/xml";
var ado = http + "schemas.microsoft.com/ado/"; // http://schemas.microsoft.com/ado/
var adoDs = ado + "2007/08/dataservices"; // http://schemas.microsoft.com/ado/2007/08/dataservices
var edmxNs = ado + "2007/06/edmx"; // http://schemas.microsoft.com/ado/2007/06/edmx
var edmNs1 = ado + "2006/04/edm"; // http://schemas.microsoft.com/ado/2006/04/edm
var edmNs1_1 = ado + "2007/05/edm"; // http://schemas.microsoft.com/ado/2007/05/edm
var edmNs1_2 = ado + "2008/01/edm"; // http://schemas.microsoft.com/ado/2008/01/edm
// There are two valid namespaces for Edm 2.0
var edmNs2a = ado + "2008/09/edm"; // http://schemas.microsoft.com/ado/2008/09/edm
var edmNs2b = ado + "2009/08/edm"; // http://schemas.microsoft.com/ado/2009/08/edm
var edmNs3 = ado + "2009/11/edm"; // http://schemas.microsoft.com/ado/2009/11/edm
var odataXmlNs = adoDs; // http://schemas.microsoft.com/ado/2007/08/dataservices
var odataMetaXmlNs = adoDs + "/metadata"; // http://schemas.microsoft.com/ado/2007/08/dataservices/metadata
var odataRelatedPrefix = adoDs + "/related/"; // http://schemas.microsoft.com/ado/2007/08/dataservices/related
var odataScheme = adoDs + "/scheme"; // http://schemas.microsoft.com/ado/2007/08/dataservices/scheme
var odataPrefix = "d";
var odataMetaPrefix = "m";
var createAttributeExtension = function (domNode, useNamespaceURI) {
/// <summary>Creates an extension object for the specified attribute.</summary>
/// <param name="domNode">DOM node for the attribute.</param>
/// <param name="useNamespaceURI" type="Boolean">Flag indicating if the namespaceURI property should be added to the extension object instead of the namespace property.</param>
/// <remarks>
/// The useNamespaceURI flag is used to prevent a breaking change from older versions of datajs in which extension
/// objects created for Atom extension attributes have the namespaceURI property instead of the namespace one.
///
/// This flag and the namespaceURI property should be deprecated in future major versions of the library.
/// </remarks>
/// <returns type="Object">The new extension object.</returns>
var extension = { name: xmlLocalName(domNode), value: domNode.value };
extension[useNamespaceURI ? "namespaceURI" : "namespace"] = xmlNamespaceURI(domNode);
return extension;
};
var createElementExtension = function (domNode, useNamespaceURI) {
/// <summary>Creates an extension object for the specified element.</summary>
/// <param name="domNode">DOM node for the element.</param>
/// <param name="useNamespaceURI" type="Boolean">Flag indicating if the namespaceURI property should be added to the extension object instead of the namespace property.</param>
/// <remarks>
/// The useNamespaceURI flag is used to prevent a breaking change from older versions of datajs in which extension
/// objects created for Atom extension attributes have the namespaceURI property instead of the namespace one.
///
/// This flag and the namespaceURI property should be deprecated in future major versions of the library.
/// </remarks>
/// <returns type="Object">The new extension object.</returns>
var attributeExtensions = [];
var childrenExtensions = [];
var i, len;
var attributes = domNode.attributes;
for (i = 0, len = attributes.length; i < len; i++) {
var attr = attributes[i];
if (xmlNamespaceURI(attr) !== xmlnsNS) {
attributeExtensions.push(createAttributeExtension(attr, useNamespaceURI));
}
}
var child = domNode.firstChild;
while (child != null) {
if (child.nodeType === 1) {
childrenExtensions.push(createElementExtension(child, useNamespaceURI));
}
child = child.nextSibling;
}
var extension = {
name: xmlLocalName(domNode),
value: xmlInnerText(domNode),
attributes: attributeExtensions,
children: childrenExtensions
};
extension[useNamespaceURI ? "namespaceURI" : "namespace"] = xmlNamespaceURI(domNode);
return extension;
};
var isCollectionItemElement = function (domElement) {
/// <summary>Checks whether the domElement is a collection item.</summary>
/// <param name="domElement">DOM element possibliy represnting a collection item.</param>
/// <returns type="Boolean">True if the domeElement belongs to the OData metadata namespace and its local name is "element"; false otherwise.</returns>
return xmlNamespaceURI(domElement) === odataXmlNs && xmlLocalName(domElement) === "element";
};
var makePropertyMetadata = function (type, extensions) {
/// <summary>Creates an object containing property metadata.</summary>
/// <param type="String" name="type">Property type name.</param>
/// <param type="Array" name="extensions">Array of attribute extension objects.</param>
/// <returns type="Object">Property metadata object cotaining type and extensions fields.</returns>
return { type: type, extensions: extensions };
};
var odataInferTypeFromPropertyXmlDom = function (domElement) {
/// <summary>Infers type of a property based on its xml DOM tree.</summary>
/// <param name="domElement">DOM element for the property.</param>
/// <returns type="String">Inferred type name; null if the type cannot be determined.</returns>
if (xmlFirstChildElement(domElement, gmlXmlNs)) {
return EDM_GEOMETRY;
}
var firstChild = xmlFirstChildElement(domElement, odataXmlNs);
if (!firstChild) {
return EDM_STRING;
}
if (isCollectionItemElement(firstChild)) {
var sibling = xmlSiblingElement(firstChild, odataXmlNs);
if (sibling && isCollectionItemElement(sibling)) {
// More than one <element> tag have been found, it can be safely assumed that this is a collection property.
return "Collection()";
}
}
return null;
};
var xmlReadODataPropertyAttributes = function (domElement) {
/// <summary>Reads the attributes of a property DOM element in an OData XML document.</summary>
/// <param name="domElement">DOM element for the property.</param>
/// <returns type="Object">Object containing the property type, if it is null, and its attribute extensions.</returns>
var type = null;
var isNull = false;
var extensions = [];
xmlAttributes(domElement, function (attribute) {
var nsURI = xmlNamespaceURI(attribute);
var localName = xmlLocalName(attribute);
var value = xmlNodeValue(attribute);
if (nsURI === odataMetaXmlNs) {
if (localName === "null") {
isNull = (value.toLowerCase() === "true");
return;
}
if (localName === "type") {
type = value;
return;
}
}
if (nsURI !== xmlNS && nsURI !== xmlnsNS) {
extensions.push(createAttributeExtension(attribute, true));
return;
}
});
return { type: (!type && isNull ? EDM_STRING : type), isNull: isNull, extensions: extensions };
};
var xmlReadODataProperty = function (domElement) {
/// <summary>Reads a property DOM element in an OData XML document.</summary>
/// <param name="domElement">DOM element for the property.</param>
/// <returns type="Object">Object with name, value, and metadata for the property.</returns>
if (xmlNamespaceURI(domElement) !== odataXmlNs) {
// domElement is not a proprety element because it is not in the odata xml namespace.
return null;
}
var propertyName = xmlLocalName(domElement);
var propertyAttributes = xmlReadODataPropertyAttributes(domElement);
var propertyIsNull = propertyAttributes.isNull;
var propertyType = propertyAttributes.type;
var propertyMetadata = makePropertyMetadata(propertyType, propertyAttributes.extensions);
var propertyValue = propertyIsNull ? null : xmlReadODataPropertyValue(domElement, propertyType, propertyMetadata);
return { name: propertyName, value: propertyValue, metadata: propertyMetadata };
};
var xmlReadODataPropertyValue = function (domElement, propertyType, propertyMetadata) {
/// <summary>Reads the value of a property in an OData XML document.</summary>
/// <param name="domElement">DOM element for the property.</param>
/// <param name="propertyType" type="String">Property type name.</param>
/// <param name="propertyMetadata" type="Object">Object that will store metadata about the property.</param>
/// <returns>Property value.</returns>
if (!propertyType) {
propertyType = odataInferTypeFromPropertyXmlDom(domElement);
propertyMetadata.type = propertyType;
}
var isGeograhpyType = isGeographyEdmType(propertyType);
if (isGeograhpyType || isGeometryEdmType(propertyType)) {
return xmlReadODataSpatialPropertyValue(domElement, propertyType, isGeograhpyType);
}
if (isPrimitiveEdmType(propertyType)) {
return xmlReadODataEdmPropertyValue(domElement, propertyType);
}
if (isCollectionType(propertyType)) {
return xmlReadODataCollectionPropertyValue(domElement, propertyType, propertyMetadata);
}
return xmlReadODataComplexPropertyValue(domElement, propertyType, propertyMetadata);
};
var xmlReadODataSpatialPropertyValue = function (domElement, propertyType, isGeography) {
/// <summary>Reads the value of an spatial property in an OData XML document.</summary>
/// <param name="property">DOM element for the spatial property.</param>
/// <param name="propertyType" type="String">Property type name.</param>
/// <param name="isGeography" type="Boolean" Optional="True">Flag indicating if the value uses a geographic reference system or not.<param>
/// <remarks>
/// When using a geographic reference system, the first component of all the coordinates in each <pos> element in the GML DOM tree is the Latitude and
/// will be deserialized as the second component of each <pos> element in the GML DOM tree.
/// </remarks>
/// <returns>Spatial property value in GeoJSON format.</returns>
var gmlRoot = xmlFirstChildElement(domElement, gmlXmlNs);
var value = gmlReadODataSpatialValue(gmlRoot, isGeography);
value.__metadata = { type: propertyType };
return value;
};
var xmlReadODataEdmPropertyValue = function (domNode, propertyType) {
/// <summary>Reads the value of an EDM property in an OData XML document.</summary>
/// <param name="donNode">DOM node for the EDM property.</param>
/// <param name="propertyType" type="String">Property type name.</param>
/// <returns>EDM property value.</returns>
var propertyValue = xmlNodeValue(domNode) || "";
switch (propertyType) {
case EDM_BOOLEAN:
return parseBool(propertyValue);
case EDM_BINARY:
case EDM_DECIMAL:
case EDM_GUID:
case EDM_INT64:
case EDM_STRING:
return propertyValue;
case EDM_BYTE:
case EDM_INT16:
case EDM_INT32:
case EDM_SBYTE:
return parseInt10(propertyValue);
case EDM_DOUBLE:
case EDM_SINGLE:
return parseFloat(propertyValue);
case EDM_TIME:
return parseDuration(propertyValue);
case EDM_DATETIME:
return parseDateTime(propertyValue);
case EDM_DATETIMEOFFSET:
return parseDateTimeOffset(propertyValue);
}
return propertyValue;
};
var xmlReadODataComplexPropertyValue = function(domElement, propertyType, propertyMetadata) {
/// <summary>Reads the value of a complex type property in an OData XML document.</summary>
/// <param name="property">DOM element for the complex type property.</param>
/// <param name="propertyType" type="String">Property type name.</param>
/// <param name="propertyMetadata" type="Object">Object that will store metadata about the property.</param>
/// <returns type="Object">Complex type property value.</returns>
var propertyValue = { __metadata: { type: propertyType } };
xmlChildElements(domElement, function(child) {
var childProperty = xmlReadODataProperty(child);
var childPropertyName = childProperty.name;
propertyMetadata.properties = propertyMetadata.properties || {};
propertyMetadata.properties[childPropertyName] = childProperty.metadata;
propertyValue[childPropertyName] = childProperty.value;
});
return propertyValue;
};
var xmlReadODataCollectionPropertyValue = function (domElement, propertyType, propertyMetadata) {
/// <summary>Reads the value of a collection property in an OData XML document.</summary>
/// <param name="property">DOM element for the collection property.</param>
/// <param name="propertyType" type="String">Property type name.</param>
/// <param name="propertyMetadata" type="Object">Object that will store metadata about the property.</param>
/// <returns type="Object">Collection property value.</returns>
var items = [];
var itemsMetadata = propertyMetadata.elements = [];
var collectionType = getCollectionType(propertyType);
xmlChildElements(domElement, function (child) {
if (isCollectionItemElement(child)) {
var itemAttributes = xmlReadODataPropertyAttributes(child);
var itemExtensions = itemAttributes.extensions;
var itemType = itemAttributes.type || collectionType;
var itemMetadata = makePropertyMetadata(itemType, itemExtensions);
var item = xmlReadODataPropertyValue(child, itemType, itemMetadata);
items.push(item);
itemsMetadata.push(itemMetadata);
}
});
return { __metadata: { type: propertyType === "Collection()" ? null : propertyType }, results: items };
};
var readODataXmlDocument = function (xmlRoot, baseURI) {
/// <summary>Reads an OData link(s) producing an object model in return.</summary>
/// <param name="xmlRoot">Top-level element to read.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the XML payload.</param>
/// <returns type="Object">The object model representing the specified element.</returns>
if (xmlNamespaceURI(xmlRoot) === odataXmlNs) {
baseURI = xmlBaseURI(xmlRoot, baseURI);
var localName = xmlLocalName(xmlRoot);
if (localName === "links") {
return readLinks(xmlRoot, baseURI);
}
if (localName === "uri") {
return readUri(xmlRoot, baseURI);
}
}
return undefined;
};
var readLinks = function (linksElement, baseURI) {
/// <summary>Deserializes an OData XML links element.</summary>
/// <param name="linksElement">XML links element.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the XML payload.</param>
/// <returns type="Object">A new object representing the links collection.</returns>
var uris = [];
xmlChildElements(linksElement, function (child) {
if (xmlLocalName(child) === "uri" && xmlNamespaceURI(child) === odataXmlNs) {
uris.push(readUri(child, baseURI));
}
});
return { results: uris };
};
var readUri = function (uriElement, baseURI) {
/// <summary>Deserializes an OData XML uri element.</summary>
/// <param name="uriElement">XML uri element.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the XML payload.</param>
/// <returns type="Object">A new object representing the uri.</returns>
var uri = xmlInnerText(uriElement) || "";
return { uri: normalizeURI(uri, baseURI) };
};
var xmlODataInferSpatialValueGeoJsonType = function (value, edmType) {
/// <summary>Infers the GeoJSON type from the spatial property value and the edm type name.</summary>
/// <param name="value" type="Object">Spatial property value in GeoJSON format.</param>
/// <param name="edmType" type="String" mayBeNull="true" optional="true">Spatial property edm type.<param>
/// <remarks>
/// If the edmType parameter is null, undefined, "Edm.Geometry" or "Edm.Geography", then the function returns
/// the GeoJSON type indicated by the value's type property.
///
/// If the edmType parameter is specified or is not one of the base spatial types, then it is used to
/// determine the GeoJSON type and the value's type property is ignored.
/// </remarks>
/// <returns>New DOM element in the GML namespace for the spatial value. </returns>
if (edmType === EDM_GEOMETRY || edmType === EDM_GEOGRAPHY) {
return value && value.type;
}
if (edmType === EDM_GEOMETRY_POINT || edmType === EDM_GEOGRAPHY_POINT) {
return GEOJSON_POINT;
}
if (edmType === EDM_GEOMETRY_LINESTRING || edmType === EDM_GEOGRAPHY_LINESTRING) {
return GEOJSON_LINESTRING;
}
if (edmType === EDM_GEOMETRY_POLYGON || edmType === EDM_GEOGRAPHY_POLYGON) {
return GEOJSON_POLYGON;
}
if (edmType === EDM_GEOMETRY_COLLECTION || edmType === EDM_GEOGRAPHY_COLLECTION) {
return GEOJSON_GEOMETRYCOLLECTION;
}
if (edmType === EDM_GEOMETRY_MULTIPOLYGON || edmType === EDM_GEOGRAPHY_MULTIPOLYGON) {
return GEOJSON_MULTIPOLYGON;
}
if (edmType === EDM_GEOMETRY_MULTILINESTRING || edmType === EDM_GEOGRAPHY_MULTILINESTRING) {
return GEOJSON_MULTILINESTRING;
}
if (edmType === EDM_GEOMETRY_MULTIPOINT || edmType === EDM_GEOGRAPHY_MULTIPOINT) {
return GEOJSON_MULTIPOINT;
}
return null;
};
var xmlNewODataMetaElement = function (dom, name, children) {
/// <summary>Creates a new DOM element in the OData metadata namespace.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Local name of the OData metadata element to create.</param>
/// <param name="children" type="Array">Array containing DOM nodes or string values that will be added as children of the new DOM element.</param>
/// <returns>New DOM element in the OData metadata namespace.</returns>
/// <remarks>
/// If a value in the children collection is a string, then a new DOM text node is going to be created
/// for it and then appended as a child of the new DOM Element.
/// </remarks>
return xmlNewElement(dom, odataMetaXmlNs, xmlQualifiedName(odataMetaPrefix, name), children);
};
var xmlNewODataMetaAttribute = function (dom, name, value) {
/// <summary>Creates a new DOM attribute in the odata namespace.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Local name of the OData attribute to create.</param>
/// <param name="value">Attribute value.</param>
/// <returns>New DOM attribute in the odata namespace.</returns>
return xmlNewAttribute(dom, odataMetaXmlNs, xmlQualifiedName(odataMetaPrefix, name), value);
};
var xmlNewODataElement = function (dom, name, children) {
/// <summary>Creates a new DOM element in the OData namespace.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Local name of the OData element to create.</param>
/// <param name="children" type="Array">Array containing DOM nodes or string values that will be added as children of the new DOM element.</param>
/// <returns>New DOM element in the OData namespace.</returns>
/// <remarks>
/// If a value in the children collection is a string, then a new DOM text node is going to be created
/// for it and then appended as a child of the new DOM Element.
/// </remarks>
return xmlNewElement(dom, odataXmlNs, xmlQualifiedName(odataPrefix, name), children);
};
var xmlNewODataPrimitiveValue = function (value, typeName) {
/// <summary>Returns the string representation of primitive value for an OData XML document.</summary>
/// <param name="value">Primivite value to format.</param>
/// <param name="typeName" type="String" optional="true">Type name of the primitive value.</param>
/// <returns type="String">Formatted primitive value.</returns>
if (typeName === EDM_DATETIME || typeName === EDM_DATETIMEOFFSET || isDate(value)) {
return formatDateTimeOffset(value);
}
if (typeName === EDM_TIME) {
return formatDuration(value);
}
return value.toString();
};
var xmlNewODataElementInfo = function (domElement, dataServiceVersion) {
/// <summary>Creates an object that represents a new DOM element for an OData XML document and the data service version it requires.</summary>
/// <param name="domElement">New DOM element for an OData XML document.</param>
/// <param name="dataServiceVersion" type="String">Required data service version by the new DOM element.</param>
/// <returns type="Object">Object containing new DOM element and its required data service version.</returns>
return { element: domElement, dsv: dataServiceVersion };
};
var xmlNewODataProperty = function (dom, name, typeName, children) {
/// <summary>Creates a new DOM element for an entry property in an OData XML document.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Property name.</param>
/// <param name="typeName" type="String" optional="true">Property type name.</param>
/// <param name="children" type="Array">Array containing DOM nodes or string values that will be added as children of the new DOM element.</param>
/// <remarks>
/// If a value in the children collection is a string, then a new DOM text node is going to be created
/// for it and then appended as a child of the new DOM Element.
/// </remarks>
/// <returns>New DOM element in the OData namespace for the entry property.</returns>
var typeAttribute = typeName ? xmlNewODataMetaAttribute(dom, "type", typeName) : null;
var property = xmlNewODataElement(dom, name, typeAttribute);
return xmlAppendChildren(property, children);
};
var xmlNewODataEdmProperty = function (dom, name, value, typeName) {
/// <summary>Creates a new DOM element for an EDM property in an OData XML document.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Property name.</param>
/// <param name="value">Property value.</param>
/// <param name="typeName" type="String" optional="true">Property type name.</param>
/// <returns type="Object">
/// Object containing the new DOM element in the OData namespace for the EDM property and the
/// required data service version for this property.
/// </returns>
var propertyValue = xmlNewODataPrimitiveValue(value, typeName);
var property = xmlNewODataProperty(dom, name, typeName, propertyValue);
return xmlNewODataElementInfo(property, /*dataServiceVersion*/"1.0");
};
var xmlNewODataNullProperty = function (dom, name, typeName, model) {
/// <summary>Creates a new DOM element for a null property in an OData XML document.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Property name.</param>
/// <param name="typeName" type="String" optional="true">Property type name.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <remarks>
/// If no typeName is specified, then it will be assumed that this is a primitive type property.
/// </remarks>
/// <returns type="Object">
/// Object containing the new DOM element in the OData namespace for the null property and the
/// required data service version for this property.
/// </returns>
var nullAttribute = xmlNewODataMetaAttribute(dom, "null", "true");
var property = xmlNewODataProperty(dom, name, typeName, nullAttribute);
var dataServiceVersion = lookupComplexType(typeName, model) ? "2.0" : "1.0";
return xmlNewODataElementInfo(property, dataServiceVersion);
};
var xmlNewODataCollectionProperty = function (dom, name, value, typeName, collectionMetadata, collectionModel, model) {
/// <summary>Creates a new DOM element for a collection property in an OData XML document.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Property name.</param>
/// <param name="value">Property value either as an array or an object representing a collection in the library's internal representation.</param>
/// <param name="typeName" type="String" optional="true">Property type name.</param>
/// <param name="collectionMetadata" type="Object" optional="true">Object containing metadata about the collection property.</param>
/// <param name="collectionModel" type="Object" optional="true">Object describing the collection property in an OData conceptual schema.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new DOM element in the OData namespace for the collection property and the
/// required data service version for this property.
/// </returns>
var itemTypeName = getCollectionType(typeName);
var items = isArray(value) ? value : value.results;
var itemMetadata = typeName ? { type: itemTypeName} : {};
itemMetadata.properties = collectionMetadata.properties;
var xmlProperty = xmlNewODataProperty(dom, name, itemTypeName ? typeName : null);
var i, len;
for (i = 0, len = items.length; i < len; i++) {
var itemValue = items[i];
var item = xmlNewODataDataElement(dom, "element", itemValue, itemMetadata, collectionModel, model);
xmlAppendChild(xmlProperty, item.element);
}
return xmlNewODataElementInfo(xmlProperty, /*dataServiceVersion*/"3.0");
};
var xmlNewODataComplexProperty = function (dom, name, value, typeName, propertyMetadata, propertyModel, model) {
/// <summary>Creates a new DOM element for a complex type property in an OData XML document.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Property name.</param>
/// <param name="value">Property value as an object in the library's internal representation.</param>
/// <param name="typeName" type="String" optional="true">Property type name.</param>
/// <param name="propertyMetadata" type="Object" optional="true">Object containing metadata about the complex type property.</param>
/// <param name="propertyModel" type="Object" optional="true">Object describing the complex type property in an OData conceptual schema.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new DOM element in the OData namespace for the complex type property and the
/// required data service version for this property.
/// </returns>
var xmlProperty = xmlNewODataProperty(dom, name, typeName);
var complexTypePropertiesMetadata = propertyMetadata.properties || {};
var complexTypeModel = lookupComplexType(typeName, model) || {};
var dataServiceVersion = "1.0";
for (var key in value) {
if (key !== "__metadata") {
var memberValue = value[key];
var memberModel = lookupProperty(complexTypeModel.property, key);
var memberMetadata = complexTypePropertiesMetadata[key] || {};
var member = xmlNewODataDataElement(dom, key, memberValue, memberMetadata, memberModel, model);
dataServiceVersion = maxVersion(dataServiceVersion, member.dsv);
xmlAppendChild(xmlProperty, member.element);
}
}
return xmlNewODataElementInfo(xmlProperty, dataServiceVersion);
};
var xmlNewODataSpatialProperty = function (dom, name, value, typeName, isGeography) {
/// <summary>Creates a new DOM element for an EDM spatial property in an OData XML document.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Property name.</param>
/// <param name="value" type="Object">GeoJSON object containing the property value.</param>
/// <param name="typeName" type="String" optional="true">Property type name.</param>
/// <returns type="Object">
/// Object containing the new DOM element in the OData namespace for the EDM property and the
/// required data service version for this property.
/// </returns>
var geoJsonType = xmlODataInferSpatialValueGeoJsonType(value, typeName);
var gmlRoot = gmlNewODataSpatialValue(dom, value, geoJsonType, isGeography);
var xmlProperty = xmlNewODataProperty(dom, name, typeName, gmlRoot);
return xmlNewODataElementInfo(xmlProperty, "3.0");
};
var xmlNewODataDataElement = function (dom, name, value, dataItemMetadata, dataItemModel, model) {
/// <summary>Creates a new DOM element for a data item in an entry, complex property, or collection property.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Data item name.</param>
/// <param name="value" optional="true" mayBeNull="true">Value of the data item, if any.</param>
/// <param name="dataItemMetadata" type="Object" optional="true">Object containing metadata about the data item.</param>
/// <param name="dataItemModel" type="Object" optional="true">Object describing the data item in an OData conceptual schema.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new DOM element in the appropriate namespace for the data item and the
/// required data service version for it.
/// </returns>
var typeName = dataItemTypeName(value, dataItemMetadata, dataItemModel);
if (isPrimitive(value)) {
return xmlNewODataEdmProperty(dom, name, value, typeName || EDM_STRING);
}
var isGeography = isGeographyEdmType(typeName);
if (isGeography || isGeometryEdmType(typeName)) {
return xmlNewODataSpatialProperty(dom, name, value, typeName, isGeography);
}
if (isCollection(value, typeName)) {
return xmlNewODataCollectionProperty(dom, name, value, typeName, dataItemMetadata, dataItemModel, model);
}
if (isNamedStream(value)) {
return null;
}
// This may be a navigation property.
var navPropKind = navigationPropertyKind(value, dataItemModel);
if (navPropKind !== null) {
return null;
}
if (value === null) {
return xmlNewODataNullProperty(dom, name, typeName);
}
return xmlNewODataComplexProperty(dom, name, value, typeName, dataItemMetadata, dataItemModel, model);
};
var odataNewLinkDocument = function (data) {
/// <summary>Writes the specified data into an OData XML document.</summary>
/// <param name="data">Data to write.</param>
/// <returns>The root of the DOM tree built.</returns>
if (data && isObject(data)) {
var dom = xmlDom();
return xmlAppendChild(dom, xmlNewODataElement(dom, "uri", data.uri));
}
// Allow for undefined to be returned.
};
var xmlParser = function (handler, text) {
/// <summary>Parses an OData XML document.</summary>
/// <param name="handler">This handler.</param>
/// <param name="text" type="String">Document text.</param>
/// <returns>An object representation of the document; undefined if not applicable.</returns>
if (text) {
var doc = xmlParse(text);
var root = xmlFirstChildElement(doc);
if (root) {
return readODataXmlDocument(root);
}
}
// Allow for undefined to be returned.
};
var xmlSerializer = function (handler, data, context) {
/// <summary>Serializes an OData XML object into a document.</summary>
/// <param name="handler">This handler.</param>
/// <param name="data" type="Object">Representation of feed or entry.</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>A text representation of the data object; undefined if not applicable.</returns>
var cType = context.contentType = context.contentType || contentType(xmlMediaType);
if (cType && cType.mediaType === xmlMediaType) {
return xmlSerialize(odataNewLinkDocument(data));
}
return undefined;
};
odata.xmlHandler = handler(xmlParser, xmlSerializer, xmlMediaType, MAX_DATA_SERVICE_VERSION);
var atomPrefix = "a";
var atomXmlNs = w3org + "2005/Atom"; // http://www.w3.org/2005/Atom
var appXmlNs = w3org + "2007/app"; // http://www.w3.org/2007/app
var odataEditMediaPrefix = adoDs + "/edit-media/"; // http://schemas.microsoft.com/ado/2007/08/dataservices/edit-media
var odataMediaResourcePrefix = adoDs + "/mediaresource/"; // http://schemas.microsoft.com/ado/2007/08/dataservices/mediaresource
var odataRelatedLinksPrefix = adoDs + "/relatedlinks/"; // http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks
var atomAcceptTypes = ["application/atom+xml", "application/atomsvc+xml", "application/xml"];
var atomMediaType = atomAcceptTypes[0];
// These are the namespaces that are not considered ATOM extension namespaces.
var nonExtensionNamepaces = [atomXmlNs, appXmlNs, xmlNS, xmlnsNS];
// These are entity property mapping paths that have well-known paths.
var knownCustomizationPaths = {
SyndicationAuthorEmail: "author/email",
SyndicationAuthorName: "author/name",
SyndicationAuthorUri: "author/uri",
SyndicationContributorEmail: "contributor/email",
SyndicationContributorName: "contributor/name",
SyndicationContributorUri: "contributor/uri",
SyndicationPublished: "published",
SyndicationRights: "rights",
SyndicationSummary: "summary",
SyndicationTitle: "title",
SyndicationUpdated: "updated"
};
var expandedFeedCustomizationPath = function (path) {
/// <summary>Returns an expanded customization path if it's well-known.</summary>
/// <param name="path" type="String">Path to expand.</param>
/// <returns type="String">Expanded path or just 'path' otherwise.</returns>
return knownCustomizationPaths[path] || path;
};
var isExtensionNs = function (nsURI) {
/// <summary>Checks whether the specified namespace is an extension namespace to ATOM.</summary>
/// <param type="String" name="nsURI">Namespace to check.</param>
/// <returns type="Boolean">true if nsURI is an extension namespace to ATOM; false otherwise.</returns>
return !(contains(nonExtensionNamepaces, nsURI));
};
var atomFeedCustomization = function (customizationModel, entityType, model, propertyName, suffix) {
/// <summary>Creates an object describing a feed customization that was delcared in an OData conceptual schema.</summary>
/// <param name="customizationModel" type="Object">Object describing the customization delcared in the conceptual schema.</param>
/// <param name="entityType" type="Object">Object describing the entity type that owns the customization in an OData conceputal schema.</param>
/// <param name="model" type="Object">Object describing an OData conceptual schema.</param>
/// <param name="propertyName" type="String" optional="true">Name of the property to which this customization applies.</param>
/// <param name="suffix" type="String" optional="true">Suffix to feed customization properties in the conceptual schema.</param>
/// <returns type="Object">Object that describes an applicable feed customization.</returns>
suffix = suffix || "";
var targetPath = customizationModel["FC_TargetPath" + suffix];
if (!targetPath) {
return null;
}
var sourcePath = customizationModel["FC_SourcePath" + suffix];
var targetXmlPath = expandedFeedCustomizationPath(targetPath);
var propertyPath = propertyName ? propertyName + (sourcePath ? "/" + sourcePath : "") : sourcePath;
var propertyType = propertyPath && lookupPropertyType(model, entityType, propertyPath);
var nsURI = customizationModel["FC_NsUri" + suffix] || null;
var nsPrefix = customizationModel["FC_NsPrefix" + suffix] || null;
var keepinContent = customizationModel["FC_KeepInContent" + suffix] || "";
if (targetPath !== targetXmlPath) {
nsURI = atomXmlNs;
nsPrefix = atomPrefix;
}
return {
contentKind: customizationModel["FC_ContentKind" + suffix],
keepInContent: keepinContent.toLowerCase() === "true",
nsPrefix: nsPrefix,
nsURI: nsURI,
propertyPath: propertyPath,
propertyType: propertyType,
entryPath: targetXmlPath
};
};
var atomApplyAllFeedCustomizations = function (entityType, model, callback) {
/// <summary>Gets all the feed customizations that have to be applied to an entry as per the enity type declared in an OData conceptual schema.</summary>
/// <param name="entityType" type="Object">Object describing an entity type in a conceptual schema.</param>
/// <param name="model" type="Object">Object describing an OData conceptual schema.</param>
/// <param name="callback" type="Function">Callback function to be invoked for each feed customization that needs to be applied.</param>
var customizations = [];
while (entityType) {
var sourcePath = entityType.FC_SourcePath;
var customization = atomFeedCustomization(entityType, entityType, model);
if (customization) {
callback(customization);
}
var properties = entityType.property || [];
var i, len;
for (i = 0, len = properties.length; i < len; i++) {
var property = properties[i];
var suffixCounter = 0;
var suffix = "";
while (customization = atomFeedCustomization(property, entityType, model, property.name, suffix)) {
callback(customization);
suffixCounter++;
suffix = "_" + suffixCounter;
}
}
entityType = lookupEntityType(entityType.baseType, model);
}
return customizations;
};
var atomReadExtensionAttributes = function (domElement) {
/// <summary>Reads ATOM extension attributes (any attribute not in the Atom namespace) from a DOM element.</summary>
/// <param name="domElement">DOM element with zero or more extension attributes.</param>
/// <returns type="Array">An array of extension attribute representations.</returns>
var extensions = [];
xmlAttributes(domElement, function (attribute) {
var nsURI = xmlNamespaceURI(attribute);
if (isExtensionNs(nsURI)) {
extensions.push(createAttributeExtension(attribute, true));
}
});
return extensions;
};
var atomReadExtensionElement = function (domElement) {
/// <summary>Reads an ATOM extension element (an element not in the ATOM namespaces).</summary>
/// <param name="domElement">DOM element not part of the atom namespace.</param>
/// <returns type="Object">Object representing the extension element.</returns>
return createElementExtension(domElement, /*addNamespaceURI*/true);
};
var atomReadDocument = function (domElement, baseURI, model) {
/// <summary>Reads an ATOM entry, feed or service document, producing an object model in return.</summary>
/// <param name="domElement">Top-level ATOM DOM element to read.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the ATOM document.</param>
/// <param name="model" type="Object">Object that describes the conceptual schema.</param>
/// <returns type="Object">The object model representing the specified element, undefined if the top-level element is not part of the ATOM specification.</returns>
var nsURI = xmlNamespaceURI(domElement);
var localName = xmlLocalName(domElement);
// Handle service documents.
if (nsURI === appXmlNs && localName === "service") {
return atomReadServiceDocument(domElement, baseURI);
}
// Handle feed and entry elements.
if (nsURI === atomXmlNs) {
if (localName === "feed") {
return atomReadFeed(domElement, baseURI, model);
}
if (localName === "entry") {
return atomReadEntry(domElement, baseURI, model);
}
}
// Allow undefined to be returned.
};
var atomReadAdvertisedActionOrFunction = function (domElement, baseURI) {
/// <summary>Reads the DOM element for an action or a function in an OData Atom document.</summary>
/// <param name="domElement">DOM element to read.</param>
/// <param name="baseURI" type="String">Base URI for normalizing the action or function target url.</param>
/// <returns type="Object">Object with title, target, and metadata fields.</returns>
var extensions = [];
var result = { extensions: extensions };
xmlAttributes(domElement, function (attribute) {
var localName = xmlLocalName(attribute);
var nsURI = xmlNamespaceURI(attribute);
var value = xmlNodeValue(attribute);
if (nsURI === null) {
if (localName === "title" || localName === "metadata") {
result[localName] = value;
return;
}
if (localName === "target") {
result.target = normalizeURI(value, xmlBaseURI(domElement, baseURI));
return;
}
}
if (isExtensionNs(nsURI)) {
extensions.push(createAttributeExtension(attribute, true));
}
});
return result;
};
var atomReadAdvertisedAction = function (domElement, baseURI, parentMetadata) {
/// <summary>Reads the DOM element for an action in an OData Atom document.</summary>
/// <param name="domElement">DOM element to read.</param>
/// <param name="baseURI" type="String">Base URI for normalizing the action or target url.</param>
/// <param name="parentMetadata" type="Object">Object to update with the action metadata.</param>
var actions = parentMetadata.actions = parentMetadata.actions || [];
actions.push(atomReadAdvertisedActionOrFunction(domElement, baseURI));
};
var atomReadAdvertisedFunction = function (domElement, baseURI, parentMetadata) {
/// <summary>Reads the DOM element for an action in an OData Atom document.</summary>
/// <param name="domElement">DOM element to read.</param>
/// <param name="baseURI" type="String">Base URI for normalizing the action or target url.</param>
/// <param name="parentMetadata" type="Object">Object to update with the action metadata.</param>
var functions = parentMetadata.functions = parentMetadata.functions || [];
functions.push(atomReadAdvertisedActionOrFunction(domElement, baseURI));
};
var atomReadFeed = function (domElement, baseURI, model) {
/// <summary>Reads a DOM element for an ATOM feed, producing an object model in return.</summary>
/// <param name="domElement">ATOM feed DOM element.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the ATOM feed.</param>
/// <param name="model">Metadata that describes the conceptual schema.</param>
/// <returns type="Object">A new object representing the feed.</returns>
var extensions = atomReadExtensionAttributes(domElement);
var feedMetadata = { feed_extensions: extensions };
var results = [];
var feed = { __metadata: feedMetadata, results: results };
baseURI = xmlBaseURI(domElement, baseURI);
xmlChildElements(domElement, function (child) {
var nsURI = xmlNamespaceURI(child);
var localName = xmlLocalName(child);
if (nsURI === odataMetaXmlNs) {
if (localName === "count") {
feed.__count = parseInt(xmlInnerText(child), 10);
return;
}
if (localName === "action") {
atomReadAdvertisedAction(child, baseURI, feedMetadata);
return;
}
if (localName === "function") {
atomReadAdvertisedFunction(child, baseURI, feedMetadata);
return;
}
}
if (isExtensionNs(nsURI)) {
extensions.push(createElementExtension(child));
return;
}
// The element should belong to the ATOM namespace.
if (localName === "entry") {
results.push(atomReadEntry(child, baseURI, model));
return;
}
if (localName === "link") {
atomReadFeedLink(child, feed, baseURI);
return;
}
if (localName === "id") {
feedMetadata.uri = normalizeURI(xmlInnerText(child), baseURI);
feedMetadata.uri_extensions = atomReadExtensionAttributes(child);
return;
}
if (localName === "title") {
feedMetadata.title = xmlInnerText(child) || "";
feedMetadata.title_extensions = atomReadExtensionAttributes(child);
return;
}
});
return feed;
};
var atomReadFeedLink = function (domElement, feed, baseURI) {
/// <summary>Reads an ATOM link DOM element for a feed.</summary>
/// <param name="domElement">ATOM link DOM element.</param>
/// <param name="feed">Feed object to be annotated with the link data.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
var link = atomReadLink(domElement, baseURI);
var href = link.href;
var rel = link.rel;
var extensions = link.extensions;
var metadata = feed.__metadata;
if (rel === "next") {
feed.__next = href;
metadata.next_extensions = extensions;
return;
}
if (rel === "self") {
metadata.self = href;
metadata.self_extensions = extensions;
return;
}
};
var atomReadLink = function (domElement, baseURI) {
/// <summary>Reads an ATOM link DOM element.</summary>
/// <param name="linkElement">DOM element to read.</param>
/// <param name="baseURI" type="String">Base URI for normalizing the link href.</param>
/// <returns type="Object">A link element representation.</returns>
baseURI = xmlBaseURI(domElement, baseURI);
var extensions = [];
var link = { extensions: extensions, baseURI: baseURI };
xmlAttributes(domElement, function (attribute) {
var nsURI = xmlNamespaceURI(attribute);
var localName = xmlLocalName(attribute);
var value = attribute.value;
if (localName === "href") {
link.href = normalizeURI(value, baseURI);
return;
}
if (localName === "type" || localName === "rel") {
link[localName] = value;
return;
}
if (isExtensionNs(nsURI)) {
extensions.push(createAttributeExtension(attribute, true));
}
});
if (!link.href) {
throw { error: "href attribute missing on link element", element: domElement };
}
return link;
};
var atomGetObjectValueByPath = function (path, item) {
/// <summary>Gets a slashed path value from the specified item.</summary>
/// <param name="path" type="String">Property path to read ('/'-separated).</param>
/// <param name="item" type="Object">Object to get value from.</param>
/// <returns>The property value, possibly undefined if any path segment is missing.</returns>
// Fast path.
if (path.indexOf('/') === -1) {
return item[path];
} else {
var parts = path.split('/');
var i, len;
for (i = 0, len = parts.length; i < len; i++) {
// Avoid traversing a null object.
if (item === null) {
return undefined;
}
item = item[parts[i]];
if (item === undefined) {
return item;
}
}
return item;
}
};
var atomSetEntryValueByPath = function (path, target, value, propertyType) {
/// <summary>Sets a slashed path value on the specified target.</summary>
/// <param name="path" type="String">Property path to set ('/'-separated).</param>
/// <param name="target" type="Object">Object to set value on.</param>
/// <param name="value">Value to set.</param>
/// <param name="propertyType" type="String" optional="true">Property type to set in metadata.</param>
var propertyName;
if (path.indexOf('/') === -1) {
target[path] = value;
propertyName = path;
} else {
var parts = path.split('/');
var i, len;
for (i = 0, len = (parts.length - 1); i < len; i++) {
// We construct each step of the way if the property is missing;
// if it's already initialized to null, we stop further processing.
var next = target[parts[i]];
if (next === undefined) {
next = {};
target[parts[i]] = next;
} else if (next === null) {
return;
}
target = next;
}
propertyName = parts[i];
target[propertyName] = value;
}
if (propertyType) {
var metadata = target.__metadata = target.__metadata || {};
var properties = metadata.properties = metadata.properties || {};
var property = properties[propertyName] = properties[propertyName] || {};
property.type = propertyType;
}
};
var atomApplyCustomizationToEntryObject = function (customization, domElement, entry) {
/// <summary>Applies a specific feed customization item to an object.</summary>
/// <param name="customization">Object with customization description.</param>
/// <param name="sourcePath">Property path to set ('source' in the description).</param>
/// <param name="entryElement">XML element for the entry that corresponds to the object being read.</param>
/// <param name="entryObject">Object being read.</param>
/// <param name="propertyType" type="String">Name of property type to set.</param>
/// <param name="suffix" type="String">Suffix to feed customization properties.</param>
var propertyPath = customization.propertyPath;
// If keepInConent equals true or the property value is null we do nothing as this overrides any other customization.
if (customization.keepInContent || atomGetObjectValueByPath(propertyPath, entry) === null) {
return;
}
var xmlNode = xmlFindNodeByPath(domElement, customization.nsURI, customization.entryPath);
// If the XML tree does not contain the necessary elements to read the value,
// then it shouldn't be considered null, but rather ignored at all. This prevents
// the customization from generating the object path down to the property.
if (!xmlNode) {
return;
}
var propertyType = customization.propertyType;
var propertyValue;
if (customization.contentKind === "xhtml") {
// Treat per XHTML in http://tools.ietf.org/html/rfc4287#section-3.1.1, including the DIV
// in the content.
propertyValue = xmlSerializeDescendants(xmlNode);
} else {
propertyValue = xmlReadODataEdmPropertyValue(xmlNode, propertyType || "Edm.String");
}
// Set the value on the entry.
atomSetEntryValueByPath(propertyPath, entry, propertyValue, propertyType);
};
var lookupPropertyType = function (metadata, owningType, path) {
/// <summary>Looks up the type of a property given its path in an entity type.</summary>
/// <param name="metadata">Metadata in which to search for base and complex types.</param>
/// <param name="owningType">Type to which property belongs.</param>
/// <param name="path" type="String" mayBeNull="false">Property path to look at.</param>
/// <returns type="String">The name of the property type; possibly null.</returns>
var parts = path.split("/");
var i, len;
while (owningType) {
// Keep track of the type being traversed, necessary for complex types.
var traversedType = owningType;
for (i = 0, len = parts.length; i < len; i++) {
// Traverse down the structure as necessary.
var properties = traversedType.property;
if (!properties) {
break;
}
// Find the property by scanning the property list (might be worth pre-processing).
var propertyFound = lookupProperty(properties, parts[i]);
if (!propertyFound) {
break;
}
var propertyType = propertyFound.type;
// We could in theory still be missing types, but that would
// be caused by a malformed path.
if (!propertyType || isPrimitiveEdmType(propertyType)) {
return propertyType || null;
}
traversedType = lookupComplexType(propertyType, metadata);
if (!traversedType) {
return null;
}
}
// Traverse up the inheritance chain.
owningType = lookupEntityType(owningType.baseType, metadata);
}
return null;
};
var atomReadEntry = function (domElement, baseURI, model) {
/// <summary>Reads a DOM element for an ATOM entry, producing an object model in return.</summary>
/// <param name="domElement">ATOM entry DOM element.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the ATOM entry.</param>
/// <param name="model">Metadata that describes the conceptual schema.</param>
/// <returns type="Object">A new object representing the entry.</returns>
var entryMetadata = {};
var entry = { __metadata: entryMetadata };
var etag = xmlAttributeValue(domElement, "etag", odataMetaXmlNs);
if (etag) {
entryMetadata.etag = etag;
}
baseURI = xmlBaseURI(domElement, baseURI);
xmlChildElements(domElement, function (child) {
var nsURI = xmlNamespaceURI(child);
var localName = xmlLocalName(child);
if (nsURI === atomXmlNs) {
if (localName === "id") {
atomReadEntryId(child, entryMetadata, baseURI);
return;
}
if (localName === "category") {
atomReadEntryType(child, entryMetadata);
return;
}
if (localName === "content") {
atomReadEntryContent(child, entry, entryMetadata, baseURI);
return;
}
if (localName === "link") {
atomReadEntryLink(child, entry, entryMetadata, baseURI, model);
return;
}
return;
}
if (nsURI === odataMetaXmlNs) {
if (localName === "properties") {
atomReadEntryStructuralObject(child, entry, entryMetadata);
return;
}
if (localName === "action") {
atomReadAdvertisedAction(child, baseURI, entryMetadata);
return;
}
if (localName === "function") {
atomReadAdvertisedFunction(child, baseURI, entryMetadata);
return;
}
}
});
// Apply feed customizations if applicable
var entityType = lookupEntityType(entryMetadata.type, model);
atomApplyAllFeedCustomizations(entityType, model, function (customization) {
atomApplyCustomizationToEntryObject(customization, domElement, entry);
});
return entry;
};
var atomReadEntryId = function (domElement, entryMetadata, baseURI) {
/// <summary>Reads an ATOM entry id DOM element.</summary>
/// <param name="domElement">ATOM id DOM element.</param>
/// <param name="entryMetadata">Entry metadata object to update with the id information.</param>
entryMetadata.uri = normalizeURI(xmlInnerText(domElement), xmlBaseURI(domElement, baseURI));
entryMetadata.uri_extensions = atomReadExtensionAttributes(domElement);
};
var atomReadEntryType = function (domElement, entryMetadata) {
/// <summary>Reads type information from an ATOM category DOM element.</summary>
/// <param name="domElement">ATOM category DOM element.</param>
/// <param name="entryMetadata">Entry metadata object to update with the type information.</param>
if (xmlAttributeValue(domElement, "scheme") === odataScheme) {
if (entryMetadata.type) {
throw { message: "Invalid AtomPub document: multiple category elements defining the entry type were encounterd withing an entry", element: domElement };
}
var typeExtensions = [];
xmlAttributes(domElement, function (attribute) {
var nsURI = xmlNamespaceURI(attribute);
var localName = xmlLocalName(attribute);
if (!nsURI) {
if (localName !== "scheme" && localName !== "term") {
typeExtensions.push(createAttributeExtension(attribute, true));
}
return;
}
if (isExtensionNs(nsURI)) {
typeExtensions.push(createAttributeExtension(attribute, true));
}
});
entryMetadata.type = xmlAttributeValue(domElement, "term");
entryMetadata.type_extensions = typeExtensions;
}
};
var atomReadEntryContent = function (domElement, entry, entryMetadata, baseURI) {
/// <summary>Reads an ATOM content DOM element.</summary>
/// <param name="domElement">ATOM content DOM element.</param>
/// <param name="entry">Entry object to update with information.</param>
/// <param name="entryMetadata">Entry metadata object to update with the content information.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the Atom entry content.</param>
var src = xmlAttributeValue(domElement, "src");
var type = xmlAttributeValue(domElement, "type");
if (src) {
if (!type) {
throw {
message: "Invalid AtomPub document: content element must specify the type attribute if the src attribute is also specified",
element: domElement
};
}
entryMetadata.media_src = normalizeURI(src, xmlBaseURI(domElement, baseURI));
entryMetadata.content_type = type;
}
xmlChildElements(domElement, function (child) {
if (src) {
throw { message: "Invalid AtomPub document: content element must not have child elements if the src attribute is specified", element: domElement };
}
if (xmlNamespaceURI(child) === odataMetaXmlNs && xmlLocalName(child) === "properties") {
atomReadEntryStructuralObject(child, entry, entryMetadata);
}
});
};
var atomReadEntryLink = function (domElement, entry, entryMetadata, baseURI, model) {
/// <summary>Reads a link element on an entry.</summary>
/// <param name="atomEntryLink">'link' element on the entry.</param>
/// <param name="entry" type="Object">Entry object to update with the link data.</param>
/// <param name="entryMetadata">Entry metadata object to update with the link metadata.</param>
/// <param name="baseURI" type="String">Base URI for normalizing the link href.</param>
/// <param name="model" type="Object">Metadata that describes the conceptual schema.</param>
var link = atomReadLink(domElement, baseURI);
var rel = link.rel;
var href = link.href;
var extensions = link.extensions;
if (rel === "self") {
entryMetadata.self = href;
entryMetadata.self_link_extensions = extensions;
return;
}
if (rel === "edit") {
entryMetadata.edit = href;
entryMetadata.edit_link_extensions = extensions;
return;
}
if (rel === "edit-media") {
entryMetadata.edit_media = link.href;
entryMetadata.edit_media_extensions = extensions;
atomReadLinkMediaEtag(link, entryMetadata);
return;
}
// This might be a named stream edit link
if (rel.indexOf(odataEditMediaPrefix) === 0) {
atomReadNamedStreamEditLink(link, entry, entryMetadata);
return;
}
// This might be a named stram media resource (read) link
if (rel.indexOf(odataMediaResourcePrefix) === 0) {
atomReadNamedStreamSelfLink(link, entry, entryMetadata);
return;
}
// This might be a navigation property
if (rel.indexOf(odataRelatedPrefix) === 0) {
atomReadNavPropLink(domElement, link, entry, entryMetadata, model);
return;
}
if (rel.indexOf(odataRelatedLinksPrefix) === 0) {
atomReadNavPropRelatedLink(link, entryMetadata);
return;
}
};
var atomReadNavPropRelatedLink = function (link, entryMetadata) {
/// <summary>Reads a link represnting the links related to a navigation property in an OData Atom document.</summary>
/// <param name="link" type="Object">Object representing the parsed link DOM element.</param>
/// <param name="entryMetadata" type="Object">Entry metadata object to update with the related links information.</param>
var propertyName = link.rel.substring(odataRelatedLinksPrefix.length);
// Set the extra property information on the entry object metadata.
entryMetadata.properties = entryMetadata.properties || {};
var propertyMetadata = entryMetadata.properties[propertyName] = entryMetadata.properties[propertyName] || {};
propertyMetadata.associationuri = link.href;
propertyMetadata.associationuri_extensions = link.extensions;
};
var atomReadNavPropLink = function (domElement, link, entry, entryMetadata, model) {
/// <summary>Reads a link representing a navigation property in an OData Atom document.</summary>
/// <param name="domElement">DOM element for a navigation property in an OData Atom document.</summary>
/// <param name="link" type="Object">Object representing the parsed link DOM element.</param>
/// <param name="entry" type="Object">Entry object to update with the navigation property.</param>
/// <param name="entryMetadata">Entry metadata object to update with the navigation property metadata.</param>
/// <param name="model" type="Object">Metadata that describes the conceptual schema.</param>
// Get any inline data.
var inlineData;
var inlineElement = xmlFirstChildElement(domElement, odataMetaXmlNs, "inline");
if (inlineElement) {
var inlineDocRoot = xmlFirstChildElement(inlineElement);
var inlineBaseURI = xmlBaseURI(inlineElement, link.baseURI);
inlineData = inlineDocRoot ? atomReadDocument(inlineDocRoot, inlineBaseURI, model) : null;
} else {
// If the link has no inline content, we consider it deferred.
inlineData = { __deferred: { uri: link.href} };
}
var propertyName = link.rel.substring(odataRelatedPrefix.length);
// Set the property value on the entry object.
entry[propertyName] = inlineData;
// Set the extra property information on the entry object metadata.
entryMetadata.properties = entryMetadata.properties || {};
var propertyMetadata = entryMetadata.properties[propertyName] = entryMetadata.properties[propertyName] || {};
propertyMetadata.extensions = link.extensions;
};
var atomReadNamedStreamEditLink = function (link, entry, entryMetadata) {
/// <summary>Reads a link representing the edit-media url of a named stream in an OData Atom document.</summary>
/// <param name="link" type="Object">Object representing the parsed link DOM element.</param>
/// <param name="entry" type="Object">Entry object to update with the named stream data.</param>
/// <param name="entryMetadata">Entry metadata object to update with the named stream metadata.</param>
var propertyName = link.rel.substring(odataEditMediaPrefix.length);
var namedStreamMediaResource = atomGetEntryNamedStreamMediaResource(propertyName, entry, entryMetadata);
var mediaResource = namedStreamMediaResource.value;
var mediaResourceMetadata = namedStreamMediaResource.metadata;
var editMedia = link.href;
mediaResource.edit_media = editMedia;
mediaResource.content_type = link.type;
mediaResourceMetadata.edit_media_extensions = link.extensions;
// If there is only the edit link, make it the media self link as well.
mediaResource.media_src = mediaResource.media_src || editMedia;
mediaResourceMetadata.media_src_extensions = mediaResourceMetadata.media_src_extensions || [];
atomReadLinkMediaEtag(link, mediaResource);
};
var atomReadNamedStreamSelfLink = function (link, entry, entryMetadata) {
/// <summary>Reads a link representing the self url of a named stream in an OData Atom document.</summary>
/// <param name="link" type="Object">Object representing the parsed link DOM element.</param>
/// <param name="entry" type="Object">Entry object to update with the named stream data.</param>
/// <param name="entryMetadata">Entry metadata object to update with the named stream metadata.</param>
var propertyName = link.rel.substring(odataMediaResourcePrefix.length);
var namedStreamMediaResource = atomGetEntryNamedStreamMediaResource(propertyName, entry, entryMetadata);
var mediaResource = namedStreamMediaResource.value;
var mediaResourceMetadata = namedStreamMediaResource.metadata;
mediaResource.media_src = link.href;
mediaResourceMetadata.media_src_extensions = link.extensions;
mediaResource.content_type = link.type;
};
var atomGetEntryNamedStreamMediaResource = function (name, entry, entryMetadata) {
/// <summary>Gets the media resource object and metadata object for a named stream in an entry object.</summary>
/// <param name="link" type="Object">Object representing the parsed link DOM element.</param>
/// <param name="entry" type="Object">Entry object from which the media resource object will be obtained.</param>
/// <param name="entryMetadata" type="Object">Entry metadata object from which the media resource metadata object will be obtained.</param>
/// <remarks>
/// If the entry doest' have a media resource for the named stream indicated by the name argument, then this function will create a new
/// one along with its metadata object.
/// <remarks>
/// <returns type="Object"> Object containing the value and metadata of the named stream's media resource. <returns>
entryMetadata.properties = entryMetadata.properties || {};
var mediaResourceMetadata = entryMetadata.properties[name];
var mediaResource = entry[name] && entry[name].__mediaresource;
if (!mediaResource) {
mediaResource = {};
entry[name] = { __mediaresource: mediaResource };
entryMetadata.properties[name] = mediaResourceMetadata = {};
}
return { value: mediaResource, metadata: mediaResourceMetadata };
};
var atomReadLinkMediaEtag = function (link, mediaResource) {
/// <summary>Gets the media etag from the link extensions and updates the media resource object with it.</summary>
/// <param name="link" type="Object">Object representing the parsed link DOM element.</param>
/// <param name="mediaResource" type="Object">Object containing media information for an OData Atom entry.</param>
/// <remarks>
/// The function will remove the extension object for the etag if it finds it in the link extensions and will set
/// its value under the media_etag property of the mediaResource object.
/// <remarks>
/// <returns type="Object"> Object containing the value and metadata of the named stream's media resource. <returns>
var extensions = link.extensions;
var i, len;
for (i = 0, len = extensions.length; i < len; i++) {
if (extensions[i].namespaceURI === odataMetaXmlNs && extensions[i].name === "etag") {
mediaResource.media_etag = extensions[i].value;
extensions.splice(i, 1);
return;
}
}
};
var atomReadEntryStructuralObject = function (domElement, parent, parentMetadata) {
/// <summary>Reads an atom entry's property as a structural object and sets its value in the parent and the metadata in the parentMetadata objects.</summary>
/// <param name="propertiesElement">XML element for the 'properties' node.</param>
/// <param name="parent">
/// Object that will contain the property value. It can be either an antom entry or
/// an atom complex property object.
/// </param>
/// <param name="parentMetadata">Object that will contain the property metadata. It can be either an atom entry metadata or a complex property metadata object</param>
xmlChildElements(domElement, function (child) {
var property = xmlReadODataProperty(child);
if (property) {
var propertyName = property.name;
var propertiesMetadata = parentMetadata.properties = parentMetadata.properties || {};
propertiesMetadata[propertyName] = property.metadata;
parent[propertyName] = property.value;
}
});
};
var atomReadServiceDocument = function (domElement, baseURI) {
/// <summary>Reads an AtomPub service document</summary>
/// <param name="atomServiceDoc">DOM element for the root of an AtomPub service document</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the AtomPub service document.</param>
/// <returns type="Object">An object that contains the properties of the service document</returns>
var workspaces = [];
var extensions = [];
baseURI = xmlBaseURI(domElement, baseURI);
// Find all the workspace elements.
xmlChildElements(domElement, function (child) {
if (xmlNamespaceURI(child) === appXmlNs && xmlLocalName(child) === "workspace") {
workspaces.push(atomReadServiceDocumentWorkspace(child, baseURI));
return;
}
extensions.push(createElementExtension(child));
});
// AtomPub (RFC 5023 Section 8.3.1) says a service document MUST contain one or
// more workspaces. Throw if we don't find any.
if (workspaces.length === 0) {
throw { message: "Invalid AtomPub service document: No workspace element found.", element: domElement };
}
return { workspaces: workspaces, extensions: extensions };
};
var atomReadServiceDocumentWorkspace = function (domElement, baseURI) {
/// <summary>Reads a single workspace element from an AtomPub service document</summary>
/// <param name="domElement">DOM element that represents a workspace of an AtomPub service document</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the AtomPub service document workspace.</param>
/// <returns type="Object">An object that contains the properties of the workspace</returns>
var collections = [];
var extensions = [];
var title; // = undefined;
baseURI = xmlBaseURI(domElement, baseURI);
xmlChildElements(domElement, function (child) {
var nsURI = xmlNamespaceURI(child);
var localName = xmlLocalName(child);
if (nsURI === atomXmlNs) {
if (localName === "title") {
if (title !== undefined) {
throw { message: "Invalid AtomPub service document: workspace has more than one child title element", element: child };
}
title = xmlInnerText(child);
return;
}
}
if (nsURI === appXmlNs) {
if (localName === "collection") {
collections.push(atomReadServiceDocumentCollection(child, baseURI));
}
return;
}
extensions.push(atomReadExtensionElement(child));
});
return { title: title || "", collections: collections, extensions: extensions };
};
var atomReadServiceDocumentCollection = function (domElement, baseURI) {
/// <summary>Reads a service document collection element into an object.</summary>
/// <param name="domElement">DOM element that represents a collection of an AtomPub service document.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the AtomPub service document collection.</param>
/// <returns type="Object">An object that contains the properties of the collection.</returns>
var href = xmlAttributeValue(domElement, "href");
if (!href) {
throw { message: "Invalid AtomPub service document: collection has no href attribute", element: domElement };
}
baseURI = xmlBaseURI(domElement, baseURI);
href = normalizeURI(href, xmlBaseURI(domElement, baseURI));
var extensions = [];
var title; // = undefined;
xmlChildElements(domElement, function (child) {
var nsURI = xmlNamespaceURI(child);
var localName = xmlLocalName(child);
if (nsURI === atomXmlNs) {
if (localName === "title") {
if (title !== undefined) {
throw { message: "Invalid AtomPub service document: collection has more than one child title element", element: child };
}
title = xmlInnerText(child);
}
return;
}
if (nsURI !== appXmlNs) {
extensions.push(atomReadExtensionElement(domElement));
}
});
// AtomPub (RFC 5023 Section 8.3.3) says the collection element MUST contain
// a title element. It's likely to be problematic if the service doc doesn't
// have one so here we throw.
if (!title) {
throw { message: "Invalid AtomPub service document: collection has no title element", element: domElement };
}
return { title: title, href: href, extensions: extensions };
};
var atomNewElement = function (dom, name, children) {
/// <summary>Creates a new DOM element in the Atom namespace.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Local name of the Atom element to create.</param>
/// <param name="children" type="Array">Array containing DOM nodes or string values that will be added as children of the new DOM element.</param>
/// <returns>New DOM element in the Atom namespace.</returns>
/// <remarks>
/// If a value in the children collection is a string, then a new DOM text node is going to be created
/// for it and then appended as a child of the new DOM Element.
/// </remarks>
return xmlNewElement(dom, atomXmlNs, xmlQualifiedName(atomPrefix, name), children);
};
var atomNewAttribute = function (dom, name, value) {
/// <summary>Creates a new DOM attribute for an Atom element in the default namespace.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Local name of the OData attribute to create.</param>
/// <param name="value">Attribute value.</param>
/// <returns>New DOM attribute in the default namespace.</returns>
return xmlNewAttribute(dom, null, name, value);
};
var atomCanRemoveProperty = function (propertyElement) {
/// <summary>Checks whether the property represented by domElement can be removed from the atom document DOM tree.</summary>
/// <param name="propertyElement">DOM element for the property to test.</param>
/// <remarks>
/// The property can only be removed if it doens't have any children and only has namespace or type declaration attributes.
/// </remarks>
/// <returns type="Boolean">True is the property can be removed; false otherwise.</returns>
if (propertyElement.childNodes.length > 0) {
return false;
}
var isEmpty = true;
var attributes = propertyElement.attributes;
var i, len;
for (i = 0, len = attributes.length; i < len && isEmpty; i++) {
var attribute = attributes[i];
isEmpty = isEmpty && isXmlNSDeclaration(attribute) ||
(xmlNamespaceURI(attribute) == odataMetaXmlNs && xmlLocalName(attribute) === "type");
}
return isEmpty;
};
var atomNewODataNavigationProperty = function (dom, name, kind, value, model) {
/// <summary>Creates a new Atom link DOM element for a navigation property in an OData Atom document.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Property name.</param>
/// <param name="kind" type="String">Navigation property kind. Expected values are "deferred", "entry", or "feed".</param>
/// <param name="value" optional="true" mayBeNull="true">Value of the navigation property, if any.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new Atom link DOM element for the navigation property and the
/// required data service version for this property.
/// </returns>
var linkType = null;
var linkContent = null;
var linkContentBodyData = null;
var href = "";
if (kind !== "deferred") {
linkType = atomNewAttribute(dom, "type", "application/atom+xml;type=" + kind);
linkContent = xmlNewODataMetaElement(dom, "inline");
if (value) {
href = value.__metadata && value.__metadata.uri || "";
linkContentBodyData =
atomNewODataFeed(dom, value, model) ||
atomNewODataEntry(dom, value, model);
xmlAppendChild(linkContent, linkContentBodyData.element);
}
} else {
href = value.__deferred.uri;
}
var navProp = atomNewElement(dom, "link", [
atomNewAttribute(dom, "href", href),
atomNewAttribute(dom, "rel", normalizeURI(name, odataRelatedPrefix)),
linkType,
linkContent
]);
return xmlNewODataElementInfo(navProp, linkContentBodyData ? linkContentBodyData.dsv : "1.0");
};
var atomNewODataEntryDataItem = function (dom, name, value, dataItemMetadata, dataItemModel, model) {
/// <summary>Creates a new DOM element for a data item in an entry, complex property, or collection property.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="name" type="String">Data item name.</param>
/// <param name="value" optional="true" mayBeNull="true">Value of the data item, if any.</param>
/// <param name="dataItemMetadata" type="Object" optional="true">Object containing metadata about the data item.</param>
/// <param name="dataItemModel" type="Object" optional="true">Object describing the data item in an OData conceptual schema.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new DOM element in the appropriate namespace for the data item and the
/// required data service version for it.
/// </returns>
if (isNamedStream(value)) {
return null;
}
var dataElement = xmlNewODataDataElement(dom, name, value, dataItemMetadata, dataItemModel, model);
if (!dataElement) {
// This may be a navigation property.
var navPropKind = navigationPropertyKind(value, dataItemModel);
dataElement = atomNewODataNavigationProperty(dom, name, navPropKind, value, model);
}
return dataElement;
};
var atomEntryCustomization = function (dom, entry, entryProperties, customization) {
/// <summary>Applies a feed customization by transforming an Atom entry DOM element as needed.</summary>
/// <param name="dom">DOM document used for creating any new DOM nodes required by the customization.</param>
/// <param name="entry">DOM element for the Atom entry to which the customization is going to be applied.</param>
/// <param name="entryProperties">DOM element containing the properties of the Atom entry.</param>
/// <param name="customization" type="Object">Object describing an applicable feed customization.</param>
/// <remarks>
/// Look into the atomfeedCustomization function for a description of the customization object.
/// </remarks>
/// <returns type="String">Data service version required by the applied customization</returns>
var atomProperty = xmlFindElementByPath(entryProperties, odataXmlNs, customization.propertyPath);
var atomPropertyNullAttribute = atomProperty && xmlAttributeNode(atomProperty, "null", odataMetaXmlNs);
var atomPropertyValue;
var dataServiceVersion = "1.0";
if (atomPropertyNullAttribute && atomPropertyNullAttribute.value === "true") {
return dataServiceVersion;
}
if (atomProperty) {
atomPropertyValue = xmlInnerText(atomProperty) || "";
if (!customization.keepInContent) {
dataServiceVersion = "2.0";
var parent = atomProperty.parentNode;
var candidate = parent;
parent.removeChild(atomProperty);
while (candidate !== entryProperties && atomCanRemoveProperty(candidate)) {
parent = candidate.parentNode;
parent.removeChild(candidate);
candidate = parent;
}
}
}
var targetNode = xmlNewNodeByPath(dom, entry,
customization.nsURI, customization.nsPrefix, customization.entryPath);
if (targetNode.nodeType === 2) {
targetNode.value = atomPropertyValue;
return dataServiceVersion;
}
var contentKind = customization.contentKind;
xmlAppendChildren(targetNode, [
contentKind && xmlNewAttribute(dom, null, "type", contentKind),
contentKind === "xhtml" ? xmlNewFragment(dom, atomPropertyValue) : atomPropertyValue
]);
return dataServiceVersion;
};
var atomNewODataEntry = function (dom, data, model) {
/// <summary>Creates a new DOM element for an Atom entry.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="data" type="Object">Entry object in the library's internal representation.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new DOM element for the Atom entry and the required data service version for it.
/// </returns>
var payloadMetadata = data.__metadata || {};
var propertiesMetadata = payloadMetadata.properties || {};
var etag = payloadMetadata.etag;
var uri = payloadMetadata.uri;
var typeName = payloadMetadata.type;
var entityType = lookupEntityType(typeName, model);
var properties = xmlNewODataMetaElement(dom, "properties");
var entry = atomNewElement(dom, "entry", [
atomNewElement(dom, "author",
atomNewElement(dom, "name")
),
etag && xmlNewODataMetaAttribute(dom, "etag", etag),
uri && atomNewElement(dom, "id", uri),
typeName && atomNewElement(dom, "category", [
atomNewAttribute(dom, "term", typeName),
atomNewAttribute(dom, "scheme", odataScheme)
]),
// TODO: MLE support goes here.
atomNewElement(dom, "content", [
atomNewAttribute(dom, "type", "application/xml"),
properties
])
]);
var dataServiceVersion = "1.0";
for (var name in data) {
if (name !== "__metadata") {
var entryDataItemMetadata = propertiesMetadata[name] || {};
var entryDataItemModel = entityType && (
lookupProperty(entityType.property, name) ||
lookupProperty(entityType.navigationProperty, name));
var entryDataItem = atomNewODataEntryDataItem(dom, name, data[name], entryDataItemMetadata, entryDataItemModel, model);
if (entryDataItem) {
var entryElement = entryDataItem.element;
var entryElementParent = (xmlNamespaceURI(entryElement) === atomXmlNs) ? entry : properties;
xmlAppendChild(entryElementParent, entryElement);
dataServiceVersion = maxVersion(dataServiceVersion, entryDataItem.dsv);
}
}
}
atomApplyAllFeedCustomizations(entityType, model, function (customization) {
var customizationDsv = atomEntryCustomization(dom, entry, properties, customization);
dataServiceVersion = maxVersion(dataServiceVersion, customizationDsv);
});
return xmlNewODataElementInfo(entry, dataServiceVersion);
};
var atomNewODataFeed = function (dom, data, model) {
/// <summary>Creates a new DOM element for an Atom feed.</summary>
/// <param name="dom">DOM document used for creating the new DOM Element.</param>
/// <param name="data" type="Object">Feed object in the library's internal representation.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new DOM element for the Atom feed and the required data service version for it.
/// </returns>
var entries = isArray(data) ? data : data.results;
if (!entries) {
return null;
}
var dataServiceVersion = "1.0";
var atomFeed = atomNewElement(dom, "feed");
var i, len;
for (i = 0, len = entries.length; i < len; i++) {
var atomEntryData = atomNewODataEntry(dom, entries[i], model);
xmlAppendChild(atomFeed, atomEntryData.element);
dataServiceVersion = maxVersion(dataServiceVersion, atomEntryData.dsv);
}
return xmlNewODataElementInfo(atomFeed, dataServiceVersion);
};
var atomNewODataDocument = function (data, model) {
/// <summary>Creates a new OData Atom document.</summary>
/// <param name="data" type="Object">Feed or entry object in the libary's internal representaion.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns type="Object">
/// Object containing the new DOM document for the Atom document and the required data service version for it.
/// </returns>
if (data) {
var atomRootWriter = isFeed(data) && atomNewODataFeed ||
isObject(data) && atomNewODataEntry;
if (atomRootWriter) {
var dom = xmlDom();
var atomRootData = atomRootWriter(dom, data, model);
if (atomRootData) {
var atomRootElement = atomRootData.element;
xmlAppendChildren(atomRootElement, [
xmlNewNSDeclaration(dom, odataMetaXmlNs, odataMetaPrefix),
xmlNewNSDeclaration(dom, odataXmlNs, odataPrefix)
]);
return xmlNewODataElementInfo(xmlAppendChild(dom, atomRootElement), atomRootData.dsv);
}
}
}
return null;
};
var atomParser = function (handler, text, context) {
/// <summary>Parses an ATOM document (feed, entry or service document).</summary>
/// <param name="handler">This handler.</param>
/// <param name="text" type="String">Document text.</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>An object representation of the document; undefined if not applicable.</returns>
if (text) {
var atomDoc = xmlParse(text);
var atomRoot = xmlFirstChildElement(atomDoc);
if (atomRoot) {
return atomReadDocument(atomRoot, null, context.metadata);
}
}
};
var atomSerializer = function (handler, data, context) {
/// <summary>Serializes an ATOM object into a document (feed or entry).</summary>
/// <param name="handler">This handler.</param>
/// <param name="data" type="Object">Representation of feed or entry.</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>An text representation of the data object; undefined if not applicable.</returns>
var cType = context.contentType = context.contentType || contentType(atomMediaType);
if (cType && cType.mediaType === atomMediaType) {
var atomDoc = atomNewODataDocument(data, context.metadata);
if (atomDoc) {
context.dataServiceVersion = maxVersion(context.dataServiceVersion || "1.0", atomDoc.dsv);
return xmlSerialize(atomDoc.element);
}
}
// Allow undefined to be returned.
};
odata.atomHandler = handler(atomParser, atomSerializer, atomAcceptTypes.join(","), MAX_DATA_SERVICE_VERSION);
var schemaElement = function (attributes, elements, text, ns) {
/// <summary>Creates an object that describes an element in an schema.</summary>
/// <param name="attributes" type="Array">List containing the names of the attributes allowed for this element.</param>
/// <param name="elements" type="Array">List containing the names of the child elements allowed for this element.</param>
/// <param name="text" type="Boolean">Flag indicating if the element's text value is of interest or not.</param>
/// <param name="ns" type="String">Namespace to which the element belongs to.</param>
/// <remarks>
/// If a child element name ends with * then it is understood by the schema that that child element can appear 0 or more times.
/// </remarks>
/// <returns type="Object">Object with attributes, elements, text, and ns fields.</returns>
return {
attributes: attributes,
elements: elements,
text: text || false,
ns: ns
};
};
// It's assumed that all elements may have Documentation children and Annotation elements.
// See http://msdn.microsoft.com/en-us/library/bb399292.aspx for a CSDL reference.
var schema = {
elements: {
Annotations: schemaElement(
/*attributes*/["Target", "Qualifier"],
/*elements*/["TypeAnnotation*", "ValueAnnotation*"]
),
Association: schemaElement(
/*attributes*/["Name"],
/*elements*/["End*", "ReferentialConstraint", "TypeAnnotation*", "ValueAnnotation*"]
),
AssociationSet: schemaElement(
/*attributes*/["Name", "Association"],
/*elements*/["End*", "TypeAnnotation*", "ValueAnnotation*"]
),
Binary: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Bool: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Collection: schemaElement(
/*attributes*/null,
/*elements*/["String*", "Int*", "Float*", "Decimal*", "Bool*", "DateTime*", "DateTimeOffset*", "Guid*", "Binary*", "Time*", "Collection*", "Record*"]
),
CollectionType: schemaElement(
/*attributes*/["ElementType", "Nullable", "DefaultValue", "MaxLength", "FixedLength", "Precision", "Scale", "Unicode", "Collation", "SRID"],
/*elements*/["CollectionType", "ReferenceType", "RowType", "TypeRef"]
),
ComplexType: schemaElement(
/*attributes*/["Name", "BaseType", "Abstract"],
/*elements*/["Property*", "TypeAnnotation*", "ValueAnnotation*"]
),
DateTime: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
DateTimeOffset: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Decimal: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
DefiningExpression: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Dependent: schemaElement(
/*attributes*/["Role"],
/*elements*/["PropertyRef*"]
),
Documentation: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
End: schemaElement(
/*attributes*/["Type", "Role", "Multiplicity", "EntitySet"],
/*elements*/["OnDelete"]
),
EntityContainer: schemaElement(
/*attributes*/["Name", "Extends"],
/*elements*/["EntitySet*", "AssociationSet*", "FunctionImport*", "TypeAnnotation*", "ValueAnnotation*"]
),
EntitySet: schemaElement(
/*attributes*/["Name", "EntityType"],
/*elements*/["TypeAnnotation*", "ValueAnnotation*"]
),
EntityType: schemaElement(
/*attributes*/["Name", "BaseType", "Abstract", "OpenType"],
/*elements*/["Key", "Property*", "NavigationProperty*", "TypeAnnotation*", "ValueAnnotation*"]
),
EnumType: schemaElement(
/*attributes*/["Name", "UnderlyingType", "IsFlags"],
/*elements*/["Member*"]
),
Float: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Function: schemaElement(
/*attributes*/["Name", "ReturnType"],
/*elements*/["Parameter*", "DefiningExpression", "ReturnType", "TypeAnnotation*", "ValueAnnotation*"]
),
FunctionImport: schemaElement(
/*attributes*/["Name", "ReturnType", "EntitySet", "IsSideEffecting", "IsComposable", "IsBindable", "EntitySetPath"],
/*elements*/["Parameter*", "ReturnType", "TypeAnnotation*", "ValueAnnotation*"]
),
Guid: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Int: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Key: schemaElement(
/*attributes*/null,
/*elements*/["PropertyRef*"]
),
LabeledElement: schemaElement(
/*attributes*/["Name"],
/*elements*/["Path", "String", "Int", "Float", "Decimal", "Bool", "DateTime", "DateTimeOffset", "Guid", "Binary", "Time", "Collection", "Record", "LabeledElement", "Null"]
),
Member: schemaElement(
/*attributes*/["Name", "Value"]
),
NavigationProperty: schemaElement(
/*attributes*/["Name", "Relationship", "ToRole", "FromRole", "ContainsTarget"],
/*elements*/["TypeAnnotation*", "ValueAnnotation*"]
),
Null: schemaElement(
/*attributes*/null,
/*elements*/null
),
OnDelete: schemaElement(
/*attributes*/["Action"]
),
Path: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Parameter: schemaElement(
/*attributes*/["Name", "Type", "Mode", "Nullable", "DefaultValue", "MaxLength", "FixedLength", "Precision", "Scale", "Unicode", "Collation", "ConcurrencyMode", "SRID"],
/*elements*/["CollectionType", "ReferenceType", "RowType", "TypeRef", "TypeAnnotation*", "ValueAnnotation*"]
),
Principal: schemaElement(
/*attributes*/["Role"],
/*elements*/["PropertyRef*"]
),
Property: schemaElement(
/*attributes*/["Name", "Type", "Nullable", "DefaultValue", "MaxLength", "FixedLength", "Precision", "Scale", "Unicode", "Collation", "ConcurrencyMode", "CollectionKind", "SRID"],
/*elements*/["CollectionType", "ReferenceType", "RowType", "TypeAnnotation*", "ValueAnnotation*"]
),
PropertyRef: schemaElement(
/*attributes*/["Name"]
),
PropertyValue: schemaElement(
/*attributes*/["Property", "Path", "String", "Int", "Float", "Decimal", "Bool", "DateTime", "DateTimeOffset", "Guid", "Binary", "Time"],
/*Elements*/["Path", "String", "Int", "Float", "Decimal", "Bool", "DateTime", "DateTimeOffset", "Guid", "Binary", "Time", "Collection", "Record", "LabeledElement", "Null"]
),
ReferenceType: schemaElement(
/*attributes*/["Type"]
),
ReferentialConstraint: schemaElement(
/*attributes*/null,
/*elements*/["Principal", "Dependent"]
),
ReturnType: schemaElement(
/*attributes*/["ReturnType", "Type", "EntitySet"],
/*elements*/["CollectionType", "ReferenceType", "RowType"]
),
RowType: schemaElement(
/*elements*/["Property*"]
),
String: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
Schema: schemaElement(
/*attributes*/["Namespace", "Alias"],
/*elements*/["Using*", "EntityContainer*", "EntityType*", "Association*", "ComplexType*", "Function*", "ValueTerm*", "Annotations*"]
),
Time: schemaElement(
/*attributes*/null,
/*elements*/null,
/*text*/true
),
TypeAnnotation: schemaElement(
/*attributes*/["Term", "Qualifier"],
/*elements*/["PropertyValue*"]
),
TypeRef: schemaElement(
/*attributes*/["Type", "Nullable", "DefaultValue", "MaxLength", "FixedLength", "Precision", "Scale", "Unicode", "Collation", "SRID"]
),
Using: schemaElement(
/*attributes*/["Namespace", "Alias"]
),
ValueAnnotation: schemaElement(
/*attributes*/["Term", "Qualifier", "Path", "String", "Int", "Float", "Decimal", "Bool", "DateTime", "DateTimeOffset", "Guid", "Binary", "Time"],
/*Elements*/["Path", "String", "Int", "Float", "Decimal", "Bool", "DateTime", "DateTimeOffset", "Guid", "Binary", "Time", "Collection", "Record", "LabeledElement", "Null"]
),
ValueTerm: schemaElement(
/*attributes*/["Name", "Type"],
/*elements*/["TypeAnnotation*", "ValueAnnotation*"]
),
// See http://msdn.microsoft.com/en-us/library/dd541238(v=prot.10) for an EDMX reference.
Edmx: schemaElement(
/*attributes*/["Version"],
/*elements*/["DataServices", "Reference*", "AnnotationsReference*"],
/*text*/false,
/*ns*/edmxNs
),
DataServices: schemaElement(
/*attributes*/null,
/*elements*/["Schema*"],
/*text*/false,
/*ns*/edmxNs
)
}
};
// See http://msdn.microsoft.com/en-us/library/ee373839.aspx for a feed customization reference.
var customizationAttributes = ["m:FC_ContentKind", "m:FC_KeepInContent", "m:FC_NsPrefix", "m:FC_NsUri", "m:FC_SourcePath", "m:FC_TargetPath"];
schema.elements.Property.attributes = schema.elements.Property.attributes.concat(customizationAttributes);
schema.elements.EntityType.attributes = schema.elements.EntityType.attributes.concat(customizationAttributes);
// See http://msdn.microsoft.com/en-us/library/dd541284(PROT.10).aspx for an EDMX reference.
schema.elements.Edmx = { attributes: ["Version"], elements: ["DataServices"], ns: edmxNs };
schema.elements.DataServices = { elements: ["Schema*"], ns: edmxNs };
// See http://msdn.microsoft.com/en-us/library/dd541233(v=PROT.10) for Conceptual Schema Definition Language Document for Data Services.
schema.elements.EntityContainer.attributes.push("m:IsDefaultEntityContainer");
schema.elements.Property.attributes.push("m:MimeType");
schema.elements.FunctionImport.attributes.push("m:HttpMethod");
schema.elements.FunctionImport.attributes.push("m:IsAlwaysBindable");
schema.elements.EntityType.attributes.push("m:HasStream");
schema.elements.DataServices.attributes = ["m:DataServiceVersion", "m:MaxDataServiceVersion"];
var scriptCase = function (text) {
/// <summary>Converts a Pascal-case identifier into a camel-case identifier.</summary>
/// <param name="text" type="String">Text to convert.</param>
/// <returns type="String">Converted text.</returns>
/// <remarks>If the text starts with multiple uppercase characters, it is left as-is.</remarks>
if (!text) {
return text;
}
if (text.length > 1) {
var firstTwo = text.substr(0, 2);
if (firstTwo === firstTwo.toUpperCase()) {
return text;
}
return text.charAt(0).toLowerCase() + text.substr(1);
}
return text.charAt(0).toLowerCase();
};
var getChildSchema = function (parentSchema, candidateName) {
/// <summary>Gets the schema node for the specified element.</summary>
/// <param name="parentSchema" type="Object">Schema of the parent XML node of 'element'.</param>
/// <param name="candidateName">XML element name to consider.</param>
/// <returns type="Object">The schema that describes the specified element; null if not found.</returns>
if (candidateName === "Documentation") {
return { isArray: true, propertyName: "documentation" };
}
var elements = parentSchema.elements;
if (!elements) {
return null;
}
var i, len;
for (i = 0, len = elements.length; i < len; i++) {
var elementName = elements[i];
var multipleElements = false;
if (elementName.charAt(elementName.length - 1) === "*") {
multipleElements = true;
elementName = elementName.substr(0, elementName.length - 1);
}
if (candidateName === elementName) {
var propertyName = scriptCase(elementName);
return { isArray: multipleElements, propertyName: propertyName };
}
}
return null;
};
// This regular expression is used to detect a feed customization element
// after we've normalized it into the 'm' prefix. It starts with m:FC_,
// followed by other characters, and ends with _ and a number.
// The captures are 0 - whole string, 1 - name as it appears in internal table.
var isFeedCustomizationNameRE = /^(m:FC_.*)_[0-9]+$/;
var isEdmNamespace = function (nsURI) {
/// <summary>Checks whether the specifies namespace URI is one of the known CSDL namespace URIs.</summary>
/// <param name="nsURI" type="String">Namespace URI to check.</param>
/// <returns type="Boolean">true if nsURI is a known CSDL namespace; false otherwise.</returns>
return nsURI === edmNs1 ||
nsURI === edmNs1_1 ||
nsURI === edmNs1_2 ||
nsURI === edmNs2a ||
nsURI === edmNs2b ||
nsURI === edmNs3;
};
var parseConceptualModelElement = function (element) {
/// <summary>Parses a CSDL document.</summary>
/// <param name="element">DOM element to parse.</param>
/// <returns type="Object">An object describing the parsed element.</returns>
var localName = xmlLocalName(element);
var nsURI = xmlNamespaceURI(element);
var elementSchema = schema.elements[localName];
if (!elementSchema) {
return null;
}
if (elementSchema.ns) {
if (nsURI !== elementSchema.ns) {
return null;
}
} else if (!isEdmNamespace(nsURI)) {
return null;
}
var item = {};
var extensions = [];
var attributes = elementSchema.attributes || [];
xmlAttributes(element, function (attribute) {
var localName = xmlLocalName(attribute);
var nsURI = xmlNamespaceURI(attribute);
var value = attribute.value;
// Don't do anything with xmlns attributes.
if (nsURI === xmlnsNS) {
return;
}
// Currently, only m: for metadata is supported as a prefix in the internal schema table,
// un-prefixed element names imply one a CSDL element.
var schemaName = null;
var handled = false;
if (isEdmNamespace(nsURI) || nsURI === null) {
schemaName = "";
} else if (nsURI === odataMetaXmlNs) {
schemaName = "m:";
}
if (schemaName !== null) {
schemaName += localName;
// Feed customizations for complex types have additional
// attributes with a suffixed counter starting at '1', so
// take that into account when doing the lookup.
var match = isFeedCustomizationNameRE.exec(schemaName);
if (match) {
schemaName = match[1];
}
if (contains(attributes, schemaName)) {
handled = true;
item[scriptCase(localName)] = value;
}
}
if (!handled) {
extensions.push(createAttributeExtension(attribute));
}
});
xmlChildElements(element, function (child) {
var localName = xmlLocalName(child);
var childSchema = getChildSchema(elementSchema, localName);
if (childSchema) {
if (childSchema.isArray) {
var arr = item[childSchema.propertyName];
if (!arr) {
arr = [];
item[childSchema.propertyName] = arr;
}
arr.push(parseConceptualModelElement(child));
} else {
item[childSchema.propertyName] = parseConceptualModelElement(child);
}
} else {
extensions.push(createElementExtension(child));
}
});
if (elementSchema.text) {
item.text = xmlInnerText(element);
}
if (extensions.length) {
item.extensions = extensions;
}
return item;
};
var metadataParser = function (handler, text) {
/// <summary>Parses a metadata document.</summary>
/// <param name="handler">This handler.</param>
/// <param name="text" type="String">Metadata text.</param>
/// <returns>An object representation of the conceptual model.</returns>
var doc = xmlParse(text);
var root = xmlFirstChildElement(doc);
return parseConceptualModelElement(root) || undefined;
};
odata.metadataHandler = handler(metadataParser, null, xmlMediaType, MAX_DATA_SERVICE_VERSION);
var PAYLOADTYPE_OBJECT = "o";
var PAYLOADTYPE_FEED = "f";
var PAYLOADTYPE_PRIMITIVE = "p";
var PAYLOADTYPE_COLLECTION = "c";
var PAYLOADTYPE_SVCDOC = "s";
var PAYLOADTYPE_LINKS = "l";
var odataNs = "odata";
var odataAnnotationPrefix = odataNs + ".";
var bindAnnotation = "@" + odataAnnotationPrefix + "bind";
var metadataAnnotation = odataAnnotationPrefix + "metadata";
var navUrlAnnotation = odataAnnotationPrefix + "navigationLinkUrl";
var typeAnnotation = odataAnnotationPrefix + "type";
var jsonLightNameMap = {
readLink: "self",
editLink: "edit",
nextLink: "__next",
mediaReadLink: "media_src",
mediaEditLink: "edit_media",
mediaContentType: "content_type",
mediaETag: "media_etag",
count: "__count",
media_src: "mediaReadLink",
edit_media: "mediaEditLink",
content_type: "mediaContentType",
media_etag: "mediaETag",
url: "uri"
};
var jsonLightAnnotations = {
metadata: "odata.metadata",
count: "odata.count",
next: "odata.nextLink",
id: "odata.id",
etag: "odata.etag",
read: "odata.readLink",
edit: "odata.editLink",
mediaRead: "odata.mediaReadLink",
mediaEdit: "odata.mediaEditLink",
mediaEtag: "odata.mediaETag",
mediaContentType: "odata.mediaContentType",
actions: "odata.actions",
functions: "odata.functions",
navigationUrl: "odata.navigationLinkUrl",
associationUrl: "odata.associationLinkUrl",
type: "odata.type"
};
var jsonLightAnnotationInfo = function (annotation) {
/// <summary>Gets the name and target of an annotation in a JSON light payload.</summary>
/// <param name="annotation" type="String">JSON light payload annotation.</param>
/// <returns type="Object">Object containing the annotation name and the target property name.</param>
if (annotation.indexOf(".") > 0) {
var targetEnd = annotation.indexOf("@");
var target = targetEnd > -1 ? annotation.substring(0, targetEnd) : null;
var name = annotation.substring(targetEnd + 1);
return {
target: target,
name: name,
isOData: name.indexOf(odataAnnotationPrefix) === 0
};
}
return null;
};
var jsonLightDataItemType = function (name, value, container, dataItemModel, model) {
/// <summary>Gets the type name of a JSON light data item that belongs to a feed, an entry, a complex type property, or a collection property.</summary>
/// <param name="name" type="String">Name of the data item for which the type name is going to be retrieved.</param>
/// <param name="value">Value of the data item.</param>
/// <param name="container" type="Object">JSON light object that owns the data item.</param>
/// <param name="dataItemModel" type="Object" optional="true">Object describing the data item in an OData conceptual schema.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <remarks>
/// This function will first try to get the type name from the data item's value itself if it is a JSON light object; otherwise
/// it will try to get it from the odata.type annotation applied to the data item in the container. Then, it will fallback to the data item model.
/// If all attempts fail, it will return null.
/// </remarks>
/// <returns type="String">Data item type name; null if the type name cannot be found.</returns>
return (isComplex(value) && value[typeAnnotation]) ||
(container && container[name + "@" + typeAnnotation]) ||
(dataItemModel && dataItemModel.type) ||
(lookupNavigationPropertyType(dataItemModel, model)) ||
null;
};
var jsonLightDataItemModel = function (name, containerModel) {
/// <summary>Gets an object describing a data item in an OData conceptual schema.</summary>
/// <param name="name" type="String">Name of the data item for which the model is going to be retrieved.</param>
/// <param name="containerModel" type="Object">Object describing the owner of the data item in an OData conceptual schema.</param>
/// <returns type="Object">Object describing the data item; null if it cannot be found.</returns>
if (containerModel) {
return lookupProperty(containerModel.property, name) ||
lookupProperty(containerModel.navigationProperty, name);
}
return null;
};
var jsonLightIsEntry = function (data) {
/// <summary>Determines whether data represents a JSON light entry object.</summary>
/// <param name="data" type="Object">JSON light object to test.</param>
/// <returns type="Boolean">True if the data is JSON light entry object; false otherwise.</returns>
return isComplex(data) && ((odataAnnotationPrefix + "id") in data);
};
var jsonLightIsNavigationProperty = function (name, data, dataItemModel) {
/// <summary>Determines whether a data item in a JSON light object is a navigation property.</summary>
/// <param name="name" type="String">Name of the data item to test.</param>
/// <param name="data" type="Object">JSON light object that owns the data item.</param>
/// <param name="dataItemModel" type="Object">Object describing the data item in an OData conceptual schema.</param>
/// <returns type="Boolean">True if the data item is a navigation property; false otherwise.</returns>
if (!!data[name + "@" + navUrlAnnotation] || (dataItemModel && dataItemModel.relationship)) {
return true;
}
// Sniff the property value.
var value = isArray(data[name]) ? data[name][0] : data[name];
return jsonLightIsEntry(value);
};
var jsonLightIsPrimitiveType = function (typeName) {
/// <summary>Determines whether a type name is a primitive type in a JSON light payload.</summary>
/// <param name="typeName" type="String">Type name to test.</param>
/// <returns type="Boolean">True if the type name an EDM primitive type or an OData spatial type; false otherwise.</returns>
return isPrimitiveEdmType(typeName) || isGeographyEdmType(typeName) || isGeometryEdmType(typeName);
};
var jsonLightReadDataAnnotations = function (data, obj, baseURI, dataModel, model) {
/// <summary>Converts annotations found in a JSON light payload object to either properties or metadata.</summary>
/// <param name="data" type="Object">JSON light payload object containing the annotations to convert.</param>
/// <param name="obj" type="Object">Object that will store the converted annotations.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="dataModel" type="Object">Object describing the JSON light payload in an OData conceptual schema.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <returns>JSON light payload object with its annotations converted to either properties or metadata.</param>
for (var name in data) {
if (name.indexOf(".") > 0 && name.charAt(0) !== "#") {
var annotationInfo = jsonLightAnnotationInfo(name);
if (annotationInfo) {
var annotationName = annotationInfo.name;
var target = annotationInfo.target;
var targetModel = null;
var targetType = null;
if (target) {
targetModel = jsonLightDataItemModel(target, dataModel);
targetType = jsonLightDataItemType(target, data[target], data, targetModel, model);
}
if (annotationInfo.isOData) {
jsonLightApplyPayloadODataAnnotation(annotationName, target, targetType, data[name], data, obj, baseURI);
} else {
obj[name] = data[name];
}
}
}
}
return obj;
};
var jsonLightApplyPayloadODataAnnotation = function (name, target, targetType, value, data, obj, baseURI) {
/// <summary>
/// Processes a JSON Light payload OData annotation producing either a property, payload metadata, or property metadata on its owner object.
/// </summary>
/// <param name="name" type="String">Annotation name.</param>
/// <param name="target" type="String">Name of the property that is being targeted by the annotation.</param>
/// <param name="targetType" type="String">Type name of the target property.</param>
/// <param name="data" type="Object">JSON light object containing the annotation.</param>
/// <param name="obj" type="Object">Object that will hold properties produced by the annotation.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
var annotation = name.substring(odataAnnotationPrefix.length);
switch (annotation) {
case "navigationLinkUrl":
jsonLightApplyNavigationUrlAnnotation(annotation, target, targetType, value, data, obj, baseURI);
return;
case "nextLink":
case "count":
jsonLightApplyFeedAnnotation(annotation, target, value, obj, baseURI);
return;
case "mediaReadLink":
case "mediaEditLink":
case "mediaContentType":
case "mediaETag":
jsonLightApplyMediaAnnotation(annotation, target, targetType, value, obj, baseURI);
return;
default:
jsonLightApplyMetadataAnnotation(annotation, target, value, obj, baseURI);
return;
}
};
var jsonLightApplyMetadataAnnotation = function (name, target, value, obj, baseURI) {
/// <summary>
/// Converts a JSON light annotation that applies to entry metadata only (i.e. odata.editLink or odata.readLink) and its value
/// into their library's internal representation and saves it back to data.
/// </summary>
/// <param name="name" type="String">Annotation name.</param>
/// <param name="target" type="String">Name of the property on which the annotation should be applied.</param>
/// <param name="value" type="Object">Annotation value.</param>
/// <param name="obj" type="Object">Object that will hold properties produced by the annotation.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
var metadata = obj.__metadata = obj.__metadata || {};
var mappedName = jsonLightNameMap[name] || name;
if (name === "editLink") {
metadata.uri = normalizeURI(value, baseURI);
metadata[mappedName] = metadata.uri;
return;
}
if (name === "readLink" || name === "associationLinkUrl") {
value = normalizeURI(value, baseURI);
}
if (target) {
var propertiesMetadata = metadata.properties = metadata.properties || {};
var propertyMetadata = propertiesMetadata[target] = propertiesMetadata[target] || {};
if (name === "type") {
propertyMetadata[mappedName] = propertyMetadata[mappedName] || value;
return;
}
propertyMetadata[mappedName] = value;
return;
}
metadata[mappedName] = value;
};
var jsonLightApplyFeedAnnotation = function (name, target, value, obj, baseURI) {
/// <summary>
/// Converts a JSON light annotation that applies to feeds only (i.e. odata.count or odata.nextlink) and its value
/// into their library's internal representation and saves it back to data.
/// </summary>
/// <param name="name" type="String">Annotation name.</param>
/// <param name="target" type="String">Name of the property on which the annotation should be applied.</param>
/// <param name="value" type="Object">Annotation value.</param>
/// <param name="obj" type="Object">Object that will hold properties produced by the annotation.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
var mappedName = jsonLightNameMap[name];
var feed = target ? obj[target] : obj;
feed[mappedName] = (name === "nextLink") ? normalizeURI(value, baseURI) : value;
};
var jsonLightApplyMediaAnnotation = function (name, target, targetType, value, obj, baseURI) {
/// <summary>
/// Converts a JSON light media annotation in and its value into their library's internal representation
/// and saves it back to data or metadata.
/// </summary>
/// <param name="name" type="String">Annotation name.</param>
/// <param name="target" type="String">Name of the property on which the annotation should be applied.</param>
/// <param name="targetType" type="String">Type name of the target property.</param>
/// <param name="value" type="Object">Annotation value.</param>
/// <param name="obj" type="Object">Object that will hold properties produced by the annotation.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
var metadata = obj.__metadata = obj.__metadata || {};
var mappedName = jsonLightNameMap[name];
if (name === "mediaReadLink" || name === "mediaEditLink") {
value = normalizeURI(value, baseURI);
}
if (target) {
var propertiesMetadata = metadata.properties = metadata.properties || {};
var propertyMetadata = propertiesMetadata[target] = propertiesMetadata[target] || {};
propertyMetadata.type = propertyMetadata.type || targetType;
obj.__metadata = metadata;
obj[target] = obj[target] || { __mediaresource: {} };
obj[target].__mediaresource[mappedName] = value;
return;
}
metadata[mappedName] = value;
};
var jsonLightApplyNavigationUrlAnnotation = function (name, target, targetType, value, data, obj, baseURI) {
/// <summary>
/// Converts a JSON light navigation property annotation and its value into their library's internal representation
/// and saves it back to data o metadata.
/// </summary>
/// <param name="name" type="String">Annotation name.</param>
/// <param name="target" type="String">Name of the property on which the annotation should be applied.</param>
/// <param name="targetType" type="String">Type name of the target property.</param>
/// <param name="value" type="Object">Annotation value.</param>
/// <param name="data" type="Object">JSON light object containing the annotation.</param>
/// <param name="obj" type="Object">Object that will hold properties produced by the annotation.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
var metadata = obj.__metadata = obj.__metadata || {};
var propertiesMetadata = metadata.properties = metadata.properties || {};
var propertyMetadata = propertiesMetadata[target] = propertiesMetadata[target] || {};
var uri = normalizeURI(value, baseURI);
if (data.hasOwnProperty(target)) {
// The navigation property is inlined in the payload,
// so the navigation link url should be pushed to the object's
// property metadata instead.
propertyMetadata.navigationLinkUrl = uri;
return;
}
obj[target] = { __deferred: { uri: uri} };
propertyMetadata.type = propertyMetadata.type || targetType;
};
var jsonLightReadDataItemValue = function (value, typeName, dataItemMetadata, baseURI, dataItemModel, model, recognizeDates) {
/// <summary>Converts the value of a data item in a JSON light object to its library representation.</summary>
/// <param name="value">Data item value to convert.</param>
/// <param name="typeName" type="String">Type name of the data item.</param>
/// <param name="dataItemMetadata" type="Object">Object containing metadata about the data item.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="dataItemModel" type="Object" optional="true">Object describing the data item in an OData conceptual schema.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns>Data item value in its library representation.</param>
if (typeof value === "string") {
return jsonLightReadStringPropertyValue(value, typeName, recognizeDates);
}
if (!jsonLightIsPrimitiveType(typeName)) {
if (isArray(value)) {
return jsonLightReadCollectionPropertyValue(value, typeName, dataItemMetadata, baseURI, model, recognizeDates);
}
if (isComplex(value)) {
return jsonLightReadComplexPropertyValue(value, typeName, dataItemMetadata, baseURI, model, recognizeDates);
}
}
return value;
};
var jsonLightReadStringPropertyValue = function (value, propertyType, recognizeDates) {
/// <summary>Convertes the value of a string property in a JSON light object to its library representation.</summary>
/// <param name="value" type="String">String value to convert.</param>
/// <param name="propertyType" type="String">Type name of the property.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns>String property value in its library representation.</returns>
switch (propertyType) {
case EDM_TIME:
return parseDuration(value);
case EDM_DATETIME:
return parseDateTime(value, /*nullOnError*/false);
case EDM_DATETIMEOFFSET:
return parseDateTimeOffset(value, /*nullOnError*/false);
}
if (recognizeDates) {
return parseDateTime(value, /*nullOnError*/true) ||
parseDateTimeOffset(value, /*nullOnError*/true) ||
value;
}
return value;
};
var jsonLightReadCollectionPropertyValue = function (value, propertyType, propertyMetadata, baseURI, model, recognizeDates) {
/// <summary>Converts the value of a collection property in a JSON light object into its library representation.</summary>
/// <param name="value" type="Array">Collection property value to convert.</param>
/// <param name="propertyType" type="String">Property type name.</param>
/// <param name="propertyMetadata" type="Object">Object containing metadata about the collection property.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns type="Object">Collection property value in its library representation.</returns>
var collectionType = getCollectionType(propertyType);
var itemsMetadata = [];
var items = [];
var i, len;
for (i = 0, len = value.length; i < len; i++) {
var itemType = jsonLightDataItemType(null, value[i]) || collectionType;
var itemMetadata = { type: itemType };
var item = jsonLightReadDataItemValue(value[i], itemType, itemMetadata, baseURI, null, model, recognizeDates);
if (!jsonLightIsPrimitiveType(itemType) && !isPrimitive(value[i])) {
itemsMetadata.push(itemMetadata);
}
items.push(item);
}
if (itemsMetadata.length > 0) {
propertyMetadata.elements = itemsMetadata;
}
return { __metadata: { type: propertyType }, results: items };
};
var jsonLightReadComplexPropertyValue = function (value, propertyType, propertyMetadata, baseURI, model, recognizeDates) {
/// <summary>Converts the value of a comples property in a JSON light object into its library representation.</summary>
/// <param name="value" type="Object">Complex property value to convert.</param>
/// <param name="propertyType" type="String">Property type name.</param>
/// <param name="propertyMetadata" type="Object">Object containing metadata about the complx type property.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns type="Object">Complex property value in its library representation.</returns>
var complexValue = jsonLightReadObject(value, { type: propertyType }, baseURI, model, recognizeDates);
var complexMetadata = complexValue.__metadata;
var complexPropertiesMetadata = complexMetadata.properties;
if (complexPropertiesMetadata) {
propertyMetadata.properties = complexPropertiesMetadata;
delete complexMetadata.properties;
}
return complexValue;
};
var jsonLightReadNavigationPropertyValue = function (value, propertyInfo, baseURI, model, recognizeDates) {
/// <summary>Converts the value of a navigation property in a JSON light object into its library representation.</summary>
/// <param name="value">Navigation property property value to convert.</param>
/// <param name="propertyInfo" type="String">Information about the property whether it's an entry, feed or complex type.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns type="Object">Collection property value in its library representation.</returns>
if (isArray(value)) {
return jsonLightReadFeed(value, propertyInfo, baseURI, model, recognizeDates);
}
if (isComplex(value)) {
return jsonLightReadObject(value, propertyInfo, baseURI, model, recognizeDates);
}
return null;
};
var jsonLightReadObject = function (data, objectInfo, baseURI, model, recognizeDates) {
/// <summary>Converts a JSON light entry or complex type object into its library representation.</summary>
/// <param name="data" type="Object">JSON light entry or complex type object to convert.</param>
/// <param name="objectInfo" type="Object">Information about the entry or complex.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns type="Object">Entry or complex type object.</param>
objectInfo = objectInfo || {};
var actualType = data[typeAnnotation] || objectInfo.type || null;
var dataModel = lookupEntityType(actualType, model);
var isEntry = true;
if (!dataModel) {
isEntry = false;
dataModel = lookupComplexType(actualType, model);
}
var metadata = { type: actualType };
var obj = { __metadata: metadata };
var propertiesMetadata = {};
var baseTypeModel;
if (isEntry && dataModel && objectInfo.entitySet && objectInfo.contentTypeOdata == "minimalmetadata") {
var serviceURI = baseURI.substring(0, baseURI.lastIndexOf("$metadata"));
baseTypeModel = null; // check if the key model is in a parent type.
if (!dataModel.key) {
baseTypeModel = dataModel;
}
while (!!baseTypeModel && !baseTypeModel.key && baseTypeModel.baseType) {
baseTypeModel = lookupEntityType(baseTypeModel.baseType, model);
}
if (dataModel.key || (!!baseTypeModel && baseTypeModel.key)) {
var entryKey;
if (dataModel.key) {
entryKey = jsonLightGetEntryKey(data, dataModel);
} else {
entryKey = jsonLightGetEntryKey(data, baseTypeModel);
}
if (entryKey) {
var entryInfo = {
key: entryKey,
entitySet: objectInfo.entitySet,
functionImport: objectInfo.functionImport,
containerName: objectInfo.containerName
};
jsonLightComputeUrisIfMissing(data, entryInfo, actualType, serviceURI, dataModel, baseTypeModel);
}
}
}
for (var name in data) {
if (name.indexOf("#") === 0) {
// This is an advertised function or action.
jsonLightReadAdvertisedFunctionOrAction(name.substring(1), data[name], obj, baseURI, model);
} else {
// Is name NOT an annotation?
if (name.indexOf(".") === -1) {
if (!metadata.properties) {
metadata.properties = propertiesMetadata;
}
var propertyValue = data[name];
var propertyModel = propertyModel = jsonLightDataItemModel(name, dataModel);
baseTypeModel = dataModel;
while (!!dataModel && propertyModel === null && baseTypeModel.baseType) {
baseTypeModel = lookupEntityType(baseTypeModel.baseType, model);
propertyModel = propertyModel = jsonLightDataItemModel(name, baseTypeModel);
}
var isNavigationProperty = jsonLightIsNavigationProperty(name, data, propertyModel);
var propertyType = jsonLightDataItemType(name, propertyValue, data, propertyModel, model);
var propertyMetadata = propertiesMetadata[name] = propertiesMetadata[name] || { type: propertyType };
if (isNavigationProperty) {
var propertyInfo = {};
if (objectInfo.entitySet !== undefined) {
var navigationPropertyEntitySetName = lookupNavigationPropertyEntitySet(propertyModel, objectInfo.entitySet.name, model);
propertyInfo = getEntitySetInfo(navigationPropertyEntitySetName, model);
}
propertyInfo.contentTypeOdata = objectInfo.contentTypeOdata;
propertyInfo.kind = objectInfo.kind;
propertyInfo.type = propertyType;
obj[name] = jsonLightReadNavigationPropertyValue(propertyValue, propertyInfo, baseURI, model, recognizeDates);
} else {
obj[name] = jsonLightReadDataItemValue(propertyValue, propertyType, propertyMetadata, baseURI, propertyModel, model, recognizeDates);
}
}
}
}
return jsonLightReadDataAnnotations(data, obj, baseURI, dataModel, model);
};
var jsonLightReadAdvertisedFunctionOrAction = function (name, value, obj, baseURI, model) {
/// <summary>Converts a JSON light advertised action or function object into its library representation.</summary>
/// <param name="name" type="String">Advertised action or function name.</param>
/// <param name="value">Advertised action or function value.</param>
/// <param name="obj" type="Object">Object that will the converted value of the advertised action or function.</param>
/// <param name="baseURI" type="String">Base URI for normalizing the action's or function's relative URIs.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <remarks>
/// Actions and functions have the same representation in json light, so to disambiguate them the function uses
/// the model object. If available, the function will look for the functionImport object that describes the
/// the action or the function. If for whatever reason the functionImport can't be retrieved from the model (like
/// there is no model available or there is no functionImport within the model), then the value is going to be treated
/// as an advertised action and stored under obj.__metadata.actions.
/// </remarks>
if (!name || !isArray(value) && !isComplex(value)) {
return;
}
var isFunction = false;
var nsEnd = name.lastIndexOf(".");
var simpleName = name.substring(nsEnd + 1);
var containerName = (nsEnd > -1) ? name.substring(0, nsEnd) : "";
var container = (simpleName === name || containerName.indexOf(".") === -1) ?
lookupDefaultEntityContainer(model) :
lookupEntityContainer(containerName, model);
if (container) {
var functionImport = lookupFunctionImport(container.functionImport, simpleName);
if (functionImport && !!functionImport.isSideEffecting) {
isFunction = !parseBool(functionImport.isSideEffecting);
}
}
var metadata = obj.__metadata;
var targetName = isFunction ? "functions" : "actions";
var metadataURI = normalizeURI(name, baseURI);
var items = (isArray(value)) ? value : [value];
var i, len;
for (i = 0, len = items.length; i < len; i++) {
var item = items[i];
if (item) {
var targetCollection = metadata[targetName] = metadata[targetName] || [];
var actionOrFunction = { metadata: metadataURI, title: item.title, target: normalizeURI(item.target, baseURI) };
targetCollection.push(actionOrFunction);
}
}
};
var jsonLightReadFeed = function (data, feedInfo, baseURI, model, recognizeDates) {
/// <summary>Converts a JSON light feed or top level collection property object into its library representation.</summary>
/// <param name="data" type="Object">JSON light feed object to convert.</param>
/// <param name="typeName" type="String">Type name of the feed or collection items.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns type="Object">Feed or top level collection object.</param>
var items = isArray(data) ? data : data.value;
var entries = [];
var i, len, entry;
for (i = 0, len = items.length; i < len; i++) {
entry = jsonLightReadObject(items[i], feedInfo, baseURI, model, recognizeDates);
entries.push(entry);
}
var feed = { results: entries };
if (isComplex(data)) {
for (var name in data) {
if (name.indexOf("#") === 0) {
// This is an advertised function or action.
feed.__metadata = feed.__metadata || {};
jsonLightReadAdvertisedFunctionOrAction(name.substring(1), data[name], feed, baseURI, model);
}
}
feed = jsonLightReadDataAnnotations(data, feed, baseURI);
}
return feed;
};
var jsonLightGetEntryKey = function (data, entityModel) {
/// <summary>Gets the key of an entry.</summary>
/// <param name="data" type="Object">JSON light entry.</param>
/// <param name="entityModel" type="String">Object describing the entry Model</param>
/// <returns type="string">Entry instance key.</returns>
var entityInstanceKey;
var entityKeys = entityModel.key.propertyRef;
var type;
entityInstanceKey = "(";
if (entityKeys.length == 1) {
type = lookupProperty(entityModel.property, entityKeys[0].name).type;
entityInstanceKey += formatLiteral(data[entityKeys[0].name], type);
} else {
var first = true;
for (var i = 0; i < entityKeys.length; i++) {
if (!first) {
entityInstanceKey += ",";
} else {
first = false;
}
type = lookupProperty(entityModel.property, entityKeys[i].name).type;
entityInstanceKey += entityKeys[i].name + "=" + formatLiteral(data[entityKeys[i].name], type);
}
}
entityInstanceKey += ")";
return entityInstanceKey;
};
var jsonLightComputeUrisIfMissing = function (data, entryInfo, actualType, serviceURI, entityModel, baseTypeModel) {
/// <summary>Compute the URI according to OData conventions if it doesn't exist</summary>
/// <param name="data" type="Object">JSON light entry.</param>
/// <param name="entryInfo" type="Object">Information about the entry includes type, key, entitySet and entityContainerName.</param>
/// <param name="actualType" type="String">Type of the entry</param>
/// <param name="serviceURI" type="String">Base URI the service.</param>
/// <param name="entityModel" type="Object">Object describing an OData conceptual schema of the entry.</param>
/// <param name="baseTypeModel" type="Object" optional="true">Object escribing an OData conceptual schema of the baseType if it exists.</param>
var lastIdSegment = data[jsonLightAnnotations.id] || data[jsonLightAnnotations.read] || data[jsonLightAnnotations.edit] || entryInfo.entitySet.name + entryInfo.key;
data[jsonLightAnnotations.id] = serviceURI + lastIdSegment;
if (!data[jsonLightAnnotations.edit]) {
data[jsonLightAnnotations.edit] = entryInfo.entitySet.name + entryInfo.key;
if (entryInfo.entitySet.entityType != actualType) {
data[jsonLightAnnotations.edit] += "/" + actualType;
}
}
data[jsonLightAnnotations.read] = data[jsonLightAnnotations.read] || data[jsonLightAnnotations.edit];
if (!data[jsonLightAnnotations.etag]) {
var etag = jsonLightComputeETag(data, entityModel, baseTypeModel);
if (!!etag) {
data[jsonLightAnnotations.etag] = etag;
}
}
jsonLightComputeStreamLinks(data, entityModel, baseTypeModel);
jsonLightComputeNavigationAndAssociationProperties(data, entityModel, baseTypeModel);
jsonLightComputeFunctionImports(data, entryInfo);
};
var jsonLightComputeETag = function (data, entityModel, baseTypeModel) {
/// <summary>Computes the etag of an entry</summary>
/// <param name="data" type="Object">JSON light entry.</param>
/// <param name="entryInfo" type="Object">Object describing the entry model.</param>
/// <param name="baseTypeModel" type="Object" optional="true">Object describing an OData conceptual schema of the baseType if it exists.</param>
/// <returns type="string">Etag value</returns>
var etag = "";
var propertyModel;
for (var i = 0; entityModel.property && i < entityModel.property.length; i++) {
propertyModel = entityModel.property[i];
etag = jsonLightAppendValueToEtag(data, etag, propertyModel);
}
if (baseTypeModel) {
for (i = 0; baseTypeModel.property && i < baseTypeModel.property.length; i++) {
propertyModel = baseTypeModel.property[i];
etag = jsonLightAppendValueToEtag(data, etag, propertyModel);
}
}
if (etag.length > 0) {
return etag + "\"";
}
return null;
};
var jsonLightAppendValueToEtag = function (data, etag, propertyModel) {
/// <summary>Adds a propery value to the etag after formatting.</summary>
/// <param name="data" type="Object">JSON light entry.</param>
/// <param name="etag" type="Object">value of the etag.</param>
/// <param name="propertyModel" type="Object">Object describing an OData conceptual schema of the property.</param>
/// <returns type="string">Etag value</returns>
if (propertyModel.concurrencyMode == "Fixed") {
if (etag.length > 0) {
etag += ",";
} else {
etag += "W/\"";
}
if (data[propertyModel.name] !== null) {
etag += formatLiteral(data[propertyModel.name], propertyModel.type);
} else {
etag += "null";
}
}
return etag;
};
var jsonLightComputeNavigationAndAssociationProperties = function (data, entityModel, baseTypeModel) {
/// <summary>Adds navigation links to the entry metadata</summary>
/// <param name="data" type="Object">JSON light entry.</param>
/// <param name="entityModel" type="Object">Object describing the entry model.</param>
/// <param name="baseTypeModel" type="Object" optional="true">Object describing an OData conceptual schema of the baseType if it exists.</param>
var navigationLinkAnnotation = "@odata.navigationLinkUrl";
var associationLinkAnnotation = "@odata.associationLinkUrl";
var navigationPropertyName, navigationPropertyAnnotation, associationPropertyAnnotation;
for (var i = 0; entityModel.navigationProperty && i < entityModel.navigationProperty.length; i++) {
navigationPropertyName = entityModel.navigationProperty[i].name;
navigationPropertyAnnotation = navigationPropertyName + navigationLinkAnnotation;
if (data[navigationPropertyAnnotation] === undefined) {
data[navigationPropertyAnnotation] = data[jsonLightAnnotations.edit] + "/" + encodeURIComponent(navigationPropertyName);
}
associationPropertyAnnotation = navigationPropertyName + associationLinkAnnotation;
if (data[associationPropertyAnnotation] === undefined) {
data[associationPropertyAnnotation] = data[jsonLightAnnotations.edit] + "/$links/" + encodeURIComponent(navigationPropertyName);
}
}
if (baseTypeModel && baseTypeModel.navigationProperty) {
for (i = 0; i < baseTypeModel.navigationProperty.length; i++) {
navigationPropertyName = baseTypeModel.navigationProperty[i].name;
navigationPropertyAnnotation = navigationPropertyName + navigationLinkAnnotation;
if (data[navigationPropertyAnnotation] === undefined) {
data[navigationPropertyAnnotation] = data[jsonLightAnnotations.edit] + "/" + encodeURIComponent(navigationPropertyName);
}
associationPropertyAnnotation = navigationPropertyName + associationLinkAnnotation;
if (data[associationPropertyAnnotation] === undefined) {
data[associationPropertyAnnotation] = data[jsonLightAnnotations.edit] + "/$links/" + encodeURIComponent(navigationPropertyName);
}
}
}
};
var formatLiteral = function (value, type) {
/// <summary>Formats a value according to Uri literal format</summary>
/// <param name="value">Value to be formatted.</param>
/// <param name="type">Edm type of the value</param>
/// <returns type="string">Value after formatting</returns>
value = "" + formatRowLiteral(value, type);
value = encodeURIComponent(value.replace("'", "''"));
switch ((type)) {
case "Edm.Binary":
return "X'" + value + "'";
case "Edm.DateTime":
return "datetime" + "'" + value + "'";
case "Edm.DateTimeOffset":
return "datetimeoffset" + "'" + value + "'";
case "Edm.Decimal":
return value + "M";
case "Edm.Guid":
return "guid" + "'" + value + "'";
case "Edm.Int64":
return value + "L";
case "Edm.Float":
return value + "f";
case "Edm.Double":
return value + "D";
case "Edm.Geography":
return "geography" + "'" + value + "'";
case "Edm.Geometry":
return "geometry" + "'" + value + "'";
case "Edm.Time":
return "time" + "'" + value + "'";
case "Edm.String":
return "'" + value + "'";
default:
return value;
}
};
var formatRowLiteral = function (value, type) {
switch (type) {
case "Edm.Binary":
return convertByteArrayToHexString(value);
default:
return value;
}
};
var jsonLightComputeFunctionImports = function (data, entryInfo) {
/// <summary>Adds functions and actions links to the entry metadata</summary>
/// <param name="entry" type="Object">JSON light entry.</param>
/// <param name="entityInfo" type="Object">Object describing the entry</param>
var functionImport = entryInfo.functionImport || [];
for (var i = 0; i < functionImport.length; i++) {
if (functionImport[i].isBindable && functionImport[i].parameter[0] && functionImport[i].parameter[0].type == entryInfo.entitySet.entityType) {
var functionImportAnnotation = "#" + entryInfo.containerName + "." + functionImport[i].name;
if (data[functionImportAnnotation] == undefined) {
data[functionImportAnnotation] = {
title: functionImport[i].name,
target: data[jsonLightAnnotations.edit] + "/" + functionImport[i].name
};
}
}
}
};
var jsonLightComputeStreamLinks = function (data, entityModel, baseTypeModel) {
/// <summary>Adds stream links to the entry metadata</summary>
/// <param name="data" type="Object">JSON light entry.</param>
/// <param name="entityModel" type="Object">Object describing the entry model.</param>
/// <param name="baseTypeModel" type="Object" optional="true">Object describing an OData conceptual schema of the baseType if it exists.</param>
if (entityModel.hasStream || (baseTypeModel && baseTypeModel.hasStream)) {
data[jsonLightAnnotations.mediaEdit] = data[jsonLightAnnotations.mediaEdit] || data[jsonLightAnnotations.mediaEdit] + "/$value";
data[jsonLightAnnotations.mediaRead] = data[jsonLightAnnotations.mediaRead] || data[jsonLightAnnotations.mediaEdit];
}
};
var jsonLightReadTopPrimitiveProperty = function (data, typeName, baseURI, recognizeDates) {
/// <summary>Converts a JSON light top level primitive property object into its library representation.</summary>
/// <param name="data" type="Object">JSON light feed object to convert.</param>
/// <param name="typeName" type="String">Type name of the primitive property.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns type="Object">Top level primitive property object.</param>
var metadata = { type: typeName };
var value = jsonLightReadDataItemValue(data.value, typeName, metadata, baseURI, null, null, recognizeDates);
return jsonLightReadDataAnnotations(data, { __metadata: metadata, value: value }, baseURI);
};
var jsonLightReadTopCollectionProperty = function (data, typeName, baseURI, model, recognizeDates) {
/// <summary>Converts a JSON light top level collection property object into its library representation.</summary>
/// <param name="data" type="Object">JSON light feed object to convert.</param>
/// <param name="typeName" type="String">Type name of the collection property.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <param name="model" type="Object" optional="true">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <returns type="Object">Top level collection property object.</param>
var propertyMetadata = {};
var value = jsonLightReadCollectionPropertyValue(data.value, typeName, propertyMetadata, baseURI, model, recognizeDates);
extend(value.__metadata, propertyMetadata);
return jsonLightReadDataAnnotations(data, value, baseURI);
};
var jsonLightReadLinksDocument = function (data, baseURI) {
/// <summary>Converts a JSON light links collection object to its library representation.</summary>
/// <param name="data" type="Object">JSON light link object to convert.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <returns type="Object">Links collection object.</param>
var items = data.value;
if (!isArray(items)) {
return jsonLightReadLink(data, baseURI);
}
var results = [];
var i, len;
for (i = 0, len = items.length; i < len; i++) {
results.push(jsonLightReadLink(items[i], baseURI));
}
var links = { results: results };
return jsonLightReadDataAnnotations(data, links, baseURI);
};
var jsonLightReadLink = function (data, baseURI) {
/// <summary>Converts a JSON light link object to its library representation.</summary>
/// <param name="data" type="Object">JSON light link object to convert.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <returns type="Object">Link object.</param>
var link = { uri: normalizeURI(data.url, baseURI) };
link = jsonLightReadDataAnnotations(data, link, baseURI);
var metadata = link.__metadata || {};
var metadataProperties = metadata.properties || {};
jsonLightRemoveTypePropertyMetadata(metadataProperties.url);
renameProperty(metadataProperties, "url", "uri");
return link;
};
var jsonLightRemoveTypePropertyMetadata = function (propertyMetadata) {
/// <summary>Removes the type property from a property metadata object.</summary>
/// <param name="propertyMetadata" type="Object">Property metadata object.</param>
if (propertyMetadata) {
delete propertyMetadata.type;
}
};
var jsonLightReadSvcDocument = function (data, baseURI) {
/// <summary>Converts a JSON light service document object to its library representation.</summary>
/// <param name="data" type="Object">JSON light service document object to convert.</param>
/// <param name="baseURI" type="String">Base URI for normalizing relative URIs found in the payload.</param>
/// <returns type="Object">Link object.</param>
var items = data.value;
var collections = [];
var workspace = jsonLightReadDataAnnotations(data, { collections: collections }, baseURI);
var metadata = workspace.__metadata || {};
var metadataProperties = metadata.properties || {};
jsonLightRemoveTypePropertyMetadata(metadataProperties.value);
renameProperty(metadataProperties, "value", "collections");
var i, len;
for (i = 0, len = items.length; i < len; i++) {
var item = items[i];
var collection = { title: item.name, href: normalizeURI(item.url, baseURI) };
collection = jsonLightReadDataAnnotations(item, collection, baseURI);
metadata = collection.__metadata || {};
metadataProperties = metadata.properties || {};
jsonLightRemoveTypePropertyMetadata(metadataProperties.name);
jsonLightRemoveTypePropertyMetadata(metadataProperties.url);
renameProperty(metadataProperties, "name", "title");
renameProperty(metadataProperties, "url", "href");
collections.push(collection);
}
return { workspaces: [workspace] };
};
var jsonLightMakePayloadInfo = function (kind, type) {
/// <summary>Creates an object containing information for the json light payload.</summary>
/// <param name="kind" type="String">JSON light payload kind, one of the PAYLOADTYPE_XXX constant values.</param>
/// <param name="typeName" type="String">Type name of the JSON light payload.</param>
/// <returns type="Object">Object with kind and type fields.</returns>
/// <field name="kind" type="String">Kind of the JSON light payload. One of the PAYLOADTYPE_XXX constant values.</field>
/// <field name="type" type="String">Data type of the JSON light payload.</field>
return { kind: kind, type: type || null };
};
var jsonLightPayloadInfo = function (data, model, inferFeedAsComplexType) {
/// <summary>Infers the information describing the JSON light payload from its metadata annotation, structure, and data model.</summary>
/// <param name="data" type="Object">Json light response payload object.</param>
/// <param name="model" type="Object">Object describing an OData conceptual schema.</param>
/// <param name="inferFeedAsComplexType" type="Boolean">True if a JSON light payload that looks like a feed should be treated as a complex type property instead.</param>
/// <remarks>
/// If the arguments passed to the function don't convey enough information about the payload to determine without doubt that the payload is a feed then it
/// will try to use the payload object structure instead. If the payload looks like a feed (has value property that is an array or non-primitive values) then
/// the function will report its kind as PAYLOADTYPE_FEED unless the inferFeedAsComplexType flag is set to true. This flag comes from the user request
/// and allows the user to control how the library behaves with an ambigous JSON light payload.
/// </remarks>
/// <returns type="Object">
/// Object with kind and type fields. Null if there is no metadata annotation or the payload info cannot be obtained..
/// </returns>
var metadataUri = data[metadataAnnotation];
if (!metadataUri || typeof metadataUri !== "string") {
return null;
}
var fragmentStart = metadataUri.lastIndexOf("#");
if (fragmentStart === -1) {
return jsonLightMakePayloadInfo(PAYLOADTYPE_SVCDOC);
}
var elementStart = metadataUri.indexOf("@Element", fragmentStart);
var fragmentEnd = elementStart - 1;
if (fragmentEnd < 0) {
fragmentEnd = metadataUri.indexOf("?", fragmentStart);
if (fragmentEnd === -1) {
fragmentEnd = metadataUri.length;
}
}
var fragment = metadataUri.substring(fragmentStart + 1, fragmentEnd);
if (fragment.indexOf("/$links/") > 0) {
return jsonLightMakePayloadInfo(PAYLOADTYPE_LINKS);
}
var fragmentParts = fragment.split("/");
if (fragmentParts.length >= 0) {
var qualifiedName = fragmentParts[0];
var typeCast = fragmentParts[1];
if (jsonLightIsPrimitiveType(qualifiedName)) {
return jsonLightMakePayloadInfo(PAYLOADTYPE_PRIMITIVE, qualifiedName);
}
if (isCollectionType(qualifiedName)) {
return jsonLightMakePayloadInfo(PAYLOADTYPE_COLLECTION, qualifiedName);
}
var entityType = typeCast;
var entitySet, functionImport, containerName;
if (!typeCast) {
var nsEnd = qualifiedName.lastIndexOf(".");
var simpleName = qualifiedName.substring(nsEnd + 1);
var container = (simpleName === qualifiedName) ?
lookupDefaultEntityContainer(model) :
lookupEntityContainer(qualifiedName.substring(0, nsEnd), model);
if (container) {
entitySet = lookupEntitySet(container.entitySet, simpleName);
functionImport = container.functionImport;
containerName = container.name;
entityType = !!entitySet ? entitySet.entityType : null;
}
}
var info;
if (elementStart > 0) {
info = jsonLightMakePayloadInfo(PAYLOADTYPE_OBJECT, entityType);
info.entitySet = entitySet;
info.functionImport = functionImport;
info.containerName = containerName;
return info;
}
if (entityType) {
info = jsonLightMakePayloadInfo(PAYLOADTYPE_FEED, entityType);
info.entitySet = entitySet;
info.functionImport = functionImport;
info.containerName = containerName;
return info;
}
if (isArray(data.value) && !lookupComplexType(qualifiedName, model)) {
var item = data.value[0];
if (!isPrimitive(item)) {
if (jsonLightIsEntry(item) || !inferFeedAsComplexType) {
return jsonLightMakePayloadInfo(PAYLOADTYPE_FEED, null);
}
}
}
return jsonLightMakePayloadInfo(PAYLOADTYPE_OBJECT, qualifiedName);
}
return null;
};
var jsonLightReadPayload = function (data, model, recognizeDates, inferFeedAsComplexType, contentTypeOdata) {
/// <summary>Converts a JSON light response payload object into its library's internal representation.</summary>
/// <param name="data" type="Object">Json light response payload object.</param>
/// <param name="model" type="Object">Object describing an OData conceptual schema.</param>
/// <param name="recognizeDates" type="Boolean" optional="true">Flag indicating whether datetime literal strings should be converted to JavaScript Date objects.</param>
/// <param name="inferFeedAsComplexType" type="Boolean">True if a JSON light payload that looks like a feed should be reported as a complex type property instead.</param>
/// <param name="contentTypeOdata" type="string">Includes the type of json ( minimalmetadata, fullmetadata .. etc )</param>
/// <returns type="Object">Object in the library's representation.</returns>
if (!isComplex(data)) {
return data;
}
contentTypeOdata = contentTypeOdata || "minimalmetadata";
var baseURI = data[metadataAnnotation];
var payloadInfo = jsonLightPayloadInfo(data, model, inferFeedAsComplexType);
if (assigned(payloadInfo)) {
payloadInfo.contentTypeOdata = contentTypeOdata;
}
var typeName = null;
if (payloadInfo) {
delete data[metadataAnnotation];
typeName = payloadInfo.type;
switch (payloadInfo.kind) {
case PAYLOADTYPE_FEED:
return jsonLightReadFeed(data, payloadInfo, baseURI, model, recognizeDates);
case PAYLOADTYPE_COLLECTION:
return jsonLightReadTopCollectionProperty(data, typeName, baseURI, model, recognizeDates);
case PAYLOADTYPE_PRIMITIVE:
return jsonLightReadTopPrimitiveProperty(data, typeName, baseURI, recognizeDates);
case PAYLOADTYPE_SVCDOC:
return jsonLightReadSvcDocument(data, baseURI);
case PAYLOADTYPE_LINKS:
return jsonLightReadLinksDocument(data, baseURI);
}
}
return jsonLightReadObject(data, payloadInfo, baseURI, model, recognizeDates);
};
var jsonLightSerializableMetadata = ["type", "etag", "media_src", "edit_media", "content_type", "media_etag"];
var formatJsonLight = function (obj, context) {
/// <summary>Converts an object in the library's internal representation to its json light representation.</summary>
/// <param name="obj" type="Object">Object the library's internal representation.</param>
/// <param name="context" type="Object">Object with the serialization context.</param>
/// <returns type="Object">Object in its json light representation.</returns>
// Regular expression used to test that the uri is for a $links document.
var linksUriRE = /\/\$links\//;
var data = {};
var metadata = obj.__metadata;
var islinks = context && linksUriRE.test(context.request.requestUri);
formatJsonLightData(obj, (metadata && metadata.properties), data, islinks);
return data;
};
var formatJsonLightMetadata = function (metadata, data) {
/// <summary>Formats an object's metadata into the appropriate json light annotations and saves them to data.</summary>
/// <param name="obj" type="Object">Object whose metadata is going to be formatted as annotations.</param>
/// <param name="data" type="Object">Object on which the annotations are going to be stored.</param>
if (metadata) {
var i, len;
for (i = 0, len = jsonLightSerializableMetadata.length; i < len; i++) {
// There is only a subset of metadata values that are interesting during update requests.
var name = jsonLightSerializableMetadata[i];
var qName = odataAnnotationPrefix + (jsonLightNameMap[name] || name);
formatJsonLightAnnotation(qName, null, metadata[name], data);
}
}
};
var formatJsonLightData = function (obj, pMetadata, data, isLinks) {
/// <summary>Formats an object's data into the appropriate json light values and saves them to data.</summary>
/// <param name="obj" type="Object">Object whose data is going to be formatted.</param>
/// <param name="pMetadata" type="Object">Object that contains metadata for the properties that are being formatted.</param>
/// <param name="data" type="Object">Object on which the formatted values are going to be stored.</param>
/// <param name="isLinks" type="Boolean">True if a links document is being formatted. False otherwise.</param>
for (var key in obj) {
var value = obj[key];
if (key === "__metadata") {
// key is the object metadata.
formatJsonLightMetadata(value, data);
} else if (key.indexOf(".") === -1) {
// key is an regular property or array element.
if (isLinks && key === "uri") {
formatJsonLightEntityLink(value, data);
} else {
formatJsonLightProperty(key, value, pMetadata, data, isLinks);
}
} else {
data[key] = value;
}
}
};
var formatJsonLightProperty = function (name, value, pMetadata, data) {
/// <summary>Formats an object's value identified by name to its json light representation and saves it to data.</summary>
/// <param name="name" type="String">Property name.</param>
/// <param name="value">Property value.</param>
/// <param name="pMetadata" type="Object">Object that contains metadata for the property that is being formatted.</param>
/// <param name="data" type="Object">Object on which the formatted value is going to be stored.</param>
// Get property type from property metadata
var propertyMetadata = pMetadata && pMetadata[name] || { properties: undefined, type: undefined };
var typeName = dataItemTypeName(value, propertyMetadata);
if (isPrimitive(value) || !value) {
// It is a primitive value then.
formatJsonLightAnnotation(typeAnnotation, name, typeName, data);
data[name] = value;
return;
}
if (isFeed(value, typeName) || isEntry(value)) {
formatJsonLightInlineProperty(name, value, data);
return;
}
if (!typeName && isDeferred(value)) {
// It is really a deferred property.
formatJsonLightDeferredProperty(name, value, data);
return;
}
if (isCollection(value, typeName)) {
// The thing is a collection, format it as one.
if (getCollectionType(typeName)) {
formatJsonLightAnnotation(typeAnnotation, name, typeName, data);
}
formatJsonLightCollectionProperty(name, value, data);
return;
}
// Format the complex property value in a new object in data[name].
data[name] = {};
formatJsonLightAnnotation(typeAnnotation, null, typeName, data[name]);
formatJsonLightData(value, propertyMetadata.properties, data[name]);
};
var formatJsonLightEntityLink = function (value, data) {
/// <summary>Formats an entity link in a $links document and saves it into data.</summary>
/// <param name="value" type="String">Entity link value.</summary>
/// <param name="data" type="Object">Object on which the formatted value is going to be stored.</param>
data.url = value;
};
var formatJsonLightDeferredProperty = function (name, value, data) {
/// <summary>Formats the object value's identified by name as an odata.navigalinkurl annotation and saves it to data.</summary>
/// <param name="name" type="String">Name of the deferred property to be formatted.</param>
/// <param name="value" type="Object">Deferred property value to be formatted.</param>
/// <param name="data" type="Object">Object on which the formatted value is going to be stored.</param>
formatJsonLightAnnotation(navUrlAnnotation, name, value.__deferred.uri, data);
};
var formatJsonLightCollectionProperty = function (name, value, data) {
/// <summary>Formats a collection property in obj identified by name as a json light collection property and saves it to data.</summary>
/// <param name="name" type="String">Name of the collection property to be formatted.</param>
/// <param name="value" type="Object">Collection property value to be formatted.</param>
/// <param name="data" type="Object">Object on which the formatted value is going to be stored.</param>
data[name] = [];
var items = isArray(value) ? value : value.results;
formatJsonLightData(items, null, data[name]);
};
var formatJsonLightInlineProperty = function (name, value, data) {
/// <summary>Formats an inline feed or entry property in obj identified by name as a json light value and saves it to data.</summary>
/// <param name="name" type="String">Name of the inline feed or entry property to be formatted.</param>
/// <param name="value" type="Object or Array">Value of the inline feed or entry property.</param>
/// <param name="data" type="Object">Object on which the formatted value is going to be stored.</param>
if (isFeed(value)) {
data[name] = [];
// Format each of the inline feed entries
var entries = isArray(value) ? value : value.results;
var i, len;
for (i = 0, len = entries.length; i < len; i++) {
formatJsonLightInlineEntry(name, entries[i], true, data);
}
return;
}
formatJsonLightInlineEntry(name, value, false, data);
};
var formatJsonLightInlineEntry = function (name, value, inFeed, data) {
/// <summary>Formats an inline entry value in the property identified by name as a json light value and saves it to data.</summary>
/// <param name="name" type="String">Name of the inline feed or entry property that owns the entry formatted.</param>
/// <param name="value" type="Object">Inline entry value to be formatted.</param>
/// <param name="inFeed" type="Boolean">True if the entry is in an inline feed; false otherwise.
/// <param name="data" type="Object">Object on which the formatted value is going to be stored.</param>
// This might be a bind instead of a deep insert.
var uri = value.__metadata && value.__metadata.uri;
if (uri) {
formatJsonLightBinding(name, uri, inFeed, data);
return;
}
var entry = formatJsonLight(value);
if (inFeed) {
data[name].push(entry);
return;
}
data[name] = entry;
};
var formatJsonLightBinding = function (name, uri, inFeed, data) {
/// <summary>Formats an entry binding in the inline property in obj identified by name as an odata.bind annotation and saves it to data.</summary>
/// <param name="name" type="String">Name of the inline property that has the binding to be formated.</param>
/// <param name="uri" type="String">Uri to the bound entry.</param>
/// <param name="inFeed" type="Boolean">True if the binding is in an inline feed; false otherwise.
/// <param name="data" type="Object">Object on which the formatted value is going to be stored.</param>
var bindingName = name + bindAnnotation;
if (inFeed) {
// The binding is inside an inline feed, so merge it with whatever other bindings already exist in data.
data[bindingName] = data[bindingName] || [];
data[bindingName].push(uri);
return;
}
// The binding is on an inline entry; it can be safely overwritten.
data[bindingName] = uri;
};
var formatJsonLightAnnotation = function (qName, target, value, data) {
/// <summary>Formats a value as a json light annotation and stores it in data</summary>
/// <param name="qName" type="String">Qualified name of the annotation.</param>
/// <param name="target" type="String">Name of the property that the metadata value targets.</param>
/// <param name="value">Annotation value.</param>
/// <param name="data" type="Object">Object on which the annotation is going to be stored.</param>
if (value !== undefined) {
if(target) {
data[target + "@" + qName] = value;
}
else {
data[qName] = value;
}
}
};
var jsonMediaType = "application/json";
var jsonContentType = contentType(jsonMediaType);
var jsonReadAdvertisedActionsOrFunctions = function (value) {
/// <summary>Reads and object containing action or function metadata and maps them into a single array of objects.</summary>
/// <param name="value" type="Object">Object containing action or function metadata.</param>
/// <returns type="Array">Array of objects containing metadata for the actions or functions specified in value.</returns>
var result = [];
for (var name in value) {
var i, len;
for (i = 0, len = value[name].length; i < len; i++) {
result.push(extend({ metadata: name }, value[name][i]));
}
}
return result;
};
var jsonApplyMetadata = function (value, metadata, dateParser, recognizeDates) {
/// <summary>Applies metadata coming from both the payload and the metadata object to the value.</summary>
/// <param name="value" type="Object">Data on which the metada is going to be applied.</param>
/// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param>
/// <param name="dateParser" type="function">Function used for parsing datetime values.</param>
/// <param name="recognizeDates" type="Boolean">
/// True if strings formatted as datetime values should be treated as datetime values. False otherwise.
/// </param>
/// <returns type="Object">Transformed data.</returns>
if (value && typeof value === "object") {
var dataTypeName;
var valueMetadata = value.__metadata;
if (valueMetadata) {
if (valueMetadata.actions) {
valueMetadata.actions = jsonReadAdvertisedActionsOrFunctions(valueMetadata.actions);
}
if (valueMetadata.functions) {
valueMetadata.functions = jsonReadAdvertisedActionsOrFunctions(valueMetadata.functions);
}
dataTypeName = valueMetadata && valueMetadata.type;
}
var dataType = lookupEntityType(dataTypeName, metadata) || lookupComplexType(dataTypeName, metadata);
var propertyValue;
if (dataType) {
var properties = dataType.property;
if (properties) {
var i, len;
for (i = 0, len = properties.length; i < len; i++) {
var property = properties[i];
var propertyName = property.name;
propertyValue = value[propertyName];
if (property.type === "Edm.DateTime" || property.type === "Edm.DateTimeOffset") {
if (propertyValue) {
propertyValue = dateParser(propertyValue);
if (!propertyValue) {
throw { message: "Invalid date/time value" };
}
value[propertyName] = propertyValue;
}
} else if (property.type === "Edm.Time") {
value[propertyName] = parseDuration(propertyValue);
}
}
}
} else if (recognizeDates) {
for (var name in value) {
propertyValue = value[name];
if (typeof propertyValue === "string") {
value[name] = dateParser(propertyValue) || propertyValue;
}
}
}
}
return value;
};
var isJsonLight = function (contentType) {
/// <summary>Tests where the content type indicates a json light payload.</summary>
/// <param name="contentType">Object with media type and properties dictionary.</param>
/// <returns type="Boolean">True is the content type indicates a json light payload. False otherwise.</returns>
if (contentType) {
var odata = contentType.properties.odata;
return odata === "nometadata" || odata === "minimalmetadata" || odata === "fullmetadata";
}
return false;
};
var normalizeServiceDocument = function (data, baseURI) {
/// <summary>Normalizes a JSON service document to look like an ATOM service document.</summary>
/// <param name="data" type="Object">Object representation of service documents as deserialized.</param>
/// <param name="baseURI" type="String">Base URI to resolve relative URIs.</param>
/// <returns type="Object">An object representation of the service document.</returns>
var workspace = { collections: [] };
var i, len;
for (i = 0, len = data.EntitySets.length; i < len; i++) {
var title = data.EntitySets[i];
var collection = {
title: title,
href: normalizeURI(title, baseURI)
};
workspace.collections.push(collection);
}
return { workspaces: [workspace] };
};
// The regular expression corresponds to something like this:
// /Date(123+60)/
//
// This first number is date ticks, the + may be a - and is optional,
// with the second number indicating a timezone offset in minutes.
//
// On the wire, the leading and trailing forward slashes are
// escaped without being required to so the chance of collisions is reduced;
// however, by the time we see the objects, the characters already
// look like regular forward slashes.
var jsonDateRE = /^\/Date\((-?\d+)(\+|-)?(\d+)?\)\/$/;
var minutesToOffset = function (minutes) {
/// <summary>Formats the given minutes into (+/-)hh:mm format.</summary>
/// <param name="minutes" type="Number">Number of minutes to format.</param>
/// <returns type="String">The minutes in (+/-)hh:mm format.</returns>
var sign;
if (minutes < 0) {
sign = "-";
minutes = -minutes;
} else {
sign = "+";
}
var hours = Math.floor(minutes / 60);
minutes = minutes - (60 * hours);
return sign + formatNumberWidth(hours, 2) + ":" + formatNumberWidth(minutes, 2);
};
var parseJsonDateString = function (value) {
/// <summary>Parses the JSON Date representation into a Date object.</summary>
/// <param name="value" type="String">String value.</param>
/// <returns type="Date">A Date object if the value matches one; falsy otherwise.</returns>
var arr = value && jsonDateRE.exec(value);
if (arr) {
// 0 - complete results; 1 - ticks; 2 - sign; 3 - minutes
var result = new Date(parseInt10(arr[1]));
if (arr[2]) {
var mins = parseInt10(arr[3]);
if (arr[2] === "-") {
mins = -mins;
}
// The offset is reversed to get back the UTC date, which is
// what the API will eventually have.
var current = result.getUTCMinutes();
result.setUTCMinutes(current - mins);
result.__edmType = "Edm.DateTimeOffset";
result.__offset = minutesToOffset(mins);
}
if (!isNaN(result.valueOf())) {
return result;
}
}
// Allow undefined to be returned.
};
// Some JSON implementations cannot produce the character sequence \/
// which is needed to format DateTime and DateTimeOffset into the
// JSON string representation defined by the OData protocol.
// See the history of this file for a candidate implementation of
// a 'formatJsonDateString' function.
var jsonParser = function (handler, text, context) {
/// <summary>Parses a JSON OData payload.</summary>
/// <param name="handler">This handler.</param>
/// <param name="text">Payload text (this parser also handles pre-parsed objects).</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>An object representation of the OData payload.</returns>
var recognizeDates = defined(context.recognizeDates, handler.recognizeDates);
var inferJsonLightFeedAsObject = defined(context.inferJsonLightFeedAsObject, handler.inferJsonLightFeedAsObject);
var model = context.metadata;
var dataServiceVersion = context.dataServiceVersion;
var dateParser = parseJsonDateString;
var json = (typeof text === "string") ? window.JSON.parse(text) : text;
if ((maxVersion("3.0", dataServiceVersion) === dataServiceVersion)) {
if (isJsonLight(context.contentType)) {
return jsonLightReadPayload(json, model, recognizeDates, inferJsonLightFeedAsObject, context.contentType.properties.odata);
}
dateParser = parseDateTime;
}
json = traverse(json.d, function (key, value) {
return jsonApplyMetadata(value, model, dateParser, recognizeDates);
});
json = jsonUpdateDataFromVersion(json, context.dataServiceVersion);
return jsonNormalizeData(json, context.response.requestUri);
};
var jsonToString = function (data) {
/// <summary>Converts the data into a JSON string.</summary>
/// <param name="data">Data to serialize.</param>
/// <returns type="String">The JSON string representation of data.</returns>
var result; // = undefined;
// Save the current date.toJSON function
var dateToJSON = Date.prototype.toJSON;
try {
// Set our own date.toJSON function
Date.prototype.toJSON = function () {
return formatDateTimeOffset(this);
};
result = window.JSON.stringify(data, jsonReplacer);
} finally {
// Restore the original toJSON function
Date.prototype.toJSON = dateToJSON;
}
return result;
};
var jsonSerializer = function (handler, data, context) {
/// <summary>Serializes the data by returning its string representation.</summary>
/// <param name="handler">This handler.</param>
/// <param name="data">Data to serialize.</param>
/// <param name="context" type="Object">Object with serialization context.</param>
/// <returns type="String">The string representation of data.</returns>
var dataServiceVersion = context.dataServiceVersion || "1.0";
var useJsonLight = defined(context.useJsonLight, handler.useJsonLight);
var cType = context.contentType = context.contentType || jsonContentType;
if (cType && cType.mediaType === jsonContentType.mediaType) {
var json = data;
if (useJsonLight || isJsonLight(cType)) {
context.dataServiceVersion = maxVersion(dataServiceVersion, "3.0");
json = formatJsonLight(data, context);
return jsonToString(json);
}
if (maxVersion("3.0", dataServiceVersion) === dataServiceVersion) {
cType.properties.odata = "verbose";
context.contentType = cType;
}
return jsonToString(json);
}
return undefined;
};
var jsonReplacer = function (_, value) {
/// <summary>JSON replacer function for converting a value to its JSON representation.</summary>
/// <param value type="Object">Value to convert.</param>
/// <returns type="String">JSON representation of the input value.</returns>
/// <remarks>
/// This method is used during JSON serialization and invoked only by the JSON.stringify function.
/// It should never be called directly.
/// </remarks>
if (value && value.__edmType === "Edm.Time") {
return formatDuration(value);
} else {
return value;
}
};
var jsonNormalizeData = function (data, baseURI) {
/// <summary>
/// Normalizes the specified data into an intermediate representation.
/// like the latest supported version.
/// </summary>
/// <param name="data" optional="false">Data to update.</param>
/// <param name="baseURI" optional="false">URI to use as the base for normalizing references.</param>
var isSvcDoc = isComplex(data) && !data.__metadata && isArray(data.EntitySets);
return isSvcDoc ? normalizeServiceDocument(data, baseURI) : data;
};
var jsonUpdateDataFromVersion = function (data, dataVersion) {
/// <summary>
/// Updates the specified data in the specified version to look
/// like the latest supported version.
/// </summary>
/// <param name="data" optional="false">Data to update.</param>
/// <param name="dataVersion" optional="true" type="String">Version the data is in (possibly unknown).</param>
// Strip the trailing comma if there.
if (dataVersion && dataVersion.lastIndexOf(";") === dataVersion.length - 1) {
dataVersion = dataVersion.substr(0, dataVersion.length - 1);
}
if (!dataVersion || dataVersion === "1.0") {
if (isArray(data)) {
data = { results: data };
}
}
return data;
};
var jsonHandler = handler(jsonParser, jsonSerializer, jsonMediaType, MAX_DATA_SERVICE_VERSION);
jsonHandler.recognizeDates = false;
jsonHandler.useJsonLight = false;
jsonHandler.inferJsonLightFeedAsObject = false;
odata.jsonHandler = jsonHandler;
var batchMediaType = "multipart/mixed";
var responseStatusRegex = /^HTTP\/1\.\d (\d{3}) (.*)$/i;
var responseHeaderRegex = /^([^()<>@,;:\\"\/[\]?={} \t]+)\s?:\s?(.*)/;
var hex16 = function () {
/// <summary>
/// Calculates a random 16 bit number and returns it in hexadecimal format.
/// </summary>
/// <returns type="String">A 16-bit number in hex format.</returns>
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substr(1);
};
var createBoundary = function (prefix) {
/// <summary>
/// Creates a string that can be used as a multipart request boundary.
/// </summary>
/// <param name="prefix" type="String" optional="true">String to use as the start of the boundary string</param>
/// <returns type="String">Boundary string of the format: <prefix><hex16>-<hex16>-<hex16></returns>
return prefix + hex16() + "-" + hex16() + "-" + hex16();
};
var partHandler = function (context) {
/// <summary>
/// Gets the handler for data serialization of individual requests / responses in a batch.
/// </summary>
/// <param name="context">Context used for data serialization.</param>
/// <returns>Handler object.</returns>
return context.handler.partHandler;
};
var currentBoundary = function (context) {
/// <summary>
/// Gets the current boundary used for parsing the body of a multipart response.
/// </summary>
/// <param name="context">Context used for parsing a multipart response.</param>
/// <returns type="String">Boundary string.</returns>
var boundaries = context.boundaries;
return boundaries[boundaries.length - 1];
};
var batchParser = function (handler, text, context) {
/// <summary>Parses a batch response.</summary>
/// <param name="handler">This handler.</param>
/// <param name="text" type="String">Batch text.</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>An object representation of the batch.</returns>
var boundary = context.contentType.properties["boundary"];
return { __batchResponses: readBatch(text, { boundaries: [boundary], handlerContext: context }) };
};
var batchSerializer = function (handler, data, context) {
/// <summary>Serializes a batch object representation into text.</summary>
/// <param name="handler">This handler.</param>
/// <param name="data" type="Object">Representation of a batch.</param>
/// <param name="context" type="Object">Object with parsing context.</param>
/// <returns>An text representation of the batch object; undefined if not applicable.</returns>
var cType = context.contentType = context.contentType || contentType(batchMediaType);
if (cType.mediaType === batchMediaType) {
return writeBatch(data, context);
}
};
var readBatch = function (text, context) {
/// <summary>
/// Parses a multipart/mixed response body from from the position defined by the context.
/// </summary>
/// <param name="text" type="String" optional="false">Body of the multipart/mixed response.</param>
/// <param name="context">Context used for parsing.</param>
/// <returns>Array of objects representing the individual responses.</returns>
var delimiter = "--" + currentBoundary(context);
// Move beyond the delimiter and read the complete batch
readTo(text, context, delimiter);
// Ignore the incoming line
readLine(text, context);
// Read the batch parts
var responses = [];
var partEnd;
while (partEnd !== "--" && context.position < text.length) {
var partHeaders = readHeaders(text, context);
var partContentType = contentType(partHeaders["Content-Type"]);
var changeResponses;
if (partContentType && partContentType.mediaType === batchMediaType) {
context.boundaries.push(partContentType.properties["boundary"]);
try {
changeResponses = readBatch(text, context);
} catch (e) {
e.response = readResponse(text, context, delimiter);
changeResponses = [e];
}
responses.push({ __changeResponses: changeResponses });
context.boundaries.pop();
readTo(text, context, "--" + currentBoundary(context));
} else {
if (!partContentType || partContentType.mediaType !== "application/http") {
throw { message: "invalid MIME part type " };
}
// Skip empty line
readLine(text, context);
// Read the response
var response = readResponse(text, context, delimiter);
try {
if (response.statusCode >= 200 && response.statusCode <= 299) {
partHandler(context.handlerContext).read(response, context.handlerContext);
} else {
// Keep track of failed responses and continue processing the batch.
response = { message: "HTTP request failed", response: response };
}
} catch (e) {
response = e;
}
responses.push(response);
}
partEnd = text.substr(context.position, 2);
// Ignore the incoming line.
readLine(text, context);
}
return responses;
};
var readHeaders = function (text, context) {
/// <summary>
/// Parses the http headers in the text from the position defined by the context.
/// </summary>
/// <param name="text" type="String" optional="false">Text containing an http response's headers</param>
/// <param name="context">Context used for parsing.</param>
/// <returns>Object containing the headers as key value pairs.</returns>
/// <remarks>
/// This function doesn't support split headers and it will stop reading when it hits two consecutive line breaks.
/// </remarks>
var headers = {};
var parts;
var line;
var pos;
do {
pos = context.position;
line = readLine(text, context);
parts = responseHeaderRegex.exec(line);
if (parts !== null) {
headers[parts[1]] = parts[2];
} else {
// Whatever was found is not a header, so reset the context position.
context.position = pos;
}
} while (line && parts);
normalizeHeaders(headers);
return headers;
};
var readResponse = function (text, context, delimiter) {
/// <summary>
/// Parses an HTTP response.
/// </summary>
/// <param name="text" type="String" optional="false">Text representing the http response.</param>
/// <param name="context" optional="false">Context used for parsing.</param>
/// <param name="delimiter" type="String" optional="false">String used as delimiter of the multipart response parts.</param>
/// <returns>Object representing the http response.</returns>
// Read the status line.
var pos = context.position;
var match = responseStatusRegex.exec(readLine(text, context));
var statusCode;
var statusText;
var headers;
if (match) {
statusCode = match[1];
statusText = match[2];
headers = readHeaders(text, context);
readLine(text, context);
} else {
context.position = pos;
}
return {
statusCode: statusCode,
statusText: statusText,
headers: headers,
body: readTo(text, context, "\r\n" + delimiter)
};
};
var readLine = function (text, context) {
/// <summary>
/// Returns a substring from the position defined by the context up to the next line break (CRLF).
/// </summary>
/// <param name="text" type="String" optional="false">Input string.</param>
/// <param name="context" optional="false">Context used for reading the input string.</param>
/// <returns type="String">Substring to the first ocurrence of a line break or null if none can be found. </returns>
return readTo(text, context, "\r\n");
};
var readTo = function (text, context, str) {
/// <summary>
/// Returns a substring from the position given by the context up to value defined by the str parameter and increments the position in the context.
/// </summary>
/// <param name="text" type="String" optional="false">Input string.</param>
/// <param name="context" type="Object" optional="false">Context used for reading the input string.</param>
/// <param name="str" type="String" optional="true">Substring to read up to.</param>
/// <returns type="String">Substring to the first ocurrence of str or the end of the input string if str is not specified. Null if the marker is not found.</returns>
var start = context.position || 0;
var end = text.length;
if (str) {
end = text.indexOf(str, start);
if (end === -1) {
return null;
}
context.position = end + str.length;
} else {
context.position = end;
}
return text.substring(start, end);
};
var writeBatch = function (data, context) {
/// <summary>
/// Serializes a batch request object to a string.
/// </summary>
/// <param name="data" optional="false">Batch request object in payload representation format</param>
/// <param name="context" optional="false">Context used for the serialization</param>
/// <returns type="String">String representing the batch request</returns>
if (!isBatch(data)) {
throw { message: "Data is not a batch object." };
}
var batchBoundary = createBoundary("batch_");
var batchParts = data.__batchRequests;
var batch = "";
var i, len;
for (i = 0, len = batchParts.length; i < len; i++) {
batch += writeBatchPartDelimiter(batchBoundary, false) +
writeBatchPart(batchParts[i], context);
}
batch += writeBatchPartDelimiter(batchBoundary, true);
// Register the boundary with the request content type.
var contentTypeProperties = context.contentType.properties;
contentTypeProperties.boundary = batchBoundary;
return batch;
};
var writeBatchPartDelimiter = function (boundary, close) {
/// <summary>
/// Creates the delimiter that indicates that start or end of an individual request.
/// </summary>
/// <param name="boundary" type="String" optional="false">Boundary string used to indicate the start of the request</param>
/// <param name="close" type="Boolean">Flag indicating that a close delimiter string should be generated</param>
/// <returns type="String">Delimiter string</returns>
var result = "\r\n--" + boundary;
if (close) {
result += "--";
}
return result + "\r\n";
};
var writeBatchPart = function (part, context, nested) {
/// <summary>
/// Serializes a part of a batch request to a string. A part can be either a GET request or
/// a change set grouping several CUD (create, update, delete) requests.
/// </summary>
/// <param name="part" optional="false">Request or change set object in payload representation format</param>
/// <param name="context" optional="false">Object containing context information used for the serialization</param>
/// <param name="nested" type="boolean" optional="true">Flag indicating that the part is nested inside a change set</param>
/// <returns type="String">String representing the serialized part</returns>
/// <remarks>
/// A change set is an array of request objects and they cannot be nested inside other change sets.
/// </remarks>
var changeSet = part.__changeRequests;
var result;
if (isArray(changeSet)) {
if (nested) {
throw { message: "Not Supported: change set nested in other change set" };
}
var changeSetBoundary = createBoundary("changeset_");
result = "Content-Type: " + batchMediaType + "; boundary=" + changeSetBoundary + "\r\n";
var i, len;
for (i = 0, len = changeSet.length; i < len; i++) {
result += writeBatchPartDelimiter(changeSetBoundary, false) +
writeBatchPart(changeSet[i], context, true);
}
result += writeBatchPartDelimiter(changeSetBoundary, true);
} else {
result = "Content-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\n";
var partContext = extend({}, context);
partContext.handler = handler;
partContext.request = part;
partContext.contentType = null;
prepareRequest(part, partHandler(context), partContext);
result += writeRequest(part);
}
return result;
};
var writeRequest = function (request) {
/// <summary>
/// Serializes a request object to a string.
/// </summary>
/// <param name="request" optional="false">Request object to serialize</param>
/// <returns type="String">String representing the serialized request</returns>
var result = (request.method ? request.method : "GET") + " " + request.requestUri + " HTTP/1.1\r\n";
for (var name in request.headers) {
if (request.headers[name]) {
result = result + name + ": " + request.headers[name] + "\r\n";
}
}
result += "\r\n";
if (request.body) {
result += request.body;
}
return result;
};
odata.batchHandler = handler(batchParser, batchSerializer, batchMediaType, MAX_DATA_SERVICE_VERSION);
var handlers = [odata.jsonHandler, odata.atomHandler, odata.xmlHandler, odata.textHandler];
var dispatchHandler = function (handlerMethod, requestOrResponse, context) {
/// <summary>Dispatches an operation to handlers.</summary>
/// <param name="handlerMethod" type="String">Name of handler method to invoke.</param>
/// <param name="requestOrResponse" type="Object">request/response argument for delegated call.</param>
/// <param name="context" type="Object">context argument for delegated call.</param>
var i, len;
for (i = 0, len = handlers.length; i < len && !handlers[i][handlerMethod](requestOrResponse, context); i++) {
}
if (i === len) {
throw { message: "no handler for data" };
}
};
odata.defaultSuccess = function (data) {
/// <summary>Default success handler for OData.</summary>
/// <param name="data">Data to process.</param>
window.alert(window.JSON.stringify(data));
};
odata.defaultError = throwErrorCallback;
odata.defaultHandler = {
read: function (response, context) {
/// <summary>Reads the body of the specified response by delegating to JSON and ATOM handlers.</summary>
/// <param name="response">Response object.</param>
/// <param name="context">Operation context.</param>
if (response && assigned(response.body) && response.headers["Content-Type"]) {
dispatchHandler("read", response, context);
}
},
write: function (request, context) {
/// <summary>Write the body of the specified request by delegating to JSON and ATOM handlers.</summary>
/// <param name="request">Reques tobject.</param>
/// <param name="context">Operation context.</param>
dispatchHandler("write", request, context);
},
maxDataServiceVersion: MAX_DATA_SERVICE_VERSION,
accept: "application/atomsvc+xml;q=0.8, application/json;odata=fullmetadata;q=0.7, application/json;q=0.5, */*;q=0.1"
};
odata.defaultMetadata = [];
odata.read = function (urlOrRequest, success, error, handler, httpClient, metadata) {
/// <summary>Reads data from the specified URL.</summary>
/// <param name="urlOrRequest">URL to read data from.</param>
/// <param name="success" type="Function" optional="true">Callback for a successful read operation.</param>
/// <param name="error" type="Function" optional="true">Callback for handling errors.</param>
/// <param name="handler" type="Object" optional="true">Handler for data serialization.</param>
/// <param name="httpClient" type="Object" optional="true">HTTP client layer.</param>
/// <param name="metadata" type="Object" optional="true">Conceptual metadata for this request.</param>
var request;
if (urlOrRequest instanceof String || typeof urlOrRequest === "string") {
request = { requestUri: urlOrRequest };
} else {
request = urlOrRequest;
}
return odata.request(request, success, error, handler, httpClient, metadata);
};
odata.request = function (request, success, error, handler, httpClient, metadata) {
/// <summary>Sends a request containing OData payload to a server.</summary>
/// <param name="request" type="Object">Object that represents the request to be sent.</param>
/// <param name="success" type="Function" optional="true">Callback for a successful read operation.</param>
/// <param name="error" type="Function" optional="true">Callback for handling errors.</param>
/// <param name="handler" type="Object" optional="true">Handler for data serialization.</param>
/// <param name="httpClient" type="Object" optional="true">HTTP client layer.</param>
/// <param name="metadata" type="Object" optional="true">Conceptual metadata for this request.</param>
success = success || odata.defaultSuccess;
error = error || odata.defaultError;
handler = handler || odata.defaultHandler;
httpClient = httpClient || odata.defaultHttpClient;
metadata = metadata || odata.defaultMetadata;
// Augment the request with additional defaults.
request.recognizeDates = defined(request.recognizeDates, odata.jsonHandler.recognizeDates);
request.callbackParameterName = defined(request.callbackParameterName, odata.defaultHttpClient.callbackParameterName);
request.formatQueryString = defined(request.formatQueryString, odata.defaultHttpClient.formatQueryString);
request.enableJsonpCallback = defined(request.enableJsonpCallback, odata.defaultHttpClient.enableJsonpCallback);
request.useJsonLight = defined(request.useJsonLight, odata.jsonHandler.enableJsonpCallback);
request.inferJsonLightFeedAsObject = defined(request.inferJsonLightFeedAsObject, odata.jsonHandler.inferJsonLightFeedAsObject);
// Create the base context for read/write operations, also specifying complete settings.
var context = {
metadata: metadata,
recognizeDates: request.recognizeDates,
callbackParameterName: request.callbackParameterName,
formatQueryString: request.formatQueryString,
enableJsonpCallback: request.enableJsonpCallback,
useJsonLight: request.useJsonLight,
inferJsonLightFeedAsObject: request.inferJsonLightFeedAsObject
};
try {
prepareRequest(request, handler, context);
return invokeRequest(request, success, error, handler, httpClient, context);
} catch (err) {
error(err);
}
};
odata.parseMetadata = function (csdlMetadataDocument) {
/// <summary>Parses the csdl metadata to DataJS metatdata format. This method can be used when the metadata is retrieved using something other than DataJS</summary>
/// <param name="atomMetadata" type="string">A string that represents the entire csdl metadata.</param>
/// <returns type="Object">An object that has the representation of the metadata in Datajs format.</returns>
return metadataParser(null, csdlMetadataDocument);
};
// Configure the batch handler to use the default handler for the batch parts.
odata.batchHandler.partHandler = odata.defaultHandler;
var localStorage = null;
var domStoreDateToJSON = function () {
/// <summary>Converts a Date object into an object representation friendly to JSON serialization.</summary>
/// <returns type="Object">Object that represents the Date.</returns>
/// <remarks>
/// This method is used to override the Date.toJSON method and is called only by
/// JSON.stringify. It should never be called directly.
/// </remarks>
var newValue = { v: this.valueOf(), t: "[object Date]" };
// Date objects might have extra properties on them so we save them.
for (var name in this) {
newValue[name] = this[name];
}
return newValue;
};
var domStoreJSONToDate = function (_, value) {
/// <summary>JSON reviver function for converting an object representing a Date in a JSON stream to a Date object</summary>
/// <param value="Object">Object to convert.</param>
/// <returns type="Date">Date object.</returns>
/// <remarks>
/// This method is used during JSON parsing and invoked only by the reviver function.
/// It should never be called directly.
/// </remarks>
if (value && value.t === "[object Date]") {
var newValue = new Date(value.v);
for (var name in value) {
if (name !== "t" && name !== "v") {
newValue[name] = value[name];
}
}
value = newValue;
}
return value;
};
var qualifyDomStoreKey = function (store, key) {
/// <summary>Qualifies the key with the name of the store.</summary>
/// <param name="store" type="Object">Store object whose name will be used for qualifying the key.</param>
/// <param name="key" type="String">Key string.</param>
/// <returns type="String">Fully qualified key string.</returns>
return store.name + "#!#" + key;
};
var unqualifyDomStoreKey = function (store, key) {
/// <summary>Gets the key part of a fully qualified key string.</summary>
/// <param name="store" type="Object">Store object whose name will be used for qualifying the key.</param>
/// <param name="key" type="String">Fully qualified key string.</param>
/// <returns type="String">Key part string</returns>
return key.replace(store.name + "#!#", "");
};
var DomStore = function (name) {
/// <summary>Constructor for store objects that use DOM storage as the underlying mechanism.</summary>
/// <param name="name" type="String">Store name.</param>
this.name = name;
};
DomStore.create = function (name) {
/// <summary>Creates a store object that uses DOM Storage as its underlying mechanism.</summary>
/// <param name="name" type="String">Store name.</param>
/// <returns type="Object">Store object.</returns>
if (DomStore.isSupported()) {
localStorage = localStorage || window.localStorage;
return new DomStore(name);
}
throw { message: "Web Storage not supported by the browser" };
};
DomStore.isSupported = function () {
/// <summary>Checks whether the underlying mechanism for this kind of store objects is supported by the browser.</summary>
/// <returns type="Boolean">True if the mechanism is supported by the browser; otherwise false.</summary>
return !!window.localStorage;
};
DomStore.prototype.add = function (key, value, success, error) {
/// <summary>Adds a new value identified by a key to the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="value">Value that is going to be added to the store.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful add operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
/// <remarks>
/// This method errors out if the store already contains the specified key.
/// </remarks>
error = error || this.defaultError;
var store = this;
this.contains(key, function (contained) {
if (!contained) {
store.addOrUpdate(key, value, success, error);
} else {
delay(error, { message: "key already exists", key: key });
}
}, error);
};
DomStore.prototype.addOrUpdate = function (key, value, success, error) {
/// <summary>Adds or updates a value identified by a key to the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="value">Value that is going to be added or updated to the store.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful add or update operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
/// <remarks>
/// This method will overwrite the key's current value if it already exists in the store; otherwise it simply adds the new key and value.
/// </remarks>
error = error || this.defaultError;
if (key instanceof Array) {
error({ message: "Array of keys not supported" });
} else {
var fullKey = qualifyDomStoreKey(this, key);
var oldDateToJSON = Date.prototype.toJSON;
try {
var storedValue = value;
if (storedValue !== undefined) {
// Dehydrate using json
Date.prototype.toJSON = domStoreDateToJSON;
storedValue = window.JSON.stringify(value);
}
// Save the json string.
localStorage.setItem(fullKey, storedValue);
delay(success, key, value);
}
catch (e) {
if (e.code === 22 || e.number === 0x8007000E) {
delay(error, { name: "QUOTA_EXCEEDED_ERR", error: e });
} else {
delay(error, e);
}
}
finally {
Date.prototype.toJSON = oldDateToJSON;
}
}
};
DomStore.prototype.clear = function (success, error) {
/// <summary>Removes all the data associated with this store object.</summary>
/// <param name="success" type="Function" optional="no">Callback for a successful clear operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
/// <remarks>
/// In case of an error, this method will not restore any keys that might have been deleted at that point.
/// </remarks>
error = error || this.defaultError;
try {
var i = 0, len = localStorage.length;
while (len > 0 && i < len) {
var fullKey = localStorage.key(i);
var key = unqualifyDomStoreKey(this, fullKey);
if (fullKey !== key) {
localStorage.removeItem(fullKey);
len = localStorage.length;
} else {
i++;
}
}
delay(success);
}
catch (e) {
delay(error, e);
}
};
DomStore.prototype.close = function () {
/// <summary>This function does nothing in DomStore as it does not have a connection model</summary>
};
DomStore.prototype.contains = function (key, success, error) {
/// <summary>Checks whether a key exists in the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="success" type="Function" optional="no">Callback indicating whether the store contains the key or not.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
error = error || this.defaultError;
try {
var fullKey = qualifyDomStoreKey(this, key);
var value = localStorage.getItem(fullKey);
delay(success, value !== null);
} catch (e) {
delay(error, e);
}
};
DomStore.prototype.defaultError = throwErrorCallback;
DomStore.prototype.getAllKeys = function (success, error) {
/// <summary>Gets all the keys that exist in the store.</summary>
/// <param name="success" type="Function" optional="no">Callback for a successful get operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
error = error || this.defaultError;
var results = [];
var i, len;
try {
for (i = 0, len = localStorage.length; i < len; i++) {
var fullKey = localStorage.key(i);
var key = unqualifyDomStoreKey(this, fullKey);
if (fullKey !== key) {
results.push(key);
}
}
delay(success, results);
}
catch (e) {
delay(error, e);
}
};
/// <summary>Identifies the underlying mechanism used by the store.</summary>
DomStore.prototype.mechanism = "dom";
DomStore.prototype.read = function (key, success, error) {
/// <summary>Reads the value associated to a key in the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful reads operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
error = error || this.defaultError;
if (key instanceof Array) {
error({ message: "Array of keys not supported" });
} else {
try {
var fullKey = qualifyDomStoreKey(this, key);
var value = localStorage.getItem(fullKey);
if (value !== null && value !== "undefined") {
// Hydrate using json
value = window.JSON.parse(value, domStoreJSONToDate);
}
else {
value = undefined;
}
delay(success, key, value);
} catch (e) {
delay(error, e);
}
}
};
DomStore.prototype.remove = function (key, success, error) {
/// <summary>Removes a key and its value from the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful remove operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
error = error || this.defaultError;
if (key instanceof Array) {
error({ message: "Batches not supported" });
} else {
try {
var fullKey = qualifyDomStoreKey(this, key);
localStorage.removeItem(fullKey);
delay(success);
} catch (e) {
delay(error, e);
}
}
};
DomStore.prototype.update = function (key, value, success, error) {
/// <summary>Updates the value associated to a key in the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="value">New value.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful update operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
/// <remarks>
/// This method errors out if the specified key is not found in the store.
/// </remarks>
error = error || this.defaultError;
var store = this;
this.contains(key, function (contained) {
if (contained) {
store.addOrUpdate(key, value, success, error);
} else {
delay(error, { message: "key not found", key: key });
}
}, error);
};
var indexedDB = window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.indexedDB;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || {};
var IDBT_READ_ONLY = IDBTransaction.READ_ONLY || "readonly";
var IDBT_READ_WRITE = IDBTransaction.READ_WRITE || "readwrite";
var getError = function (error, defaultError) {
/// <summary>Returns either a specific error handler or the default error handler</summary>
/// <param name="error" type="Function">The specific error handler</param>
/// <param name="defaultError" type="Function">The default error handler</param>
/// <returns type="Function">The error callback</returns>
return function (e) {
var errorFunc = error || defaultError;
if (!errorFunc) {
return;
}
// Old api quota exceeded error support.
if (Object.prototype.toString.call(e) === "[object IDBDatabaseException]") {
if (e.code === 11 /* IndexedDb disk quota exceeded */) {
errorFunc({ name: "QuotaExceededError", error: e });
return;
}
errorFunc(e);
return;
}
var errName;
try {
var errObj = e.target.error || e;
errName = errObj.name;
} catch (ex) {
errName = (e.type === "blocked") ? "IndexedDBBlocked" : "UnknownError";
}
errorFunc({ name: errName, error: e });
};
};
var openStoreDb = function (store, success, error) {
/// <summary>Opens the store object's indexed db database.</summary>
/// <param name="store" type="IndexedDBStore">The store object</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var storeName = store.name;
var dbName = "_datajs_" + storeName;
var request = indexedDB.open(dbName);
request.onblocked = error;
request.onerror = error;
request.onupgradeneeded = function () {
var db = request.result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName);
}
};
request.onsuccess = function (event) {
var db = request.result;
if (!db.objectStoreNames.contains(storeName)) {
// Should we use the old style api to define the database schema?
if ("setVersion" in db) {
var versionRequest = db.setVersion("1.0");
versionRequest.onsuccess = function () {
var transaction = versionRequest.transaction;
transaction.oncomplete = function () {
success(db);
};
db.createObjectStore(storeName, null, false);
};
versionRequest.onerror = error;
versionRequest.onblocked = error;
return;
}
// The database doesn't have the expected store.
// Fabricate an error object for the event for the schema mismatch
// and error out.
event.target.error = { name: "DBSchemaMismatch" };
error(event);
return;
}
db.onversionchange = function(event) {
event.target.close();
};
success(db);
};
};
var openTransaction = function (store, mode, success, error) {
/// <summary>Opens a new transaction to the store</summary>
/// <param name="store" type="IndexedDBStore">The store object</param>
/// <param name="mode" type="Short">The read/write mode of the transaction (constants from IDBTransaction)</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var storeName = store.name;
var storeDb = store.db;
var errorCallback = getError(error, store.defaultError);
if (storeDb) {
success(storeDb.transaction(storeName, mode));
return;
}
openStoreDb(store, function (db) {
store.db = db;
success(db.transaction(storeName, mode));
}, errorCallback);
};
var IndexedDBStore = function (name) {
/// <summary>Creates a new IndexedDBStore.</summary>
/// <param name="name" type="String">The name of the store.</param>
/// <returns type="Object">The new IndexedDBStore.</returns>
this.name = name;
};
IndexedDBStore.create = function (name) {
/// <summary>Creates a new IndexedDBStore.</summary>
/// <param name="name" type="String">The name of the store.</param>
/// <returns type="Object">The new IndexedDBStore.</returns>
if (IndexedDBStore.isSupported()) {
return new IndexedDBStore(name);
}
throw { message: "IndexedDB is not supported on this browser" };
};
IndexedDBStore.isSupported = function () {
/// <summary>Returns whether IndexedDB is supported.</summary>
/// <returns type="Boolean">True if IndexedDB is supported, false otherwise.</returns>
return !!indexedDB;
};
IndexedDBStore.prototype.add = function (key, value, success, error) {
/// <summary>Adds a key/value pair to the store</summary>
/// <param name="key" type="String">The key</param>
/// <param name="value" type="Object">The value</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var name = this.name;
var defaultError = this.defaultError;
var keys = [];
var values = [];
if (key instanceof Array) {
keys = key;
values = value;
} else {
keys = [key];
values = [value];
}
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onabort = getError(error, defaultError, key, "add");
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(key, value);
}
};
for (var i = 0; i < keys.length && i < values.length; i++) {
transaction.objectStore(name).add({ v: values[i] }, keys[i]);
}
}, error);
};
IndexedDBStore.prototype.addOrUpdate = function (key, value, success, error) {
/// <summary>Adds or updates a key/value pair in the store</summary>
/// <param name="key" type="String">The key</param>
/// <param name="value" type="Object">The value</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var name = this.name;
var defaultError = this.defaultError;
var keys = [];
var values = [];
if (key instanceof Array) {
keys = key;
values = value;
} else {
keys = [key];
values = [value];
}
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onabort = getError(error, defaultError);
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(key, value);
}
};
for (var i = 0; i < keys.length && i < values.length; i++) {
var record = { v: values[i] };
transaction.objectStore(name).put(record, keys[i]);
}
}, error);
};
IndexedDBStore.prototype.clear = function (success, error) {
/// <summary>Clears the store</summary>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var name = this.name;
var defaultError = this.defaultError;
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onerror = getError(error, defaultError);
transaction.oncomplete = function () {
success();
};
transaction.objectStore(name).clear();
}, error);
};
IndexedDBStore.prototype.close = function () {
/// <summary>Closes the connection to the database</summary>
if (this.db) {
this.db.close();
this.db = null;
}
};
IndexedDBStore.prototype.contains = function (key, success, error) {
/// <summary>Returns whether the store contains a key</summary>
/// <param name="key" type="String">The key</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var name = this.name;
var defaultError = this.defaultError;
openTransaction(this, IDBT_READ_ONLY, function (transaction) {
var objectStore = transaction.objectStore(name);
var request = objectStore["get"](key);
transaction.oncomplete = function () {
success(!!request.result);
};
transaction.onerror = getError(error, defaultError);
}, error);
};
IndexedDBStore.prototype.defaultError = throwErrorCallback;
IndexedDBStore.prototype.getAllKeys = function (success, error) {
/// <summary>Gets all the keys from the store</summary>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var name = this.name;
var defaultError = this.defaultError;
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
var results = [];
transaction.oncomplete = function () {
success(results);
};
var request = transaction.objectStore(name).openCursor();
request.onerror = getError(error, defaultError);
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
results.push(cursor.key);
// Some tools have issues because continue is a javascript reserved word.
cursor["continue"].call(cursor);
}
};
}, error);
};
/// <summary>Identifies the underlying mechanism used by the store.</summary>
IndexedDBStore.prototype.mechanism = "indexeddb";
IndexedDBStore.prototype.read = function (key, success, error) {
/// <summary>Reads the value for the specified key</summary>
/// <param name="key" type="String">The key</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
/// <remarks>If the key does not exist, the success handler will be called with value = undefined</remarks>
var name = this.name;
var defaultError = this.defaultError;
var keys = (key instanceof Array) ? key : [key];
openTransaction(this, IDBT_READ_ONLY, function (transaction) {
var values = [];
transaction.onerror = getError(error, defaultError, key, "read");
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(keys[0], values[0]);
}
};
for (var i = 0; i < keys.length; i++) {
// Some tools have issues because get is a javascript reserved word.
var objectStore = transaction.objectStore(name);
var request = objectStore["get"].call(objectStore, keys[i]);
request.onsuccess = function (event) {
var record = event.target.result;
values.push(record ? record.v : undefined);
};
}
}, error);
};
IndexedDBStore.prototype.remove = function (key, success, error) {
/// <summary>Removes the specified key from the store</summary>
/// <param name="key" type="String">The key</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var name = this.name;
var defaultError = this.defaultError;
var keys = (key instanceof Array) ? key : [key];
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onerror = getError(error, defaultError);
transaction.oncomplete = function () {
success();
};
for (var i = 0; i < keys.length; i++) {
// Some tools have issues because continue is a javascript reserved word.
var objectStore = transaction.objectStore(name);
objectStore["delete"].call(objectStore, keys[i]);
}
}, error);
};
IndexedDBStore.prototype.update = function (key, value, success, error) {
/// <summary>Updates a key/value pair in the store</summary>
/// <param name="key" type="String">The key</param>
/// <param name="value" type="Object">The value</param>
/// <param name="success" type="Function">The success callback</param>
/// <param name="error" type="Function">The error callback</param>
var name = this.name;
var defaultError = this.defaultError;
var keys = [];
var values = [];
if (key instanceof Array) {
keys = key;
values = value;
} else {
keys = [key];
values = [value];
}
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onabort = getError(error, defaultError);
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(key, value);
}
};
for (var i = 0; i < keys.length && i < values.length; i++) {
var request = transaction.objectStore(name).openCursor(IDBKeyRange.only(keys[i]));
var record = { v: values[i] };
request.pair = { key: keys[i], value: record };
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
cursor.update(event.target.pair.value);
} else {
transaction.abort();
}
};
}
}, error);
};
var MemoryStore = function (name) {
/// <summary>Constructor for store objects that use a sorted array as the underlying mechanism.</summary>
/// <param name="name" type="String">Store name.</param>
var holes = [];
var items = [];
var keys = {};
this.name = name;
var getErrorCallback = function (error) {
return error || this.defaultError;
};
var validateKeyInput = function (key, error) {
/// <summary>Validates that the specified key is not undefined, not null, and not an array</summary>
/// <param name="key">Key value.</param>
/// <param name="error" type="Function">Error callback.</param>
/// <returns type="Boolean">True if the key is valid. False if the key is invalid and the error callback has been queued for execution.</returns>
var messageString;
if (key instanceof Array) {
messageString = "Array of keys not supported";
}
if (key === undefined || key === null) {
messageString = "Invalid key";
}
if (messageString) {
delay(error, { message: messageString });
return false;
}
return true;
};
this.add = function (key, value, success, error) {
/// <summary>Adds a new value identified by a key to the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="value">Value that is going to be added to the store.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful add operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
/// <remarks>
/// This method errors out if the store already contains the specified key.
/// </remarks>
error = getErrorCallback(error);
if (validateKeyInput(key, error)) {
if (!keys.hasOwnProperty(key)) {
this.addOrUpdate(key, value, success, error);
} else {
error({ message: "key already exists", key: key });
}
}
};
this.addOrUpdate = function (key, value, success, error) {
/// <summary>Adds or updates a value identified by a key to the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="value">Value that is going to be added or updated to the store.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful add or update operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
/// <remarks>
/// This method will overwrite the key's current value if it already exists in the store; otherwise it simply adds the new key and value.
/// </remarks>
error = getErrorCallback(error);
if (validateKeyInput(key, error)) {
var index = keys[key];
if (index === undefined) {
if (holes.length > 0) {
index = holes.splice(0, 1);
} else {
index = items.length;
}
}
items[index] = value;
keys[key] = index;
delay(success, key, value);
}
};
this.clear = function (success) {
/// <summary>Removes all the data associated with this store object.</summary>
/// <param name="success" type="Function" optional="no">Callback for a successful clear operation.</param>
items = [];
keys = {};
holes = [];
delay(success);
};
this.contains = function (key, success) {
/// <summary>Checks whether a key exists in the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="success" type="Function" optional="no">Callback indicating whether the store contains the key or not.</param>
var contained = keys.hasOwnProperty(key);
delay(success, contained);
};
this.getAllKeys = function (success) {
/// <summary>Gets all the keys that exist in the store.</summary>
/// <param name="success" type="Function" optional="no">Callback for a successful get operation.</param>
var results = [];
for (var name in keys) {
results.push(name);
}
delay(success, results);
};
this.read = function (key, success, error) {
/// <summary>Reads the value associated to a key in the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful reads operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
error = getErrorCallback(error);
if (validateKeyInput(key, error)) {
var index = keys[key];
delay(success, key, items[index]);
}
};
this.remove = function (key, success, error) {
/// <summary>Removes a key and its value from the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful remove operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
error = getErrorCallback(error);
if (validateKeyInput(key, error)) {
var index = keys[key];
if (index !== undefined) {
if (index === items.length - 1) {
items.pop();
} else {
items[index] = undefined;
holes.push(index);
}
delete keys[key];
// The last item was removed, no need to keep track of any holes in the array.
if (items.length === 0) {
holes = [];
}
}
delay(success);
}
};
this.update = function (key, value, success, error) {
/// <summary>Updates the value associated to a key in the store.</summary>
/// <param name="key" type="String">Key string.</param>
/// <param name="value">New value.</param>
/// <param name="success" type="Function" optional="no">Callback for a successful update operation.</param>
/// <param name="error" type="Function" optional="yes">Callback for handling errors. If not specified then store.defaultError is invoked.</param>
/// <remarks>
/// This method errors out if the specified key is not found in the store.
/// </remarks>
error = getErrorCallback(error);
if (validateKeyInput(key, error)) {
if (keys.hasOwnProperty(key)) {
this.addOrUpdate(key, value, success, error);
} else {
error({ message: "key not found", key: key });
}
}
};
};
MemoryStore.create = function (name) {
/// <summary>Creates a store object that uses memory storage as its underlying mechanism.</summary>
/// <param name="name" type="String">Store name.</param>
/// <returns type="Object">Store object.</returns>
return new MemoryStore(name);
};
MemoryStore.isSupported = function () {
/// <summary>Checks whether the underlying mechanism for this kind of store objects is supported by the browser.</summary>
/// <returns type="Boolean">True if the mechanism is supported by the browser; otherwise false.</returns>
return true;
};
MemoryStore.prototype.close = function () {
/// <summary>This function does nothing in MemoryStore as it does not have a connection model.</summary>
};
MemoryStore.prototype.defaultError = throwErrorCallback;
/// <summary>Identifies the underlying mechanism used by the store.</summary>
MemoryStore.prototype.mechanism = "memory";
var mechanisms = {
indexeddb: IndexedDBStore,
dom: DomStore,
memory: MemoryStore
};
odatajs.defaultStoreMechanism = "best";
odatajs.createStore = function (name, mechanism) {
/// <summary>Creates a new store object.</summary>
/// <param name="name" type="String">Store name.</param>
/// <param name="mechanism" type="String" optional="true">A specific mechanism to use (defaults to best, can be "best", "dom", "indexeddb", "webdb").</param>
/// <returns type="Object">Store object.</returns>
if (!mechanism) {
mechanism = odatajs.defaultStoreMechanism;
}
if (mechanism === "best") {
mechanism = (DomStore.isSupported()) ? "dom" : "memory";
}
var factory = mechanisms[mechanism];
if (factory) {
return factory.create(name);
}
throw { message: "Failed to create store", name: name, mechanism: mechanism };
};
var appendQueryOption = function (uri, queryOption) {
/// <summary>Appends the specified escaped query option to the specified URI.</summary>
/// <param name="uri" type="String">URI to append option to.</param>
/// <param name="queryOption" type="String">Escaped query option to append.</param>
var separator = (uri.indexOf("?") >= 0) ? "&" : "?";
return uri + separator + queryOption;
};
var appendSegment = function (uri, segment) {
/// <summary>Appends the specified segment to the given URI.</summary>
/// <param name="uri" type="String">URI to append a segment to.</param>
/// <param name="segment" type="String">Segment to append.</param>
/// <returns type="String">The original URI with a new segment appended.</returns>
var index = uri.indexOf("?");
var queryPortion = "";
if (index >= 0) {
queryPortion = uri.substr(index);
uri = uri.substr(0, index);
}
if (uri[uri.length - 1] !== "/") {
uri += "/";
}
return uri + segment + queryPortion;
};
var buildODataRequest = function (uri, options) {
/// <summary>Builds a request object to GET the specified URI.</summary>
/// <param name="uri" type="String">URI for request.</param>
/// <param name="options" type="Object">Additional options.</param>
return {
method: "GET",
requestUri: uri,
user: options.user,
password: options.password,
enableJsonpCallback: options.enableJsonpCallback,
callbackParameterName: options.callbackParameterName,
formatQueryString: options.formatQueryString
};
};
var findQueryOptionStart = function (uri, name) {
/// <summary>Finds the index where the value of a query option starts.</summary>
/// <param name="uri" type="String">URI to search in.</param>
/// <param name="name" type="String">Name to look for.</param>
/// <returns type="Number">The index where the query option starts.</returns>
var result = -1;
var queryIndex = uri.indexOf("?");
if (queryIndex !== -1) {
var start = uri.indexOf("?" + name + "=", queryIndex);
if (start === -1) {
start = uri.indexOf("&" + name + "=", queryIndex);
}
if (start !== -1) {
result = start + name.length + 2;
}
}
return result;
};
var queryForData = function (uri, options, success, error) {
/// <summary>Gets data from an OData service.</summary>
/// <param name="uri" type="String">URI to the OData service.</param>
/// <param name="options" type="Object">Object with additional well-known request options.</param>
/// <param name="success" type="Function">Success callback.</param>
/// <param name="error" type="Function">Error callback.</param>
/// <returns type="Object">Object with an abort method.</returns>
var request = queryForDataInternal(uri, options, [], success, error);
return request;
};
var queryForDataInternal = function (uri, options, data, success, error) {
/// <summary>Gets data from an OData service taking into consideration server side paging.</summary>
/// <param name="uri" type="String">URI to the OData service.</param>
/// <param name="options" type="Object">Object with additional well-known request options.</param>
/// <param name="data" type="Array">Array that stores the data provided by the OData service.</param>
/// <param name="success" type="Function">Success callback.</param>
/// <param name="error" type="Function">Error callback.</param>
/// <returns type="Object">Object with an abort method.</returns>
var request = buildODataRequest(uri, options);
var currentRequest = odata.request(request, function (newData) {
var next = newData.__next;
var results = newData.results;
data = data.concat(results);
if (next) {
currentRequest = queryForDataInternal(next, options, data, success, error);
} else {
success(data);
}
}, error, undefined, options.httpClient, options.metadata);
return {
abort: function () {
currentRequest.abort();
}
};
};
var ODataCacheSource = function (options) {
/// <summary>Creates a data cache source object for requesting data from an OData service.</summary>
/// <param name="options">Options for the cache data source.</param>
/// <returns type="ODataCacheSource">A new data cache source instance.</returns>
var that = this;
var uri = options.source;
that.identifier = normalizeURICase(encodeURI(decodeURI(uri)));
that.options = options;
that.count = function (success, error) {
/// <summary>Gets the number of items in the collection.</summary>
/// <param name="success" type="Function">Success callback with the item count.</param>
/// <param name="error" type="Function">Error callback.</param>
/// <returns type="Object">Request object with an abort method./<param>
var options = that.options;
return odata.request(
buildODataRequest(appendSegment(uri, "$count"), options),
function (data) {
var count = parseInt10(data.toString());
if (isNaN(count)) {
error({ message: "Count is NaN", count: count });
} else {
success(count);
}
}, error, undefined, options.httpClient, options.metadata);
};
that.read = function (index, count, success, error) {
/// <summary>Gets a number of consecutive items from the collection.</summary>
/// <param name="index" type="Number">Zero-based index of the items to retrieve.</param>
/// <param name="count" type="Number">Number of items to retrieve.</param>
/// <param name="success" type="Function">Success callback with the requested items.</param>
/// <param name="error" type="Function">Error callback.</param>
/// <returns type="Object">Request object with an abort method./<param>
var queryOptions = "$skip=" + index + "&$top=" + count;
return queryForData(appendQueryOption(uri, queryOptions), that.options, success, error);
};
return that;
};
var appendPage = function (operation, page) {
/// <summary>Appends a page's data to the operation data.</summary>
/// <param name="operation" type="Object">Operation with (i)ndex, (c)ount and (d)ata.</param>
/// <param name="page" type="Object">Page with (i)ndex, (c)ount and (d)ata.</param>
var intersection = intersectRanges(operation, page);
if (intersection) {
var start = intersection.i - page.i;
var end = start + (operation.c - operation.d.length);
operation.d = operation.d.concat(page.d.slice(start, end));
}
};
var intersectRanges = function (x, y) {
/// <summary>Returns the {(i)ndex, (c)ount} range for the intersection of x and y.</summary>
/// <param name="x" type="Object">Range with (i)ndex and (c)ount members.</param>
/// <param name="y" type="Object">Range with (i)ndex and (c)ount members.</param>
/// <returns type="Object">The intersection (i)ndex and (c)ount; undefined if there is no intersection.</returns>
var xLast = x.i + x.c;
var yLast = y.i + y.c;
var resultIndex = (x.i > y.i) ? x.i : y.i;
var resultLast = (xLast < yLast) ? xLast : yLast;
var result;
if (resultLast >= resultIndex) {
result = { i: resultIndex, c: resultLast - resultIndex };
}
return result;
};
var checkZeroGreater = function (val, name) {
/// <summary>Checks whether val is a defined number with value zero or greater.</summary>
/// <param name="val" type="Number">Value to check.</param>
/// <param name="name" type="String">Parameter name to use in exception.</param>
if (val === undefined || typeof val !== "number") {
throw { message: "'" + name + "' must be a number." };
}
if (isNaN(val) || val < 0 || !isFinite(val)) {
throw { message: "'" + name + "' must be greater than or equal to zero." };
}
};
var checkUndefinedGreaterThanZero = function (val, name) {
/// <summary>Checks whether val is undefined or a number with value greater than zero.</summary>
/// <param name="val" type="Number">Value to check.</param>
/// <param name="name" type="String">Parameter name to use in exception.</param>
if (val !== undefined) {
if (typeof val !== "number") {
throw { message: "'" + name + "' must be a number." };
}
if (isNaN(val) || val <= 0 || !isFinite(val)) {
throw { message: "'" + name + "' must be greater than zero." };
}
}
};
var checkUndefinedOrNumber = function (val, name) {
/// <summary>Checks whether val is undefined or a number</summary>
/// <param name="val" type="Number">Value to check.</param>
/// <param name="name" type="String">Parameter name to use in exception.</param>
if (val !== undefined && (typeof val !== "number" || isNaN(val) || !isFinite(val))) {
throw { message: "'" + name + "' must be a number." };
}
};
var removeFromArray = function (arr, item) {
/// <summary>Performs a linear search on the specified array and removes the first instance of 'item'.</summary>
/// <param name="arr" type="Array">Array to search.</param>
/// <param name="item">Item being sought.</param>
/// <returns type="Boolean">Whether the item was removed.</returns>
var i, len;
for (i = 0, len = arr.length; i < len; i++) {
if (arr[i] === item) {
arr.splice(i, 1);
return true;
}
}
return false;
};
var estimateSize = function (obj) {
/// <summary>Estimates the size of an object in bytes.</summary>
/// <param name="obj" type="Object">Object to determine the size of.</param>
/// <returns type="Integer">Estimated size of the object in bytes.</returns>
var size = 0;
var type = typeof obj;
if (type === "object" && obj) {
for (var name in obj) {
size += name.length * 2 + estimateSize(obj[name]);
}
} else if (type === "string") {
size = obj.length * 2;
} else {
size = 8;
}
return size;
};
var snapToPageBoundaries = function (lowIndex, highIndex, pageSize) {
/// <summary>Snaps low and high indices into page sizes and returns a range.</summary>
/// <param name="lowIndex" type="Number">Low index to snap to a lower value.</param>
/// <param name="highIndex" type="Number">High index to snap to a higher value.</param>
/// <param name="pageSize" type="Number">Page size to snap to.</param>
/// <returns type="Object">A range with (i)ndex and (c)ount of elements.</returns>
lowIndex = Math.floor(lowIndex / pageSize) * pageSize;
highIndex = Math.ceil((highIndex + 1) / pageSize) * pageSize;
return { i: lowIndex, c: highIndex - lowIndex };
};
// The DataCache is implemented using state machines. The following constants are used to properly
// identify and label the states that these machines transition to.
// DataCache state constants
var CACHE_STATE_DESTROY = "destroy";
var CACHE_STATE_IDLE = "idle";
var CACHE_STATE_INIT = "init";
var CACHE_STATE_READ = "read";
var CACHE_STATE_PREFETCH = "prefetch";
var CACHE_STATE_WRITE = "write";
// DataCacheOperation state machine states.
// Transitions on operations also depend on the cache current of the cache.
var OPERATION_STATE_CANCEL = "cancel";
var OPERATION_STATE_END = "end";
var OPERATION_STATE_ERROR = "error";
var OPERATION_STATE_START = "start";
var OPERATION_STATE_WAIT = "wait";
// Destroy state machine states
var DESTROY_STATE_CLEAR = "clear";
// Read / Prefetch state machine states
var READ_STATE_DONE = "done";
var READ_STATE_LOCAL = "local";
var READ_STATE_SAVE = "save";
var READ_STATE_SOURCE = "source";
var DataCacheOperation = function (stateMachine, promise, isCancelable, index, count, data, pending) {
/// <summary>Creates a new operation object.</summary>
/// <param name="stateMachine" type="Function">State machine that describes the specific behavior of the operation.</param>
/// <param name="promise" type ="DjsDeferred">Promise for requested values.</param>
/// <param name="isCancelable" type ="Boolean">Whether this operation can be canceled or not.</param>
/// <param name="index" type="Number">Index of first item requested.</param>
/// <param name="count" type="Number">Count of items requested.</param>
/// <param name="data" type="Array">Array with the items requested by the operation.</param>
/// <param name="pending" type="Number">Total number of pending prefetch records.</param>
/// <returns type="DataCacheOperation">A new data cache operation instance.</returns>
/// <field name="p" type="DjsDeferred">Promise for requested values.</field>
/// <field name="i" type="Number">Index of first item requested.</field>
/// <field name="c" type="Number">Count of items requested.</field>
/// <field name="d" type="Array">Array with the items requested by the operation.</field>
/// <field name="s" type="Array">Current state of the operation.</field>
/// <field name="canceled" type="Boolean">Whether the operation has been canceled.</field>
/// <field name="pending" type="Number">Total number of pending prefetch records.</field>
/// <field name="oncomplete" type="Function">Callback executed when the operation reaches the end state.</field>
var stateData;
var cacheState;
var that = this;
that.p = promise;
that.i = index;
that.c = count;
that.d = data;
that.s = OPERATION_STATE_START;
that.canceled = false;
that.pending = pending;
that.oncomplete = null;
that.cancel = function () {
/// <summary>Transitions this operation to the cancel state and sets the canceled flag to true.</summary>
/// <remarks>The function is a no-op if the operation is non-cancelable.</summary>
if (!isCancelable) {
return;
}
var state = that.s;
if (state !== OPERATION_STATE_ERROR && state !== OPERATION_STATE_END && state !== OPERATION_STATE_CANCEL) {
that.canceled = true;
transition(OPERATION_STATE_CANCEL, stateData);
}
};
that.complete = function () {
/// <summary>Transitions this operation to the end state.</summary>
transition(OPERATION_STATE_END, stateData);
};
that.error = function (err) {
/// <summary>Transitions this operation to the error state.</summary>
if (!that.canceled) {
transition(OPERATION_STATE_ERROR, err);
}
};
that.run = function (state) {
/// <summary>Executes the operation's current state in the context of a new cache state.</summary>
/// <param name="state" type="Object">New cache state.</param>
cacheState = state;
that.transition(that.s, stateData);
};
that.wait = function (data) {
/// <summary>Transitions this operation to the wait state.</summary>
transition(OPERATION_STATE_WAIT, data);
};
var operationStateMachine = function (opTargetState, cacheState, data) {
/// <summary>State machine that describes all operations common behavior.</summary>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation. The operation will remain in this state until the cache has been fully initialized.
if (cacheState !== CACHE_STATE_INIT) {
stateMachine(that, opTargetState, cacheState, data);
}
break;
case OPERATION_STATE_WAIT:
// Wait state indicating that the operation is active but waiting for an asynchronous operation to complete.
stateMachine(that, opTargetState, cacheState, data);
break;
case OPERATION_STATE_CANCEL:
// Cancel state.
stateMachine(that, opTargetState, cacheState, data);
that.fireCanceled();
transition(OPERATION_STATE_END);
break;
case OPERATION_STATE_ERROR:
// Error state. Data is expected to be an object detailing the error condition.
stateMachine(that, opTargetState, cacheState, data);
that.canceled = true;
that.fireRejected(data);
transition(OPERATION_STATE_END);
break;
case OPERATION_STATE_END:
// Final state of the operation.
if (that.oncomplete) {
that.oncomplete(that);
}
if (!that.canceled) {
that.fireResolved();
}
stateMachine(that, opTargetState, cacheState, data);
break;
default:
// Any other state is passed down to the state machine describing the operation's specific behavior.
stateMachine(that, opTargetState, cacheState, data);
break;
}
};
var transition = function (state, data) {
/// <summary>Transitions this operation to a new state.</summary>
/// <param name="state" type="Object">State to transition the operation to.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
that.s = state;
stateData = data;
operationStateMachine(state, cacheState, data);
};
that.transition = transition;
return that;
};
DataCacheOperation.prototype.fireResolved = function () {
/// <summary>Fires a resolved notification as necessary.</summary>
// Fire the resolve just once.
var p = this.p;
if (p) {
this.p = null;
p.resolve(this.d);
}
};
DataCacheOperation.prototype.fireRejected = function (reason) {
/// <summary>Fires a rejected notification as necessary.</summary>
// Fire the rejection just once.
var p = this.p;
if (p) {
this.p = null;
p.reject(reason);
}
};
DataCacheOperation.prototype.fireCanceled = function () {
/// <summary>Fires a canceled notification as necessary.</summary>
this.fireRejected({ canceled: true, message: "Operation canceled" });
};
var DataCache = function (options) {
/// <summary>Creates a data cache for a collection that is efficiently loaded on-demand.</summary>
/// <param name="options">
/// Options for the data cache, including name, source, pageSize,
/// prefetchSize, cacheSize, storage mechanism, and initial prefetch and local-data handler.
/// </param>
/// <returns type="DataCache">A new data cache instance.</returns>
var state = CACHE_STATE_INIT;
var stats = { counts: 0, netReads: 0, prefetches: 0, cacheReads: 0 };
var clearOperations = [];
var readOperations = [];
var prefetchOperations = [];
var actualCacheSize = 0; // Actual cache size in bytes.
var allDataLocal = false; // Whether all data is local.
var cacheSize = undefinedDefault(options.cacheSize, 1048576); // Requested cache size in bytes, default 1 MB.
var collectionCount = 0; // Number of elements in the server collection.
var highestSavedPage = 0; // Highest index of all the saved pages.
var highestSavedPageSize = 0; // Item count of the saved page with the highest index.
var overflowed = cacheSize === 0; // If the cache has overflowed (actualCacheSize > cacheSize or cacheSize == 0);
var pageSize = undefinedDefault(options.pageSize, 50); // Number of elements to store per page.
var prefetchSize = undefinedDefault(options.prefetchSize, pageSize); // Number of elements to prefetch from the source when the cache is idling.
var version = "1.0";
var cacheFailure;
var pendingOperations = 0;
var source = options.source;
if (typeof source === "string") {
// Create a new cache source.
source = new ODataCacheSource(options);
}
source.options = options;
// Create a cache local store.
var store = odatajs.createStore(options.name, options.mechanism);
var that = this;
that.onidle = options.idle;
that.stats = stats;
that.count = function () {
/// <summary>Counts the number of items in the collection.</summary>
/// <returns type="Object">A promise with the number of items.</returns>
if (cacheFailure) {
throw cacheFailure;
}
var deferred = createDeferred();
var canceled = false;
if (allDataLocal) {
delay(function () {
deferred.resolve(collectionCount);
});
return deferred.promise();
}
// TODO: Consider returning the local data count instead once allDataLocal flag is set to true.
var request = source.count(function (count) {
request = null;
stats.counts++;
deferred.resolve(count);
}, function (err) {
request = null;
deferred.reject(extend(err, { canceled: canceled }));
});
return extend(deferred.promise(), {
cancel: function () {
/// <summary>Aborts the count operation.</summary>
if (request) {
canceled = true;
request.abort();
request = null;
}
}
});
};
that.clear = function () {
/// <summary>Cancels all running operations and clears all local data associated with this cache.</summary>
/// <remarks>
/// New read requests made while a clear operation is in progress will not be canceled.
/// Instead they will be queued for execution once the operation is completed.
/// </remarks>
/// <returns type="Object">A promise that has no value and can't be canceled.</returns>
if (cacheFailure) {
throw cacheFailure;
}
if (clearOperations.length === 0) {
var deferred = createDeferred();
var op = new DataCacheOperation(destroyStateMachine, deferred, false);
queueAndStart(op, clearOperations);
return deferred.promise();
}
return clearOperations[0].p;
};
that.filterForward = function (index, count, predicate) {
/// <summary>Filters the cache data based a predicate.</summary>
/// <param name="index" type="Number">The index of the item to start filtering forward from.</param>
/// <param name="count" type="Number">Maximum number of items to include in the result.</param>
/// <param name="predicate" type="Function">Callback function returning a boolean that determines whether an item should be included in the result or not.</param>
/// <remarks>
/// Specifying a negative count value will yield all the items in the cache that satisfy the predicate.
/// </remarks>
/// <returns type="DjsDeferred">A promise for an array of results.</returns>
return filter(index, count, predicate, false);
};
that.filterBack = function (index, count, predicate) {
/// <summary>Filters the cache data based a predicate.</summary>
/// <param name="index" type="Number">The index of the item to start filtering backward from.</param>
/// <param name="count" type="Number">Maximum number of items to include in the result.</param>
/// <param name="predicate" type="Function">Callback function returning a boolean that determines whether an item should be included in the result or not.</param>
/// <remarks>
/// Specifying a negative count value will yield all the items in the cache that satisfy the predicate.
/// </remarks>
/// <returns type="DjsDeferred">A promise for an array of results.</returns>
return filter(index, count, predicate, true);
};
that.readRange = function (index, count) {
/// <summary>Reads a range of adjacent records.</summary>
/// <param name="index" type="Number">Zero-based index of record range to read.</param>
/// <param name="count" type="Number">Number of records in the range.</param>
/// <remarks>
/// New read requests made while a clear operation is in progress will not be canceled.
/// Instead they will be queued for execution once the operation is completed.
/// </remarks>
/// <returns type="DjsDeferred">
/// A promise for an array of records; less records may be returned if the
/// end of the collection is found.
/// </returns>
checkZeroGreater(index, "index");
checkZeroGreater(count, "count");
if (cacheFailure) {
throw cacheFailure;
}
var deferred = createDeferred();
// Merging read operations would be a nice optimization here.
var op = new DataCacheOperation(readStateMachine, deferred, true, index, count, [], 0);
queueAndStart(op, readOperations);
return extend(deferred.promise(), {
cancel: function () {
/// <summary>Aborts the readRange operation.</summary>
op.cancel();
}
});
};
that.ToObservable = that.toObservable = function () {
/// <summary>Creates an Observable object that enumerates all the cache contents.</summary>
/// <returns>A new Observable object that enumerates all the cache contents.</returns>
if (!window.Rx || !window.Rx.Observable) {
throw { message: "Rx library not available - include rx.js" };
}
if (cacheFailure) {
throw cacheFailure;
}
return window.Rx.Observable.CreateWithDisposable(function (obs) {
var disposed = false;
var index = 0;
var errorCallback = function (error) {
if (!disposed) {
obs.OnError(error);
}
};
var successCallback = function (data) {
if (!disposed) {
var i, len;
for (i = 0, len = data.length; i < len; i++) {
// The wrapper automatically checks for Dispose
// on the observer, so we don't need to check it here.
obs.OnNext(data[i]);
}
if (data.length < pageSize) {
obs.OnCompleted();
} else {
index += pageSize;
that.readRange(index, pageSize).then(successCallback, errorCallback);
}
}
};
that.readRange(index, pageSize).then(successCallback, errorCallback);
return { Dispose: function () { disposed = true; } };
});
};
var cacheFailureCallback = function (message) {
/// <summary>Creates a function that handles a callback by setting the cache into failure mode.</summary>
/// <param name="message" type="String">Message text.</param>
/// <returns type="Function">Function to use as error callback.</returns>
/// <remarks>
/// This function will specifically handle problems with critical store resources
/// during cache initialization.
/// </remarks>
return function (error) {
cacheFailure = { message: message, error: error };
// Destroy any pending clear or read operations.
// At this point there should be no prefetch operations.
// Count operations will go through but are benign because they
// won't interact with the store.
var i, len;
for (i = 0, len = readOperations.length; i < len; i++) {
readOperations[i].fireRejected(cacheFailure);
}
for (i = 0, len = clearOperations.length; i < len; i++) {
clearOperations[i].fireRejected(cacheFailure);
}
// Null out the operation arrays.
readOperations = clearOperations = null;
};
};
var changeState = function (newState) {
/// <summary>Updates the cache's state and signals all pending operations of the change.</summary>
/// <param name="newState" type="Object">New cache state.</param>
/// <remarks>This method is a no-op if the cache's current state and the new state are the same.</remarks>
if (newState !== state) {
state = newState;
var operations = clearOperations.concat(readOperations, prefetchOperations);
var i, len;
for (i = 0, len = operations.length; i < len; i++) {
operations[i].run(state);
}
}
};
var clearStore = function () {
/// <summary>Removes all the data stored in the cache.</summary>
/// <returns type="DjsDeferred">A promise with no value.</returns>
var deferred = new DjsDeferred();
store.clear(function () {
// Reset the cache settings.
actualCacheSize = 0;
allDataLocal = false;
collectionCount = 0;
highestSavedPage = 0;
highestSavedPageSize = 0;
overflowed = cacheSize === 0;
// version is not reset, in case there is other state in eg V1.1 that is still around.
// Reset the cache stats.
stats = { counts: 0, netReads: 0, prefetches: 0, cacheReads: 0 };
that.stats = stats;
store.close();
deferred.resolve();
}, function (err) {
deferred.reject(err);
});
return deferred;
};
var dequeueOperation = function (operation) {
/// <summary>Removes an operation from the caches queues and changes the cache state to idle.</summary>
/// <param name="operation" type="DataCacheOperation">Operation to dequeue.</param>
/// <remarks>This method is used as a handler for the operation's oncomplete event.</remarks>
var removed = removeFromArray(clearOperations, operation);
if (!removed) {
removed = removeFromArray(readOperations, operation);
if (!removed) {
removeFromArray(prefetchOperations, operation);
}
}
pendingOperations--;
changeState(CACHE_STATE_IDLE);
};
var fetchPage = function (start) {
/// <summary>Requests data from the cache source.</summary>
/// <param name="start" type="Number">Zero-based index of items to request.</param>
/// <returns type="DjsDeferred">A promise for a page object with (i)ndex, (c)ount, (d)ata.</returns>
var deferred = new DjsDeferred();
var canceled = false;
var request = source.read(start, pageSize, function (data) {
var page = { i: start, c: data.length, d: data };
deferred.resolve(page);
}, function (err) {
deferred.reject(err);
});
return extend(deferred, {
cancel: function () {
if (request) {
request.abort();
canceled = true;
request = null;
}
}
});
};
var filter = function (index, count, predicate, backwards) {
/// <summary>Filters the cache data based a predicate.</summary>
/// <param name="index" type="Number">The index of the item to start filtering from.</param>
/// <param name="count" type="Number">Maximum number of items to include in the result.</param>
/// <param name="predicate" type="Function">Callback function returning a boolean that determines whether an item should be included in the result or not.</param>
/// <param name="backwards" type="Boolean">True if the filtering should move backward from the specified index, falsey otherwise.</param>
/// <remarks>
/// Specifying a negative count value will yield all the items in the cache that satisfy the predicate.
/// </remarks>
/// <returns type="DjsDeferred">A promise for an array of results.</returns>
index = parseInt10(index);
count = parseInt10(count);
if (isNaN(index)) {
throw { message: "'index' must be a valid number.", index: index };
}
if (isNaN(count)) {
throw { message: "'count' must be a valid number.", count: count };
}
if (cacheFailure) {
throw cacheFailure;
}
index = Math.max(index, 0);
var deferred = createDeferred();
var arr = [];
var canceled = false;
var pendingReadRange = null;
var readMore = function (readIndex, readCount) {
if (!canceled) {
if (count >= 0 && arr.length >= count) {
deferred.resolve(arr);
} else {
pendingReadRange = that.readRange(readIndex, readCount).then(function (data) {
for (var i = 0, length = data.length; i < length && (count < 0 || arr.length < count); i++) {
var dataIndex = backwards ? length - i - 1 : i;
var item = data[dataIndex];
if (predicate(item)) {
var element = {
index: readIndex + dataIndex,
item: item
};
backwards ? arr.unshift(element) : arr.push(element);
}
}
// Have we reached the end of the collection?
if ((!backwards && data.length < readCount) || (backwards && readIndex <= 0)) {
deferred.resolve(arr);
} else {
var nextIndex = backwards ? Math.max(readIndex - pageSize, 0) : readIndex + readCount;
readMore(nextIndex, pageSize);
}
}, function (err) {
deferred.reject(err);
});
}
}
};
// Initially, we read from the given starting index to the next/previous page boundary
var initialPage = snapToPageBoundaries(index, index, pageSize);
var initialIndex = backwards ? initialPage.i : index;
var initialCount = backwards ? index - initialPage.i + 1 : initialPage.i + initialPage.c - index;
readMore(initialIndex, initialCount);
return extend(deferred.promise(), {
cancel: function () {
/// <summary>Aborts the filter operation</summary>
if (pendingReadRange) {
pendingReadRange.cancel();
}
canceled = true;
}
});
};
var fireOnIdle = function () {
/// <summary>Fires an onidle event if any functions are assigned.</summary>
if (that.onidle && pendingOperations === 0) {
that.onidle();
}
};
var prefetch = function (start) {
/// <summary>Creates and starts a new prefetch operation.</summary>
/// <param name="start" type="Number">Zero-based index of the items to prefetch.</param>
/// <remarks>
/// This method is a no-op if any of the following conditions is true:
/// 1.- prefetchSize is 0
/// 2.- All data has been read and stored locally in the cache.
/// 3.- There is already an all data prefetch operation queued.
/// 4.- The cache has run out of available space (overflowed).
/// <remarks>
if (allDataLocal || prefetchSize === 0 || overflowed) {
return;
}
if (prefetchOperations.length === 0 || (prefetchOperations[0] && prefetchOperations[0].c !== -1)) {
// Merging prefetch operations would be a nice optimization here.
var op = new DataCacheOperation(prefetchStateMachine, null, true, start, prefetchSize, null, prefetchSize);
queueAndStart(op, prefetchOperations);
}
};
var queueAndStart = function (op, queue) {
/// <summary>Queues an operation and runs it.</summary>
/// <param name="op" type="DataCacheOperation">Operation to queue.</param>
/// <param name="queue" type="Array">Array that will store the operation.</param>
op.oncomplete = dequeueOperation;
queue.push(op);
pendingOperations++;
op.run(state);
};
var readPage = function (key) {
/// <summary>Requests a page from the cache local store.</summary>
/// <param name="key" type="Number">Zero-based index of the reuqested page.</param>
/// <returns type="DjsDeferred">A promise for a found flag and page object with (i)ndex, (c)ount, (d)ata, and (t)icks.</returns>
var canceled = false;
var deferred = extend(new DjsDeferred(), {
cancel: function () {
/// <summary>Aborts the readPage operation.</summary>
canceled = true;
}
});
var error = storeFailureCallback(deferred, "Read page from store failure");
store.contains(key, function (contained) {
if (canceled) {
return;
}
if (contained) {
store.read(key, function (_, data) {
if (!canceled) {
deferred.resolve(data !== undefined, data);
}
}, error);
return;
}
deferred.resolve(false);
}, error);
return deferred;
};
var savePage = function (key, page) {
/// <summary>Saves a page to the cache local store.</summary>
/// <param name="key" type="Number">Zero-based index of the requested page.</param>
/// <param name="page" type="Object">Object with (i)ndex, (c)ount, (d)ata, and (t)icks.</param>
/// <returns type="DjsDeferred">A promise with no value.</returns>
var canceled = false;
var deferred = extend(new DjsDeferred(), {
cancel: function () {
/// <summary>Aborts the readPage operation.</summary>
canceled = true;
}
});
var error = storeFailureCallback(deferred, "Save page to store failure");
var resolve = function () {
deferred.resolve(true);
};
if (page.c > 0) {
var pageBytes = estimateSize(page);
overflowed = cacheSize >= 0 && cacheSize < actualCacheSize + pageBytes;
if (!overflowed) {
store.addOrUpdate(key, page, function () {
updateSettings(page, pageBytes);
saveSettings(resolve, error);
}, error);
} else {
resolve();
}
} else {
updateSettings(page, 0);
saveSettings(resolve, error);
}
return deferred;
};
var saveSettings = function (success, error) {
/// <summary>Saves the cache's current settings to the local store.</summary>
/// <param name="success" type="Function">Success callback.</param>
/// <param name="error" type="Function">Errror callback.</param>
var settings = {
actualCacheSize: actualCacheSize,
allDataLocal: allDataLocal,
cacheSize: cacheSize,
collectionCount: collectionCount,
highestSavedPage: highestSavedPage,
highestSavedPageSize: highestSavedPageSize,
pageSize: pageSize,
sourceId: source.identifier,
version: version
};
store.addOrUpdate("__settings", settings, success, error);
};
var storeFailureCallback = function (deferred/*, message*/) {
/// <summary>Creates a function that handles a store error.</summary>
/// <param name="deferred" type="DjsDeferred">Deferred object to resolve.</param>
/// <param name="message" type="String">Message text.</param>
/// <returns type="Function">Function to use as error callback.</returns>
/// <remarks>
/// This function will specifically handle problems when interacting with the store.
/// </remarks>
return function (/*error*/) {
// var console = window.console;
// if (console && console.log) {
// console.log(message);
// console.dir(error);
// }
deferred.resolve(false);
};
};
var updateSettings = function (page, pageBytes) {
/// <summary>Updates the cache's settings based on a page object.</summary>
/// <param name="page" type="Object">Object with (i)ndex, (c)ount, (d)ata.</param>
/// <param name="pageBytes" type="Number">Size of the page in bytes.</param>
var pageCount = page.c;
var pageIndex = page.i;
// Detect the collection size.
if (pageCount === 0) {
if (highestSavedPage === pageIndex - pageSize) {
collectionCount = highestSavedPage + highestSavedPageSize;
}
} else {
highestSavedPage = Math.max(highestSavedPage, pageIndex);
if (highestSavedPage === pageIndex) {
highestSavedPageSize = pageCount;
}
actualCacheSize += pageBytes;
if (pageCount < pageSize && !collectionCount) {
collectionCount = pageIndex + pageCount;
}
}
// Detect the end of the collection.
if (!allDataLocal && collectionCount === highestSavedPage + highestSavedPageSize) {
allDataLocal = true;
}
};
var cancelStateMachine = function (operation, opTargetState, cacheState, data) {
/// <summary>State machine describing the behavior for cancelling a read or prefetch operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <remarks>
/// This state machine contains behavior common to read and prefetch operations.
/// </remarks>
var canceled = operation.canceled && opTargetState !== OPERATION_STATE_END;
if (canceled) {
if (opTargetState === OPERATION_STATE_CANCEL) {
// Cancel state.
// Data is expected to be any pending request made to the cache.
if (data && data.cancel) {
data.cancel();
}
}
}
return canceled;
};
var destroyStateMachine = function (operation, opTargetState, cacheState) {
/// <summary>State machine describing the behavior of a clear operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <remarks>
/// Clear operations have the highest priority and can't be interrupted by other operations; however,
/// they will preempt any other operation currently executing.
/// </remarks>
var transition = operation.transition;
// Signal the cache that a clear operation is running.
if (cacheState !== CACHE_STATE_DESTROY) {
changeState(CACHE_STATE_DESTROY);
return true;
}
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation.
transition(DESTROY_STATE_CLEAR);
break;
case OPERATION_STATE_END:
// State that signals the operation is done.
fireOnIdle();
break;
case DESTROY_STATE_CLEAR:
// State that clears all the local data of the cache.
clearStore().then(function () {
// Terminate the operation once the local store has been cleared.
operation.complete();
});
// Wait until the clear request completes.
operation.wait();
break;
default:
return false;
}
return true;
};
var prefetchStateMachine = function (operation, opTargetState, cacheState, data) {
/// <summary>State machine describing the behavior of a prefetch operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <remarks>
/// Prefetch operations have the lowest priority and will be interrupted by operations of
/// other kinds. A preempted prefetch operation will resume its execution only when the state
/// of the cache returns to idle.
///
/// If a clear operation starts executing then all the prefetch operations are canceled,
/// even if they haven't started executing yet.
/// </remarks>
// Handle cancelation
if (!cancelStateMachine(operation, opTargetState, cacheState, data)) {
var transition = operation.transition;
// Handle preemption
if (cacheState !== CACHE_STATE_PREFETCH) {
if (cacheState === CACHE_STATE_DESTROY) {
if (opTargetState !== OPERATION_STATE_CANCEL) {
operation.cancel();
}
} else if (cacheState === CACHE_STATE_IDLE) {
// Signal the cache that a prefetch operation is running.
changeState(CACHE_STATE_PREFETCH);
}
return true;
}
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation.
if (prefetchOperations[0] === operation) {
transition(READ_STATE_LOCAL, operation.i);
}
break;
case READ_STATE_DONE:
// State that determines if the operation can be resolved or has to
// continue processing.
// Data is expected to be the read page.
var pending = operation.pending;
if (pending > 0) {
pending -= Math.min(pending, data.c);
}
// Are we done, or has all the data been stored?
if (allDataLocal || pending === 0 || data.c < pageSize || overflowed) {
operation.complete();
} else {
// Continue processing the operation.
operation.pending = pending;
transition(READ_STATE_LOCAL, data.i + pageSize);
}
break;
default:
return readSaveStateMachine(operation, opTargetState, cacheState, data, true);
}
}
return true;
};
var readStateMachine = function (operation, opTargetState, cacheState, data) {
/// <summary>State machine describing the behavior of a read operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <remarks>
/// Read operations have a higher priority than prefetch operations, but lower than
/// clear operations. They will preempt any prefetch operation currently running
/// but will be interrupted by a clear operation.
///
/// If a clear operation starts executing then all the currently running
/// read operations are canceled. Read operations that haven't started yet will
/// wait in the start state until the destory operation finishes.
/// </remarks>
// Handle cancelation
if (!cancelStateMachine(operation, opTargetState, cacheState, data)) {
var transition = operation.transition;
// Handle preemption
if (cacheState !== CACHE_STATE_READ && opTargetState !== OPERATION_STATE_START) {
if (cacheState === CACHE_STATE_DESTROY) {
if (opTargetState !== OPERATION_STATE_START) {
operation.cancel();
}
} else if (cacheState !== CACHE_STATE_WRITE) {
// Signal the cache that a read operation is running.
changeState(CACHE_STATE_READ);
}
return true;
}
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation.
// Wait until the cache is idle or prefetching.
if (cacheState === CACHE_STATE_IDLE || cacheState === CACHE_STATE_PREFETCH) {
// Signal the cache that a read operation is running.
changeState(CACHE_STATE_READ);
if (operation.c > 0) {
// Snap the requested range to a page boundary.
var range = snapToPageBoundaries(operation.i, operation.c, pageSize);
transition(READ_STATE_LOCAL, range.i);
} else {
transition(READ_STATE_DONE, operation);
}
}
break;
case READ_STATE_DONE:
// State that determines if the operation can be resolved or has to
// continue processing.
// Data is expected to be the read page.
appendPage(operation, data);
var len = operation.d.length;
// Are we done?
if (operation.c === len || data.c < pageSize) {
// Update the stats, request for a prefetch operation.
stats.cacheReads++;
prefetch(data.i + data.c);
// Terminate the operation.
operation.complete();
} else {
// Continue processing the operation.
transition(READ_STATE_LOCAL, data.i + pageSize);
}
break;
default:
return readSaveStateMachine(operation, opTargetState, cacheState, data, false);
}
}
return true;
};
var readSaveStateMachine = function (operation, opTargetState, cacheState, data, isPrefetch) {
/// <summary>State machine describing the behavior for reading and saving data into the cache.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <param name="isPrefetch" type="Boolean">Flag indicating whether a read (false) or prefetch (true) operation is running.
/// <remarks>
/// This state machine contains behavior common to read and prefetch operations.
/// </remarks>
var error = operation.error;
var transition = operation.transition;
var wait = operation.wait;
var request;
switch (opTargetState) {
case OPERATION_STATE_END:
// State that signals the operation is done.
fireOnIdle();
break;
case READ_STATE_LOCAL:
// State that requests for a page from the local store.
// Data is expected to be the index of the page to request.
request = readPage(data).then(function (found, page) {
// Signal the cache that a read operation is running.
if (!operation.canceled) {
if (found) {
// The page is in the local store, check if the operation can be resolved.
transition(READ_STATE_DONE, page);
} else {
// The page is not in the local store, request it from the source.
transition(READ_STATE_SOURCE, data);
}
}
});
break;
case READ_STATE_SOURCE:
// State that requests for a page from the cache source.
// Data is expected to be the index of the page to request.
request = fetchPage(data).then(function (page) {
// Signal the cache that a read operation is running.
if (!operation.canceled) {
// Update the stats and save the page to the local store.
if (isPrefetch) {
stats.prefetches++;
} else {
stats.netReads++;
}
transition(READ_STATE_SAVE, page);
}
}, error);
break;
case READ_STATE_SAVE:
// State that saves a page to the local store.
// Data is expected to be the page to save.
// Write access to the store is exclusive.
if (cacheState !== CACHE_STATE_WRITE) {
changeState(CACHE_STATE_WRITE);
request = savePage(data.i, data).then(function (saved) {
if (!operation.canceled) {
if (!saved && isPrefetch) {
operation.pending = 0;
}
// Check if the operation can be resolved.
transition(READ_STATE_DONE, data);
}
changeState(CACHE_STATE_IDLE);
});
}
break;
default:
// Unknown state that can't be handled by this state machine.
return false;
}
if (request) {
// The operation might have been canceled between stack frames do to the async calls.
if (operation.canceled) {
request.cancel();
} else if (operation.s === opTargetState) {
// Wait for the request to complete.
wait(request);
}
}
return true;
};
// Initialize the cache.
store.read("__settings", function (_, settings) {
if (assigned(settings)) {
var settingsVersion = settings.version;
if (!settingsVersion || settingsVersion.indexOf("1.") !== 0) {
cacheFailureCallback("Unsupported cache store version " + settingsVersion)();
return;
}
if (pageSize !== settings.pageSize || source.identifier !== settings.sourceId) {
// The shape or the source of the data was changed so invalidate the store.
clearStore().then(function () {
// Signal the cache is fully initialized.
changeState(CACHE_STATE_IDLE);
}, cacheFailureCallback("Unable to clear store during initialization"));
} else {
// Restore the saved settings.
actualCacheSize = settings.actualCacheSize;
allDataLocal = settings.allDataLocal;
cacheSize = settings.cacheSize;
collectionCount = settings.collectionCount;
highestSavedPage = settings.highestSavedPage;
highestSavedPageSize = settings.highestSavedPageSize;
version = settingsVersion;
// Signal the cache is fully initialized.
changeState(CACHE_STATE_IDLE);
}
} else {
// This is a brand new cache.
saveSettings(function () {
// Signal the cache is fully initialized.
changeState(CACHE_STATE_IDLE);
}, cacheFailureCallback("Unable to write settings during initialization."));
}
}, cacheFailureCallback("Unable to read settings from store."));
return that;
};
odatajs.createDataCache = function (options) {
/// <summary>Creates a data cache for a collection that is efficiently loaded on-demand.</summary>
/// <param name="options">
/// Options for the data cache, including name, source, pageSize,
/// prefetchSize, cacheSize, storage mechanism, and initial prefetch and local-data handler.
/// </param>
/// <returns type="DataCache">A new data cache instance.</returns>
checkUndefinedGreaterThanZero(options.pageSize, "pageSize");
checkUndefinedOrNumber(options.cacheSize, "cacheSize");
checkUndefinedOrNumber(options.prefetchSize, "prefetchSize");
if (!assigned(options.name)) {
throw { message: "Undefined or null name", options: options };
}
if (!assigned(options.source)) {
throw { message: "Undefined source", options: options };
}
return new DataCache(options);
};
})(this);