@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
416 lines (347 loc) • 15.9 kB
JavaScript
;
var XMLWriter = require('xml-writer');
var InternalError = require('./../utils/errors/internalError');
var typeConverter = require('./../utils/typeConverter');
var dbSegment = require('./../db/dbSegment');
var Http404_NotFound = require('./../utils/errors/http/notFound');
var associationsUtil = require('./../utils/associations');
var NotImplemented = require('./../utils/errors/http/notImplemented');
module.exports = AtomSerializer;
function AtomSerializer(context) {
if(!context) {
throw new InternalError('Context must be specified.');
}
this._context = context;
this._xmlWriter = new XMLWriter();
}
function startDocument(xmlWriter) {
xmlWriter.startDocument("1.0", "utf-8", true);
}
/**
* Serializes OData feed represented as the dbSegment input parameter.
* @param dbSegment - DB segment, which represents the OData feed, which should be serialized.
* @param inlineCount - inlineCount value for the feed. This parameter must be provided when $inlinecount query option
* has been specified with value "allpages".
* @returns XML string, which represents the Atom Feed for the specified dbSegment.
*/
AtomSerializer.prototype.serializeFeed = function(dbSegment, inlineCount) {
startDocument(this._xmlWriter);
var feedInfo = createRootFeedInfo(this._context, dbSegment);
_serializeFeed(this._context, this._xmlWriter, feedInfo, dbSegment, inlineCount);
this._xmlWriter.endDocument();
return this._xmlWriter.toString();
};
/**
* Serializes OData entry represented as the dbSegment input parameter.
* @param dbSegment - DB segment, which represents the OData entry, which should be serialized.
* @returns XML string, which represents the Atom Entry for the specified dbSegment.
*/
AtomSerializer.prototype.serializeEntity = function(dbSegment) {
startDocument(this._xmlWriter);
var entity = getEntityFromSegment(dbSegment);
var converters = createConverters(dbSegment);
_serializeEntry(this._context, this._xmlWriter, dbSegment, entity, converters, createNamespaceAttributes(this._context));
this._xmlWriter.endDocument();
return this._xmlWriter.toString();
};
/**
* Serializes OData property represented as the dbSegment input parameter.
*/
AtomSerializer.prototype.serializeProperty = function() {
throw new NotImplemented("Atom format is not supported for property serialization.", this._context);
};
function _serializeFeed(context, xmlWriter, feedInfo, segment, inlineCount) {
xmlWriter.startElement("feed");
if(feedInfo.attributes) {
addAttributes(xmlWriter, feedInfo.attributes);
}
createFeedHeader(context, xmlWriter, feedInfo);
addInlineCount(xmlWriter, inlineCount);
var converters = createConverters(segment);
for (var i = 0; i < feedInfo.entries.length; i++) {
_serializeEntry(context, xmlWriter, segment, feedInfo.entries[i], converters);
}
xmlWriter.endElement();
}
function addInlineCount(xmlWriter, inlineCount) {
if(inlineCount) {
xmlWriter.writeElement("m:count", inlineCount);
}
}
/**
* Creates top XML elements for the OData feed element.
* @param context - OData context.
* @param xmlWriter - XML writer, which should be used during the serialization.
* @param feedInfo - object, which encapsulates the required info about the OData feed.
*/
function createFeedHeader(context, xmlWriter, feedInfo) {
createTitleElement(xmlWriter, feedInfo.title);
xmlWriter.writeElement('id', context.uriTree.baseUrl + feedInfo.href);
// $SAPINFO 'updated' element is not added to the output for backwards compatibility with XS1
// xmlWriter.writeElement('updated', new Date().toString());
createAuthorElement(xmlWriter);
createLink(xmlWriter, "self", feedInfo.title, feedInfo.href);
}
function createLink(xmlWriter, relation, title, href) {
xmlWriter.startElement('link');
xmlWriter.writeAttribute('rel', relation);
xmlWriter.writeAttribute('title', title);
xmlWriter.writeAttribute('href', href);
xmlWriter.endElement();
}
function createTitleElement(xmlWriter, title) {
title = title || "";
xmlWriter.startElement('title');
xmlWriter.writeAttribute('type', 'text');
xmlWriter.text(title);
xmlWriter.endElement();
}
function createAuthorElement(xmlWriter) {
xmlWriter.startElement("author");
xmlWriter.startElement("name").endElement();
xmlWriter.endElement();
}
function _serializeEntry(context, xmlWriter, segment, entity, converters, attributes) {
xmlWriter.startElement("entry");
if(attributes) {
addAttributes(xmlWriter, attributes);
}
createEntryHeader(context, xmlWriter, segment, entity);
serializeNavigationProperties(context, xmlWriter, segment, entity);
serializePrimitiveProperties(context, xmlWriter, segment, entity, converters);
xmlWriter.endElement();
}
/**
* Retrieves OData entity from the specified dbSegment.
* @param dbSegment
* @returns JSON object containing the entity data or undefined, if the specified dbSegment does not contain the
* required entity.
*/
function getEntityFromSegment(dbSegment) {
var entities = dbSegment.getRowsWithGenKey();
if(!entities || !entities.length || entities.length === 0) {
throw new Http404_NotFound('Entity not found.');
}
return entities[0];
}
function serializeNavigationProperties(context, xmlWriter, segment, entity) {
for (var i = 0; i < segment._SelectedNavigations.length; i++) {
var selectedNavigation = segment._SelectedNavigations[i];
var navigationSegment = segment.getRelevantNavigationSegments()[selectedNavigation];
serializeNavigationProperty(context, xmlWriter, segment, navigationSegment, entity, selectedNavigation);
}
}
/**
* Returns true if the navigation property with the specified navPropertyName and belonging to the specified
* navigationSegment is a collection (i.e. target EDM multiplicity is *). The method returns false otherwise.
*/
function isNavigationCollection(context, parentSegment, navPropertyName, navigationSegment) {
if(navigationSegment) {
return navigationSegment.isCollection;
}
var navigation = parentSegment.entityType.getNavigation(navPropertyName);
var association = context.gModel.getAssociation(navigation.association);
var targetEnd = associationsUtil.getTargetEnd(navigation, association, parentSegment.entityType);
return targetEnd.multiplicity === "*";
}
function serializeNavigationProperty(context, xmlWriter, parentSegment, navigationSegment, parentEntity, navPropertyName) {
var isCollection = isNavigationCollection(context, parentSegment, navPropertyName, navigationSegment);
startNavigationLink(xmlWriter, parentSegment, navPropertyName, isCollection, parentEntity);
if(navigationSegment && navigationSegment.isExpand && navigationSegment.kind === dbSegment.DBS_Navigation) {
xmlWriter.startElement("m:inline");
if (isCollection) {
serializeNavigationCollection(context, xmlWriter, navPropertyName, navigationSegment, parentSegment, parentEntity);
} else {
serializeNavigationEntity(context, xmlWriter, navigationSegment, parentEntity["1row"]);
}
xmlWriter.endElement();
}
xmlWriter.endElement();
}
/**
* Creates 'info' object, which is required for Atom feed serialization for the specified navigation property.
* @param navPropertyName - name of the navigation property.
* @param navigationSegment - DB segment, which represents the navigation property.
* @param parentSegment - DB segment, which is a parent segment for the DB segment, which represents the navigation
* property.
* @param parentEntity - OData entity, which contains the navigation property.
* @returns JSON object containing all the required info for the serialization of the multi-valued navigation property.
*/
function createFeedInfoForNavProperty(navPropertyName, navigationSegment, parentSegment, parentEntity) {
var navigationEntities = getNavigationEntities(navigationSegment, parentEntity['1row']);
var parentEntityHref = createEntityRelativeUrl(parentSegment, parentEntity);
var feedHref = parentEntityHref + "/" + navPropertyName;
return createFeedInfo(navPropertyName, feedHref, navigationEntities);
}
function serializeNavigationCollection(context, xmlWriter, navPropertyName, navigationSegment, parentSegment, parentEntity) {
var feedInfo = createFeedInfoForNavProperty(navPropertyName, navigationSegment, parentSegment, parentEntity);
if(feedInfo.entries.length === 0) {
return;
}
_serializeFeed(context, xmlWriter, feedInfo, navigationSegment);
}
function startNavigationLink(xmlWriter, parentSegment, navPropertyName, isNavigationCollection, parentEntity) {
var type = isNavigationCollection ? "feed" : "entry";
xmlWriter.startElement('link');
xmlWriter.writeAttribute('rel', "http://schemas.microsoft.com/ado/2007/08/dataservices/related/" + navPropertyName);
xmlWriter.writeAttribute('type', "application/atom+xml;type=" + type);
xmlWriter.writeAttribute('title', navPropertyName);
var parentEntityUrl = createEntityRelativeUrl(parentSegment, parentEntity);
xmlWriter.writeAttribute('href', parentEntityUrl + '/' + navPropertyName);
}
/**
* Retrieves OData entities for the navigation property, which is represented by the specified navigationSegment.
* @param navigationSegment - DB segment, which represents the navigation property.
* @param parentId - ID of the parent entity. i.e. of the entity, which contains the navigation property.
*/
function getNavigationEntities(navigationSegment, parentId) {
var entities = navigationSegment.getRowsWithGenKey();
var navigationEntities = [];
var navigationEntity;
while((navigationEntity = entities[navigationSegment.sql.readPosition]) && (navigationEntity['0row'] === parentId)) {
navigationEntities.push(navigationEntity);
navigationSegment.sql.readPosition++;
}
return navigationEntities;
}
function serializeNavigationEntity(context, xmlWriter, navigationSegment, parentId) {
var entity = getNavigationEntity(navigationSegment, parentId);
if(!entity) {
return;
}
var converters = createConverters(navigationSegment);
_serializeEntry(context, xmlWriter, navigationSegment, entity, converters);
}
/**
* Retrieves OData entity for the navigation property, which is represented by the specified navigationSegment.
* @param navigationSegment - DB segment for the navigation property.
* @param parentId - ID of the parent OData entity.
*/
function getNavigationEntity(navigationSegment, parentId) {
var entities = navigationSegment.getRowsWithGenKey();
for(var i = 0; i < entities.length; i++) {
var entity = entities[i];
if(entity["0row"] === parentId) {
return entity;
}
}
}
function serializePrimitiveProperties(context, xmlWriter, segment, entity, converters) {
writeCategory(context, xmlWriter, segment);
xmlWriter.startElement("content");
xmlWriter.writeAttribute("type", "application/xml");
xmlWriter.startElement("m:properties");
var selectedProperties = segment.getSelectedPropsWithGenKey();
for(var i = 0; i < selectedProperties.length; i++) {
var propertyName = selectedProperties[i];
var propertyValue = converters[i](entity[propertyName]);
var propertyType = getEdmPropertyType(segment, propertyName);
serializeEntityProperty(xmlWriter, propertyName, propertyType, propertyValue);
}
xmlWriter.endElement();
xmlWriter.endElement();
}
function writeCategory(context, xmlWriter, segment) {
xmlWriter.startElement("category");
var term = context.gModel.getNamespace() + "." + segment.entityType.name + "Type";
xmlWriter.writeAttribute("term", term);
xmlWriter.writeAttribute("scheme", "http://schemas.microsoft.com/ado/2007/08/dataservices/scheme");
xmlWriter.endElement();
}
function getEdmPropertyType(segment, propertyName) {
var dbType = getPropertyDBType(segment, propertyName);
return typeConverter.dbTypeNameToODataTypeName[dbType];
}
function getPropertyDBType(segment, propertyName) {
var entityType = segment.entityType;
var property = entityType.propertiesMap[propertyName];
if(property) {
return property.DATA_TYPE_NAME;
}
// if there is no property with the required name in the propertiesMap, then check whether the required property
// represents a generated key.
if(entityType.keys.generatedKey === propertyName) {
// VARCHAR is always used as the type for the generated keys.
return "VARCHAR";
}
throw new Error(propertyName + " is an invalid entity type property.");
}
function serializeEntityProperty(xmlWriter, propertyName, propertyType, propertyValue) {
propertyName = "d:" + propertyName;
xmlWriter.startElement(propertyName);
xmlWriter.writeAttribute("m:type", propertyType);
/* jshint eqnull:true */
if(propertyValue == null) {
xmlWriter.writeAttribute("m:null", "true");
} else {
xmlWriter.text(propertyValue.toString());
}
xmlWriter.endElement();
}
/**
* Creates top level XML elements for the Atom entry element.
*/
function createEntryHeader(context, xmlWriter, segment, entry) {
var entityUrl = createEntityAbsoluteUrl(context, segment, entry);
xmlWriter.writeElement("id", entityUrl);
createTitleElement(xmlWriter);
createAuthorElement(xmlWriter);
createEntityEditLink(xmlWriter, segment, entry);
}
function createEntityEditLink(xmlWriter, segment, entity) {
var entityRelativeUrl = createEntityRelativeUrl(segment, entity);
createLink(xmlWriter, "edit", segment.entityType.name, entityRelativeUrl);
}
function createEntityAbsoluteUrl(context, segment, entity) {
return context.uriTree.baseUrl + createEntityRelativeUrl(segment, entity);
}
function createEntityRelativeUrl(segment, entity) {
var relativeUrl = segment.entityType.name;
var keysProperties = segment.getKeysProperties();
relativeUrl += '(';
relativeUrl += keysProperties.map(toValues).join(',');
return relativeUrl + ')';
function toValues(keyProperty, index, array) {
var value = typeConverter.serializeDbValueToUriLiteral(entity[index.toString()]/*keys[index]*/, keyProperty);
if (array.length === 1) {
return value;
}
return keyProperty.COLUMN_NAME + '=' + value;
}
}
function createConverters(segment) {
return segment.getSelectedPropsWithGenKey().map(function toConverter(property) {
return segment.entityType.converterMapToXMLPayload[property];
});
}
function createRootFeedInfo(context, segment) {
var feedTitle = getLastUriSegment(context);
var feedInfo = createFeedInfo(feedTitle, feedTitle, segment.getRowsWithGenKey());
feedInfo.attributes = createNamespaceAttributes(context);
return feedInfo;
}
function getLastUriSegment(context) {
var uriSegments = context.uriTree.segments.decoded;
return uriSegments[uriSegments.length - 1];
}
function createFeedInfo(title, href, entries) {
var feedInfo = {};
feedInfo.title = title;
feedInfo.href = href;
feedInfo.entries = entries;
return feedInfo;
}
function createNamespaceAttributes(context) {
return {
"xml:base" : context.uriTree.baseUrl,
"xmlns:d" : "http://schemas.microsoft.com/ado/2007/08/dataservices",
"xmlns:m" : "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
"xmlns" : "http://www.w3.org/2005/Atom"
};
}
function addAttributes(xmlWriter, attributes) {
/*jshint forin: false */
for(var attributeName in attributes) {
xmlWriter.writeAttribute(attributeName, attributes[attributeName]);
}
}