// Copyright (c) Microsoft.  All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
// files (the "Software"), to deal  in the Software without restriction, including without limitation the rights  to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// datajs.js

(function (window, undefined) {
    if (!window.datajs) {
        window.datajs = {};
    }

    if (!window.OData) {
        window.OData = {};
    }

    var datajs = window.datajs;
    var odata = window.OData;
    

    // Provides an enumeration of possible kinds of payloads.
    var payloadType = {
        batch: "batch",
        complexType: "complex",
        entry: "entry",
        feedOrLinks: "feedOrLinks",
        primitiveType: "primitive",
        svcDoc: "service document",
        unknown: "unknown",
        none: "none"
    };

    // Provides an enumeration of possible kinds of properties.
    var propertyKind = {
        complex: "complex",
        deferred: "deferred",
        inline: "inline",
        primitive: "primitive",
        none: "none"
    };

    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 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);
            return;
        }

        var args = Array.prototype.slice.call(arguments, 1);
        window.setTimeout(function () {
            callback.apply(this, args)
        }, 0);
    };

    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.
        return (value.__edmType === "Edm.DateTimeOffset" || (!value.__edmType && value.__offset));
    };

    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>

        var hasOffset = isDateTimeOffset(value);
        var offset = getCanonicalTimezone(value.__offset);
        if (hasOffset && offset !== "Z") {
            // We're about to change the value, so make a copy.
            value = new Date(value.valueOf());

            var timezone = parseTimezone(offset);
            var hours = value.getUTCHours() + (timezone.d * timezone.h);
            var minutes = value.getMinutes() + (timezone.d * timezone.m);

            value.setUTCHours(hours, minutes);
        } else if (!hasOffset) {
            // Don't suffix a 'Z' for Edm.DateTime values.
            offset = "";
        }

        var year = value.getUTCFullYear();
        var month = value.getUTCMonth() + 1;
        var sign = "";
        if (year <= 0) {
            year = -(year - 1);
            sign = "-";
        }

        // Avoid generating milliseconds if not necessary.
        var ms = value.getUTCMilliseconds();
        if (ms === 0) {
            ms = "";
        } else {
            ms = "." + formatNumberWidth(ms.toString(), 3);
        }

        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 formatNumberWidth = function (value, width) {
        /// <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>
        /// <returns type="String">Text representation.</returns>
        var result = value.toString(10);
        while (result.length < width) {
            result = "0" + result;
        }

        return result;
    };

    var getCanonicalTimezone = function (timezone) {
        /// <summary>Gets the canonical timezone representation.</summary>
        /// <param name="timezone" type="String">Timezone representation.</param>
        /// <returns type="String">An 'Z' string if the timezone is absent or 0; the timezone otherwise.</returns>

        return (!timezone || timezone === "Z" || timezone === "+00:00" || timezone === "-00:00") ? "Z" : timezone;
    };

    var invokeRequest = function (request, success, error, handler, httpClient, context) {
        /// <summary>Sends a request containing OData payload to a server.</summary>
        /// <param name="request">Object that represents the request to be sent..</param>
        /// <param name="success">Callback for a successful read operation.</param>
        /// <param name="error">Callback for handling errors.</param>
        /// <param name="handler">Handler for data serialization.</param>
        /// <param name="httpClient">HTTP client layer.</param>
        /// <param name="context">Context used for processing the request</param>
        return httpClient.request(request, function (response) {
            try {
                if (response.headers) {
                    normalizeHeaders(response.headers);
                }

                if (response.data === undefined) {
                    handler.read(response, context);
                }
            } catch (err) {
                if (err.request === undefined) {
                    err.request = request;
                }
                if (err.response === undefined) {
                    err.response = response;
                }
                error(err);
            }

            success(response.data, response);
        }, error);
    };

    var isArray = function (value) {
        /// <summary>Checks whether the specified value is an array object.</summary>
        /// <param name="value">Value to check.</param>
        /// <returns type="Boolean">true if the value is an array object; false otherwise.</returns>

        return Object.prototype.toString.call(value) === "[object Array]";
    };

    var isDate = function (value) {
        /// <summary>Checks whether the specified value is a Date object.</summary>
        /// <param name="value">Value to check.</param>
        /// <returns type="Boolean">true if the value is a Date object; false otherwise.</returns>

        return Object.prototype.toString.call(value) === "[object Date]";
    };

    var lookupTypeInMetadata = function (name, metadata, kind) {
        /// <summary>Looks up a type object by name.</summary>
        /// <param name="name" type="String">Name, possibly null or empty.</param>
        /// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param>
        /// <param name="kind" type="String">Kind of type to look for; one of 'entityType' or 'complexType'.</param>
        /// <returns>An type description if the name is found; null otherwise.</returns>

        if (!name || !metadata) {
            return null;
        }

        if (isArray(metadata)) {
            var i, len, result;
            for (i = 0, len = metadata.length; i < len; i++) {
                result = lookupTypeInSchema(name, metadata[i], kind);
                if (result) {
                    return result;
                }
            }
        } else {
            return lookupTypeInSchema(name, metadata, kind);
        }
    };

    var lookupComplexType = function (name, metadata) {
        /// <summary>Looks up a complex type object by name.</summary>
        /// <param name="name" type="String">Name, possibly null or empty.</param>
        /// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param>
        /// <returns>A complex type description if the name is found; null otherwise.</returns>

        return lookupTypeInMetadata(name, metadata, "complexType");
    };

    var lookupEntityType = function (name, metadata) {
        /// <summary>Looks up an entity type object by name.</summary>
        /// <param name="name" type="String">Name, possibly null or empty.</param>
        /// <param name="metadata">Metadata store; one of edmx, schema, or an array of any of them.</param>
        /// <returns>An entity type description if the name is found; null otherwise.</returns>

        return lookupTypeInMetadata(name, metadata, "entityType");
    };

    var lookupTypeInSchema = function (name, metadata, kind) {
        /// <summary>Looks up an entity type object by name.</summary>
        /// <param name="name" type="String">Name (assigned).</param>
        /// <param name="metadata">Metadata store; one of edmx, schema.</param>
        /// <param name="kind" type="String">Kind of type to look for; one of 'entityType' or 'complexType'.</param>
        /// <returns>An entity type description if the name is found; null otherwise.</returns>
        /// <remarks>
        /// metadata is considered an edmx object if it contains a dataServices object.
        /// </remarks>

        if (!metadata) {
            return null;
        }

        var i, len;
        if (metadata.dataServices) {
            var schema, result;
            schema = metadata.dataServices.schema;
            for (i = 0, len = schema.length; i < len; i++) {
                result = lookupTypeInSchema(name, schema[i], kind);
                if (result) {
                    return result;
                }
            }
        } else {
            var ns = metadata["namespace"];

            // The name should be the namespace qualified name in 'ns'.'type' format.
            if (name.indexOf(ns) !== 0) {
                return null;
            }
            if (name.charAt(ns.length) !== ".") {
                return null;
            }

            var nameOnly = name.substr(ns.length + 1);
            var types = metadata[kind];
            if (types) {
                for (i = 0, len = types.length; i < len; i++) {
                    if (types[i].name === nameOnly) {
                        return types[i];
                    }
                }
            }
        }

        return null;
    };

    var normalHeaders = {
        "accept": "Accept",
        "content-type": "Content-Type",
        "dataserviceversion": "DataServiceVersion",
        "maxdataserviceversion": "MaxDataServiceVersion"
    };

    var normalizeHeaders = function (headers) {
        /// <summary>Normalizes headers so they can be found with consistent casing.</summary>
        /// <param name="headers" type="Object">Dictionary of name/value pairs.</param>

        for (var name in headers) {
            var lowerName = name.toLowerCase();
            var normalName = normalHeaders[lowerName];
            if (normalName && lowerName !== normalName) {
                var val = headers[name];
                delete headers[name];
                headers[normalName] = val;
            }
        }
    };

    var undefinedDefault = function (value, defaultValue) {
        /// <summary>Returns a default value in place of undefined.</summary>
        /// <param name="value" mayBeNull="true" optional="true">Value to check.</param>
        /// <param name="defaultValue">Value to return if value is undefined.</param>
        /// <returns>value if it's defined; defaultValue otherwise.</returns>
        /// <remarks>
        /// This should only be used for cases where falsy values are valid;
        /// otherwise the pattern should be 'x = (value) ? value : defaultValue;'.
        /// </remarks>
        return (value !== undefined) ? value : defaultValue;
    };

    var parseInt10 = function (value) {
        /// <summary>Parses a value in base 10.</summary>
        /// <param name="value" type="String">String value to parse.</param>
        /// <returns type="Number">The parsed value, NaN if not a valid value.</returns>

        return parseInt(value, 10);
    };

    var parseTimezone = function (timezone) {
        /// <summary>Parses a timezone description in (+|-)nn:nn format.</summary>
        /// <param name="timezone" type="String">Timezone offset.</param>
        /// <returns type="Object">
        /// An object with a (d)irection property of 1 for + and -1 for -,
        /// offset (h)ours and offset (m)inutes.
        /// </returns>

        var direction = timezone.substring(0, 1);
        direction = (direction === "+") ? 1 : -1;

        var offsetHours = parseInt10(timezone.substring(1));
        var offsetMinutes = parseInt10(timezone.substring(timezone.indexOf(":") + 1));
        return { d: direction, h: offsetHours, m: offsetMinutes };
    };

    var payloadTypeOf = function (data) {
        /// <summary>Determines the kind of payload applicable for the specified value.</summary>
        /// <param name="value">Value to check.</param>
        /// <returns type="String">One of the values declared on the payloadType object.</returns>

        switch (typeof (data)) {
            case "object":
                if (!data) {
                    return payloadType.none;
                }
                if (isArray(data) || isArray(data.results)) {
                    return payloadType.feedOrLinks;
                }
                if (data.__metadata && data.__metadata.uri !== undefined) {
                    return payloadType.entry;
                }
                if (isArray(data.EntitySets)) {
                    return payloadType.svcDoc;
                }
                if (isArray(data.__batchRequests)) {
                    return payloadType.batch;
                }
                if (isDate(data)) {
                    return payloadType.primitiveType;
                }

                return payloadType.complexType;

            case "string":
            case "number":
            case "boolean":
                return payloadType.primitiveType;

            default:
                return payloadType.unknown;
        }
    };

    var prepareRequest = function (request, handler, context) {
        /// <summary>Prepares a request object so that it can be sent through the network.</summary>
        /// <param name="request">Object that represents the request to be sent.</param>
        /// <param name="handler">Handler for data serialization</param>
        /// <param name="context">Context used for preparing the request</param>

        // Default to GET if no method has been specified.    
        if (!request.method) {
            request.method = "GET";
        }

        if (!request.headers) {
            request.headers = {};
        } else {
            normalizeHeaders(request.headers);
        }

        if (request.headers.Accept === undefined) {
            request.headers.Accept = handler.accept;
        }

        if (assigned(request.data) && request.body === undefined) {
            handler.write(request, context);
        }
    };

    var propertyKindOf = function (value) {
        /// <summary>Determines the kind of property for the specified value.</summary>
        /// <param name="value">Value to check.</param>
        /// <returns type="String">One of the values declared on the propertyKind object.</returns>

        switch (payloadTypeOf(value)) {
            case payloadType.complexType:
                if (value.__deferred && value.__deferred.uri) {
                    return propertyKind.deferred;
                }

                return propertyKind.complex;

            case payloadType.feedOrLinks:
            case payloadType.entry:
                return propertyKind.inline;

            case payloadType.primitiveType:
                return propertyKind.primitive;

            default:
                return propertyKind.none;
        }
    };

    var throwErrorCallback = function (error) {
        /// <summary>Default error handler.</summary>
        /// <param name="error" type="Object">Error to handle.</param>
        throw error;
    };

    var trimString = function (str) {
        /// <summary>Removes leading and trailing whitespaces from a string.</summary>
        /// <param name="str" type="String" optional="false" mayBeNull="false">String to trim</param>
        /// <returns type="String">The string with no leading or trailing whitespace.</returns>

        if (str.trim) {
            return str.trim();
        }

        return str.replace(/^\s+|\s+$/g, '');
    };

    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 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 "".concat(
            normInfo.scheme || "",
            normInfo.authority || "",
            normInfo.path || "",
            normInfo.query || "",
            normInfo.fragment || "");
    };

    var mergeUriPathWithBase = function (uriPath, basePath) {
        /// <summary>Merges the path of a relative URI and a base URI.</summary>
        /// <param name="uriPath" type="String>Relative URI path.</param>
        /// <param name="basePath" type="String">Base URI path.</param>
        /// <returns type="String">A string with the merged path.</returns>

        var path = "/";
        var end;

        if (basePath) {
            end = basePath.lastIndexOf("/");
            path = basePath.substring(0, end);

            if (path.charAt(path.length - 1) !== "/") {
                path = path + "/";
            }
        }

        return path + uriPath;
    };

    var removeDotsFromPath = function (path) {
        /// <summary>Removes the special folders . and .. from a URI's path.</summary>
        /// <param name="path" type="string">URI path component.</param>
        /// <returns type="String">Path without any . and .. folders.</returns>

        var result = "";
        var segment = "";
        var end;

        while (path) {
            if (path.indexOf("..") === 0 || path.indexOf(".") === 0) {
                path = path.replace(/^\.\.?\/?/g, "");
            } else if (path.indexOf("/..") === 0) {
                path = path.replace(/^\/\..\/?/g, "/");
                end = result.lastIndexOf("/");
                if (end === -1) {
                    result = "";
                } else {
                    result = result.substring(0, end);
                }
            } else if (path.indexOf("/.") === 0) {
                path = path.replace(/^\/\.\/?/g, "/");
            } else {
                segment = path;
                end = path.indexOf("/", 1);
                if (end !== -1) {
                    segment = path.substring(0, end);
                }
                result = result + segment;
                path = path.replace(segment, "");
            }
        }
        return result;
    };



    var ticks = 0;

    var canUseJSONP = function (request) {
        /// <summary>
        /// Checks whether the specified request can be satisfied with a JSONP request.
        /// </summary>
        /// <param name="request">Request object to check.</param>
        /// <returns type="Boolean">true if the request can be satisfied; false otherwise.</returns>

        // Requests that 'degrade' without changing their meaning by going through JSONP
        // are considered usable.
        //
        // We allow data to come in a different format, as the servers SHOULD honor the Accept
        // request but may in practice return content with a different MIME type.
        if (request.method && request.method !== "GET") {
            return false;
        }

        return true;
    };

    var 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;
        }

        // TODO: need to consider default ports, username and password (but error makes fallback to origin, which is benign)
        // assert: colon > 0, because isAbsoluteUrl is true.
        var locationDomain = window.location.protocol + "//" + window.location.host + "/";
        if (url.indexOf(locationDomain) === 0) {
            return true;
        } else {
            return false;
        }
    };

    var removeCallback = function (tick, name) {
        /// <summary>Removes a callback used for a JSONP request.</summary>
        /// <param name="tick" type="Number">Tick count used on the callback.</param>
        /// <param name="name" type="String">Function name to remove.</param>

        try {
            delete window[name];
        } catch (err) {
            window[name] = undefined;
            if (tick === ticks - 1) {
                ticks -= 1;
            }
        }
    };

    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];
            }
        }
    };

    odata.defaultHttpClient = {
        callbackParameterName: "$callback",

        formatQueryString: "$format=json",

        enableJsonpCallback: false,

        request: function (request, success, error) {
            /// <summary>Performs a network request.</summary>
            /// <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 aborted = false;
            result.abort = function () {
                if (!xhr) {
                    return;
                }

                aborted = true;
                xhr.abort();
                xhr = null;
                error({ message: "Request aborted" });
            };

            var name;
            var url = request.requestUri;
            if (!this.enableJsonpCallback || isLocalUrl(url)) {

                xhr = createXmlHttpRequest();
                xhr.onreadystatechange = function () {
                    if (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 };

                    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);

                // 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 = function () {
                        if (xhr) {
                            xhr = null;
                            error({ message: "Request timed out" });
                        }
                    };
                }

                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();
                name = "handleJSONP_" + tickText;
                window[name] = function (data) {
                    removeCallback(name, tick);
                    success({ body: data, statusCode: 200, headers: { "Content-Type": "application/json"} });
                };

                var scriptTag = document.createElement("SCRIPT");
                scriptTag.setAttribute("type", "text/javascript");

                var queryStringParams = this.callbackParameterName + "=" + name;
                if (this.formatQueryString) {
                    queryStringParams += "&" + this.formatQueryString;
                }

                var qIndex = url.indexOf("?");
                if (qIndex === -1) {
                    url = url + "?" + queryStringParams;
                } else if (qIndex === url.length - 1) {
                    url = url + queryStringParams;
                } else {
                    url = url + "&" + queryStringParams;
                }

                scriptTag.setAttribute("src", url);

                // Insert SCRIPT element and help the GC in case these are
                // referenced by the abort closure.
                var head = document.getElementsByTagName("HEAD")[0];
                head.appendChild(scriptTag);
                head = scriptTag = null;
            }

            return result;
        }
    };



    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, metadata, 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="metadata">Conceptual metadata of a request or response.</param>
        /// <param name="handler">Handler object that is processing a resquest or response.</param>
        /// <returns>Context object.</returns>

        return {
            contentType: contentType,
            dataServiceVersion: dataServiceVersion,
            metadata: metadata,
            handler: handler
        };
    };

    var fixRequestHeader = function (request, name, value) {
        /// <summary>Sets a request header's value. If the header has already a value other than undefined, null or empty string, then this method does nothing.</summary>
        /// <param name="request">Request object on which the header will be set.</summary>
        /// <param name="name" type="String">Header name.</param>
        /// <param name="value" type="String">Header value.</param>
        if (!request) {
            return;
        }

        var headers = request.headers;
        if (!headers[name]) {
            headers[name] = value;
        }
    };

    var fixDataServiceVersion = function (context, version) {
        /// <summary>Sets the dataServiceVersion component of the context. If the component has already a value other than undefined, null or empty string, then this method does nothing.</summary>
        /// <param name="context">Context object used for serialization.</summary>
        /// <param name="version" type="String">Version value.</param>

        if (!context.dataServiceVersion) {
            context.dataServiceVersion = version;
        }
    };

    var getRequestOrResponseHeader = function (requestOrResponse, name) {
        /// <summary>Gets the value of a request or response header.</summary>
        /// <param name="requestOrResponse">Object representing a request or a response.</summary>
        /// <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.</summary>
        /// <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 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.</summary>
        /// <returns type="String">Data service version; undefined if the header cannot be found.</returns>

        // TODO: add version string parsing and validation.
        return getRequestOrResponseHeader(requestOrResponse, "DataServiceVersion");
    };

    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.</summary>
        /// <param name="cType">Object with 'mediaType' and a 'properties' dictionary.</summary>
        /// <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.</summary>
        /// <param name="parseCallback" type="Function">Parser function that will process the response payload.</summary>
        /// <param name="response">HTTP response whose payload is going to be processed.</summary>
        /// <param name="context">Object used as the context for processing the response.</summary>
        /// <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) ? context.metadata : null, 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.</summary>
        /// <param name="serializeCallback" type="Function">Serializer function that will generate the request payload.</summary>
        /// <param name="response">HTTP request whose payload is going to be generated.</summary>
        /// <param name="context">Object used as the context for serializing the request.</summary>
        /// <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) ? context.metadata : null, handler);
            writeContext.request = request;

            request.body = serializeCallback(handler, request.data, writeContext);

            if (request.body !== undefined) {
                fixRequestHeader(request, "DataServiceVersion", writeContext.dataServiceVersion || "1.0");
                fixRequestHeader(request, "Content-Type", contentTypeToString(writeContext.contentType));
                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.</summary>
        /// <param name="serializeCallback" type="Function">Serializer function that will generate the request payload.</summary>
        /// <param name="accept" type="String">String containing a comma separated list of the mime types that this handler can work with.</summary>
        /// <param name="maxDataServiceVersion" type="String">String indicating the highest version of the protocol that this handler can work with.</summary>
        /// <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", "2.0");



    var xmlMediaType = "application/xml";

    // URI prefixes to generate smaller code.
    var http = "http://";
    var w3org = http + "www.w3.org/";               // http://www.w3.org/
    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 xmlnsNS = w3org + "2000/xmlns/";            // http://www.w3.org/2000/xmlns/
    var xmlNS = w3org + "XML/1998/namespace";       // http://www.w3.org/XML/1998/namespace
    var edmxNs = ado + "2007/06/edmx";              // http://schemas.microsoft.com/ado/2007/06/edmx
    var edmNs = ado + "2008/09/edm";                // http://schemas.microsoft.com/ado/2008/09/edm
    var edmNs2 = ado + "2006/04/edm";               // http://schemas.microsoft.com/ado/2006/04/edm
    var edmNs3 = ado + "2007/05/edm";               // http://schemas.microsoft.com/ado/2007/05/edm
    var atomXmlNs = w3org + "2005/Atom";            // http://www.w3.org/2005/Atom
    var appXmlNs = w3org + "2007/app";              // http://www.w3.org/2007/app
    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 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 appendAsXml = function (domNode, xmlAsText) {
        /// <summary>Appends an XML text fragment into the specified DOM element node.</summary>
        /// <param name="domNode">DOM node for the parent element.</param>
        /// <param name="xmlAsText" type="String" mayBeNull="false">XML to append as a child of element.</param>

        var value = "<c>" + xmlAsText + "</c>";
        var parsed = xml.parse(value, null);

        var doc = domNode.ownerDocument;
        var imported = parsed.domNode;
        if ("importNode" in doc) {
            imported = doc.importNode(parsed.domNode, true);
        }

        var importedChild = imported.firstChild;
        while (importedChild) {
            domNode.appendChild(importedChild);
            importedChild = importedChild.nextSibling;
        }
    };

    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 createDomParser = function () {
        /// <summary>Creates a DOM parser object.</summary>
        /// <returns type="DOMParser">DOMParser object.</returns>
        var exception;
        var result;
        if (window.ActiveXObject) {
            try {
                result = new ActiveXObject("Msxml2.DOMDocument.6.0");
                result.async = false;

                return result;
            } catch (_) {
                try {
                    result = new ActiveXObject("Msxml2.DOMDocument.3.0");

                    result.async = false;
                    safeSetProperty(result, "ProhibitDTD", true);
                    safeSetProperty(result, "MaxElementDepth", 256);
                    safeSetProperty(result, "AllowDocumentFunction", false);
                    safeSetProperty(result, "AllowXsltScript", false);

                    return result;
                } catch (e) {
                    exception = e;
                }
            }
        } else {
            if (window.DOMParser) {
                return new window.DOMParser();
            }
            exception = { message: "XML DOM parser not supported" };
        }
        throw exception;
    };

    var getSingleElementByTagNameNS = function (domNode, namespaceURI, localName) {
        /// <summary>Gets the first element under 'domNode' with the spacified name and namespace.</summary>
        /// <param name="domNode">DOM element node.</param>
        /// <param name="namespaceURI" type="String">The namespace URI of the element to match.</param>
        /// <param name="localName" type="String">The local name of the element to match.</param>
        /// <returns>The first element found, null if none.</returns>
        /// <remarks>namespaceURI should be a specific namespace, otherwise the behavior is unspecified.</remarks>

        var result;
        if (domNode.getElementsByTagNameNS) {
            result = domNode.getElementsByTagNameNS(namespaceURI, localName);
            if (result.length !== 0) {
                return result[0];
            }
        } else {
            var child = domNode.firstChild;
            while (child) {
                if (child.nodeType === 1 &&
                    xml.localName(child) === localName &&
                    child.namespaceURI === namespaceURI) {
                    return child;
                }

                child = child.nextSibling;
            }
        }

        return null;
    };

    var xml = {
        addNamespaceAttribute: function (domNode, name, attributeNamespace) {
            /// <summary>Adds a namespace declaration attribute to the specified element node.</summary>
            /// <param name="domNode">DOM node for the element.</param>
            /// <param name="domNode" type="String">Attribute name, eg: xmlns, xmlns:foo, xmlns:bar.</param>
            /// <param name="attributeNamespace" type="String">Namespace to associate.</param>

            var doc = domNode.ownerDocument;
            var attribute;
            if (doc.createAttributeNS) {
                attribute = doc.createAttributeNS(xmlnsNS, name);
            } else {
                attribute = doc.createNode(2, name, xmlnsNS);
            }

            attribute.nodeValue = attributeNamespace;
            domNode.setAttributeNode(attribute);
        },

        appendPreserving: function (domNode, text) {
            /// <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(text)) {
                var attr = this.newDomAttribute(domNode, "space", xmlNS, "xml");
                attr.value = "preserve";
            }

            var textNode = domNode.ownerDocument.createTextNode(text);
            domNode.appendChild(textNode);
        },

        attributes: 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 attribute;
            var domNode = element.domNode;
            var i, len;
            for (i = 0, len = domNode.attributes.length; i < len; i++) {
                attribute = domNode.attributes.item(i);
                onAttributeCallback(this._wrapNode(attribute));
            }
        },

        attribute: function (element, localName, nsURI) {
            /// <summary>Returns the value of an xml element attribute.</summary>
            return this._attribute(element.domNode, localName, nsURI);
        },

        attributeNode: function (domNode, localName, nsURI) {
            /// <summary>Gets an attribute node from an element.</summary>
            /// <param name="domNode">DOM node for the parent element.</param>
            /// <param name="localName" type="String">Local name for the attribute.</param>
            /// <param name="nsURI" type="String">Namespace URI for the attribute.</param>
            /// <returns>The attribute node, null if not found.</returns>

            var attributes = domNode.attributes;
            if (attributes.getNamedItemNS) {
                return attributes.getNamedItemNS(nsURI, localName);
            }

            return attributes.getQualifiedItem(localName, nsURI);
        },

        childElements: function (element, onElementCallback) {
            /// <summary>Iterates through the XML element's child elements and invokes the callback function for each one.</summary>
            /// <param name="element">Wrapped element to iterate over.</param>
            /// <param name="onElementCallback" type="Function">Callback function to invoke with wrapped element nodes.</param>

            var child = element.domNode.firstChild;
            var childBaseURI;
            while (child !== null) {
                if (child.nodeType === 1) {
                    childBaseURI = normalizeURI(this._baseURI(child), element.baseURI);
                    onElementCallback(this._wrapNode(child, childBaseURI));
                }

                child = child.nextSibling;
            }
        },

        firstElement: function (element) {
            /// <summary>Returns the first child element (wrapped) of the specified element.</summary>
            /// <param name="element">Element to get first child for.</param>
            /// <returns>This first child element (wrapped), null if there is none.</returns>

            var child = element.domNode.firstChild;
            var childBaseURI;
            while (child !== null) {
                if (child.nodeType === 1) {
                    childBaseURI = normalizeURI(this._baseURI(child), element.baseURI);
                    return this._wrapNode(child, childBaseURI);
                }

                child = child.nextSibling;
            }

            return null;
        },

        innerText: function (domNode) {
            /// <summary>Returns the text value of an XML element.</summary>
            /// <param name="domNode">DOM element</param>
            /// <returns type="String">
            /// The text content of the node or the concatenated text
            /// representing the node and its descendants; never null.
            /// </returns>

            var result = domNode.text;
            if (result !== undefined) {
                return result;
            }

            result = "";

            var cursor = domNode.firstChild;
            if (cursor) {
                do {
                    // Process the node.
                    if (cursor.nodeType === 3 || cursor.nodeType === 4) {
                        result += cursor.nodeValue;
                    }

                    // Advance the node.
                    var next = cursor.firstChild;
                    if (!next) {
                        while (cursor !== domNode) {
                            next = cursor.nextSibling;
                            if (next) {
                                cursor = next;
                                break;
                            } else {
                                cursor = cursor.parentNode;
                            }
                        }
                    } else {
                        cursor = next;
                    }
                } while (cursor !== domNode);
            }

            return result;
        },

        localName: function (node) {
            /// <summary>Returns the localName of a XML node.</summary>
            /// <param name="node">DOM node to get value for.</param>
            /// <returns type="String">localName of node.</returns>

            if (node.localName) {
                return node.localName;
            }

            return node.baseName;
        },

        newDocument: function (name, nsURI) {
            /// <summary>Creates a new XML document.</summary>
            /// <param name="name" type="String">Local name of the root element.</param>
            /// <param name="nsURI" type="String">Namespace of the root element.</param>
            /// <returns>The wrapped root element of the document.</returns>

            var dom;

            if (window.ActiveXObject) {
                dom = createDomParser();
                dom.documentElement = dom.createNode(1, name, nsURI);
            }
            else if (window.document.implementation && window.document.implementation.createDocument) {
                dom = window.document.implementation.createDocument(nsURI, name, null);
            }

            // Set the processing instructions.
            var pi = dom.createProcessingInstruction("xml", "version=\'1.0\' encoding=\'utf-8\'");
            dom.insertBefore(pi, dom.documentElement);

            return this._wrapNode(dom.documentElement);
        },

        newDomAttribute: function (domNode, localName, nsURI, nsPrefix) {
            /// <summary>Creates a new unwrapped DOM attribute.</summary>
            /// <param name="domNode">Parent element to which new node should be added.</param>
            /// <param name="localName" type="String">Local name for attribute.</param>
            /// <param name="nsURI" type="String">Namespace URI for the attribute.</param>
            /// <param name="nsPrefix" type="String">Namespace prefix for attribute to be created.</param>
            /// <returns>The created DOM attribute.</returns>

            var doc = domNode.ownerDocument;
            var attribute;
            localName = (nsPrefix) ? (nsPrefix + ":" + localName) : localName;
            if (doc.createAttributeNS) {
                attribute = doc.createAttributeNS(nsURI, localName);
                domNode.setAttributeNodeNS(attribute);
            } else {
                attribute = doc.createNode(2, localName, nsURI || undefined);
                domNode.setAttributeNode(attribute);
            }

            return attribute;
        },

        newDomElement: function (domNode, localName, nsURI, nsPrefix) {
            /// <summary>Creates a new unwrapped DOM element.</summary>
            /// <param name="domNode">Parent element to which new node should be added.</param>
            /// <param name="localName" type="String">Local name for element.</param>
            /// <param name="nsURI" type="String">Namespace URI for the element.</param>
            /// <param name="nsPrefix" type="String">Namespace prefix for attribute to be created.</param>
            /// <returns>The created DOM element.</returns>

            var doc = domNode.ownerDocument;
            var element;
            localName = (nsPrefix) ? (nsPrefix + ":" + localName) : localName;
            if (doc.createElementNS) {
                element = doc.createElementNS(nsURI, localName);
            } else {
                element = doc.createNode(1, localName, nsURI || undefined);
            }

            domNode.appendChild(element);
            return element;
        },

        newElement: function (parent, name, nsURI, value) {
            /// <summary>Creates a new wrapped element.</summary>
            /// <param name="parent">Wrapped parent element to which new node should be added.</param>
            /// <param name="name" type="String">Local name for element.</param>
            /// <param name="nsURI" type="String">Namespace URI for the element.</param>
            /// <param name="value" type="String" mayBeNull="true">Text value to append in element, if applicable.</param>
            /// <returns>The created wrapped element.</returns>

            var dom = parent.domNode.ownerDocument;

            var element;
            if (dom.createElementNS) {
                element = dom.createElementNS(nsURI, name);
            } else {
                element = dom.createNode(1, name, nsURI || undefined);
            }

            if (value) {
                this.appendPreserving(element, value);
            }

            parent.domNode.appendChild(element);
            return this._wrapNode(element);
        },

        newAttribute: function (element, name, nsURI, value) {
            /// <summary>Creates a new wrapped attribute.</summary>
            /// <param name="element">Wrapped parent element to which new node should be added.</param>
            /// <param name="name" type="String">Local name for element.</param>
            /// <param name="nsURI" type="String">Namespace URI for the element.</param>
            /// <param name="value" type="String">Node value for the attribute.</param>
            /// <returns>The created wrapped attribute.</returns>

            var dom = element.domNode.ownerDocument;

            var attribute;
            if (dom.createAttributeNS) {
                attribute = dom.createAttributeNS(nsURI, name);
                attribute.value = value;
                element.domNode.setAttributeNodeNS(attribute);
            } else {
                attribute = dom.createNode(2, name, nsURI || undefined);
                attribute.value = value;
                element.domNode.setAttributeNode(attribute);
            }

            return this._wrapNode(attribute);
        },

        qualifyXmlTagName: function (name, prefix) {
            /// <summary>Qualifies an XML tag name with the specified prefix.</summary>
            /// <param name="name" type="String">Element name</param>
            /// <param name="prefix" type="String">Prefix to use, possibly null.</param>
            /// <returns type="String">The qualified name.</returns>
            /// <remarks>This operates at the lexical level - there is no awareness of in-scope prefixes.</remarks>

            if (prefix) {
                return prefix + ":" + name;
            }

            return name;
        },

        parse: function (text, baseURI) {
            /// <summary>Returns an XML document from the specified text.</summary>
            /// <param name="text" type="String">Document text.</param>
            /// <param name="baseURI" type="String">Base URI for the document.</param>
            /// <returns>The wrapped root element of the document.</returns>

            var root = this._parse(text);
            var rootBaseURI = normalizeURI(this._baseURI(root), baseURI);
            return this._wrapNode(root, rootBaseURI);
        },

        serialize: function (root) {
            /// <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 dom = root.domNode.ownerDocument;
            return this.serializeNode(dom);
        },

        serializeChildren: 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 "";
            }

            var fragment = domNode.ownerDocument.createDocumentFragment();
            for (i = 0; i < len; i++) {
                fragment.appendChild(children[i].cloneNode(true));
            }

            return this.serializeNode(fragment);
        },

        serializeNode: 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" };
        },

        _attribute: function (domNode, localName, nsURI) {
            /// <summary>Gets the value of a DOM attribute.</summary>
            /// <param name="domNode">DOM element to get attribute for.</param>
            /// <param name="localName" type="String">Name of the attribute to get.</param>
            /// <param name="nsURI" type="String">Namespace of the attribute to get.</param>
            /// <returns type="String">Value of the attribute if found; null otherwise.</returns>

            if (domNode.getAttributeNS) {
                return domNode.getAttributeNS(nsURI || null, localName);
            }

            // The method is not supported so we work with the attributes collection. 
            var node = domNode.attributes.getQualifiedItem(localName, nsURI);
            if (node) {
                return node.value;
            }

            return null;
        },

        _baseURI: function (domNode) {
            /// <summary>Gets the value of the xml:base attribute on the specified element.</summary>
            /// <param name="domNode">Element to get attribute value from.</param>
            /// <returns type="String">Value of the xml:base attribute if found; null otherwise.</returns>

            return this._attribute(domNode, "base", xmlNS);
        },

        _parse: function (text) {
            /// <summary>Returns an XML document from the specified text.</summary>
            /// <param name="text" type="String">Document text.</param>
            /// <returns>The root element of the document.</returns>

            var dom = createDomParser();
            // TODO: add DOMParser error handling code. FireFox, IE8, IE9 have different mechanisms to report the parser 
            // errors, IE9 and FireFox overload the same property (DOMParser.parseError);

            if (dom.parseFromString) {
                dom = dom.parseFromString(text, "text/xml");
            } else {
                dom.loadXML(text);
                if (dom.parseError.errorCode !== 0) {
                    throw { message: dom.parseError.reason, errorXmlText: text, srcText: dom.parseError.srcText };
                }
            }

            return dom.documentElement;
        },

        _wrapNode: function (domNode, baseURI) {
            /// <summary>Creates a wrapped DOM node.</summary>
            /// <param name="domNode">DOM node to wrap.</param>
            /// <param name="baseURI" type="String">Base URI in scope for the node.</param>
            /// <returns type="Object">
            /// An object with the wrapped domNode, information about its in-scope URI, and simple 
            /// access to name and namespace.
            /// </returns>

            var nsURI = domNode.namespaceURI;
            var nodeName = domNode.nodeName;

            // MSXML 3.0 doesn't return a namespaceURI for xmlns attributes.
            if (!nsURI) {
                if (domNode.nodeType === 2 && (nodeName === "xmlns" || nodeName.indexOf("xmlns:", 0) === 0)) {
                    nsURI = xmlnsNS;
                } else {
                    nsURI = null;
                }
            }

            return {
                baseURI: baseURI,
                domNode: domNode,
                localName: this.localName(domNode),
                nsURI: nsURI
            };
        }
    };

    var readODataXmlDocument = function (xmlRoot) {
        /// <summary>Reads an OData link(s) producing an object model in return.</summary>
        /// <param name="xmlRoot">Top-level element to read.</param>
        /// <returns type="Object">The object model representing the specified element.</returns>

        if (xmlRoot.nsURI === odataXmlNs) {
            switch (xmlRoot.localName) {
                case "links":
                    return readLinks(xmlRoot);
                case "uri":
                    return readUri(xmlRoot);
            }
        }
        return undefined;
    };

    var readLinks = function (linksElement) {
        /// <summary>Deserializes an OData XML links element.</summary>
        /// <param name="linksElement">XML links element.</param>
        /// <returns type="Object">A new object representing the links collection.</returns>

        var uris = [];

        xml.childElements(linksElement, function (child) {
            if (child.localName === "uri" && child.nsURI === odataXmlNs) {
                var uri = readUri(child);
                uris.push(uri);
            }
        });

        return { results: uris };
    };

    var readUri = function (uriElement) {
        /// <summary>Deserializes an OData XML uri element.</summary>
        /// <param name="uriElement">XML uri element.</param>
        /// <returns type="Object">A new object representing the uri.</returns>

        return { uri: xml.innerText(uriElement.domNode) };
    };

    var writeODataXmlDocument = 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 (payloadTypeOf(data) === payloadType.complexType &&
           !data.__metadata && data.hasOwnProperty("uri")) {
            return writeUri(data);
        }

        // Allow for undefined to be returned.
    };

    var writeUri = function (data) {
        /// <summary>Writes the specified uri data into an OData XML uri document.</summary>
        /// <param name="data">Uri data to write in intermediate format.</param>
        /// <returns>The feed element of the DOM tree built.</returns>

        var uri = writeODataXmlNode(null, "uri", odataXmlNs);
        if (data.uri) {
            xml.appendPreserving(uri.domNode, data.uri);
        }

        return uri;
    };

    var writeODataXmlNode = function (parent, name, nsURI) {
        /// <summary>Writes the root of an OData XML document, possibly under an existing element.</summary>
        /// <param name="parent" mayBeNull="true">Element under which to create a new element.</param>
        /// <param name="name">Name for the new element.</param>
        /// <param name="nsURI" type="String">Namespace URI for the element.</param>
        /// <returns>The created element.</returns>

        if (parent) {
            return xml.newElement(parent, name, nsURI);
        }
        return xml.newDocument(name, nsURI);
    };

    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 root = xml.parse(text);
            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);
        var result = undefined;
        if (cType && cType.mediaType === xmlMediaType) {
            var xmlDoc = writeODataXmlDocument(data);
            if (xmlDoc) {
                result = xml.serialize(xmlDoc);
            }
        }
        return result;
    };

    odata.xmlHandler = handler(xmlParser, xmlSerializer, xmlMediaType, "2.0");

    odata.xml = xml;



    var atomAcceptTypes = ["application/atom+xml", "application/atomsvc+xml", "application/xml"];
    var atomMediaType = atomAcceptTypes[0];

    var inlineTag = xml.qualifyXmlTagName("inline", "m");
    var propertiesTag = xml.qualifyXmlTagName("properties", "m");
    var propertyTypeAttribute = xml.qualifyXmlTagName("type", "m");
    var propertyNullAttribute = xml.qualifyXmlTagName("null", "m");

    // 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 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 propertyTypeDefaultConverter = function (propertyValue) {
        /// <summary>Does a no-op conversion on the specified value.</summary>
        /// <param name="propertyValue">Value to convert.</param>
        /// <returns>Original property value.</returns>

        return propertyValue;
    };

    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>

        return propertyValue === "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) {
        /// <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")) {
            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];
        if (!ms) {
            ms = 0;
        } else {
            if (ms.length > 4) {
                throw { message: "Cannot parse date/time value to given precision." };
            }
            while (ms.length < 4) {
                ms += "0";
            }
            ms = parseInt10(ms.substring(1));
        }

        // 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]);
        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())) {
            throw { message: "Invalid date/time value" };
        }

        if (withOffset) {
            result.__edmType = "Edm.DateTimeOffset";
            result.__offset = offset;
        }

        return result;
    };

    var parseDateTime = function (propertyValue) {
        /// <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);
    };

    var parseDateTimeOffset = function (propertyValue) {
        /// <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);
    };

    var parseTime = function () {
        /// <summary>Parses a string into a Time 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 proeprty.
        /// </remarks>

        // Edm.Time is conceptually the time component of a datetime, but
        // serialization can also be a duration.
        throw { message: "Edm.Time not supported" };
    };

    // Property type converters are parsers that convert strings into typed values.
    var propertyTypeConverters = {
        "Edm.Boolean": parseBool,
        "Edm.Binary": propertyTypeDefaultConverter,
        "Edm.DateTime": parseDateTime,
        "Edm.DateTimeOffset": parseDateTimeOffset,
        "Edm.Time": parseTime,
        "Edm.Decimal": propertyTypeDefaultConverter,
        "Edm.Guid": propertyTypeDefaultConverter,
        "Edm.String": propertyTypeDefaultConverter,
        "Edm.Byte": parseInt10,
        "Edm.Double": parseFloat,
        "Edm.Single": parseFloat,
        "Edm.Int16": parseInt10,
        "Edm.Int32": parseInt10,
        "Edm.Int64": propertyTypeDefaultConverter,
        "Edm.SByte": parseInt10
    };

    var formatToString = function (value) {
        /// <summary>Formats a value by invoking .toString() on it.</summary>
        /// <param name="value" mayBeNull="false">Value to format.</param>
        /// <returns type="String">Formatted text.</returns>

        return value.toString();
    };

    var formatTime = parseTime;

    // Property type formatters are serializers that convert typed values into strings.
    var propertyTypeFormatters = {
        "Edm.Binary": formatToString,
        "Edm.Boolean": formatToString,
        "Edm.Byte": formatToString,
        "Edm.DateTime": formatDateTimeOffset,
        "Edm.DateTimeOffset": formatDateTimeOffset,
        "Edm.Decimal": formatToString,
        "Edm.Double": formatToString,
        "Edm.Guid": formatToString,
        "Edm.Int16": formatToString,
        "Edm.Int32": formatToString,
        "Edm.Int64": formatToString,
        "Edm.SByte": formatToString,
        "Edm.Single": formatToString,
        "Edm.String": formatToString,
        "Edm.Time": formatTime
    };

    var isPrimitiveType = function (typeName) {
        /// <summary>Checks whether the specified type name is a primitive type.</summary>
        /// <param name="typeName" type="String" mayBeNull="true">Name of type to check.</param>
        /// <returns type="Boolean">
        /// true if the type is the name of a primitive type; false otherwise.
        /// </returns>

        return typeName && (propertyTypeConverters[typeName] !== undefined);
    };

    var convertFromAtomPropertyText = function (value, targetType) {
        /// <summary>Converts a text value to the specified target type.</summary>
        /// <param name="value" type="String" mayBeNull="true">Text value to convert.</param>
        /// <param name="targetType" type="String" mayBeNull="true">Name of type to convert from.</param>
        /// <returns>The converted value.</returns>

        if (value !== null && targetType) {
            var converter = propertyTypeConverters[targetType];
            if (converter) {
                value = converter(value);
            }
        }

        return value;
    };

    var convertToAtomPropertyText = function (value, targetType) {
        /// <summary>Converts a typed value from the specified target type into a text value.</summary>
        /// <param name="value">Typed value to convert.</param>
        /// <param name="targetType" type="String" mayBeNull="true">Name of type to convert to. <param>
        /// <returns type="String">The converted value as text.</returns>

        if (value !== null && targetType) {
            if (isDate(value)) {
                targetType = (isDateTimeOffset(value)) ? "Edm.DateTimeOffset" : "Edm.DateTime";
            }

            var converter = propertyTypeFormatters[targetType];
            if (converter) {
                value = converter(value);
            }
        }

        return value;
    };

    var readAtomDocument = function (atomElement, metadata) {
        /// <summary>Reads an ATOM entry, feed or service document, producing an object model in return.</summary>
        /// <param name="atomElement">Top-level element to read.</param>
        /// <param name="metadata">Metadata 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>

        // Handle feed and entry elements.
        if (atomElement.nsURI === atomXmlNs) {
            switch (atomElement.localName) {
                case "feed":
                    return readAtomFeed(atomElement, metadata);
                case "entry":
                    return readAtomEntry(atomElement, metadata);
            }
        }

        // Handle service documents. 
        if (atomElement.nsURI === appXmlNs && atomElement.localName === "service") {
            return readAtomServiceDocument(atomElement);
        }

        // Allow undefined to be returned.
    };

    var readAtomFeed = function (atomFeed, metadata) {
        /// <summary>Deserializes an ATOM feed element.</summary>
        /// <param name="atomFeed">ATOM feed element.</param>
        /// <param name="metadata">Metadata that describes the conceptual schema.</param>
        /// <returns type="Object">A new object representing the feed.</returns>
        var feedMetadata = {};
        var feed = {
            results: [],
            __metadata: feedMetadata
        };

        feedMetadata.feed_extensions = readAtomExtensionAttributes(atomFeed);

        xml.childElements(atomFeed, function (feedChild) {
            switch (feedChild.nsURI) {
                case atomXmlNs:
                    switch (feedChild.localName) {
                        case "id":
                            feedMetadata.uri = normalizeURI(xml.innerText(feedChild.domNode), feedChild.baseURI);
                            feedMetadata.uri_extensions = readAtomExtensionAttributes(feedChild);
                            break;
                        case "title":
                            feedMetadata.title = xml.innerText(feedChild.domNode);
                            feedMetadata.title_extensions = readAtomExtensionAttributes(feedChild);
                            break;
                        case "entry":
                            var entry = readAtomEntry(feedChild, metadata);
                            feed.results.push(entry);
                            break;
                        case "link":
                            readAtomFeedLink(feedChild, feed);
                            break;
                    }
                    return;
                case odataMetaXmlNs:
                    if (feedChild.localName === "count") {
                        feed.__count = parseInt10(xml.innerText(feedChild.domNode));
                        return;
                    }
                    break;
            }

            var extension = readAtomExtensionElement(feedChild);
            feedMetadata.feed_extensions.push(extension);
        });

        return feed;
    };

    var readAtomFeedLink = function (feedLinkElement, feed) {
        /// <summary>Reads an ATOM link element for a feed.</summary>
        /// <param name="feedLinkElement">Link element to read.</param>
        /// <param name="feed">Feed object to be annotated with information.</param>

        var link = readAtomLink(feedLinkElement);
        var feedMetadata = feed.__metadata;
        switch (link.rel) {
            case "next":
                feed.__next = link.href;
                feedMetadata.next_extensions = link.extensions;
                break;
            case "self":
                feedMetadata.self = link.href;
                feedMetadata.self_extensions = link.extensions;
                break;
        }
    };

    var readAtomLink = function (linkElement) {
        /// <summary>Reads an ATOM link element.</summary>
        /// <param name="linkElement">Link element to read.</param>
        /// <returns type="Object">A link element representation.</returns>

        var link = { extensions: [] };
        var linkExtension;

        xml.attributes(linkElement, function (attribute) {
            if (!attribute.nsURI) {
                switch (attribute.localName) {
                    case "href":
                        link.href = normalizeURI(attribute.domNode.nodeValue, linkElement.baseURI);
                        return;
                    case "type":
                    case "rel":
                        link[attribute.localName] = attribute.domNode.nodeValue;
                        return;
                }
            }

            if (isExtensionNs(attribute.nsURI)) {
                linkExtension = readAtomExtensionAttribute(attribute);
                link.extensions.push(linkExtension);
            }
        });

        if (!link.href) {
            throw { error: "href attribute missing on link element", element: linkElement };
        }

        return link;
    };

    var readAtomExtensionElement = function (atomExtension) {
        /// <summary>Reads an ATOM extension element (an element not in the ATOM namespaces).</summary>
        /// <param name="atomExtension">ATOM extension element.</param>
        /// <returns type="Object">An extension element representation.</returns>

        var extension = {
            name: atomExtension.localName,
            namespaceURI: atomExtension.nsURI,
            attributes: readAtomExtensionAttributes(atomExtension),
            children: []
        };

        xml.childElements(atomExtension, function (child) {
            var childExtension = readAtomExtensionElement(child);
            extension.children.push(childExtension);
        });

        if (extension.children.length === 0) {
            var text = xml.innerText(atomExtension.domNode);
            if (text) {
                extension.value = text;
            }
        }

        return extension;
    };

    var readAtomExtensionAttributes = function (xmlElement) {
        /// <summary>Reads ATOM extension attributes from an element.</summary>
        /// <param name="xmlElement">ATOM element with zero or more extension attributes.</param>
        /// <returns type="Array">An array of extension attribute representations.</returns>

        var extensions = [];
        xml.attributes(xmlElement, function (attribute) {
            if (isExtensionNs(attribute.nsURI)) {
                var extension = readAtomExtensionAttribute(attribute);
                extensions.push(extension);
            }
        });

        return extensions;
    };

    var readAtomExtensionAttribute = function (attribute) {
        /// <summary>Reads an ATOM extension attribute into an object.</summary>
        /// <param name="attribute">ATOM extension attribute.</param>
        /// <returns type="Object">An object with the attribute information.</returns>

        return {
            name: attribute.localName,
            namespaceURI: attribute.nsURI,
            value: attribute.domNode.nodeValue
        };
    };

    var getObjectValueByPath = 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 setObjectValueByPath = 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>

        // Fast path.
        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 expandCustomizationPath = function (path) {
        /// <summary>Expands a customization path if it's well-known.</summary>
        /// <param name="path" type="String">Path to expand.</param>
        /// <returns type="String">Expanded path or 'path' otherwise.</returns>

        return knownCustomizationPaths[path] || path;
    };

    var getXmlPathValue = function (ns, xmlPath, element, readAsXml) {
        /// <summary>Returns the text value of the element or attribute at xmlPath.</summary>
        /// <param name="ns" type="String">Namespace for elements to match.</param>
        /// <param name="xmlPath" type="String">
        /// '/'-separated list of element names, possibly ending with a '@'-prefixed attribute name.
        /// </param>
        /// <param name="element">Root element.</param>
        /// <param name="readAsXml">Whether the value should be read as XHTML.</param>
        /// <returns type="String">The text value of the node found, null if none.</returns>

        var parts = xmlPath.split('/');
        var i, len;
        for (i = 0, len = parts.length; i < len; i++) {
            if (parts[i].charAt(0) === "@") {
                return xml._attribute(element, parts[i].substring(1), ns);
            } else {
                element = getSingleElementByTagNameNS(element, ns, parts[i]);
                if (!element) {
                    return undefined;
                }
            }
        }

        if (readAsXml) {
            // Treat per XHTML in http://tools.ietf.org/html/rfc4287#section-3.1.1, including the DIV
            // in the content.
            return xml.serializeChildren(element);
        } else {
            return xml.innerText(element);
        }
    };

    var setXmlPathValue = function (ns, nsPrefix, xmlPath, element, writeAsXml, value) {
        /// <summary>Sets the text value of the element or attribute at xmlPath.</summary>
        /// <param name="ns" type="String">Namespace for elements to match.</param>
        /// <param name="nsPrefix" type="String">Namespace prefix for elements to be created.</param>
        /// <param name="xmlPath" type="String">
        /// '/'-separated list of element names, possibly ending with a '@'-prefixed attribute name.
        /// </param>
        /// <param name="element">Root element.</param>
        /// <param name="writeAsXml" type="Boolean">Whether the value should be written as XHTML.</param>
        /// <param name="value" type="String">The text value of the node to write.</param>

        var target = element;
        var parts = xmlPath.split('/');
        var i, len;
        for (i = 0, len = parts.length; i < len; i++) {
            var next;
            if (parts[i].charAt(0) === "@") {
                next = xml.attributeNode(target, parts[i].substring(1), ns);
                if (!next) {
                    next = xml.newDomAttribute(target, parts[i].substring(1), ns, nsPrefix);
                }
            } else {
                next = getSingleElementByTagNameNS(target, ns, parts[i]);
                if (!next) {
                    next = xml.newDomElement(target, parts[i], ns, nsPrefix);
                }
            }

            target = next;
        }

        // Target can be an attribute (2) or an element (1).
        if (target.nodeType === 2) {
            target.value = value;
        } else {
            // The element should be empty at this point; we won't erase its contents.
            if (writeAsXml) {
                target.setAttribute("type", "xhtml");
                appendAsXml(target, value);
            } else {
                xml.appendPreserving(target, value);
            }
        }
    };

    var isElementEmpty = function (element) {
        /// <summary>Checks whether the specified XML element is empty.</summary>
        /// <param name="element">DOM element node to check.</param>
        /// <returns type="Boolean">true if the element is empty; false otherwise.</returns>
        /// <remarks>
        /// The element is considered empty if it doesn't have any attributes other than
        /// namespace declarations and if it has no child nodes.
        /// </remarks>

        // If there are any child elements or text nodes, it's not empty.
        if (element.childNodes.length) {
            return false;
        }

        // If there are no attributes, then we know it's already empty.
        var attributes = element.attributes;
        var len = attributes.length;
        if (len === 0) {
            return true;
        }

        // Otherwise, we have to search for attributes that aren't namespace declarations.
        for (var i = 0; i < len; i++) {
            var attributeName = attributes[i].nodeName;
            if (attributeName !== "xmlns" && attributeName.indexOf("xmlns:") !== 0) {
                return false;
            }
        }

        return true;
    };
    var removeXmlProperty = function (entryElement, propertyPath) {
        /// <summary>Removes a property from an entry.</summary>
        /// <param name="entryElement">XML element for an ATOM OData entry.</param>
        /// <param name="propertyPath" type="String">Property path to an element.</param>

        // Get the 'properties' node from 'content' or 'properties'.
        var propertiesElement = getSingleElementByTagNameNS(entryElement.domNode, odataMetaXmlNs, "properties");
        if (!propertiesElement) {
            var contentElement = getSingleElementByTagNameNS(entryElement.domNode, atomXmlNs, "content");
            if (contentElement) {
                propertiesElement = getSingleElementByTagNameNS(contentElement, odataMetaXmlNs, "properties");
            }
        }

        if (propertiesElement) {
            // Traverse down to the parent of the property path.
            var propertyParentElement = propertiesElement;
            var parts = propertyPath.split("/");
            var i, len;
            for (i = 0, len = (parts.length - 1); i < len; i++) {
                propertyParentElement = getSingleElementByTagNameNS(propertyParentElement, odataXmlNs, parts[i]);
                if (!propertyParentElement) {
                    return;
                }
            }

            // Remove the child from its parent.
            var propertyElement = getSingleElementByTagNameNS(propertyParentElement, odataXmlNs, parts[i]);
            if (propertyElement) {
                propertyParentElement.removeChild(propertyElement);
            }

            // Remove empty elements up the parent chain.
            var candidate = propertyParentElement;
            while (candidate !== propertiesElement && isElementEmpty(candidate)) {
                var parent = candidate.parentNode;
                parent.removeChild(candidate);
                candidate = parent;
            }
        }
    };

    var applyEntryCustomizationToEntry = function (customization, sourcePath, entryElement, entryObject, propertyType, suffix, context) {
        /// <summary>Applies a specific feed customization item to an entry.</summary>
        /// <param name="customization">Object with customization description.</param>
        /// <param name="sourcePath">Property path to map ('source' in the description).</param>
        /// <param name="entryElement">XML element for the entry that corresponds to the object being written.</param>
        /// <param name="entryObject">Object being written.</param>
        /// <param name="propertyType" type="String">Name of property type to write.</param>
        /// <param name="suffix" type="String">Suffix to feed customization properties.</param>
        /// <param name="context">Context used for serialization.</param>

        var targetPath = customization["FC_TargetPath" + suffix];
        var xmlPath = expandCustomizationPath(targetPath);
        var xmlNamespace = (targetPath !== xmlPath) ? atomXmlNs : customization["FC_NsUri" + suffix];
        var keepInContent = (customization["FC_KeepInContent" + suffix] === "true") ? true : false;
        var writeAsXhtml = (customization["FC_ContentKind" + suffix] === "xhtml");
        var prefix = customization["FC_NsPrefix" + suffix] || null;

        // Get the value to be written.
        var value = getObjectValueByPath(sourcePath, entryObject);

        // Special case: for null values, the 'property' should be left as it was generated.
        // undefined values will appear when metadata describe a property the object doesn't have.
        if (!assigned(value)) {
            return;
        }

        // Remove the property if it should not be kept in content.
        if (!keepInContent) {
            fixDataServiceVersion(context, "2.0");
            removeXmlProperty(entryElement, sourcePath);
        }

        // Set/create the subtree for the property path with the appropriate value.
        value = convertToAtomPropertyText(value, propertyType);
        setXmlPathValue(xmlNamespace, prefix, xmlPath, entryElement.domNode, writeAsXhtml, value);
    };

    var applyEntryCustomizationToObject = function (customization, sourcePath, entryElement, entryObject, propertyType, suffix) {
        /// <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>

        // If keepInConent equals true then we do nothing as the object has been deserialized at this point.
        if (customization["FC_KeepInContent" + suffix] === "true") {
            return;
        }
        // An existing 'null' means that the property was set to null in the properties,
        // which overrides other items.
        if (getObjectValueByPath(sourcePath, entryObject) === null) {
            return;
        }

        var targetPath = customization["FC_TargetPath" + suffix];
        var xmlPath = expandCustomizationPath(targetPath);
        var xmlNamespace = (targetPath !== xmlPath) ? atomXmlNs : customization["FC_NsUri" + suffix];
        var readAsXhtml = (customization["FC_ContentKind" + suffix] === "xhtml");
        var value = getXmlPathValue(xmlNamespace, xmlPath, entryElement.domNode, readAsXhtml);

        // 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 (value === undefined) {
            return;
        }

        value = convertFromAtomPropertyText(value, propertyType);

        // Set the value on the object.
        setObjectValueByPath(sourcePath, entryObject, value, propertyType);
    };

    var lookupPropertyType = function (metadata, entityType, 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="entityType">Entity 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 (entityType) {
            // Keep track of the type being traversed, necessary for complex types.
            var traversedType = entityType;

            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 j, propLength;
                var found = false;
                for (j = 0, propLength = properties.length; j < propLength; j++) {
                    if (properties[j].name === parts[i]) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    break;
                }

                var propertyType = properties[j].type;

                // We could in theory still be missing types, but that would
                // be caused by a malformed path.
                if (!propertyType || isPrimitiveType(propertyType)) {
                    return propertyType || null;
                }

                traversedType = lookupComplexType(propertyType, metadata);
                if (!traversedType) {
                    return null;
                }
            }

            // Traverse up the inheritance chain.
            entityType = lookupEntityType(entityType.baseType, metadata);
        }

        return null;
    };

    var applyMetadataToObject = function (entryElement, entryObject, metadata) {
        /// <summary>Applies feed customization properties to an object being read.</summary>
        /// <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="metadata">Metadata that describes the conceptual schema.</param>

        if (!metadata || metadata.length === 0) {
            return;
        }

        var typeName = entryObject.__metadata.type;
        while (typeName) {
            var entityType = lookupEntityType(typeName, metadata);
            if (!entityType) {
                return;
            }

            // Apply all feed customizations from the entity and each of its properties.
            var propertyType;
            var source = entityType.FC_SourcePath;
            if (source) {
                propertyType = lookupPropertyType(metadata, entityType, source);
                applyEntryCustomizationToObject(entityType, source, entryElement, entryObject, propertyType, "");
            }

            var properties = entityType.property;
            if (properties) {
                var i, len;
                for (i = 0, len = properties.length; i < len; i++) {
                    var property = properties[i];
                    var suffixCounter = 0;
                    var suffix = "";
                    while (property["FC_TargetPath" + suffix]) {
                        source = property.name;
                        propertyType = property.type;

                        var sourcePath = property["FC_SourcePath" + suffix];
                        if (sourcePath) {
                            source += "/" + sourcePath;
                            propertyType = lookupPropertyType(metadata, entityType, source);
                        }

                        applyEntryCustomizationToObject(property, source, entryElement, entryObject, propertyType, suffix);
                        suffixCounter++;
                        suffix = "_" + suffixCounter;
                    }
                }
            }

            // Apply feed customizations from base types.
            typeName = entityType.baseType;
        }
    };

    var readAtomEntry = function (atomEntry, metadata) {
        /// <summary>Reads an ATOM entry in OData format.</summary>
        /// <param name="atomEntry">XML element for the entry.</param>
        /// <param name="metadata">Metadata that describes the conceptual schema.</param>
        /// <returns type="Object">An object in payload representation format.</returns>

        var entryMetadata = {};
        var entry = {
            __metadata: entryMetadata
        };

        var etag = xml.attribute(atomEntry, "etag", odataMetaXmlNs);
        if (etag) {
            entryMetadata.etag = etag;
        }

        xml.childElements(atomEntry, function (entryChild) {
            if (entryChild.nsURI === atomXmlNs) {
                switch (entryChild.localName) {
                    case "id":
                        entryMetadata.uri = normalizeURI(xml.innerText(entryChild.domNode), entryChild.baseURI);
                        entryMetadata.uri_extensions = readAtomExtensionAttributes(entryChild);
                        break;
                    case "category":
                        readAtomEntryType(entryChild, entry, entryMetadata);
                        break;
                    case "content":
                        readAtomEntryContent(entryChild, entry);
                        break;
                    case "link":
                        readAtomEntryLink(entryChild, entry, metadata);
                        break;
                }
            }

            if (entryChild.nsURI === odataMetaXmlNs && entryChild.localName === "properties") {
                readAtomEntryStructuralObject(entryChild, entry, entryMetadata);
            }
        });

        // Apply feed customizations if applicable.
        applyMetadataToObject(atomEntry, entry, metadata);

        return entry;
    };

    var readAtomEntryType = function (atomCategory, entry, entryMetadata) {
        /// <summary>Reads type information from an ATOM category element.</summary>
        /// <param name="atomCategory">XML category element.</param>
        /// <param name="entry">Entry object to update with information.</param>
        /// <param name="entryMetadata">entry.__metadata, passed as an argument simply to help minify the code.</param>

        var scheme = xml.attribute(atomCategory, "scheme");
        var term = xml.attribute(atomCategory, "term");

        if (scheme === odataScheme) {
            if (entry.__metadata.type) {
                throw { message: "Invalid AtomPub document: multiple category elements defining the entry type were encounterd withing an entry", element: atomCategory };
            }

            entryMetadata.type = term;
            entryMetadata.type_extensions = [];

            var typeExtension;
            xml.attributes(atomCategory, function (attribute) {
                if (!attribute.nsURI) {
                    if (attribute.localName !== "scheme" && attribute.localName !== "term") {
                        typeExtension = readAtomExtensionAttribute(attribute);
                        entryMetadata.type_extensions.push(typeExtension);
                    }
                } else if (isExtensionNs(attribute.nsURI)) {
                    typeExtension = readAtomExtensionAttribute(attribute);
                    entryMetadata.type_extensions.push(typeExtension);
                }
            });
        }
    };

    var readAtomEntryContent = function (atomEntryContent, entry) {
        /// <summary>Reads an ATOM content element.</summary>
        /// <param name="atomEntryContent">XML entry element.</param>
        /// <param name="entry">Entry object to update with information.</param>

        var src = xml.attribute(atomEntryContent, "src");
        var type = xml.attribute(atomEntryContent, "type");
        var entryMetadata = entry.__metadata;

        if (src) {
            if (!type) {
                throw { message: "Invalid AtomPub document: content element must specify the type attribute if the src attribute is also specified", element: atomEntryContent };
            }

            entryMetadata.media_src = normalizeURI(src, atomEntryContent.baseURI);
            entryMetadata.content_type = type;
        }

        xml.childElements(atomEntryContent, function (contentChild) {
            if (src) {
                throw { message: "Invalid AtomPub document: content element must not have child elements if the src attribute is specified", element: atomEntryContent };
            }

            if (contentChild.nsURI === odataMetaXmlNs && contentChild.localName === "properties") {
                readAtomEntryStructuralObject(contentChild, entry, entryMetadata);
            }
        });
    };

    var readAtomEntryEditMediaLink = function (link, entry, entryMetadata) {
        /// <summary>Reads an ATOM media link element.</summary>
        /// <param name="link">Link representation (not the XML element).</param>
        /// <param name="entry">Entry object to update with information.</param>
        /// <param name="entryMetadata">entry.__metadata, passed as an argument simply to help minify the code.</param>

        entryMetadata.edit_media = link.href;
        entryMetadata.edit_media_extensions = [];

        // Read the link extensions.
        var i, len;
        for (i = 0, len = link.extensions.length; i < len; i++) {
            if (link.extensions[i].namespaceURI === odataMetaXmlNs && link.extensions[i].name === "etag") {
                entryMetadata.media_etag = link.extensions[i].value;
            } else {
                entryMetadata.edit_media_extensions.push(link.extensions[i]);
            }
        }
    };

    var readAtomEntryLink = function (atomEntryLink, entry, metadata) {
        /// <summary>Reads a link element on an entry.</summary>
        /// <param name="atomEntryLink">'link' element on the entry.</param>
        /// <param name="entry">An object in payload representation format.</entry>
        /// <param name="metadata">Metadata that describes the conceptual schema.</param>

        var link = readAtomLink(atomEntryLink);
        var entryMetadata = entry.__metadata;
        switch (link.rel) {
            case "self":
                entryMetadata.self = link.href;
                entryMetadata.self_link_extensions = link.extensions;
                break;
            case "edit":
                entryMetadata.edit = link.href;
                entryMetadata.edit_link_extensions = link.extensions;
                break;
            case "edit-media":
                readAtomEntryEditMediaLink(link, entry, entryMetadata);
                break;
            default:
                if (link.rel.indexOf(odataRelatedPrefix) === 0) {
                    readAtomEntryDeferredProperty(atomEntryLink, link, entry, metadata);
                }
        }
    };

    var readAtomEntryDeferredProperty = function (atomEntryLink, link, entry, metadata) {
        /// <summary>Reads a potentially-deferred property on an entry.</summary>
        /// <param name="atomEntryLink">'link' element on the entry.</param>
        /// <param name="link">Parsed link object.</param>
        /// <param name="entry">An object in payload representation format.</entry>
        /// <param name="metadata">Metadata that describes the conceptual schema.</param>

        var propertyName = link.rel.substring(odataRelatedPrefix.length);

        // undefined is used as a flag that inline data was not found (as opposed to
        // found with a value or with null).
        var inlineData = undefined;

        // Get any inline data.
        xml.childElements(atomEntryLink, function (child) {
            if (child.nsURI === odataMetaXmlNs && child.localName === "inline") {
                var inlineRoot = xml.firstElement(child);
                if (inlineRoot) {
                    inlineData = readAtomDocument(inlineRoot, metadata);
                } else {
                    inlineData = null;
                }
            }
        });

        // If the link has no inline content, we consider it deferred.
        if (inlineData === undefined) {
            inlineData = { __deferred: { uri: link.href} };
        }

        // Set the property value on the entry object.
        entry[propertyName] = inlineData;

        // Set the extra property information on the entry object metadata. 
        entry.__metadata.properties = entry.__metadata.properties || {};
        entry.__metadata.properties[propertyName] = {
            extensions: link.extensions
        };
    };

    var readAtomEntryStructuralObject = function (propertiesElement, 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>

        xml.childElements(propertiesElement, function (property) {
            if (property.nsURI === odataXmlNs) {
                parentMetadata.properties = parentMetadata.properties || {};
                readAtomEntryProperty(property, parent, parentMetadata.properties);
            }
        });
    };

    var readAtomEntryProperty = function (property, parent, metadata) {
        /// <summary>Reads a property on an ATOM OData entry.</summary>
        /// <param name="property">XML element for the property.</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="metadata">Metadata for the object that will contain the property value.</param>

        var propertyNullValue = null;
        var propertyTypeValue = "Edm.String";
        var propertyExtensions = [];

        xml.attributes(property, function (attribute) {
            if (attribute.nsURI === odataMetaXmlNs) {
                switch (attribute.localName) {
                    case "null":
                        propertyNullValue = attribute.domNode.nodeValue;
                        return;
                    case "type":
                        propertyTypeValue = attribute.domNode.nodeValue;
                        return;
                };
            }

            if (isExtensionNs(attribute.nsURI)) {
                var extension = readAtomExtensionAttribute(attribute);
                propertyExtensions.push(extension);
            }
        });

        var propertyValue = null;
        var propertyMetadata = {
            type: propertyTypeValue,
            extensions: propertyExtensions
        };

        if (propertyNullValue !== "true") {
            propertyValue = xml.innerText(property.domNode);
            if (isPrimitiveType(propertyTypeValue)) {
                propertyValue = convertFromAtomPropertyText(propertyValue, propertyTypeValue);
            } else {
                // Probe for a complex type and read it.
                if (xml.firstElement(property)) {
                    propertyValue = { __metadata: { type: propertyTypeValue} };
                    readAtomEntryStructuralObject(property, propertyValue, propertyMetadata);
                }
            }
        }

        parent[property.localName] = propertyValue;
        metadata[property.localName] = propertyMetadata;
    };

    var readAtomServiceDocument = function (atomServiceDoc) {
        /// <summary>Reads an atom service document</summary>
        /// <param name="atomServiceDoc">An element node that represents the root service element of an AtomPub service document</param>
        /// <returns type="Object">An object that contains the properties of the service document</returns>

        // Consider handling Accept and Category elements.

        var serviceDoc = {
            workspaces: [],
            extensions: []
        };

        // Find all the workspace elements.
        xml.childElements(atomServiceDoc, function (child) {
            if (child.nsURI === appXmlNs && child.localName === "workspace") {
                var workspace = readAtomServiceDocumentWorkspace(child);
                serviceDoc.workspaces.push(workspace);
            } else {
                var serviceExtension = readAtomExtensionElement(atomServiceDoc);
                serviceDoc.extensions.push(serviceExtension);
            }
        });

        // 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 (serviceDoc.workspaces.length === 0) {
            throw { message: "Invalid AtomPub service document: No workspace element found.", element: atomServiceDoc };
        }

        return serviceDoc;
    };

    var readAtomServiceDocumentWorkspace = function (atomWorkspace) {
        /// <summary>Reads a single workspace element from an atom service document</summary>
        /// <param name="atomWorkspace">An element node that represents a workspace element of an AtomPub service document</param>
        /// <returns type="Object">An object that contains the properties of the workspace</returns>

        var workspace = {
            collections: [],
            extensions: []
        };

        xml.childElements(atomWorkspace, function (child) {
            if (child.nsURI === atomXmlNs) {
                if (child.localName === "title") {
                    if (atomWorkspace.title) {
                        throw { message: "Invalid AtomPub service document: workspace has more than one child title element", element: child };
                    }

                    workspace.title = xml.innerText(child.domNode);
                }
            } else if (child.nsURI === appXmlNs) {
                if (child.localName === "collection") {
                    var collection = readAtomServiceDocumentCollection(child, workspace);
                    workspace.collections.push(collection);
                }
            } else {
                var extension = readAtomExtensionElement(atomWorkspace);
                workspace.extensions.push(extension);
            }
        });

        workspace.title = workspace.title || "";

        return workspace;
    };

    var readAtomServiceDocumentCollection = function (atomCollection) {
        /// <summary>Reads a service document collection element into an object.</summary>
        /// <param name="atomCollection">An element node that represents a collection element of an AtomPub service document.</param>
        /// <returns type="Object">An object that contains the properties of the collection.</returns>

        var collection = {
            href: xml.attribute(atomCollection, "href"),
            extensions: []
        };

        if (!collection.href) {
            throw { message: "Invalid AtomPub service document: collection has no href attribute", element: atomCollection };
        }

        collection.href = normalizeURI(collection.href, atomCollection.baseURI);

        xml.childElements(atomCollection, function (child) {
            if (child.nsURI === atomXmlNs) {
                if (child.localName === "title") {
                    if (collection.title) {
                        throw { message: "Invalid AtomPub service document: collection has more than one child title element", element: child };
                    }

                    collection.title = xml.innerText(child.domNode);
                }
            } else if (child.nsURI !== appXmlNs) {
                var extension = readAtomExtensionElement(atomCollection);
                collection.extensions.push(extension);
            }
        });

        // 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 (!collection.title) {
            throw { message: "Invalid AtomPub service document: collection has no title element", element: atomCollection };
        }

        return collection;
    };

    var writeAtomDocument = function (data, context) {
        /// <summary>Writes the specified data into an OData ATOM document.</summary>
        /// <param name="data">Data to write.</param>
        /// <param name="context">Context used for serialization.</param>
        /// <returns>The root of the DOM tree built.</returns>

        var docRoot;
        var type = payloadTypeOf(data);
        switch (type) {
            case payloadType.feedOrLinks:
                docRoot = writeAtomFeed(null, data, context);
                break;
            case payloadType.entry:
                // FALLTHROUGH
            case payloadType.complexType:
                docRoot = writeAtomEntry(null, data, context);
                break;
        }

        return docRoot;
    };

    var writeAtomRoot = function (parent, name) {
        /// <summary>Writes the root of an ATOM document, possibly under an existing element.</summary>
        /// <param name="parent" mayBeNull="true">Element under which to create a new element.</param>
        /// <param name="name">Name for the new element, to be created in the ATOM namespace.</param>
        /// <returns>The created element.</returns>

        if (parent) {
            return xml.newElement(parent, name, atomXmlNs);
        }

        var result = xml.newDocument(name, atomXmlNs);

        // Add commonly used namespaces.
        // ATOM is implied by the just-created element.
        // xml.addNamespaceAttribute(result.domNode, "xmlns", atomXmlNs);
        xml.addNamespaceAttribute(result.domNode, "xmlns:d", odataXmlNs);
        xml.addNamespaceAttribute(result.domNode, "xmlns:m", odataMetaXmlNs);

        return result;
    };

    var writeAtomFeed = function (parent, data, context) {
        /// <summary>Writes the specified feed data into an OData ATOM feed.</summary>
        /// <param name="parent" mayBeNull="true">Parent to append feed tree to.</param>
        /// <param name="data">Feed data to write.</param>
        /// <param name="context">Context used for serialization.</param>
        /// <returns>The feed element of the DOM tree built.</returns>

        var feed = writeAtomRoot(parent, "feed");
        var entries = (isArray(data)) ? data : data.results;
        if (entries) {
            var i, len;
            for (i = 0, len = entries.length; i < len; i++) {
                writeAtomEntry(feed, entries[i], context);
            }
        }

        return feed;
    };

    var writeAtomEntry = function (parent, data, context) {
        /// <summary>Appends an ATOM entry XML payload to the parent node.</summary>
        /// <param name="parent">Parent element.</param>
        /// <param name="data">Data object to write in intermediate format.</param>
        /// <param name="context">Context used for serialization.</param>
        /// <returns>The new entry.</returns>

        var entry = writeAtomRoot(parent, "entry");

        // Set up a default empty author name as required by ATOM.
        var author = xml.newElement(entry, "author", atomXmlNs);
        xml.newElement(author, "name", atomXmlNs);

        // Set up a default empty title as required by ATOM.
        xml.newElement(entry, "title", atomXmlNs);

        var content = xml.newElement(entry, "content", atomXmlNs);
        xml.newAttribute(content, "type", null, "application/xml");

        var properties = xml.newElement(content, propertiesTag, odataMetaXmlNs);

        var propertiesMetadata = (data.__metadata) ? data.__metadata.properties : null;

        writeAtomEntryMetadata(entry, data.__metadata);
        writeAtomEntryProperties(entry, properties, data, propertiesMetadata, context);
        applyMetadataToEntry(entry, data, context);

        return entry;
    };

    var applyMetadataToEntry = function (entry, data, context) {
        /// <summary>Applies feed customizations to the specified entry element.</summary>
        /// <param name="entry">Entry to apply feed customizations to.</param>
        /// <param name="data">Data object associated with the entry.</param>
        /// <param name="context">Context used for serialization.</param>

        if (!data.__metadata) {
            return;
        }

        var metadata = context.metadata;
        var entityType = lookupEntityType(data.__metadata.type, metadata);
        while (entityType) {
            // Apply all feed customizations from the entity and each of its properties.
            var propertyType;
            var source = entityType.FC_SourcePath;
            if (source) {
                propertyType = lookupPropertyType(metadata, entityType, source);
                applyEntryCustomizationToEntry(entityType, source, entry, data, propertyType, "", context);
            }

            var properties = entityType.property;
            if (properties) {
                var i, len;
                for (i = 0, len = properties.length; i < len; i++) {
                    var property = properties[i];
                    var suffixCounter = 0;
                    var suffix = "";
                    while (property["FC_TargetPath" + suffix]) {
                        source = property.name;
                        if (property["FC_SourcePath" + suffix]) {
                            source += "/" + property["FC_SourcePath" + suffix];
                        }

                        applyEntryCustomizationToEntry(property, source, entry, data, property.type, suffix, context);
                        suffixCounter++;
                        suffix = "_" + suffixCounter;
                    }
                }
            }

            // Apply feed customizations from base types.
            entityType = lookupEntityType(entityType.baseType, metadata);
        }
    };

    var writeAtomEntryMetadata = function (entry, metadata) {
        /// <summary>Writes the content of metadata into the specified DOM entry element.</summary>
        /// <param name="entry">DOM entry element.</param>
        /// <param name="metadata" mayBeNull="true">Object __metadata to write.</param>

        if (metadata) {
            // Write the etag if present.
            if (metadata.etag) {
                xml.newAttribute(entry, "etag", odataMetaXmlNs, metadata.etag);
            }

            // Write the ID if present.
            if (metadata.uri) {
                xml.newElement(entry, "id", atomXmlNs, metadata.uri);
            }

            // Write the type name if present.
            if (metadata.type) {
                var category = xml.newElement(entry, "category", atomXmlNs);
                xml.newAttribute(category, "term", null, metadata.type);
                xml.newAttribute(category, "scheme", null, odataScheme);
            }
        }
    };

    var writeAtomEntryLink = function (entry, href, rel) {
        /// <summary>Writes an ATOM link into an entry.</summary>
        /// <param name="entry">DOM entry element to add link to.</param>
        /// <param name="href" type="String">Value for href attribute in link element.</param>
        /// <param name="rel" type="String">Value for rel attribute in link element</param>
        /// <returns>The new link element.</returns>

        var link = xml.newElement(entry, "link", atomXmlNs);
        xml.newAttribute(link, "rel", null, rel);
        xml.newAttribute(link, "href", null, href);
        return link;
    };

    var writeAtomEntryProperties = function (entry, parentElement, data, propertiesMetadata, context) {
        /// <summary>Writes the properties of an entry or complex type.</summary>
        /// <param name="entry" mayBeNull="true">Entry object being written out; null if this is a complex type.</param>
        /// <param name="parentElement">Parent DOM element under which the property should be added.</param>
        /// <param name="data">Data object to write in intermediate format.</param>
        /// <param name="propertiesMetadata" mayBeNull="true">Instance metadata about properties of the 'data' object.</param>
        /// <param name="context">Context used for serialization.</param>

        var name, value, kind, propertyMetadata;
        for (name in data) {
            if (name !== "__metadata") {
                value = data[name];
                kind = propertyKindOf(value);
                switch (kind) {
                    case propertyKind.primitive:
                    case propertyKind.complex:
                        propertyMetadata = (propertiesMetadata) ? propertiesMetadata[name] : null;
                        writeAtomEntryProperty(parentElement, name, kind, value, propertyMetadata, context);
                        break;
                    case propertyKind.deferred:
                    case propertyKind.inline:
                        writeAtomEntryDeferredProperty(entry, kind, name, value, context);
                        break;
                    case propertyKind.none:
                        // This could be a null primitive property or a null link.
                        if (propertyMetadata && propertyMetadata[name] && !isPrimitiveType(propertyMetadata[name])) {
                            // TODO: this is a null - should it be serialized?
                        } else {
                            // This is a a null primitive property.
                            writeAtomEntryProperty(parentElement, name, propertyKind.primitive, value, null, context);
                        }

                        break;
                }
            }
        }
    };

    var writeAtomEntryProperty = function (parentElement, name, kind, value, propertiesMetadata, context) {
        /// <summary>Writes a single property for an entry or complex type.</summary>
        /// <param name="parentElement">Parent DOM element under which the property should be added.</param>
        /// <param name="name" type="String">Property name.</param>
        /// <param name="kind" type="String">Property kind description (from propertyKind values).</param>
        /// <param name="value" mayBeNull="true">Property value.</param>
        /// <param name="propertiesMetadata" mayBeNull="true">Instance metadata about properties of the 'data' object.</param>
        /// <param name="context">Serialization context.</param>

        var propertyTagName = xml.qualifyXmlTagName(name, "d");
        var propertyType = propertiesMetadata && propertiesMetadata.type;
        var property;
        if (kind === propertyKind.complex) {
            property = xml.newElement(parentElement, propertyTagName, odataXmlNs);
            var propertyMetadata;
            if (propertiesMetadata) {
                propertyMetadata = propertiesMetadata.properties;
            }

            writeAtomEntryProperties(null, property, value, propertyMetadata, context);
        } else {
            // Default the conversion to string if no property type has been defined.
            property = xml.newElement(parentElement, propertyTagName, odataXmlNs, convertToAtomPropertyText(value, propertyType || "Edm.String"));
        }

        if (value === null) {
            xml.newAttribute(property, propertyNullAttribute, odataMetaXmlNs, "true");
        }

        if (propertyType) {
            xml.newAttribute(property, propertyTypeAttribute, odataMetaXmlNs, propertyType);
        }
    };

    var writeAtomEntryDeferredProperty = function (entry, kind, name, value, context) {
        /// <summary>Writes a single property for an entry or complex type.</summary>
        /// <param name="entry">Entry object being written out.</param>
        /// <param name="name" type="String">Property name.</param>
        /// <param name="kind" type="String">Property kind description (from propertyKind values).</param>
        /// <param name="value" mayBeNull="true">Property value.</param>
        /// <param name="context">Serialization context.</param>
        /// <remarks>entry cannot be null because that would indicate a complex type, which don't support links.</remarks>

        var href;
        var inlineWriter;
        var inlineType;
        if (kind === propertyKind.inline) {
            href = (value.__metadata) ? value.__metadata.uri : "";
            inlineType = payloadTypeOf(value);
            switch (inlineType) {
                case payloadType.entry:
                    inlineWriter = writeAtomEntry;
                    break;
                case payloadType.feedOrLinks:
                    inlineType = "feed";
                    inlineWriter = writeAtomFeed;
                    break;
                default:
                    throw { message: "Invalid payload for inline navigation property: " + inlineType };
            }
        } else {
            href = value.__deferred.uri;
        }

        var rel = normalizeURI(name, odataRelatedPrefix);
        var link = writeAtomEntryLink(entry, href, rel);
        if (inlineWriter) {
            var inlineRoot = xml.newElement(link, inlineTag, odataMetaXmlNs);
            xml.newAttribute(link, "type", null, "application/atom+xml;type=" + inlineType);
            inlineWriter(inlineRoot, value, context);
        }
    };

    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 atomRoot = xml.parse(text);
            if (atomRoot) {
                return readAtomDocument(atomRoot, 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);
        var result = undefined;
        if (cType && cType.mediaType === atomMediaType) {
            var atomDoc = writeAtomDocument(data, context);
            result = xml.serialize(atomDoc);
        }

        return result;
    };

    odata.atomHandler = handler(atomParser, atomSerializer, atomAcceptTypes.join(","), "2.0");



    // 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: {
            Association: {
                attributes: ["Name"],
                elements: ["End*", "ReferentialConstraint"]
            },
            AssociationSet: {
                attributes: ["Name", "Association"],
                elements: ["End*"]
            },
            CollectionType: {
                attributes: ["ElementType", "Nullable", "DefaultValue", "MaxLength", "FixedLength", "Precision", "Scale", "Unicode", "Collation"]
            },
            ComplexType: {
                attributes: ["Name", "BaseType", "Abstract"],
                elements: ["Property*"]
            },
            DefiningExpression: {
                text: true
            },
            Dependent: {
                attributes: ["Role"],
                elements: ["PropertyRef*"]
            },
            Documentation: {
                text: true
            },
            End: {
                attributes: ["Type", "Role", "Multiplicity", "EntitySet"],
                elements: ["OnDelete"]
            },
            EntityContainer: {
                attributes: ["Name", "Extends"],
                elements: ["EntitySet*", "AssociationSet*", "FunctionImport*"]
            },
            EntitySet: {
                attributes: ["Name", "EntityType"]
            },
            EntityType: {
                attributes: ["Name", "BaseType", "Abstract", "OpenType"],
                elements: ["Key", "Property*", "NavigationProperty*"]
            },
            Function: {
                attributes: ["Name", "ReturnType"],
                elements: ["Parameter*", "DefiningExpression", "ReturnType"]
            },
            FunctionImport: {
                attributes: ["Name", "ReturnType", "EntitySet"],
                elements: ["Parameter*"]
            },
            Key: {
                elements: ["PropertyRef*"]
            },
            NavigationProperty: {
                attributes: ["Name", "Relationship", "ToRole", "FromRole"]
            },
            OnDelete: {
                attributes: ["Action"]
            },
            Parameter: {
                attributes: ["Name", "Type", "Mode", "MaxLength", "Precision", "Scale"]
            },
            Principal: {
                attributes: ["Role"],
                elements: ["PropertyRef*"]
            },
            Property: {
                attributes: ["Name", "Type", "Nullable", "DefaultValue", "MaxLength", "FixedLength", "Precision", "Scale", "Unicode", "Collation", "ConcurrencyMode"]
            },
            PropertyRef: {
                attributes: ["Name"]
            },
            ReferenceType: {
                attributes: ["Type"]
            },
            ReferentialConstraint: {
                elements: ["Principal", "Dependent"]
            },
            ReturnType: {
                attributes: ["ReturnType"],
                elements: ["CollectionType", "ReferenceType", "RowType"]
            },
            RowType: {
                elements: ["Property*"]
            },
            Schema: {
                attributes: ["Namespace", "Alias"],
                elements: ["Using*", "EntityContainer*", "EntityType*", "Association*", "ComplexType*", "Function*"]
            },
            TypeRef: {
                attributes: ["Type", "Nullable", "DefaultValue", "MaxLength", "FixedLength", "Precision", "Scale", "Unicode", "Collation"]
            },
            Using: {
                attributes: ["Namespace", "Alias"]
            }
        }
    };

    // 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.EntityType.attributes.push("m:HasStream");
    schema.elements.DataServices.attributes = ["m:DataServiceVersion"];

    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;
    };

    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>

        if (!element.domNode) {
            element = xml._wrapNode(element, "");
        }

        var localName = element.localName;
        var elementSchema = schema.elements[localName];
        if (!elementSchema) {
            return null;
        }

        if (elementSchema.ns) {
            if (element.nsURI !== elementSchema.ns) {
                return null;
            }
        } else if (element.nsURI !== edmNs && element.nsURI !== edmNs2 && element.nsURI !== edmNs3) {
            return null;
        }

        var item = {};
        var attributes = elementSchema.attributes;
        if (attributes) {
            var i, len;
            for (i = 0, len = attributes.length; i < len; i++) {
                // TODO: support namespaces in attributes
                var propertyName = attributes[i];
                var colonIndex = propertyName.indexOf(":");
                var attributeNs = "";
                if (colonIndex > 0) {
                    // Currently, only m: for metadata is supported as a prefix in the schema.
                    // attributeNs = propertyName.substr(0, colonIndex);
                    attributeNs = odataMetaXmlNs;
                    propertyName = propertyName.substr(colonIndex + 1);
                }

                var attribute = xml.attribute(element, propertyName, attributeNs);
                if (attribute) {
                    item[scriptCase(propertyName)] = attribute;

                    // Feed customizations for complex types have additional
                    // attributes with a suffixed counter starting at '1'.
                    var suffixCounter = 1;
                    while (1) {
                        var suffixedPropertyName = propertyName + "_" + suffixCounter;
                        attribute = xml.attribute(element, suffixedPropertyName, attributeNs);
                        if (!attribute) {
                            break;
                        }

                        item[scriptCase(suffixedPropertyName)] = attribute;
                        suffixCounter++;
                    }
                }
            }
        }

        xml.childElements(element, function (child) {
            var childSchema = getChildSchema(elementSchema, child.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);
                }
            }
        });

        if (elementSchema.text) {
            item.text = xml.innerText(element);
        }

        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 = xml.parse(text);
        return parseConceptualModelElement(doc) || undefined;
    };

    odata.metadataHandler = handler(metadataParser, null, xmlMediaType, "1.0");



    var jsonMediaType = "application/json";

    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 traverseInternal = 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 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, callback);
                result = callback(name, result);
                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 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 metadata = context.metadata;

        var json = (typeof text === "string") ? window.JSON.parse(text) : text;
        json = traverse(json, function (key, value) {
            if (value && typeof value === "object") {
                var dataTypeName = value.__metadata && value.__metadata.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];
                            if (property.type === "Edm.DateTime" || property.type === "Edm.DateTimeOffset") {
                                propertyValue = value[property.name];
                                if (propertyValue) {
                                    propertyValue = parseJsonDateString(propertyValue);
                                    if (!propertyValue) {
                                        throw { message: "Invalid date/time value" };
                                    }

                                    value[property.name] = propertyValue;
                                }
                            }
                        }
                    }
                } else if (handler.recognizeDates) {
                    for (var name in value) {
                        propertyValue = value[name];
                        if (typeof propertyValue === "string") {
                            value[name] = parseJsonDateString(propertyValue) || propertyValue;
                        }
                    }
                }
            }

            return value;
        }).d;

        json = jsonUpdateDataFromVersion(json, context.dataServiceVersion);
        json = jsonNormalizeData(json, context.response.requestUri);
        return json;
    };

    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 result = undefined;
        var cType = context.contentType = context.contentType || contentType(jsonMediaType);
        if (cType && cType.mediaType === jsonMediaType) {
            var version = context.dataServiceVersion;
            var json = data;

            if (version && version !== "1.0") {
                json = { results: [data] };
            }

            // 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(json);
            }
            finally {
                // Restore the original toJSON function
                Date.prototype.toJSON = dateToJSON;
            }
        }
        return result;
    };

    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>

        if (payloadTypeOf(data) === payloadType.svcDoc) {
            return normalizeServiceDocument(data, baseURI);
        } else {
            return 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) {
            // Try to detect whether this is an array, in which case it
            // should probably be a feed structure - indicates V1 behavior.
            if (isArray(data)) {
                dataVersion = "1.0";
            }
        }

        // If the data is in the latest version, there is nothing to update.
        if (dataVersion === "2.0") {
            return data;
        }

        if (dataVersion === "1.0") {
            if (isArray(data)) {
                data = { results: data };
            }
        }

        return data;
    };

    odata.jsonHandler = handler(jsonParser, jsonSerializer, jsonMediaType, "2.0");
    odata.jsonHandler.recognizeDates = false;


    var batchMediaType = "multipart/mixed";
    var responseStatusRegex = /^HTTP\/1\.\d (\d{3}) (.*)$/i;

    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"]);

            if (partContentType && partContentType.mediaType === batchMediaType) {
                context.boundaries.push(partContentType.properties["boundary"]);
                responses.push({ __changeResponses: readBatch(text, context) });
                context.boundaries.pop();
                readTo(text, context, "--" + currentBoundary(context));
            } else {
                if (!partContentType || partContentType.mediaType !== "application/http") {
                    throw { message: "invalid MIME part type " };
                }

                // 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 start;
        var line;
        while ((line = readLine(text, context))) {
            start = line.indexOf(":");
            if (start !== -1) {
                headers[trimString(line.substring(0, start))] = trimString(line.substring(start + 1));
            }
        }

        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 match = responseStatusRegex.exec(readLine(text, context));
        if (!match) {
            throw { message: "Invalid HTTP response" };
        }

        // Build the response object.
        return {
            statusCode: match[1],
            statusText: match[2],
            headers: readHeaders(text, context),
            body: readTo(text, context, 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" 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 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>

        var type = payloadTypeOf(data);
        if (type !== payloadType.batch) {
            throw { message: "Serialization of batches of type \"" + type + "\" is not supported" };
        }

        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";
            prepareRequest(part, partHandler(context), { metadata: context.metadata });
            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, "1.0");



    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(data.toString());
    };

    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);
        },

        accept: "application/atomsvc+xml;q=0.8, 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="url" type="String">URL to read data from.</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 response.</param>
        /// <param name="httpClient">HTTP client layer.</param>
        /// <param name="metadata">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">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="metadata">Conceptual metadata for this request.</param>

        if (!success) {
            success = odata.defaultSuccess;
        }

        if (!error) {
            error = odata.defaultError;
        }

        if (!handler) {
            handler = odata.defaultHandler;
        }

        if (!httpClient) {
            httpClient = odata.defaultHttpClient;
        }

        if (!metadata) {
            metadata = odata.defaultMetadata;
        }

        var context = { metadata: metadata };

        try {
            prepareRequest(request, handler, context);
            return invokeRequest(request, success, error, handler, httpClient, context);
        } catch (err) {
            error(err);
        }
    };

    // Configure the batch handler to use the default handler for the batch parts.
    odata.batchHandler.partHandler = odata.defaultHandler;



    var localStorage = window.localStorage;

    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.</return>
        /// <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>Converts an object representing a Date in a JSON stream to a Date object</summary>
        /// <returns type="Date">Date object.</return>
        /// <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.</return>

        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</return>

        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</return>

        if (DomStore.isSupported()) {
            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 localStorage && true;
    };

    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) {
                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 destroy 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;

        if (key instanceof Array) {
            error({ message: "Array of keys not supported" });
        } else {
            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, function (k, v) {
                        // Date values are a special case, and along with arrays, are
                        // the only supported objects.
                        if (v && typeof v === "object" && v.t) {
                            v = domStoreJSONToDate(v);
                        }
                        return v;
                    });
                }
                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 mozIndexedDB = window.mozIndexedDB;

    var IDBTransaction = window.IDBTransaction;
    var IDBKeyRange = window.IDBKeyRange;

    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) {
            if (error) {
                error(e);
            } else if (defaultError) {
                defaultError(e);
            }
        };
    };

    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 name = store.name;
        var db = store.db;
        var errorCallback = getError(error, store.defaultError);

        if (db) {
            success(db.transaction(name, mode));
        } else {
            var request = mozIndexedDB.open("_datajs_" + name);
            request.onsuccess = function (event) {
                db = store.db = event.target.result;
                if (!db.objectStoreNames.contains(name)) {
                    var versionRequest = db.setVersion("1.0");
                    versionRequest.onsuccess = function () {
                        db.createObjectStore(name, null, false);
                        success(db.transaction(name, mode));
                    };
                    versionRequest.onerror = errorCallback;
                    versionRequest.onblocked = errorCallback;
                } else {
                    success(db.transaction(name, mode));
                }
            };
            request.onerror = getError(error, this.defaultError);
        }
    };

    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</return>
        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</return>
        if (IndexedDBStore.isSupported()) {
            return new IndexedDBStore(name);
        } else {
            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</return>
        return true && mozIndexedDB;
    };

    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, IDBTransaction.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++) {
                transaction.objectStore(name).add(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, IDBTransaction.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++) {
                transaction.objectStore(name).put(values[i], 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, IDBTransaction.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 tdo 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, IDBTransaction.READ_ONLY, function (transaction) {
            var request = transaction.objectStore(name).openCursor(IDBKeyRange.only(key));
            transaction.oncomplete = function () {
                success(request.result !== undefined);
            };
            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, IDBTransaction.READ_ONLY, 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.page = function (size, success, error) {
        /// <summary>Creates a page with the specified size</summary>
        /// <param name="size" type="Integer">The page size</param>
        /// <param name="success" type="Function">The success callback</param>
        /// <param name="error" type="Function">The error callback</param>
        return new IndexedDBStorePage(this, size, success, error);
    }

    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, IDBTransaction.READ_WRITE, function (transaction) {
            var values = [];

            transaction.onerror = getError(error, defaultError);
            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) {
                    values.push(event.target.result);
                };
            }
        }, error);
    };

    IndexedDBStore.prototype.remove = function (key, success, error) {
        /// <summary>Removes the specified 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, IDBTransaction.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, IDBTransaction.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]));
                request.pair = { key: keys[i], value: values[i] };
                request.onsuccess = function (event) {
                    var cursor = event.target.result;
                    if (cursor) {
                        cursor.update(event.target.pair.value);
                    } else {
                        transaction.abort();
                    }
                };
            }
        }, error);
    };

    var IndexedDBStorePage = function (store, size, success, error) {
        /// <summary>Creates a page with the specified size</summary>
        /// <param name="store" type="Object">The store to be paged over</param>
        /// <param name="size" type="Integer">The page size</param>
        /// <param name="success" type="Function">The success callback</param>
        /// <param name="error" type="Function">The error callback</param>
        var that = this;

        this.keys = [];
        this.values = [];

        this.next = function (success, error) {
            /// <summary>Move to the next page</summary>
            /// <param name="success" type="Function">The success callback</param>
            /// <param name="error" type="Function">The error callback</param>
            var lastKey = that.keys.length > 0 ? that.keys[that.keys.length - 1] : undefined;
            that.keys = []
            that.values = [];
            openTransaction(store, IDBTransaction.READ_ONLY, function (transaction) {
                var storeName = store.name;
                var request = transaction.objectStore(storeName).openCursor(lastKey ? IDBKeyRange.lowerBound(lastKey, true) : undefined);
                request.onsuccess = function (event) {
                    var cursor = event.target.result;
                    if (cursor) {
                        that.keys.push(cursor.key);
                        that.values.push(cursor.value);

                        if (that.keys.length < size) {
                            cursor["continue"].call(cursor);
                        }
                    }
                };
                request.onerror = getError(error, store.defaultError);
                transaction.oncomplete = function () {
                    success(that);
                };
            }, error);
        };

        this.next(success, 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 destroy 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 });
                }
            }
        };

        // TODO: Add iterator support. It is possible to pass the sort criteria to the iterator and have it handle the index creation and stuff. 
    };

    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</return>
        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.</summary>
        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";
    


    // Array that stores the mechanism objects. The array is assumed to be sorted by relevance of the mechanisms.
    var mechanisms = [
        { name: "indexeddb", factory: IndexedDBStore },
        { name: "dom", factory: DomStore },
        { name: "memory", factory: MemoryStore }
    ];

    datajs.defaultStoreMechanism = "best";

    datajs.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>

        var result;

        if (mechanism === undefined) {
            mechanism = datajs.defaultStoreMechanism;
        }

        var i, len;
        for (i = 0, len = mechanisms.length; i < len && !result; i++) {
            var factory = mechanisms[i].factory;
            if ((mechanism === "best" || mechanism === mechanisms[i].name) && factory.isSupported()) {
                result = factory.create(name);
            }
        }

        if (!result) {
            throw { message: "Failed to create store", name: name, mechanism: mechanism };
        }

        return result;
    };



    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 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.</params>
        /// <param name="y" type="Object">Range with (i)ndex and (c)ount members.</params>
        /// <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 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 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 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>
    }

    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();

            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;

                delete this._done;
            }
        },

        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);
                }

                delete this._fail;
            }
        },

        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 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 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 };
    };

    var DataCacheOperation = function (index, count, promise, canceled, pending, isPrefetch) {
        /// <summary>Creates a new operation object.</summary
        /// <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="p" type="DjsDeferred">Promise for requested values.</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="isPrefetch" type="Boolean">Whether this is a prefetch operation.</field>

        this.i = index;
        this.c = count;
        this.d = (isPrefetch) ? null : [];
        this.p = promise;
        this.canceled = canceled;
        this.pending = pending;
        this.isPrefetch = isPrefetch;
    };

    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.fireCanceled = function () {
        /// <summary>Fires a canceled notification as necessary.</summary>

        // Fire the rejection just once.
        var p = this.p;
        if (p) {
            this.p = null;
            p.reject({ canceled: true, message: "Operation canceled" });
        }
    };

    var DataCache = function (options, store) {
        /// <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 and initial prefetch and local-data handler.
        /// </param>
        /// <param name="store">Store object in which the cache will persist the data</param>
        /// <returns type="DataCache">A new data cache instance.</returns>


        // var cacheSize = undefinedDefault(options.cacheSize, 5000);

        var cacheStore = store;
        var cacheStoreReady = false;

        var actualCacheSize = 0;
        var allDataLocal = false;
        var collectionCount = 0;
        var lastPageIndex = 0;
        var lastTick = 0;
        var pageSize = undefinedDefault(options.pageSize, 50);
        var prefetchSize = undefinedDefault(options.prefetchSize, this._pageSize);
        var source = normalizeURI(options.source);

        var readOperations = [];
        var prefetchOperations = [];
        var currentOperation, currentOperationSource;
        var stats = { counts: 0, netReads: 0, prefetches: 0, cacheReads: 0 };
        var that = this;

        var destroyOperation;

        that.onprefetch = options.prefetch;
        that.onlocaldata = options.localdata;
        that.stats = stats;

        that.count = function () {
            /// <summary>Counts the number of items in the collection.</summary>
            /// <returns type="DjsDeferred">A promise with the number of items.</returns>
            var deferred = createDeferred();
            var canceled;
            var operation = odata.read(appendSegment(source, "$count"), function (data) {
                stats.counts++;
                operation = null;
                deferred.resolve(parseInt10(data.toString()));
            }, function (err) {
                operation = null;
                deferred.reject(extend(err, { canceled: canceled }));
            });

            return extend(deferred.promise(), {
                cancel: function () {
                    /// <summary>Aborts the count operation.</summary>
                    if (operation) {
                        canceled = true;
                        operation.abort();
                        operation = null;
                    }
                }
            });
        };

        that.destroy = function () {
            /// <summary>Destroys all local data associated with this cache.</summary>
            /// <returns type="DjsDeferred">A promise with no value.</returns>

            var deferred = createDeferred();

            if (!cacheStoreReady) {
                destroyOperation = deferred;
            } else {
                destroyCacheStore(deferred);
            }
            // TODO: reject all pending promises.

            return deferred.promise();
        };

        that.invalidate = function (/* item */) {
            /// <summary>Invalidates an item in the cache.</summary>
            /// <param name="item">Item to invalidate.</param>
            /// <returns type="DjsDeferred">A promise with no value.</returns>

            var deferred = createDeferred();
            return deferred.promise();
        };

        that.invalidateAll = function () {
            /// <summary>Invalidates all data in the cache so it's re-fetched the next time it's needed.</summary>
            /// <returns type="DjsDeferred">A promise with no value.</returns>

            var deferred = createDeferred();
            return deferred.promise();
        };


        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>
            /// <returns type="DjsDeferred">
            /// A promise for an array of records; less records may be returned if the
            /// end of the collection is found.
            /// </returns>

            index = parseInt10(index);
            count = parseInt10(count);

            if (index < 0 || isNaN(index)) {
                throw { message: "Invalid index", index: index };
            }
            if (count < 0 || isNaN(count)) {
                throw { message: "Invalid count", count: count };
            }

            var result = createDeferred();

            // Create a new operation and try to resolve it purely from the
            // cache, while other operations may be queued up to use the network.
            var operation = new DataCacheOperation(index, count, result, false, 0, false);
            processOperation(operation);


            return extend(result.promise(), {
                cancel: function () {
                    /// <summary>Aborts the readRange operation.</summary>
                    if (removeFromArray(readOperations, operation)) {
                        operation.canceled = true;
                    }
                }
            });
        };

        var coversLast = function (range) {
            /// <summary>Checks whether the specified range covers the last element of the collection.</summary>
            /// <param name="range" type="Object">Range with (i)ndex and (c)ount.</param>
            /// <returns type="Boolean">
            /// true if the range is known to cover the last element; false otherwise.
            /// </returns>
            /// <remarks>Always returns 'false' if the end of the collection is not known.</remarks>

            return collectionCount && ((range.i + range.c) >= collectionCount);
        };

        var appendPageData = 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 cancelOperation = function (operation, operationSource) {
            /// <summary>Cancels the specified operation from the given source.</summary>
            /// <param name="operation" type="DataCacheOperation">Operation to cancel.</param>
            /// <param name="operationSource" type="Array">Source for the operation.</param>

            dequeueOperation(operation, operationSource);
            operation.fireCanceled();
            if (!currentOperation) {
                processOperation();
            }
        };

        var destroyCacheStore = function (deferred) {
            /// <summary>Destroys the data in the cacheStore and closes it.</summary>
            /// <param name="deferred" type="DjsDeferred"> 
            /// Deferred object. If jQuery is installed, then a jQuery
            /// Deferred object is returned, which provides a superset of features.</param>

            cacheStore.clear(function () {
                cacheStore.close();
                deferred.resolve();
            }, function (error) {
                deferred.reject(error);
            });
        };

        var dequeueOperation = function (operation, operationSource) {
            /// <summary>Dequeues the specified operation from the given source.</summary>
            /// <param name="operation" type="DataCacheOperation">Operation to dequeue.</param>
            /// <param name="operationSource" type="Array" mayBeNull="true">Source for the operation.</param>

            if (currentOperation === operation) {
                currentOperation = null;
                currentOperationSource = null;
            }

            if (operationSource) {
                removeFromArray(operationSource, operation);
            }
        }

        var processOperation = function (operation) {
            /// <summary>Process a pending operation from the queue.</summary>
            /// <param name="operation" type="DataCacheOperation" optional="true">Read operation to be processed immediately.</param>
            /// <remarks>
            /// How operations are processed:
            ///
            /// There are two queues: one for read operations (high priority) and one for 
            /// prefetch operations (low priority).
            /// 
            /// When a read operation is requested, a new operation is created and an attempt
            /// is made to satisfy it directly from the store. If network access is needed,
            /// it is added to the read queue.
            /// 
            /// Operation processing works by getting the highest priority item (first item 
            /// from the read queue, otherwise first item from the prefetch queue) and trying 
            /// to satisfy it from the cache. If the operation to be satisfied is from a read
            /// operation, we queue a prefetch operation and assemble the results; if it is a
            /// prefetch operation, we continue with the remainder in page-sized chunks as
            /// necessary. A satisfied operation is removed from its queue. If it cannot be 
            /// satisfied from the cache, a 'get page' operation from the server is issued. 
            /// 
            /// Once the 'get page' is done, we merge the results into the cache and again 
            /// to process an operation. Presumably the same operation is picked, this time 
            /// it's satisfied, and results are returned.
            /// </remarks>

            // Read the most important operation to work on next.
            var operationSource;
            if (!operation) {
                if (!currentOperation) {
                    if (readOperations.length) {
                        currentOperationSource = readOperations;
                    } else if (prefetchOperations.length) {
                        currentOperationSource = prefetchOperations;
                    } else {
                        currentOperation = currentOperationSource = null;
                        return;
                    }
                    currentOperation = currentOperationSource[0];
                }

                operationSource = currentOperationSource;
                operation = currentOperation;
            } else if (!cacheStoreReady) {
                readOperations.push(operation);
                return;
            }

            if (operation.canceled) {
                cancelOperation(operation, operationSource);
                return;
            }

            var range = snapToPageBoundaries(operation.i, operation.i + operation.c, pageSize);
            var cacheStoreError = function (err) {
                readPageError(err, operation, operationSource);
            };

            cacheStore.contains(range.i, function (contained) {
                if (operation.canceled) {
                    cancelOperation(operation, operationSource);
                    return;
                }

                if (contained) {
                    cacheStore.read(range.i, function (key, page) {
                        if (operation.canceled) {
                            cancelOperation(operation, operationSource);
                            return;
                        }
                        if (page !== undefined) {
                            readCachePage(page, operation, operationSource);
                            processOperation((!operationSource) ? operation : undefined);
                        } else {
                            readPageFromNetwork(range.i, operation, operationSource);
                        }
                    }, cacheStoreError);
                } else {
                    // We cannot fulfill our promise because the page is not in the cacheStore.
                    if (operationSource) {
                        readPageFromNetwork(range.i, operation, operationSource);
                    } else {
                        readOperations.push(operation);
                        processOperation();
                    }
                }
            }, cacheStoreError);
        };

        var readPageError = function (err, operation, operationSource) {
            /// <summary>Error handler for reading pages from the cache store or the network.</summary>
            /// <param name="err" type="Object">Error object.</param>
            /// <param name="operation" type="Object">Operation with (i)ndex, (c)ount and (d)ata.</param>
            /// <param name="operationSource" type="Array">Array of operation objects.</param>

            dequeueOperation(operation, operationSource);

            if (operation.p) {
                operation.p.reject(err);
                operation.p = null;
            }

            if (operation.isPrefetch && that.onprefetch) {
                that.onprefetch(err);
            }

            if (!currentOperation) {
                processOperation();
            }
        };

        var readCachePage = function (page, operation, operationSource) {
            /// <summary>Reads a cache page.</summary>
            /// <param name="page" type="Object" maybeNull="false">Page with (i)ndex, (c)ount and (d)ata.</param>
            /// <param name="operation" type="Object">Operation with (i)ndex, (c)ount and (d)ata.</param>
            /// <param name="operationSource" type="Array">Array of operation objects.</param>

            var isPrefetch = operation.isPrefetch;
            lastPageIndex = Math.max(lastPageIndex, page.i);

            if (actualCacheSize <= lastPageIndex) {
                actualCacheSize = lastPageIndex + page.d.length;
            }

            if (!isPrefetch) {
                appendPageData(operation, page);
                if (operation.d.length === operation.c || operation.d.length === collectionCount) {

                    stats.cacheReads++;
                    dequeueOperation(operation, operationSource);
                    operation.fireResolved();

                    if (!allDataLocal && !coversLast(operation)) {
                        if ((prefetchSize < 0 && prefetchOperations.length === 0) || prefetchSize > 0) {
                            prefetchOperations.push(
                                new DataCacheOperation(operation.i + operation.c, pageSize, null, false, prefetchSize, true));
                        }
                    }
                } else {
                    operation.i = page.i + pageSize;
                }
            } else {
                var keepGoing = operation.pending < 0;
                if (!coversLast(operation) && (keepGoing || operation.pending > operation.c)) {
                    prefetchOperations.push(
                                new DataCacheOperation(operation.i + operation.c, pageSize, null, false, (keepGoing) ? -1 : (operation.pending - operation.c), true));
                } else {
                    // Did we ever prefetched something?
                    if (that.onprefetch && operationSource.length === 1) {
                        that.onprefetch();
                    }
                }
                dequeueOperation(operation, operationSource);
            }
        };

        var readPageFromNetwork = function (index, operation, operationSource) {
            /// <summary>Reads a data cache page from the network.</summary>
            /// <param name="index" type="Integer">Requested page index.</param>
            /// <param name="operation" type="Object">Operation with (i)ndex, (c)ount and (d)ata.</param>
            /// <param name="operationSource" type="Array">Array of operation objects.</param>
            var isPrefetch = operation.isPrefetch;

            queryForPage({ i: index, c: pageSize }, function (_, page) {
                if (page) {
                    if (isPrefetch) {
                        stats.prefetches++;
                    } else {
                        stats.netReads++;
                    }
                    readCachePage(page, operation, operationSource);
                } else {
                    dequeueOperation(operation, operationSource);

                    if (!isPrefetch) {
                        stats.cacheReads++;
                        if (operation.canceled) {
                            cancelOperation(operation, operationSource);
                            return;
                        }
                        operation.fireResolved();
                    } else {
                        if (that.onprefetch && operationSource.length === 0) {
                            that.onprefetch();
                        }
                    }
                }
                processOperation();
            }, function (err) {
                readPageError(err, operation, operationSource);
            });
        };

        var queryForPage = function (range, success, error) {
            /// <summary>Get a single page from the server and insert it into the cache.</summary>
            /// <param name="range" type="Object">Range describing items to get.</param>
            /// <param name="success" type="Function">Success callback.</param>
            /// <param name="error" type="Function">Error callback.</param>

            var index = range.i;
            var count = range.c;
            var queryOptions = "$skip=" + index + "&$top=" + count;
            var uri = appendQueryOption(source, queryOptions);

            var pageData;
            var pageDataLength;

            var queryForPageData = function (uri, done) {
                odata.read(uri, function (data) {
                    if (!pageData) {
                        pageData = data.results;
                        pageDataLength = pageData.length;
                    } else {
                        // Merge the elements in the page data
                        var newData = data.results;
                        var newDataLength = newData.length;
                        var i, len;
                        for (i = 0, len = newDataLength; i < len; i++) {
                            pageData.splice(pageDataLength, 0, newData[i]);
                        }
                        pageDataLength += newDataLength;
                    }

                    var next = data.__next;
                    if (next) {
                        queryForPageData(next, done);
                    } else {
                        done(pageData);
                    }
                }, error);
            };

            queryForPageData(uri, function (data) {
                savePageToCacheStore(range, data, success, error);
            });
        };

        var saveSettingsToCacheStore = function (success, error) {
            /// <summary>Saves the cache settings to the cache storage.</summary>
            /// <param name="success" type="Function">Success callback.</param>
            /// <param name="error" type="Function">Error callback.</param>

            var settings = {
                actualCacheSize: actualCacheSize,
                allDataLocal: allDataLocal,
                lastPageIndex: lastPageIndex,
                pageSize: pageSize,
                source: source
            };

            cacheStore.addOrUpdate("settings", settings, success, error);
        };

        var savePageToCacheStore = function (range, data, success, error) {
            /// <summary>Saves a page to the cache storage.</summary>
            /// <param name="range" type="Object">Range describing the page start (i)ndex and element (c)ount.</param>
            /// <param name="data" type="Array">Page data.</param>
            /// <param name="success" type="Function">Success callback.</param>
            /// <param name="error" type="Function">Error callback.</param>

            var index = range.i;
            var count = range.c;
            var dataLength = data.length;

            // Update the cache and return the results.
            // TODO: coalesce, overwrite, evict (possibly detect shifts?)

            if (dataLength > 0) {
                actualCacheSize += dataLength;
                var cachePage = { i: index, c: dataLength, d: data, t: lastTick++ };
                cacheStore.addOrUpdate(index, cachePage, success, error);
                saveSettingsToCacheStore(function () { }, error);
            } else {
                delay(success);
            }

            var oldCollectionCount = collectionCount;
            // Detect the end of the collection.
            if (dataLength < count) {
                if (dataLength) {
                    collectionCount = index + dataLength;
                } else {
                    // We've found the end if we have an item immediately prior to the
                    // one we've just indexed.
                    if (lastPageIndex) {
                        var snappedPage = snapToPageBoundaries(index, index + count, pageSize);
                        if (lastPageIndex === snappedPage.i - count) {
                            collectionCount = actualCacheSize;
                        }
                    }
                }
            }

            // We just added data and have a full cache, so fire the local data event if the collection has changed at all.
            if ((collectionCount - oldCollectionCount) > 0 && actualCacheSize === collectionCount) {
                allDataLocal = true;
                if (that.onlocaldata) {
                    saveSettingsToCacheStore(function () { }, error);
                    that.onlocaldata();
                }
            }
        };

        var initializationDone = function () {
            /// <summary>Signals that the cache storage has been initialized properly and starts processing any pending read operations.</summary>
            cacheStoreReady = true;
            if (allDataLocal && that.onlocaldata) {
                that.onlocaldata();
            }
            processOperation();
        };

        // Initialize the cacheStore
        cacheStore.read("settings", function (key, settings) {
            // We have a pending destroy operation?
            if (destroyOperation) {
                destroyCacheStore(destroyOperation);
                destroyOperation.then(function () {
                    destroyOperation = null;
                    initializationDone();
                }, throwErrorCallback);
                return;
            }

            if (assigned(settings)) {
                if (pageSize !== settings.pageSize || source !== settings.source) {
                    cacheStore.clear(function () {
                        saveSettingsToCacheStore(initializationDone, throwErrorCallback);
                    }, throwErrorCallback);
                } else {
                    actualCacheSize = settings.actualCacheSize;
                    allDataLocal = settings.allDataLocal;
                    lastPageIndex = settings.lastPafgeInde;
                    initializationDone();
                }
            } else {
                saveSettingsToCacheStore(initializationDone, throwErrorCallback);
            }
        }, throwErrorCallback);

        return that;
    };

    datajs.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 and initial prefetch handler.
        /// </param>
        /// <returns type="DataCache">A new data cache instance.</returns>
        var pageSize = (options.pageSize !== undefined) ? parseInt10(options.pageSize) : undefined;
        if (pageSize !== undefined && (pageSize <= 0 || isNaN(pageSize))) {
            throw { message: "Invalid pageSize", options: options };
        }

        var store = datajs.createStore(options.name, options.mechanism);
        return new DataCache(options, store);
    };



})(this);
