@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
429 lines (381 loc) • 14 kB
JavaScript
"use strict";
var StateMaschine = require('./../utils/stateMaschine');
var PreconditionFailedError = require('./../utils/errors/http/preconditionFailed');
var PreconditionRequiredError = require('./../utils/errors/http/preconditionRequired');
var NotModifiedError = require('./../utils/errors/http/notModified');
var BadRequestError = require('./../utils/errors/http/badRequest');
var NotFoundError = require('./../utils/errors/http/notFound');
var InternalError = require('./../utils/errors/internalError');
var uriTypes = require('./../uri/uriType');
function debug(context, text, arg1) {
if (context && context.logger && context.logger.debug) {
context.logger.debug('conditionalHttpHandler',[text, arg1].join(" "));
}
}
module.exports = ConditionalHttpHandler;
/**
*Constructor. Creates the internal states also.
*
*@param {object} context Current xsodata context object
*/
function ConditionalHttpHandler(context) {
var states = createStates();
this._stateMaschine = new StateMaschine(states, context);
this.on = this._stateMaschine.on.bind(this._stateMaschine);
}
/**
*Static helper method for usage in async.waterfall() to enable conditional http handling.
*
*@param {object} context Current xsodata context object
*@param {function} asyncDone Callback called on finish or error. Expected signature is
* function(error, context){}
*/
ConditionalHttpHandler.processConditionalRequest =
function processConditionalRequest(context, asyncDone) {
var handler = new ConditionalHttpHandler(context);
handler.on("error", function (error) {
asyncDone(error, context);
});
handler.on("final", function () {
asyncDone(null, context);
});
handler.initialize();
};
/**
* Initializes and starts the inner statemaschine. This triggers the conditional http handling.
*/
ConditionalHttpHandler.prototype.initialize = function initialize() {
return this._stateMaschine.initialize();
};
/**
* Returns the current statemaschine. The statemaschine handles the conditional http handling by
* transitioning from state to state.
*/
ConditionalHttpHandler.prototype.getStateMaschine = function getStateMaschine() {
return this._stateMaschine;
};
/**
* Creates the internal states needed by the internal statemaschine. These states represent
* the conditional http handling.
* For state documentation see Diagramm in folder "internal/documentation/
* BLI70 - BLI70 - Conditional http handling/Activity etag handling Process request v5.pdf".
*/
function createStates() {
return {
initializeWith: "Start",
states: {
"Start": {
action: function () {
debug(this.getExternalContext(),"Starting conditional http handling...");
debug(this.getExternalContext(), "Processing request method: ",
this.getExternalContext().request.method);
debug(this.getExternalContext(), "Start");
this.next("If-Match exists");
}
},
"If-Match exists": {
action: function () {
if (getHeader(this.getExternalContext(), "If-Match")){
debug(this.getExternalContext(),"If-Match exists: ", true);
this.next("Is conditional request supported 1");
} else {
debug(this.getExternalContext(),"If-Match exists: ", false);
this.next("Is modification request");
}
}
},
"Is conditional request supported 1": {
// We need more than one kind of this state because there will be different outcomes
action: function () {
var isSupported = isConditionalRequestSupported(this.getExternalContext());
debug(this.getExternalContext(),"Is conditional request supported 1: ", isSupported);
if (isSupported === true) {
this.next("Is concurrent entity 1");
} else {
this.next("400 Bad request");
}
}
},
"Is conditional request supported 2": {
action: function () {
// We need more than one kind of this state because there will be different outcomes
var isSupported = isConditionalRequestSupported(this.getExternalContext());
debug(this.getExternalContext(),"Is conditional request supported 2: ", isSupported);
if (isSupported) {
this.next("428 Precondition required");
} else {
this.next("process request");
}
}
},
"Is conditional request supported 3": {
action: function () {
// We need more than one kind of this state because there will be different outcomes
var isSupported = isConditionalRequestSupported(this.getExternalContext());
debug(this.getExternalContext(),"Is conditional request supported 3: ", isSupported);
if (isSupported) {
this.next("Is concurrent entity 3");
} else {
this.next("400 Bad request");
}
}
},
"Is concurrent entity 1": {
action: function () {
// We need more than one kind of this state because there will be different outcomes
var hasToken = hasConcurrencyToken(this.getExternalContext());
debug(this.getExternalContext(),"Is concurrent entity 1: ", hasToken);
if (hasToken === true) {
this.next("Evaluate If-Match");
} else {
this.next("400 Bad request");
}
}
},
"Is concurrent entity 2": {
action: function () {
// We need more than one kind of this state because there will be different outcomes
var hasToken = hasConcurrencyToken(this.getExternalContext());
debug(this.getExternalContext(),"Is concurrent entity 2: ", hasToken);
if (hasToken === true) {
this.next("Is conditional request supported 2");
} else {
this.next("If-None-Match exists");
}
}
},
"Is concurrent entity 3": {
action: function () {
// We need more than one kind of this state because there will be different outcomes
var hasToken = hasConcurrencyToken(this.getExternalContext());
debug(this.getExternalContext(),"Is concurrent entity 3: ", hasToken);
if (hasToken === true) {
this.next("Evaluate If-None-Match");
} else {
this.next("400 Bad request");
}
}
},
"Evaluate If-Match": {
action: function () {
var etagTemp,
ifMatchHeader = getHeader(this.getExternalContext(), "If-Match");
debug(this.getExternalContext(),"Evaluate If-Match");
debug(this.getExternalContext()," If-Match header: ", ifMatchHeader);
if(ifMatchHeader === "*"){
return this.next("If-None-Match exists");
} else {
getEtag.call(this, this.getExternalContext(), function (error, etag) {
if (error) {
return this.next("UnexpectedError", error);
}
if (etag === undefined) {
return this.next("404 Not found");
}
etagTemp = buildEtagHeaderValue(etag);
debug(this.getExternalContext()," entity etag: ", etagTemp);
if (etagTemp === ifMatchHeader) {
this.next("If-None-Match exists");
} else {
this.next("412 Precondition failed");
}
}.bind(this));
}
}
},
"Evaluate If-None-Match": {
action: function () {
var etagTemp,
ifNoneMatchHeader = getHeader(this.getExternalContext(), "If-None-Match");
debug(this.getExternalContext(),"Evaluate If-None-Match");
debug(this.getExternalContext()," If-None-Match header: ", ifNoneMatchHeader);
if(ifNoneMatchHeader === "*"){
if (this.getExternalContext().request.method === "GET") {
this.next("304 Not modified");
} else {
this.next("412 Precondition failed");
}
} else {
getEtag.call(this, this.getExternalContext(), function (error, etag) {
if (error) {
return this.next("UnexpectedError", error);
}
if (etag === undefined) {
return this.next("404 Not found");
}
etagTemp = buildEtagHeaderValue(etag);
debug(this.getExternalContext()," entity etag: ", etagTemp);
if (etagTemp === ifNoneMatchHeader) {
if (this.getExternalContext().request.method === "GET") {
this.next("304 Not modified");
} else {
this.next("412 Precondition failed");
}
} else {
this.next("process request");
}
}.bind(this));
}
}
},
"If-None-Match exists": {
action: function () {
if (getHeader(this.getExternalContext(), "If-None-Match")){
debug(this.getExternalContext(),"If-None-Match exists: ", true);
this.next("Is conditional request supported 3");
} else {
debug(this.getExternalContext(),"If-None-Match exists: ", false);
this.next("process request");
}
}
},
"Is modification request": {
action: function () {
var isModification = isModificationRequest(this.getExternalContext());
debug(this.getExternalContext(),"Is modification request: ", isModification);
if (isModification){
this.next("Is concurrent entity 2");
} else {
this.next("If-None-Match exists");
}
}
},
"process request": {
action: function () {
debug(this.getExternalContext(),"process request");
debug(this.getExternalContext(),"... leaving conditional http handling");
this.setFinal(true);
}
},
"400 Bad request": {
action: function () {
debug(this.getExternalContext(),"400 Bad request");
throw new BadRequestError("Bad request", this.getExternalContext());
}
},
"412 Precondition failed": {
action: function () {
debug(this.getExternalContext(),"412 Precondition failed");
throw new PreconditionFailedError("Precondition failed",
this.getExternalContext());
}
},
"428 Precondition required": {
action: function () {
debug(this.getExternalContext(),"428 Precondition required");
throw new PreconditionRequiredError("Precondition required",
this.getExternalContext());
}
},
"304 Not modified": {
action: function () {
debug(this.getExternalContext(),"304 Not modified");
throw new NotModifiedError("Not modified",
this.getExternalContext());
}
},
"404 Not found": {
action: function () {
debug(this.getExternalContext(),"404 Not found");
throw new NotFoundError("404 Not found",
this.getExternalContext());
}
},
"UnexpectedError": {
action: function (source, error) {
debug(this.getExternalContext(),"UnexpectedError occured", error);
var context = this.getExternalContext();
throw new InternalError("Unexpected error", context, error);
}
}
}
};
}
/**
* Returns the http header value found in context for given name parameter.
*
*@param {object} context Current xsodata context object
*@param {string} name The name of the header field to get the value for
*@return {string} The value of the header or null
*/
function getHeader(context, name) {
if (!context.request.headers) {
return null;
}
if (context.request.headers[name]) {
return context.request.headers[name];
}
if (context.request.headers[name.toLowerCase()]) {
return context.request.headers[name.toLowerCase()];
}
return null;
}
/**
* Determines if the http request is any kind of modification request. A modification request
* is one of MERGE, DELETE, or PUT. POST isn't because it is not allowed in this context of
* conditional http hanndling.
*
*@param {object} context Current xsodata context object
*@return {boolean} true if the request is kind of modification else false
*/
function isModificationRequest(context) {
var method = context.request.method;
return method === "PUT" || method === "DELETE";
}
/**
* Determines if the requested entity resource is a current entity or not. If the requested
* entity does does not support currcurrent mode by definition this method returns false.
*
*@param {object} context Current xsodata context object
*@return {boolean} true if the entity is a concurrent entity else false
*/
function hasConcurrencyToken(context) {
// if (!context.oData.dbSegment || !context.oData.dbSegment.entityType) {
// return false;
// }
return context.oData.dbSegment.entityType.hasConcurrencyToken();
}
/**
* Returns the generated etag for the requested entity. The etag value is generated on-the-fly
* and can be used to evalute If-Match oder If-None-Match header. Etag has the form of "ABCDEF....".
*
*@param {object} context Current xsodata context object
*@param {function} callback Callback called on etag generation. Expected signature is
* function(error, etag){}
*/
function getEtag(context, callback) {
/*jshint validthis:true */
var self = this;
if (self._cachedEtag) {
callback(null, self._cachedEtag);
} else {
context.oData.dbSegmentLast.getETag(context, function (error, etag) {
self._cachedEtag = etag;
callback(error, etag);
});
}
}
/**
* Determines if the current request URI is one of the allowed odata URI's for conditional http
* handling and if the current http request is not a POST request.
*
*@param {object} context Current xsodata context object
*@return {boolean} true if the request is supported else false
*/
function isConditionalRequestSupported(context) {
var uriType = context.uriTree.uriType;
var isSupported = uriType === uriTypes.URI2 || uriType === uriTypes.URI5A ||
uriType === uriTypes.URI5B || uriType === uriTypes.URI6A;
isSupported = context.request.method !== "POST" && isSupported;
return isSupported;
}
/**
* Builds the header equivalent ETag http header value. Meaning that if an etag 'abc' is provided
* this method returns 'W/"abc"' which is equivalent to the http weak ETag header specification.
*
* @param etag {String} The raw etag value to wrap
* @returns {string} The http header equivalent value
*/
function buildEtagHeaderValue(etag) {
return 'W/"' + etag + '"';
}