http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/503b4417/src/lib/odata/batch.js ---------------------------------------------------------------------- diff --git a/src/lib/odata/batch.js b/src/lib/odata/batch.js new file mode 100644 index 0000000..c71fc31 --- /dev/null +++ b/src/lib/odata/batch.js @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @module odata/batch */ + +var utils = require('./../utils.js'); +var odataUtils = require('./odatautils.js'); +var odataHandler = require('./handler.js'); + +var extend = utils.extend; +var isArray = utils.isArray; +var trimString = utils.trimString; + +var contentType = odataHandler.contentType; +var handler = odataHandler.handler; +var isBatch = odataUtils.isBatch; +var MAX_DATA_SERVICE_VERSION = odataHandler.MAX_DATA_SERVICE_VERSION; +var normalizeHeaders = odataUtils.normalizeHeaders; +//TODO var payloadTypeOf = odata.payloadTypeOf; +var prepareRequest = odataUtils.prepareRequest; + + + + + +// Imports + + + +// CONTENT START +var batchMediaType = "multipart/mixed"; +var responseStatusRegex = /^HTTP\/1\.\d (\d{3}) (.*)$/i; +var responseHeaderRegex = /^([^()<>@,;:\\"\/[\]?={} \t]+)\s?:\s?(.*)/; + +/* Calculates a random 16 bit number and returns it in hexadecimal format. + * @returns {String} A 16-bit number in hex format. + */ +function hex16() { + + return Math.floor((1 + Math.random()) * 0x10000).toString(16).substr(1); +} + +/* Creates a string that can be used as a multipart request boundary. + * @param {String} [prefix] - + * @returns {String} Boundary string of the format: <prefix><hex16>-<hex16>-<hex16> + */ +function createBoundary(prefix) { + + return prefix + hex16() + "-" + hex16() + "-" + hex16(); +} + +/* Gets the handler for data serialization of individual requests / responses in a batch. + * @param context - Context used for data serialization. + * @returns Handler object + */ +function partHandler(context) { + + return context.handler.partHandler; +} + +/* Gets the current boundary used for parsing the body of a multipart response. + * @param context - Context used for parsing a multipart response. + * @returns {String} Boundary string. + */ +function currentBoundary(context) { + var boundaries = context.boundaries; + return boundaries[boundaries.length - 1]; +} + +/** Parses a batch response. + * @param handler - This handler. + * @param {String} text - Batch text. + * @param {Object} context - Object with parsing context. + * @return An object representation of the batch. + */ +function batchParser(handler, text, context) { + + var boundary = context.contentType.properties["boundary"]; + return { __batchResponses: readBatch(text, { boundaries: [boundary], handlerContext: context }) }; +} + +/** Serializes a batch object representation into text. + * @param handler - This handler. + * @param {Object} data - Representation of a batch. + * @param {Object} context - Object with parsing context. + * @return An text representation of the batch object; undefined if not applicable.# + */ +function batchSerializer(handler, data, context) { + + var cType = context.contentType = context.contentType || contentType(batchMediaType); + if (cType.mediaType === batchMediaType) { + return writeBatch(data, context); + } +} + +/* Parses a multipart/mixed response body from from the position defined by the context. + * @param {String} text - Body of the multipart/mixed response. + * @param context - Context used for parsing. + * @return Array of objects representing the individual responses. + */ +function readBatch(text, context) { + var delimiter = "--" + currentBoundary(context); + + // Move beyond the delimiter and read the complete batch + readTo(text, context, delimiter); + + // Ignore the incoming line + readLine(text, context); + + // Read the batch parts + var responses = []; + var partEnd; + + while (partEnd !== "--" && context.position < text.length) { + var partHeaders = readHeaders(text, context); + var partContentType = contentType(partHeaders["Content-Type"]); + + var changeResponses; + if (partContentType && partContentType.mediaType === batchMediaType) { + context.boundaries.push(partContentType.properties.boundary); + try { + changeResponses = readBatch(text, context); + } catch (e) { + e.response = readResponse(text, context, delimiter); + changeResponses = [e]; + } + responses.push({ __changeResponses: changeResponses }); + context.boundaries.pop(); + readTo(text, context, "--" + currentBoundary(context)); + } else { + if (!partContentType || partContentType.mediaType !== "application/http") { + throw { message: "invalid MIME part type " }; + } + // Skip empty line + readLine(text, context); + // Read the response + var response = readResponse(text, context, delimiter); + try { + if (response.statusCode >= 200 && response.statusCode <= 299) { + partHandler(context.handlerContext).read(response, context.handlerContext); + } else { + // Keep track of failed responses and continue processing the batch. + response = { message: "HTTP request failed", response: response }; + } + } catch (e) { + response = e; + } + + responses.push(response); + } + + partEnd = text.substr(context.position, 2); + + // Ignore the incoming line. + readLine(text, context); + } + return responses; +} + +/* Parses the http headers in the text from the position defined by the context. +* @param {String} text - Text containing an http response's headers</param> +* @param context - Context used for parsing. +* @returns Object containing the headers as key value pairs. +* This function doesn't support split headers and it will stop reading when it hits two consecutive line breaks. +*/ +function readHeaders(text, context) { + var headers = {}; + var parts; + var line; + var pos; + + do { + pos = context.position; + line = readLine(text, context); + parts = responseHeaderRegex.exec(line); + if (parts !== null) { + headers[parts[1]] = parts[2]; + } else { + // Whatever was found is not a header, so reset the context position. + context.position = pos; + } + } while (line && parts); + + normalizeHeaders(headers); + + return headers; +} + +/* Parses an HTTP response. + * @param {String} text -Text representing the http response. + * @param context optional - Context used for parsing. + * @param {String} delimiter -String used as delimiter of the multipart response parts. + * @return Object representing the http response. + */ +function readResponse(text, context, delimiter) { + // Read the status line. + var pos = context.position; + var match = responseStatusRegex.exec(readLine(text, context)); + + var statusCode; + var statusText; + var headers; + + if (match) { + statusCode = match[1]; + statusText = match[2]; + headers = readHeaders(text, context); + readLine(text, context); + } else { + context.position = pos; + } + + return { + statusCode: statusCode, + statusText: statusText, + headers: headers, + body: readTo(text, context, "\r\n" + delimiter) + }; +} + +/** Returns a substring from the position defined by the context up to the next line break (CRLF). + * @param {String} text - Input string. + * @param context - Context used for reading the input string. + * @returns {String} Substring to the first ocurrence of a line break or null if none can be found. + */ +function readLine(text, context) { + + return readTo(text, context, "\r\n"); +} + +/** 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. + * @param {String} text - Input string.</param> + * @param context - Context used for reading the input string.</param> + * @param {String} [str] - Substring to read up to. + * @returns {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. + */ +function readTo(text, context, str) { + 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); +} + +/** Serializes a batch request object to a string. + * @param data - Batch request object in payload representation format + * @param context - Context used for the serialization + * @returns {String} String representing the batch request + */ +function writeBatch(data, context) { + if (!isBatch(data)) { + throw { message: "Data is not a batch object." }; + } + + var batchBoundary = createBoundary("batch_"); + var batchParts = data.__batchRequests; + var batch = ""; + var i, len; + for (i = 0, len = batchParts.length; i < len; i++) { + batch += writeBatchPartDelimiter(batchBoundary, false) + + writeBatchPart(batchParts[i], context); + } + batch += writeBatchPartDelimiter(batchBoundary, true); + + // Register the boundary with the request content type. + var contentTypeProperties = context.contentType.properties; + contentTypeProperties.boundary = batchBoundary; + + return batch; +} + +/** Creates the delimiter that indicates that start or end of an individual request. + * @param {String} boundary Boundary string used to indicate the start of the request</param> + * @param {Boolean} close - Flag indicating that a close delimiter string should be generated + * @returns {String} Delimiter string + */ +function writeBatchPartDelimiter(boundary, close) { + var result = "\r\n--" + boundary; + if (close) { + result += "--"; + } + + return result + "\r\n"; +} + +/** 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. + * @param part - Request or change set object in payload representation format</param> + * @param context - Object containing context information used for the serialization</param> + * @param {boolean} [nested] - + * @returns {String} String representing the serialized part + * A change set is an array of request objects and they cannot be nested inside other change sets. + */ +function writeBatchPart(part, context, nested) { + + + var changeSet = part.__changeRequests; + var result; + if (isArray(changeSet)) { + if (nested) { + throw { message: "Not Supported: change set nested in other change set" }; + } + + var changeSetBoundary = createBoundary("changeset_"); + result = "Content-Type: " + batchMediaType + "; boundary=" + changeSetBoundary + "\r\n"; + var i, len; + for (i = 0, len = changeSet.length; i < len; i++) { + result += writeBatchPartDelimiter(changeSetBoundary, false) + + writeBatchPart(changeSet[i], context, true); + } + + result += writeBatchPartDelimiter(changeSetBoundary, true); + } else { + result = "Content-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\n"; + var partContext = extend({}, context); + partContext.handler = handler; + partContext.request = part; + partContext.contentType = null; + + prepareRequest(part, partHandler(context), partContext); + result += writeRequest(part); + } + + return result; +} + +/* Serializes a request object to a string. + * @param request - Request object to serialize</param> + * @returns {String} String representing the serialized request + */ +function writeRequest(request) { + 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; +} + + + +/** batchHandler (see {@link module:odata/batch~batchParser}) */ +exports.batchHandler = handler(batchParser, batchSerializer, batchMediaType, MAX_DATA_SERVICE_VERSION); +exports.batchSerializer = batchSerializer; +exports.writeRequest = writeRequest; \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/503b4417/src/lib/odata/handler.js ---------------------------------------------------------------------- diff --git a/src/lib/odata/handler.js b/src/lib/odata/handler.js new file mode 100644 index 0000000..cbd09c6 --- /dev/null +++ b/src/lib/odata/handler.js @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @module odata/handler */ + + +var utils = require('./../utils.js'); +var oDataUtils = require('./odatautils.js'); + +// Imports. +var assigned = utils.assigned; +var extend = utils.extend; +var trimString = utils.trimString; +var maxVersion = oDataUtils.maxVersion; +var MAX_DATA_SERVICE_VERSION = "4.0"; + +/** Parses a string into an object with media type and properties. + * @param {String} str - String with media type to parse. + * @return null if the string is empty; an object with 'mediaType' and a 'properties' dictionary otherwise. + */ +function contentType(str) { + + 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 }; +} + +/** Serializes an object with media type and properties dictionary into a string. + * @param contentType - Object with media type and properties dictionary to serialize. + * @return String representation of the media type object; undefined if contentType is null or undefined.</returns> + */ +function contentTypeToString(contentType) { + if (!contentType) { + return undefined; + } + + var result = contentType.mediaType; + var property; + for (property in contentType.properties) { + result += ";" + property + "=" + contentType.properties[property]; + } + return result; +} + +/** Creates an object that is going to be used as the context for the handler's parser and serializer. + * @param contentType - Object with media type and properties dictionary. + * @param {String} dataServiceVersion - String indicating the version of the protocol to use. + * @param context - Operation context. + * @param handler - Handler object that is processing a resquest or response. + * @return Context object.</returns> + */ +function createReadWriteContext(contentType, dataServiceVersion, context, handler) { + + var rwContext = {}; + extend(rwContext, context); + extend(rwContext, { + contentType: contentType, + dataServiceVersion: dataServiceVersion, + handler: handler + }); + + return rwContext; +} + +/** 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. + * @param request - Request object on which the header will be set. + * @param {String} name - Header name. + * @param {String} value - Header value. + */ +function fixRequestHeader(request, name, value) { + if (!request) { + return; + } + + var headers = request.headers; + if (!headers[name]) { + headers[name] = value; + } +} + +/** Sets the DataServiceVersion header of the request if its value is not yet defined or of a lower version. + * @param request - Request object on which the header will be set. + * @param {String} version - Version value. + * If the request has already a version value higher than the one supplied the this function does nothing. + */ +function fixDataServiceVersionHeader(request, version) { + + if (request) { + var headers = request.headers; + var dsv = headers["OData-Version"]; + headers["OData-Version"] = dsv ? maxVersion(dsv, version) : version; + } +} + +/** Gets the value of a request or response header. + * @param requestOrResponse - Object representing a request or a response. + * @param {String} name - Name of the header to retrieve. + * @returns {String} String value of the header; undefined if the header cannot be found. + */ +function getRequestOrResponseHeader(requestOrResponse, name) { + + var headers = requestOrResponse.headers; + return (headers && headers[name]) || undefined; +} + +/** Gets the value of the Content-Type header from a request or response. + * @param requestOrResponse - Object representing a request or a response. + * @returns {Object} Object with 'mediaType' and a 'properties' dictionary; null in case that the header is not found or doesn't have a value. + */ +function getContentType(requestOrResponse) { + + return contentType(getRequestOrResponseHeader(requestOrResponse, "Content-Type")); +} + +var versionRE = /^\s?(\d+\.\d+);?.*$/; +/** Gets the value of the DataServiceVersion header from a request or response. + * @param requestOrResponse - Object representing a request or a response. + * @returns {String} Data service version; undefined if the header cannot be found. + */ +function getDataServiceVersion(requestOrResponse) { + + var value = getRequestOrResponseHeader(requestOrResponse, "OData-Version"); + if (value) { + var matches = versionRE.exec(value); + if (matches && matches.length) { + return matches[1]; + } + } + + // Fall through and return undefined. +} + +/** Checks that a handler can process a particular mime type. + * @param handler - Handler object that is processing a resquest or response. + * @param cType - Object with 'mediaType' and a 'properties' dictionary. + * @returns {Boolean} True if the handler can process the mime type; false otherwise. + * + * 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. + */ +function handlerAccepts(handler, cType) { + return handler.accept.indexOf(cType.mediaType) >= 0; +} + +/** Invokes the parser associated with a handler for reading the payload of a HTTP response. + * @param handler - Handler object that is processing the response. + * @param {Function} parseCallback - Parser function that will process the response payload. + * @param response - HTTP response whose payload is going to be processed. + * @param context - Object used as the context for processing the response. + * @returns {Boolean} True if the handler processed the response payload and the response.data property was set; false otherwise. + */ +function handlerRead(handler, parseCallback, response, context) { + + if (!response || !response.headers) { + return false; + } + + var cType = getContentType(response); + var version = getDataServiceVersion(response) || ""; + var body = response.body; + + if (!assigned(body)) { + return false; + } + + if (handlerAccepts(handler, cType)) { + var readContext = createReadWriteContext(cType, version, context, handler); + readContext.response = response; + response.data = parseCallback(handler, body, readContext); + return response.data !== undefined; + } + + return false; +} + +/** Invokes the serializer associated with a handler for generating the payload of a HTTP request. + * @param handler - Handler object that is processing the request. + * @param {Function} serializeCallback - Serializer function that will generate the request payload. + * @param response - HTTP request whose payload is going to be generated. + * @param context - Object used as the context for serializing the request. + * @returns {Boolean} True if the handler serialized the request payload and the request.body property was set; false otherwise. + */ +function handlerWrite(handler, serializeCallback, request, context) { + if (!request || !request.headers) { + return false; + } + + var cType = getContentType(request); + var version = getDataServiceVersion(request); + + if (!cType || handlerAccepts(handler, cType)) { + var writeContext = createReadWriteContext(cType, version, context, handler); + writeContext.request = request; + + request.body = serializeCallback(handler, request.data, writeContext); + + if (request.body !== undefined) { + fixDataServiceVersionHeader(request, writeContext.dataServiceVersion || "4.0"); + + fixRequestHeader(request, "Content-Type", contentTypeToString(writeContext.contentType)); + fixRequestHeader(request, "OData-MaxVersion", handler.maxDataServiceVersion); + return true; + } + } + + return false; +} + +/** Creates a handler object for processing HTTP requests and responses. + * @param {Function} parseCallback - Parser function that will process the response payload. + * @param {Function} serializeCallback - Serializer function that will generate the request payload. + * @param {String} accept - String containing a comma separated list of the mime types that this handler can work with. + * @param {String} maxDataServiceVersion - String indicating the highest version of the protocol that this handler can work with. + * @returns {Object} Handler object. + */ +function handler(parseCallback, serializeCallback, accept, maxDataServiceVersion) { + + 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); + } + }; +} + +function textParse(handler, body /*, context */) { + return body; +} + +function textSerialize(handler, data /*, context */) { + if (assigned(data)) { + return data.toString(); + } else { + return undefined; + } +} + + + + +exports.textHandler = handler(textParse, textSerialize, "text/plain", MAX_DATA_SERVICE_VERSION); + +exports.contentType = contentType; +exports.contentTypeToString = contentTypeToString; +exports.handler = handler; +exports.createReadWriteContext = createReadWriteContext; +exports.fixRequestHeader = fixRequestHeader; +exports.getRequestOrResponseHeader = getRequestOrResponseHeader; +exports.getContentType = getContentType; +exports.getDataServiceVersion = getDataServiceVersion; +exports.MAX_DATA_SERVICE_VERSION = MAX_DATA_SERVICE_VERSION; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/503b4417/src/lib/odata/json.js ---------------------------------------------------------------------- diff --git a/src/lib/odata/json.js b/src/lib/odata/json.js new file mode 100644 index 0000000..6a6968a --- /dev/null +++ b/src/lib/odata/json.js @@ -0,0 +1,917 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @module odata/json */ + + + +var utils = require('./../utils.js'); +var oDataUtils = require('./odatautils.js'); +var oDataHandler = require('./handler.js'); + +var odataNs = "odata"; +var odataAnnotationPrefix = odataNs + "."; +var contextUrlAnnotation = "@" + odataAnnotationPrefix + "context"; + +var assigned = utils.assigned; +var defined = utils.defined; +var isArray = utils.isArray; +//var isDate = utils.isDate; +var isObject = utils.isObject; +//var normalizeURI = utils.normalizeURI; +var parseInt10 = utils.parseInt10; +var getFormatKind = utils.getFormatKind; + +var formatDateTimeOffset = oDataUtils.formatDateTimeOffset; +var formatDuration = oDataUtils.formatDuration; +var formatNumberWidth = oDataUtils.formatNumberWidth; +var getCanonicalTimezone = oDataUtils.getCanonicalTimezone; +var handler = oDataUtils.handler; +var isComplex = oDataUtils.isComplex; +var isPrimitive = oDataUtils.isPrimitive; +var isCollectionType = oDataUtils.isCollectionType; +var lookupComplexType = oDataUtils.lookupComplexType; +var lookupEntityType = oDataUtils.lookupEntityType; +var lookupSingleton = oDataUtils.lookupSingleton; +var lookupEntitySet = oDataUtils.lookupEntitySet; +var lookupDefaultEntityContainer = oDataUtils.lookupDefaultEntityContainer; +var lookupProperty = oDataUtils.lookupProperty; +var MAX_DATA_SERVICE_VERSION = oDataUtils.MAX_DATA_SERVICE_VERSION; +var maxVersion = oDataUtils.maxVersion; +var XXXparseDateTime = oDataUtils.XXXparseDateTime; + +var isPrimitiveEdmType = oDataUtils.isPrimitiveEdmType; +var isGeographyEdmType = oDataUtils.isGeographyEdmType; +var isGeometryEdmType = oDataUtils.isGeometryEdmType; + +var PAYLOADTYPE_FEED = "f"; +var PAYLOADTYPE_ENTRY = "e"; +var PAYLOADTYPE_PROPERTY = "p"; +var PAYLOADTYPE_COLLECTION = "c"; +var PAYLOADTYPE_ENUMERATION_PROPERTY = "enum"; +var PAYLOADTYPE_SVCDOC = "s"; +var PAYLOADTYPE_ENTITY_REF_LINK = "erl"; +var PAYLOADTYPE_ENTITY_REF_LINKS = "erls"; + +var PAYLOADTYPE_VALUE = "v"; + +var PAYLOADTYPE_DELTA = "d"; +var DELTATYPE_FEED = "f"; +var DELTATYPE_DELETED_ENTRY = "de"; +var DELTATYPE_LINK = "l"; +var DELTATYPE_DELETED_LINK = "dl"; + +var jsonMediaType = "application/json"; +var jsonContentType = oDataHandler.contentType(jsonMediaType); + + +// 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+)?\)\/$/; + +/** Formats the given minutes into (+/-)hh:mm format. + * @param {Number} minutes - Number of minutes to format. + * @returns {String} The minutes in (+/-)hh:mm format. + */ +function minutesToOffset(minutes) { + + 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); +} + +/** Parses the JSON Date representation into a Date object. + * @param {String} value - String value. + * @returns {Date} A Date object if the value matches one; falsy otherwise. + */ +function parseJsonDateString(value) { + + 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. + +/** Parses a JSON OData payload. + * @param handler - This handler. + * @param text - Payload text (this parser also handles pre-parsed objects). + * @param {Object} context - Object with parsing context. + * @return An object representation of the OData payload.</returns> + */ +function jsonParser(handler, text, context) { + var recognizeDates = defined(context.recognizeDates, handler.recognizeDates); + var model = context.metadata; + var json = (typeof text === "string") ? JSON.parse(text) : text; + var metadataContentType; + if (assigned(context.contentType) && assigned(context.contentType.properties)) { + metadataContentType = context.contentType.properties["odata.metadata"]; //TODO convert to lower before comparism + } + + var payloadFormat = getFormatKind(metadataContentType, 1); // none: 0, minimal: 1, full: 2 + + // No errors should be throw out if we could not parse the json payload, instead we should just return the original json object. + if (payloadFormat === 0) { + return json; + } + else if (payloadFormat === 1) { + return readPayloadMinimal(json, model, recognizeDates); + } + else if (payloadFormat === 2) { + // to do: using the EDM Model to get the type of each property instead of just guessing. + return readPayloadFull(json, model, recognizeDates); + } + else { + return json; + } +} + + +function addType(data, name, value ) { + var fullName = name + '@odata.type'; + + if ( data[fullName] === undefined) { + data[fullName] = '#' + value; + } +} + +function addTypeNoEdm(data, name, value ) { + var fullName = name + '@odata.type'; + + if ( data[fullName] === undefined) { + if ( value.substring(0,4)==='Edm.') { + data[fullName] = '#' + value.substring(4); + } else { + data[fullName] = '#' + value; + } + + } +} + +function addTypeColNoEdm(data, name, value ) { + var fullName = name + '@odata.type'; + + if ( data[fullName] === undefined) { + if ( value.substring(0,4)==='Edm.') { + data[fullName] = '#Collection('+value.substring(4)+ ')'; + } else { + data[fullName] = '#Collection('+value+ ')'; + } + } +} + + +/* Adds typeinformation for String, Boolean and numerical EDM-types. + * The type is determined from the odata-json-format-v4.0.doc specification + * @param data - Date which will be extendet + * @param {Boolean} recognizeDates - True if strings formatted as datetime values should be treated as datetime values. False otherwise. + * @returns An object representation of the OData payload. + */ +function readPayloadFull(data, model, recognizeDates) { + var type; + if (utils.isObject(data)) { + for (var key in data) { + if (data.hasOwnProperty(key)) { + if (key.indexOf('@') === -1) { + if (utils.isArray(data[key])) { + for (var i = 0; i < data[key].length; ++i) { + readPayloadFull(data[key][i], model, recognizeDates); + } + } else if (utils.isObject(data[key])) { + if (data[key] !== null) { + //don't step into geo.. objects + var isGeo = false; + type = data[key+'@odata.type']; + if (type && (isGeographyEdmType(type) || isGeometryEdmType(type))) { + // is gemometry type + } else { + readPayloadFull(data[key], model, recognizeDates); + } + } + } else { + type = data[key + '@odata.type']; + + // On .Net OData library, some basic EDM type is omitted, e.g. Edm.String, Edm.Int, and etc. + // For the full metadata payload, we need to full fill the @data.type for each property if it is missing. + // We do this is to help the OlingoJS consumers to easily get the type of each property. + if (!assigned(type)) { + // Guessing the "type" from the type of the value is not the right way here. + // To do: we need to get the type from metadata instead of guessing. + var typeFromObject = typeof data[key]; + if (typeFromObject === 'string') { + addType(data, key, 'String'); + } else if (typeFromObject === 'boolean') { + addType(data, key, 'Boolean'); + } else if (typeFromObject === 'number') { + if (data[key] % 1 === 0) { // has fraction + addType(data, key, 'Int32'); // the biggst integer + } else { + addType(data, key, 'Decimal'); // the biggst float single,doulbe,decimal + } + } + } + else { + if (recognizeDates) { + convertDatesNoEdm(data, key, type.substring(1)); + } + } + } + } + } + } + } + + return data; +} + +/** Serializes the data by returning its string representation. + * @param handler - This handler. + * @param data - Data to serialize. + * @param {Object} context - Object with serialization context. + * @returns {String} The string representation of data. + */ +function jsonSerializer(handler, data, context) { + + var dataServiceVersion = context.dataServiceVersion || "4.0"; + var cType = context.contentType = context.contentType || jsonContentType; + + if (cType && cType.mediaType === jsonContentType.mediaType) { + context.dataServiceVersion = maxVersion(dataServiceVersion, "4.0"); + var newdata = formatJsonRequestPayload(data); + if (newdata) { + return JSON.stringify(newdata); + } + } + + return undefined; +} + +function formatJsonRequestPayload(data) { + if (!data) { + return data; + } + + if (isPrimitive(data)) { + return data; + } + + if (isArray(data)) { + var newArrayData = []; + var i, len; + for (i = 0, len = data.length; i < len; i++) { + newArrayData[i] = formatJsonRequestPayload(data[i]); + } + + return newArrayData; + } + + var newdata = {}; + for (var property in data) { + if (isJsonSerializableProperty(property)) { + newdata[property] = formatJsonRequestPayload(data[property]); + } + } + + return newdata; +} + +/** JSON replacer function for converting a value to its JSON representation. + * @param {Object} value - Value to convert.</param> + * @returns {String} JSON representation of the input value. + * This method is used during JSON serialization and invoked only by the JSON.stringify function. + * It should never be called directly. + */ +function jsonReplacer(_, value) { + + + if (value && value.__edmType === "Edm.Time") { + return formatDuration(value); + } else { + return value; + } +} + + +/** Creates an object containing information for the json payload. + * @param {String} kind - JSON payload kind, one of the PAYLOADTYPE_XXX constant values. + * @param {String} typeName - Type name of the JSON payload. + * @returns {Object} Object with kind and type fields. + */ +function jsonMakePayloadInfo(kind, type) { + + /// TODO docu + /// <field name="kind" type="String">Kind of the JSON payload. One of the PAYLOADTYPE_XXX constant values.</field> + /// <field name="type" type="String">Data type of the JSON payload.</field> + + return { kind: kind, type: type || null }; +} + +/** Creates an object containing information for the context + * TODO check dou layout + * @returns {Object} Object with type information + * @returns {Object.detectedPayloadKind(optional)} see constants starting with PAYLOADTYPE_ + * @returns {Object.deltaKind(optional)} deltainformation, one of the following valus DELTATYPE_FEED | DELTATYPE_DELETED_ENTRY | DELTATYPE_LINK | DELTATYPE_DELETED_LINK + * @returns {Object.typeName(optional)} name of the type + * @returns {Object.type(optional)} object containing type information for entity- and complex-types ( null if a typeName is a primitive) +*/ +function parseContextUriFragment( fragments, model ) { + var ret = {}; + + if (fragments.indexOf('/') === -1 ) { + if (fragments.length === 0) { + // Capter 10.1 + ret.detectedPayloadKind = PAYLOADTYPE_SVCDOC; + return ret; + } else if (fragments === 'Edm.Null') { + // Capter 10.15 + ret.detectedPayloadKind = PAYLOADTYPE_VALUE; + ret.isNullProperty = true; + return ret; + } else if (fragments === 'Collection($ref)') { + // Capter 10.11 + ret.detectedPayloadKind = PAYLOADTYPE_ENTITY_REF_LINKS; + return ret; + } else if (fragments === '$ref') { + // Capter 10.12 + ret.detectedPayloadKind = PAYLOADTYPE_ENTITY_REF_LINK; + return ret; + } else { + //TODO check for navigation resource + } + } + + ret.type = undefined; + ret.typeName = undefined; + + var fragmentParts = fragments.split("/"); + var type; + + for(var i = 0; i < fragmentParts.length; ++i) { + var fragment = fragmentParts[i]; + if (ret.typeName === undefined) { + //preparation + if ( fragment.indexOf('(') !== -1 ) { + //remove the query function, cut fragment to matching '(' + var index = fragment.length - 2 ; + for ( var rCount = 1; rCount > 0 && index > 0; --index) { + if ( fragment.charAt(index)=='(') { + rCount --; + } else if ( fragment.charAt(index)==')') { + rCount ++; + } + } + + if (index === 0) { + //TODO throw error + } + + //remove the projected entity from the fragment; TODO decide if we want to store the projected entity + var inPharenthesis = fragment.substring(index+2,fragment.length - 1); + fragment = fragment.substring(0,index+1); + + if (utils.startsWith(fragment, 'Collection')) { + ret.detectedPayloadKind = PAYLOADTYPE_COLLECTION; + // Capter 10.14 + ret.typeName = inPharenthesis; + + type = lookupEntityType(ret.typeName, model); + if ( type !== null) { + ret.type = type; + continue; + } + type = lookupComplexType(ret.typeName, model); + if ( type !== null) { + ret.type = type; + continue; + } + + ret.type = null;//in case of #Collection(Edm.String) only lastTypeName is filled + continue; + } else { + // projection: Capter 10.7, 10.8 and 10.9 + ret.projection = inPharenthesis; + } + } + + + if (jsonIsPrimitiveType(fragment)) { + ret.typeName = fragment; + ret.type = null; + ret.detectedPayloadKind = PAYLOADTYPE_VALUE; + continue; + } + + var container = lookupDefaultEntityContainer(model); + + //check for entity + var entitySet = lookupEntitySet(container.entitySet, fragment); + if ( entitySet !== null) { + ret.typeName = entitySet.entityType; + ret.type = lookupEntityType( ret.typeName, model); + ret.name = fragment; + ret.detectedPayloadKind = PAYLOADTYPE_FEED; + // Capter 10.2 + continue; + } + + //check for singleton + var singleton = lookupSingleton(container.singleton, fragment); + if ( singleton !== null) { + ret.typeName = singleton.entityType; + ret.type = lookupEntityType( ret.typeName, model); + ret.name = fragment; + ret.detectedPayloadKind = PAYLOADTYPE_ENTRY; + // Capter 10.4 + continue; + } + + + + //TODO throw ERROR + } else { + //check for $entity + if (utils.endsWith(fragment, '$entity') && (ret.detectedPayloadKind === PAYLOADTYPE_FEED)) { + //TODO ret.name = fragment; + ret.detectedPayloadKind = PAYLOADTYPE_ENTRY; + // Capter 10.3 and 10.6 + continue; + } + + //check for derived types + if (fragment.indexOf('.') !== -1) { + // Capter 10.6 + ret.typeName = fragment; + type = lookupEntityType(ret.typeName, model); + if ( type !== null) { + ret.type = type; + continue; + } + type = lookupComplexType(ret.typeName, model); + if ( type !== null) { + ret.type = type; + continue; + } + + //TODO throw ERROR invalid type + } + + //check for property value + if ( ret.detectedPayloadKind === PAYLOADTYPE_FEED || ret.detectedPayloadKind === PAYLOADTYPE_ENTRY) { + var property = lookupProperty(ret.type.property, fragment); + if (property !== null) { + //PAYLOADTYPE_COLLECTION + ret.typeName = property.type; + + + if (utils.startsWith(property.type, 'Collection')) { + ret.detectedPayloadKind = PAYLOADTYPE_COLLECTION; + var tmp12 = property.type.substring(10+1,property.type.length - 1); + ret.typeName = tmp12; + ret.type = lookupComplexType(tmp12, model); + ret.detectedPayloadKind = PAYLOADTYPE_COLLECTION; + } else { + ret.type = lookupComplexType(property.type, model); + ret.detectedPayloadKind = PAYLOADTYPE_PROPERTY; + } + + ret.name = fragment; + // Capter 10.15 + } + continue; + } + + if (fragment === '$delta') { + ret.deltaKind = DELTATYPE_FEED; + continue; + } else if (utils.endsWith(fragment, '/$deletedEntity')) { + ret.deltaKind = DELTATYPE_DELETED_ENTRY; + continue; + } else if (utils.endsWith(fragment, '/$link')) { + ret.deltaKind = DELTATYPE_LINK; + continue; + } else if (utils.endsWith(fragment, '/$deletedLink')) { + ret.deltaKind = DELTATYPE_DELETED_LINK; + continue; + } + //TODO throw ERROr + } + } + + return ret; +} + +/** Infers the information describing the JSON payload from its metadata annotation, structure, and data model. + * @param {Object} data - Json response payload object. + * @param {Object} model - Object describing an OData conceptual schema. + * If the arguments passed to the function don't convey enough information about the payload to determine without doubt that the payload is a feed then it + * will try to use the payload object structure instead. If the payload looks like a feed (has value property that is an array or non-primitive values) then + * the function will report its kind as PAYLOADTYPE_FEED unless the inferFeedAsComplexType flag is set to true. This flag comes from the user request + * and allows the user to control how the library behaves with an ambigous JSON payload. + * @return Object with kind and type fields. Null if there is no metadata annotation or the payload info cannot be obtained.. +*/ +function createPayloadInfo(data, model) { + + + var metadataUri = data[contextUrlAnnotation]; + if (!metadataUri || typeof metadataUri !== "string") { + return null; + } + + var fragmentStart = metadataUri.lastIndexOf("#"); + if (fragmentStart === -1) { + return jsonMakePayloadInfo(PAYLOADTYPE_SVCDOC); + } + + var fragment = metadataUri.substring(fragmentStart + 1); + return parseContextUriFragment(fragment,model); +} + +/** Processe a JSON response payload with metadata-minimal + * @param {Object} data - Json response payload object + * @param {Object} model - Object describing an OData conceptual schema + * @param {Boolean} recognizeDates - Flag indicating whether datetime literal strings should be converted to JavaScript Date objects. + * @returns {Object} Object in the library's representation. + */ +function readPayloadMinimal(data, model, recognizeDates) { + + if (!assigned(model) || isArray(model)) { + return data; + } + + var baseURI = data[contextUrlAnnotation]; + var payloadInfo = createPayloadInfo(data, model); + + switch (payloadInfo.detectedPayloadKind) { + case PAYLOADTYPE_VALUE: + return readPayloadMinimalProperty(data, model, payloadInfo, baseURI, recognizeDates); + case PAYLOADTYPE_FEED: + return readPayloadMinimalFeed(data, model, payloadInfo, baseURI, recognizeDates); + case PAYLOADTYPE_ENTRY: + return readPayloadMinimalEntry(data, model, payloadInfo, baseURI, recognizeDates); + case PAYLOADTYPE_COLLECTION: + return readPayloadMinimalCollection(data, model, payloadInfo, baseURI, recognizeDates); + case PAYLOADTYPE_PROPERTY: + return readPayloadMinimalProperty(data, model, payloadInfo, baseURI, recognizeDates); + case PAYLOADTYPE_SVCDOC: + return data; + case PAYLOADTYPE_LINKS: + return data; + } + + return data; +} + +/** Gets the key of an entry. + * @param {Object} data - JSON entry. + * + * @returns {string} Entry instance key. + */ +function jsonGetEntryKey(data, entityModel) { + + var entityInstanceKey; + var entityKeys = entityModel.key[0].propertyRef; + var type; + entityInstanceKey = "("; + if (entityKeys.length == 1) { + type = lookupProperty(entityModel.property, entityKeys[0].name).type; + entityInstanceKey += formatLiteral(data[entityKeys[0].name], type); + } else { + var first = true; + for (var i = 0; i < entityKeys.length; i++) { + if (!first) { + entityInstanceKey += ","; + } else { + first = false; + } + type = lookupProperty(entityModel.property, entityKeys[i].name).type; + entityInstanceKey += entityKeys[i].name + "=" + formatLiteral(data[entityKeys[i].name], type); + } + } + entityInstanceKey += ")"; + return entityInstanceKey; +} + +function readPayloadMinimalProperty(data, model, collectionInfo, baseURI, recognizeDates) { + if (collectionInfo.type !== null) { + readPayloadMinimalObject(data, collectionInfo, baseURI, model, recognizeDates); + } else { + addTypeNoEdm(data,'value', collectionInfo.typeName); + //data['[email protected]'] = '#'+collectionInfo.typeName; + } + return data; +} + +function readPayloadMinimalCollection(data, model, collectionInfo, baseURI, recognizeDates) { + //data['@odata.type'] = '#Collection('+collectionInfo.typeName + ')'; + addTypeColNoEdm(data,'', collectionInfo.typeName); + + if (collectionInfo.type !== null) { + var entries = []; + + var items = data.value; + for (i = 0, len = items.length; i < len; i++) { + var item = items[i]; + if ( defined(item['@odata.type'])) { // in case of mixed collections + var typeName = item['@odata.type'].substring(1); + var type = lookupEntityType( typeName, model); + var entryInfo = { + contentTypeOdata : collectionInfo.contentTypeOdata, + detectedPayloadKind : collectionInfo.detectedPayloadKind, + name : collectionInfo.name, + type : type, + typeName : typeName + }; + + entry = readPayloadMinimalObject(item, entryInfo, baseURI, model, recognizeDates); + } else { + entry = readPayloadMinimalObject(item, collectionInfo, baseURI, model, recognizeDates); + } + + entries.push(entry); + } + data.value = entries; + } + return data; +} + +function readPayloadMinimalFeed(data, model, feedInfo, baseURI, recognizeDates) { + var entries = []; + var items = data.value; + for (i = 0, len = items.length; i < len; i++) { + var item = items[i]; + if ( defined(item['@odata.type'])) { // in case of mixed feeds + var typeName = item['@odata.type'].substring(1); + var type = lookupEntityType( typeName, model); + var entryInfo = { + contentTypeOdata : feedInfo.contentTypeOdata, + detectedPayloadKind : feedInfo.detectedPayloadKind, + name : feedInfo.name, + type : type, + typeName : typeName + }; + + entry = readPayloadMinimalObject(item, entryInfo, baseURI, model, recognizeDates); + } else { + entry = readPayloadMinimalObject(item, feedInfo, baseURI, model, recognizeDates); + } + + entries.push(entry); + } + data.value = entries; + return data; +} + +function readPayloadMinimalEntry(data, model, entryInfo, baseURI, recognizeDates) { + return readPayloadMinimalObject(data, entryInfo, baseURI, model, recognizeDates); +} + +/** Formats a value according to Uri literal format + * @param value - Value to be formatted. + * @param type - Edm type of the value + * @returns {string} Value after formatting + */ +function formatLiteral(value, type) { + + value = "" + formatRowLiteral(value, type); + value = encodeURIComponent(value.replace("'", "''")); + switch ((type)) { + case "Edm.Binary": + return "X'" + value + "'"; + case "Edm.DateTime": + return "datetime" + "'" + value + "'"; + case "Edm.DateTimeOffset": + return "datetimeoffset" + "'" + value + "'"; + case "Edm.Decimal": + return value + "M"; + case "Edm.Guid": + return "guid" + "'" + value + "'"; + case "Edm.Int64": + return value + "L"; + case "Edm.Float": + return value + "f"; + case "Edm.Double": + return value + "D"; + case "Edm.Geography": + return "geography" + "'" + value + "'"; + case "Edm.Geometry": + return "geometry" + "'" + value + "'"; + case "Edm.Time": + return "time" + "'" + value + "'"; + case "Edm.String": + return "'" + value + "'"; + default: + return value; + } +} + +function formatRowLiteral(value, type) { + switch (type) { + case "Edm.Binary": + return convertByteArrayToHexString(value); + default: + return value; + } +} + +function convertDates(data, propertyName,type) { + if (type === 'Edm.Date') { + data[propertyName] = oDataUtils.parseDate(data[propertyName], true); + } else if (type === 'Edm.DateTimeOffset') { + data[propertyName] = oDataUtils.parseDateTimeOffset(data[propertyName], true); + } else if (type === 'Edm.Duration') { + data[propertyName] = oDataUtils.parseDuration(data[propertyName], true); + } else if (type === 'Edm.Time') { + data[propertyName] = oDataUtils.parseTime(data[propertyName], true); + } +} + +function convertDatesNoEdm(data, propertyName,type) { + if (type === 'Date') { + data[propertyName] = oDataUtils.parseDate(data[propertyName], true); + } else if (type === 'DateTimeOffset') { + data[propertyName] = oDataUtils.parseDateTimeOffset(data[propertyName], true); + } else if (type === 'Duration') { + data[propertyName] = oDataUtils.parseDuration(data[propertyName], true); + } else if (type === 'Time') { + data[propertyName] = oDataUtils.parseTime(data[propertyName], true); + } +} + +function checkProperties(data, objectInfoType, baseURI, model, recognizeDates) { + for (var name in data) { + if (name.indexOf("@") === -1) { + var curType = objectInfoType; + var propertyValue = data[name]; + var property = lookupProperty(curType.property,name); //TODO SK add check for parent type + + while (( property === null) && (curType.baseType !== undefined)) { + curType = lookupEntityType(curType.baseType, model); + property = lookupProperty(curType.property,name); + } + + if ( isArray(propertyValue)) { + //data[name+'@odata.type'] = '#' + property.type; + if (isCollectionType(property.type)) { + addTypeColNoEdm(data,name,property.type.substring(11,property.type.length-1)); + } else { + addTypeNoEdm(data,name,property.type); + } + + + for ( var i = 0; i < propertyValue.length; i++) { + readPayloadMinimalComplexObject(propertyValue[i], property, baseURI, model, recognizeDates); + } + } else if (isObject(propertyValue) && (propertyValue !== null)) { + readPayloadMinimalComplexObject(propertyValue, property, baseURI, model, recognizeDates); + } else { + //data[name+'@odata.type'] = '#' + property.type; + addTypeNoEdm(data,name,property.type); + if (recognizeDates) { + convertDates(data, name, property.type); + } + } + } + } +} + +function readPayloadMinimalComplexObject(data, property, baseURI, model, recognizeDates) { + var type = property.type; + if (isCollectionType(property.type)) { + type =property.type.substring(11,property.type.length-1); + } + + //data['@odata.type'] = '#'+type; + addType(data,'',property.type); + + + var propertyType = lookupComplexType(type, model); + if (propertyType === null) { + return; //TODO check what to do if the type is not known e.g. type #GeometryCollection + } + + checkProperties(data, propertyType, baseURI, model, recognizeDates); +} + +function readPayloadMinimalObject(data, objectInfo, baseURI, model, recognizeDates) { + //data['@odata.type'] = '#'+objectInfo.typeName; + addType(data,'',objectInfo.typeName); + + var keyType = objectInfo.type; + while ((defined(keyType)) && ( keyType.key === undefined) && (keyType.baseType !== undefined)) { + keyType = lookupEntityType(keyType.baseType, model); + } + + //if ((keyType !== undefined) && (keyType.key !== undefined)) { + if (keyType.key !== undefined) { + var lastIdSegment = objectInfo.name + jsonGetEntryKey(data, keyType); + data['@odata.id'] = baseURI.substring(0, baseURI.lastIndexOf("$metadata")) + lastIdSegment; + data['@odata.editLink'] = lastIdSegment; + } + + var serviceURI = baseURI.substring(0, baseURI.lastIndexOf("$metadata")); + //json ComputeUrisIfMissing(data, entryInfo, actualType, serviceURI, dataModel, baseTypeModel); + + checkProperties(data, objectInfo.type, baseURI, model, recognizeDates); + + return data; +} + +var jsonSerializableMetadata = ["@odata.id", "@odata.type"]; + +function isJsonSerializableProperty(property) { + if (!property) { + return false; + } + + if (property.indexOf("@odata.") == -1) { + return true; + } + + var i, len; + for (i = 0, len = jsonSerializableMetadata.length; i < len; i++) { + var name = jsonSerializableMetadata[i]; + if (property.indexOf(name) != -1) { + return true; + } + } + + return false; +} + +/** Determines whether a type name is a primitive type in a JSON payload. + * @param {String} typeName - Type name to test. + * @returns {Boolean} True if the type name an EDM primitive type or an OData spatial type; false otherwise. + */ +function jsonIsPrimitiveType(typeName) { + + return isPrimitiveEdmType(typeName) || isGeographyEdmType(typeName) || isGeometryEdmType(typeName); +} + + +var jsonHandler = oDataHandler.handler(jsonParser, jsonSerializer, jsonMediaType, MAX_DATA_SERVICE_VERSION); +jsonHandler.recognizeDates = false; + + + +exports.createPayloadInfo = createPayloadInfo; +exports.jsonHandler = jsonHandler; +exports.jsonParser = jsonParser; +exports.jsonSerializer = jsonSerializer; +exports.parseJsonDateString = parseJsonDateString; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/503b4417/src/lib/odata/metadata.js ---------------------------------------------------------------------- diff --git a/src/lib/odata/metadata.js b/src/lib/odata/metadata.js new file mode 100644 index 0000000..e37161f --- /dev/null +++ b/src/lib/odata/metadata.js @@ -0,0 +1,523 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @module odata/metadata */ + +var utils = require('./../utils.js'); +var oDSxml = require('./../xml.js'); +var odataHandler = require('./handler.js'); + + + +// imports +var contains = utils.contains; +var normalizeURI = utils.normalizeURI; +var xmlAttributes = oDSxml.xmlAttributes; +var xmlChildElements = oDSxml.xmlChildElements; +var xmlFirstChildElement = oDSxml.xmlFirstChildElement; +var xmlInnerText = oDSxml.xmlInnerText; +var xmlLocalName = oDSxml.xmlLocalName; +var xmlNamespaceURI = oDSxml.xmlNamespaceURI; +var xmlNS = oDSxml.xmlNS; +var xmlnsNS = oDSxml.xmlnsNS; +var xmlParse = oDSxml.xmlParse; + +var ado = oDSxml.http + "docs.oasis-open.org/odata/"; // http://docs.oasis-open.org/odata/ +var adoDs = ado + "ns"; // http://docs.oasis-open.org/odata/ns +var edmxNs = adoDs + "/edmx"; // http://docs.oasis-open.org/odata/ns/edmx +var edmNs1 = adoDs + "/edm"; // http://docs.oasis-open.org/odata/ns/edm +var odataMetaXmlNs = adoDs + "/metadata"; // http://docs.oasis-open.org/odata/ns/metadata +var MAX_DATA_SERVICE_VERSION = odataHandler.MAX_DATA_SERVICE_VERSION; + +var xmlMediaType = "application/xml"; + +/** Creates an object that describes an element in an schema. + * @param {Array} attributes - List containing the names of the attributes allowed for this element. + * @param {Array} elements - List containing the names of the child elements allowed for this element. + * @param {Boolean} text - Flag indicating if the element's text value is of interest or not. + * @param {String} ns - Namespace to which the element belongs to. + * If a child element name ends with * then it is understood by the schema that that child element can appear 0 or more times. + * @returns {Object} Object with attributes, elements, text, and ns fields. + */ +function schemaElement(attributes, elements, text, ns) { + + return { + attributes: attributes, + elements: elements, + text: text || false, + ns: ns + }; +} + +// It's assumed that all elements may have Documentation children and Annotation elements. +// See http://docs.oasis-open.org/odata/odata/v4.0/cs01/part3-csdl/odata-v4.0-cs01-part3-csdl.html for a CSDL reference. +var schema = { + elements: { + Action: schemaElement( + /*attributes*/["Name", "IsBound", "EntitySetPath"], + /*elements*/["ReturnType", "Parameter*", "Annotation*"] + ), + ActionImport: schemaElement( + /*attributes*/["Name", "Action", "EntitySet", "Annotation*"] + ), + Annotation: schemaElement( + /*attributes*/["Term", "Qualifier", "Binary", "Bool", "Date", "DateTimeOffset", "Decimal", "Duration", "EnumMember", "Float", "Guid", "Int", "String", "TimeOfDay", "AnnotationPath", "NavigationPropertyPath", "Path", "PropertyPath", "UrlRef"], + /*elements*/["Binary*", "Bool*", "Date*", "DateTimeOffset*", "Decimal*", "Duration*", "EnumMember*", "Float*", "Guid*", "Int*", "String*", "TimeOfDay*", "And*", "Or*", "Not*", "Eq*", "Ne*", "Gt*", "Ge*", "Lt*", "Le*", "AnnotationPath*", "Apply*", "Cast*", "Collection*", "If*", "IsOf*", "LabeledElement*", "LabeledElementReference*", "Null*", "NavigationPropertyPath*", "Path*", "PropertyPath*", "Record*", "UrlRef*", "Annotation*"] + ), + AnnotationPath: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Annotations: schemaElement( + /*attributes*/["Target", "Qualifier"], + /*elements*/["Annotation*"] + ), + Apply: schemaElement( + /*attributes*/["Function"], + /*elements*/["String*", "Path*", "LabeledElement*", "Annotation*"] + ), + And: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Or: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Not: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Eq: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Ne: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Gt: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Ge: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Lt: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Le: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Binary: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Bool: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Cast: schemaElement( + /*attributes*/["Type"], + /*elements*/["Path*", "Annotation*"] + ), + Collection: schemaElement( + /*attributes*/null, + /*elements*/["Binary*", "Bool*", "Date*", "DateTimeOffset*", "Decimal*", "Duration*", "EnumMember*", "Float*", "Guid*", "Int*", "String*", "TimeOfDay*", "And*", "Or*", "Not*", "Eq*", "Ne*", "Gt*", "Ge*", "Lt*", "Le*", "AnnotationPath*", "Apply*", "Cast*", "Collection*", "If*", "IsOf*", "LabeledElement*", "LabeledElementReference*", "Null*", "NavigationPropertyPath*", "Path*", "PropertyPath*", "Record*", "UrlRef*"] + ), + ComplexType: schemaElement( + /*attributes*/["Name", "BaseType", "Abstract", "OpenType"], + /*elements*/["Property*", "NavigationProperty*", "Annotation*"] + ), + Date: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + DateTimeOffset: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Decimal: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Duration: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + EntityContainer: schemaElement( + /*attributes*/["Name", "Extends"], + /*elements*/["EntitySet*", "Singleton*", "ActionImport*", "FunctionImport*", "Annotation*"] + ), + EntitySet: schemaElement( + /*attributes*/["Name", "EntityType", "IncludeInServiceDocument"], + /*elements*/["NavigationPropertyBinding*", "Annotation*"] + ), + EntityType: schemaElement( + /*attributes*/["Name", "BaseType", "Abstract", "OpenType", "HasStream"], + /*elements*/["Key*", "Property*", "NavigationProperty*", "Annotation*"] + ), + EnumMember: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + EnumType: schemaElement( + /*attributes*/["Name", "UnderlyingType", "IsFlags"], + /*elements*/["Member*"] + ), + Float: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Function: schemaElement( + /*attributes*/["Name", "IsBound", "IsComposable", "EntitySetPath"], + /*elements*/["ReturnType", "Parameter*", "Annotation*"] + ), + FunctionImport: schemaElement( + /*attributes*/["Name", "Function", "EntitySet", "IncludeInServiceDocument", "Annotation*"] + ), + Guid: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + If: schemaElement( + /*attributes*/null, + /*elements*/["Path*", "String*", "Annotation*"] + ), + Int: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + IsOf: schemaElement( + /*attributes*/["Type", "MaxLength", "Precision", "Scale", "Unicode", "SRID", "DefaultValue", "Annotation*"], + /*elements*/["Path*"] + ), + Key: schemaElement( + /*attributes*/null, + /*elements*/["PropertyRef*"] + ), + LabeledElement: schemaElement( + /*attributes*/["Name"], + /*elements*/["Binary*", "Bool*", "Date*", "DateTimeOffset*", "Decimal*", "Duration*", "EnumMember*", "Float*", "Guid*", "Int*", "String*", "TimeOfDay*", "And*", "Or*", "Not*", "Eq*", "Ne*", "Gt*", "Ge*", "Lt*", "Le*", "AnnotationPath*", "Apply*", "Cast*", "Collection*", "If*", "IsOf*", "LabeledElement*", "LabeledElementReference*", "Null*", "NavigationPropertyPath*", "Path*", "PropertyPath*", "Record*", "UrlRef*", "Annotation*"] + ), + LabeledElementReference: schemaElement( + /*attributes*/["Term"], + /*elements*/["Binary*", "Bool*", "Date*", "DateTimeOffset*", "Decimal*", "Duration*", "EnumMember*", "Float*", "Guid*", "Int*", "String*", "TimeOfDay*", "And*", "Or*", "Not*", "Eq*", "Ne*", "Gt*", "Ge*", "Lt*", "Le*", "AnnotationPath*", "Apply*", "Cast*", "Collection*", "If*", "IsOf*", "LabeledElement*", "LabeledElementReference*", "Null*", "NavigationPropertyPath*", "Path*", "PropertyPath*", "Record*", "UrlRef*"] + ), + Member: schemaElement( + /*attributes*/["Name", "Value"], + /*element*/["Annotation*"] + ), + NavigationProperty: schemaElement( + /*attributes*/["Name", "Type", "Nullable", "Partner", "ContainsTarget"], + /*elements*/["ReferentialConstraint*", "OnDelete*", "Annotation*"] + ), + NavigationPropertyBinding: schemaElement( + /*attributes*/["Path", "Target"] + ), + NavigationPropertyPath: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Null: schemaElement( + /*attributes*/null, + /*elements*/["Annotation*"] + ), + OnDelete: schemaElement( + /*attributes*/["Action"], + /*elements*/["Annotation*"] + ), + Path: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Parameter: schemaElement( + /*attributes*/["Name", "Type", "Nullable", "MaxLength", "Precision", "Scale", "SRID"], + /*elements*/["Annotation*"] + ), + Property: schemaElement( + /*attributes*/["Name", "Type", "Nullable", "MaxLength", "Precision", "Scale", "Unicode", "SRID", "DefaultValue"], + /*elements*/["Annotation*"] + ), + PropertyPath: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + PropertyRef: schemaElement( + /*attributes*/["Name", "Alias"] + ), + PropertyValue: schemaElement( + /*attributes*/["Property", "Path"], + /*elements*/["Binary*", "Bool*", "Date*", "DateTimeOffset*", "Decimal*", "Duration*", "EnumMember*", "Float*", "Guid*", "Int*", "String*", "TimeOfDay*", "And*", "Or*", "Not*", "Eq*", "Ne*", "Gt*", "Ge*", "Lt*", "Le*", "AnnotationPath*", "Apply*", "Cast*", "Collection*", "If*", "IsOf*", "LabeledElement*", "LabeledElementReference*", "Null*", "NavigationPropertyPath*", "Path*", "PropertyPath*", "Record*", "UrlRef*", "Annotation*"] + ), + Record: schemaElement( + /*attributes*/null, + /*Elements*/["PropertyValue*", "Property*", "Annotation*"] + ), + ReferentialConstraint: schemaElement( + /*attributes*/["Property", "ReferencedProperty", "Annotation*"] + ), + ReturnType: schemaElement( + /*attributes*/["Type", "Nullable", "MaxLength", "Precision", "Scale", "SRID"] + ), + String: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + Schema: schemaElement( + /*attributes*/["Namespace", "Alias"], + /*elements*/["Action*", "Annotations*", "Annotation*", "ComplexType*", "EntityContainer", "EntityType*", "EnumType*", "Function*", "Term*", "TypeDefinition*", "Annotation*"] + ), + Singleton: schemaElement( + /*attributes*/["Name", "Type"], + /*elements*/["NavigationPropertyBinding*", "Annotation*"] + ), + Term: schemaElement( + /*attributes*/["Name", "Type", "BaseTerm", "DefaultValue ", "AppliesTo", "Nullable", "MaxLength", "Precision", "Scale", "SRID"], + /*elements*/["Annotation*"] + ), + TimeOfDay: schemaElement( + /*attributes*/null, + /*elements*/null, + /*text*/true + ), + TypeDefinition: schemaElement( + /*attributes*/["Name", "UnderlyingType", "MaxLength", "Unicode", "Precision", "Scale", "SRID"], + /*elements*/["Annotation*"] + ), + UrlRef: schemaElement( + /*attributes*/null, + /*elements*/["Binary*", "Bool*", "Date*", "DateTimeOffset*", "Decimal*", "Duration*", "EnumMember*", "Float*", "Guid*", "Int*", "String*", "TimeOfDay*", "And*", "Or*", "Not*", "Eq*", "Ne*", "Gt*", "Ge*", "Lt*", "Le*", "AnnotationPath*", "Apply*", "Cast*", "Collection*", "If*", "IsOf*", "LabeledElement*", "LabeledElementReference*", "Null*", "NavigationPropertyPath*", "Path*", "PropertyPath*", "Record*", "UrlRef*", "Annotation*"] + ), + + // See http://msdn.microsoft.com/en-us/library/dd541238(v=prot.10) for an EDMX reference. + Edmx: schemaElement( + /*attributes*/["Version"], + /*elements*/["DataServices", "Reference*"], + /*text*/false, + /*ns*/edmxNs + ), + DataServices: schemaElement( + /*attributes*/["m:MaxDataServiceVersion", "m:DataServiceVersion"], + /*elements*/["Schema*"], + /*text*/false, + /*ns*/edmxNs + ), + Reference: schemaElement( + /*attributes*/["Uri"], + /*elements*/["Include*", "IncludeAnnotations*", "Annotation*"] + ), + Include: schemaElement( + /*attributes*/["Namespace", "Alias"] + ), + IncludeAnnotations: schemaElement( + /*attributes*/["TermNamespace", "Qualifier", "TargetNamespace"] + ) + } +}; + + +/** Converts a Pascal-case identifier into a camel-case identifier. + * @param {String} text - Text to convert. + * @returns {String} Converted text. + * If the text starts with multiple uppercase characters, it is left as-is.</remarks> + */ +function scriptCase(text) { + + 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(); +} + +/** Gets the schema node for the specified element. + * @param {Object} parentSchema - Schema of the parent XML node of 'element'. + * @param candidateName - XML element name to consider. + * @returns {Object} The schema that describes the specified element; null if not found. + */ +function getChildSchema(parentSchema, candidateName) { + + 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; +} + +/** Checks whether the specifies namespace URI is one of the known CSDL namespace URIs. + * @param {String} nsURI - Namespace URI to check. + * @returns {Boolean} true if nsURI is a known CSDL namespace; false otherwise. + */ +function isEdmNamespace(nsURI) { + + return nsURI === edmNs1; +} + +/** Parses a CSDL document. + * @param element - DOM element to parse. + * @returns {Object} An object describing the parsed element. + */ +function parseConceptualModelElement(element) { + + var localName = xmlLocalName(element); + var nsURI = xmlNamespaceURI(element); + var elementSchema = schema.elements[localName]; + if (!elementSchema) { + return null; + } + + if (elementSchema.ns) { + if (nsURI !== elementSchema.ns) { + return null; + } + } else if (!isEdmNamespace(nsURI)) { + return null; + } + + var item = {}; + var attributes = elementSchema.attributes || []; + xmlAttributes(element, function (attribute) { + + var localName = xmlLocalName(attribute); + var nsURI = xmlNamespaceURI(attribute); + var value = attribute.value; + + // Don't do anything with xmlns attributes. + if (nsURI === xmlnsNS) { + return; + } + + // Currently, only m: for metadata is supported as a prefix in the internal schema table, + // un-prefixed element names imply one a CSDL element. + var schemaName = null; + var handled = false; + if (isEdmNamespace(nsURI) || nsURI === null) { + schemaName = ""; + } else if (nsURI === odataMetaXmlNs) { + schemaName = "m:"; + } + + if (schemaName !== null) { + schemaName += localName; + + if (contains(attributes, schemaName)) { + item[scriptCase(localName)] = value; + } + } + + }); + + xmlChildElements(element, function (child) { + var localName = xmlLocalName(child); + var childSchema = getChildSchema(elementSchema, localName); + if (childSchema) { + if (childSchema.isArray) { + var arr = item[childSchema.propertyName]; + if (!arr) { + arr = []; + item[childSchema.propertyName] = arr; + } + arr.push(parseConceptualModelElement(child)); + } else { + item[childSchema.propertyName] = parseConceptualModelElement(child); + } + } + }); + + if (elementSchema.text) { + item.text = xmlInnerText(element); + } + + return item; +} + +/** Parses a metadata document. + * @param handler - This handler. + * @param {String} text - Metadata text. + * @returns An object representation of the conceptual model.</returns> + */ +function metadataParser(handler, text) { + + var doc = xmlParse(text); + var root = xmlFirstChildElement(doc); + return parseConceptualModelElement(root) || undefined; +} + + + +exports.metadataHandler = odataHandler.handler(metadataParser, null, xmlMediaType, MAX_DATA_SERVICE_VERSION); + +exports.schema = schema; +exports.scriptCase = scriptCase; +exports.getChildSchema = getChildSchema; +exports.parseConceptualModelElement = parseConceptualModelElement; +exports.metadataParser = metadataParser; \ No newline at end of file
