http.v3.rs.js Maven / Gradle / Ivy
/*
* Copyright (c) 2010-2019 SAP and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP - initial API and implementation
*/
/**
* @module http/v3/rs
* @example
* ```js
* var rs = require("http/v3/rs")
* ```
*/
/************************************
* ResourceMappings builder API *
************************************/
/**
* Compares two arrays for equality by inspecting if they are arrays, refer to the same instance,
* have same length and contain equal components in the same order.
*
* @param {array} source The source array to compare to
* @param {array} target The target array to compare with
* @return {Boolean} true if the arrays are equal, false otherwise
* @private
*/
var arrayEquals = function(source, target){
if(source===target)
return true;
if(!Array.isArray(source) || !Array.isArray(source))
return false;
if(source!==undefined && target===undefined || source===undefined && target!==undefined)
return false;
if(source.length !== target.length)
return false;
for(var i=0; i 1 ? handlers : handlers[0];
};
var buildMethod = function(sMethodName, args){
if(args.length>0){
if(typeof args[0] === 'function')
return this.method(sMethodName).serve(args[0]);
else if(typeof args[0] === 'object')
return this.method(sMethodName, args[0]);
else
throw Error('Invalid argument: Resource.' + sMethodName + ' method first argument must be valid javascript function or configuration object, but instead is ' + (typeof args[0]) + ' ' + args[0]);
} else {
return this.method(sMethodName);
}
return;
};
/**
* Creates a handling specification for the HTTP method "GET".
*
* Same as invoking method("get") on a resource.
*
* @param {Function|Object} [fServeCb|oConfiguration] serve function callback or oConfiguraiton to initilaize the method
*/
Resource.prototype.get = function(){
return buildMethod.call(this, 'get', arguments);
};
/**
* Creates a handling specification for the HTTP method "POST".
*
* Same as invoking method("post") on a resource.
*
* @param {Function|Object} [fServeCb|oConfiguration] serve function callback or oConfiguraiton to initilaize the method
*/
Resource.prototype.post = function(){
return buildMethod.call(this, 'post', arguments);
};
/**
* Creates a handling specification for the HTTP method "PUT".
*
* Same as invoking method("put") on a resource.
*
* @param {Function|Object} [fServeCb|oConfiguration] serve function callback or oConfiguraiton to initilaize the method
*/
Resource.prototype.put = function(){
return buildMethod.call(this, 'put', arguments);
};
/**
* Creates a handling specification for the HTTP method "DELETE".
*
* Same as invoking method("delete") on a resource.
*
* @param {Function|Object} [fServeCb|oConfiguration] serve function callback or oConfiguraiton to initilaize the method
*/
Resource.prototype["delete"] = Resource.prototype.remove = function(){
return buildMethod.call(this, 'delete', arguments);
};
/**
* Finds a ResourceMethod with the given constraints.
*
* @param {String} sMethod the name of the method property of the ResourceMethod in search
* @param {Array} arrConsumesMimeTypeStrings the consumes constraint property of the ResourceMethod in search
* @param {Array} arrProducesMimeTypeStrings the produces constraint property of the ResourceMethod in search
*/
Resource.prototype.find = function(sVerb, arrConsumesMimeTypeStrings, arrProducesMimeTypeStrings){
var hit;
Object.keys(this.cfg).filter(function(sVerbName){
return sVerb === undefined || (sVerb!==undefined && sVerb === sVerbName);
}).forEach(function(sVerbName){
this.cfg[sVerbName].forEach(function(verbHandlerSpec){
if(arrayEquals(verbHandlerSpec.consumes, arrConsumesMimeTypeStrings) && arrayEquals(verbHandlerSpec.produces, arrProducesMimeTypeStrings)){
hit = new ResourceMethod(verbHandlerSpec, this.controller, this, this.mappings);
return;
}
});
if(hit)
return;
}.bind(this));
return hit;
};
/**
* Returns the configuration of this resource.
*
*/
Resource.prototype.configuration = function(){
return this.cfg;
};
/**
* Instructs redirection of the request base don the parameter. If it is a stirng representing URI, the request will be
* redirected to this URI for any method. If it's a function it will be invoked and epxected to return a URI string to redirect to.
*
* @param {Function|String}
*/
Resource.prototype.redirect = function(fRedirector){
if(typeof fRedirector === 'string'){
fRedirector = function(){
return fRedirector;
}
}
return handlerFunction.apply(this, ['redirect', fRedirector]);
};
/**
* Disables the ResourceMethods that match the given constraints
*/
Resource.prototype.disable = function(sVerb, arrConsumesTypeStrings, arrProducesTypeStrings){
Object.keys(this.cfg).filter(function(sVerbName){
return !(sVerb === undefined || (sVerb!==undefined && sVerb === sVerbName));
}).forEach(function(sVerbName){
this.cfg[sVerbName].forEach(function(verbHandlerSpec, i, verbHandlerSpecs){
if(arrayEquals(verbHandlerSpec.consumes, arrConsumesTypeStrings) && arrayEquals(verbHandlerSpec.produces, arrProducesTypeStrings))
verbHandlerSpecs.splice(i, 1);
});
});
return this;
};
/**
* Disables all but 'read' HTTP methods in this resource.
*/
Resource.prototype.readonly = function(){
Object.keys(this.cfg).forEach(function(method){
if(['get','head','trace'].indexOf(method)<0)
delete this.cfg[method];
}.bind(this));
return this;
};
/****************************
* ResourceMappings API *
****************************/
/**
* Constructor function for ResourceMappings instances.
* A ResourceMapping abstracts the mappings between resource URL path templates and their corresponding resource handler
* specifications. Generally, it's used internally by the HttpController exposed by the service factory function adn it is
* where all settings provided by the fluent API ultimately end up. Another utilization of it is as initial configuration,
* which is less error prone and config changes-friendly than constructing JSON manually for the same purpose.
*
* @class
* @param {Object} [oConfiguration]
* @param {HttpController} [controller] The controller instance, for which this ResourceMappings handles configuration
* @returns {ResourceMappings}
* @static
*/
var ResourceMappings = exports.ResourceMappings = function(oConfiguration, controller){
this.resources = {};
if(oConfiguration){
Object.keys(oConfiguration).forEach(function(sPath){
this.resources[sPath] = this.resource(sPath, oConfiguration[sPath], controller);
}.bind(this));
}
if(controller){
this.controller = controller;
this.execute = controller.execute.bind(controller);
}
};
/**
* Creates new Resource object. The second, optional argument can be used to initialize the resource prior to manipulating it.
*
* @param {String} sPath
* @param {Object} [oConfiguration]
*
* @returns {Resource}
*/
ResourceMappings.prototype.path = ResourceMappings.prototype.resourcePath = ResourceMappings.prototype.resource = function(sPath, oConfiguration){
if(this.resources[sPath] === undefined)
this.resources[sPath] = new Resource(sPath, oConfiguration, this.controller, this);
return this.resources[sPath];
};
/**
* Returns the configuration object for this ResourceMappings.
*/
ResourceMappings.prototype.configuration = function(){
var _cfg = {};
Object.keys(this.resources).forEach(function(sPath){
_cfg[sPath] = this.resources[sPath].configuration();
}.bind(this));
return _cfg;
};
/**
* Removes all but GET resource handlers.
*/
ResourceMappings.prototype.readonly = function(){
Object.keys(this.resources).forEach(function(sPath){
this.resources[sPath].readonly();
}.bind(this));
return this;
};
/**
* Disables resource handling specifications mathcing the arguments, effectively removing them from this API.
*/
ResourceMappings.prototype.disable = function(sPath, sVerb, arrConsumes, arrProduces){
Object.keys(this.resources[sPath]).forEach(function(resource){
resource.disable(sVerb, arrConsumes, arrProduces);
}.bind(this));
return this;
};
/**
* Provides a reference to a handler specification matching the supplied arguments.
*/
ResourceMappings.prototype.find = function(sPath, sVerb, arrConsumes, arrProduces){
if(this.resources[sPath]){
var hit = this.resources[sPath].find(sVerb, arrConsumes, arrProduces);
if(hit)
return hit;
}
return;
};
/**************************
* HttpController API *
**************************/
/**
* Constructor function for HttpController instances.
*
* @class
* @param {ResourceMappings|Object} [oMappings] the mappings configuration for this controller.
*
*/
var HttpController = exports.HttpController = function(oMappings){
this.logger = require('log/logging').getLogger('http.rs.controller');
//var xss = require("utils/xss");
var self =this;
var matchRequestUrl = function(requestPath, method, cfg){
var pathDefs = Object.keys(cfg);
var matches = [];
for(var i=0; i0)
reqPathSegments = requestPath.split('/');
else
reqPathSegments = [];
if(pathDefSegments.length === reqPathSegments.length){
var verbHandlers = Object.keys(cfg[pathDef]);
if(verbHandlers && verbHandlers.length>0 && verbHandlers.indexOf(method)>-1){
var pathParams = {};
var resolvedPathDefSegments = pathDefSegments.map(function(pSeg, i){
pSeg = pSeg.trim();
var matcher = pSeg.match(/{(.*?)}/);
if(matcher!==null) {
var param = matcher[1];
pathParams[param] = reqPathSegments[i];
return reqPathSegments[i];
} else {
return pSeg;
}
});
var p = resolvedPathDefSegments.join('/');
if(p === requestPath){
resolvedPath = p;
var match = {w:0, p: resolvedPath, d: pathDef};
if(Object.keys(pathParams).length>0){
match.pathParams = pathParams;
}
matches.push(match);
}
}
}
}
}
//sort matches by weight
matches = matches.sort(function(p, n){
if(n.w === p.w){
//the one with less placeholders wins
var m1= p.d.match(/{(.*?)}/g);
var placeholdersCount1 = m1!==null?m1.length:0;
var m2= n.d.match(/{(.*?)}/g);
var placeholdersCount2 = m2!==null?m2.length:0;
if(placeholdersCount1 > placeholdersCount2){
n.w = n.w+1;
} else if(placeholdersCount1 < placeholdersCount2){
p.w = p.w+1;
}
}
return n.w - p.w;
});
return matches;
};
// content-type, consumes
// accepts, produces
var isMimeTypeCompatible = this.isMimeTypeCompatible = function(source, target){
if(source === target)
return true;
var targetM = target.split('/');
var sourceM = source.split('/');
if((targetM[0] === '*' && targetM[1] === sourceM[1]) || (source[0] === '*' && targetM[1] === sourceM[1]) )
return true;
if((targetM[1] === '*' && targetM[0] === sourceM[0]) || (sourceM[1] === '*' && targetM[0] === sourceM[0]))
return true;
};
var normalizeMediaTypeHeaderValue = this.normalizeMediaTypeHeaderValue = function(sMediaType){
if(sMediaType === undefined || sMediaType === null)
return;
sMediaType = sMediaType.split(',');//convert to array
sMediaType = sMediaType.map(function(mimeTypeEntry){
return mimeTypeEntry.replace('\\','').split(';')[0].trim();//remove escaping, remove quality or other atributes
});
return sMediaType;
};
//find MIME types intersections
var matchMediaType = function(request, producesMediaTypes, consumesMediaTypes){
var isProduceMatched = false;
var acceptsMediaTypes = normalizeMediaTypeHeaderValue(request.getHeader('Accept'));
if(!acceptsMediaTypes || acceptsMediaTypes.indexOf('*/*')>-1){ //output media type is not restricted
isProduceMatched = true;
} else {
var matchedProducesMIME;
if(producesMediaTypes && producesMediaTypes.length){
matchedProducesMIME = acceptsMediaTypes.filter(function(acceptsMediaType) {
return producesMediaTypes.filter(function(producesMediaType){
return isMimeTypeCompatible(acceptsMediaType, producesMediaType)
}).length > 0;
});
isProduceMatched = matchedProducesMIME && matchedProducesMIME.length>0;
}
}
var isConsumeMatched = false;
var contentTypeMediaTypes = normalizeMediaTypeHeaderValue(request.getContentType());
if(!consumesMediaTypes || consumesMediaTypes.indexOf('*')>-1){ //input media type is not restricted
isConsumeMatched = true;
} else {
var matchedConsumesMIME;
if(contentTypeMediaTypes && consumesMediaTypes && consumesMediaTypes.length){
matchedConsumesMIME = contentTypeMediaTypes.filter(function(contentTypeMediaType) {
return consumesMediaTypes.filter(function(consumesMediaType){
return isMimeTypeCompatible(contentTypeMediaType, consumesMediaType);
}).length > 0;
});
isConsumeMatched = matchedConsumesMIME && matchedConsumesMIME.length>0;
}
}
return isProduceMatched && isConsumeMatched;
};
var catchErrorHandler = function(logctx, ctx, err, request, response){
if(ctx.suppressStack){
var detailsMsg = (ctx.errorName || "") + (ctx.errorCode ? " ["+ctx.errorCode+"]": "") + (ctx.errorMessage ? ": "+ctx.errorMessage : "");
this.logger.info('Serving resource[{}], Verb[{}], Content-Type[{}], Accept[{}] finished in error. {}', logctx.path, logctx.method, logctx.contentType, logctx.accepts, detailsMsg);
} else
this.logger.error('Serving resource['+logctx.path+'], Verb['+logctx.method+'], Content-Type['+logctx.contentType+'], Accept['+logctx.accepts+'] finished in error', err);
var httpErrorCode = ctx.httpErrorCode || response.INTERNAL_SERVER_ERROR;
var errorMessage = ctx.errorMessage || (err && err.message);
var errorName = ctx.errorName || (err && err.name);
var errorCode = ctx.errorCode;
this.sendError(httpErrorCode, errorCode, errorName, errorMessage);
};
this.execute = function(request, response){
request = request || require("http/v3/request");
var requestPath = request.getResourcePath();
var method = request.getMethod().toLowerCase();
var _oConfiguration = self.resourceMappings.configuration();
var matches = matchRequestUrl(requestPath, method, _oConfiguration);
var resourceHandler;
if(matches && matches[0]){
var verbHandlers = _oConfiguration[matches[0].d][method];
if(verbHandlers){
resourceHandler = verbHandlers.filter(function(handlerDef){
return matchMediaType(request, handlerDef.produces, handlerDef.consumes);
})[0];
}
}
response = response || require("http/v3/response");
var queryParams = request.getQueryParametersMap() || {};
var acceptsHeader = normalizeMediaTypeHeaderValue(request.getHeader('Accept')) || '[]';
var contentTypeHeader = normalizeMediaTypeHeaderValue(request.getHeader('Content-Type')) || '[]';
var resourcePath = requestPath;
if(resourceHandler){
var ctx = {
"pathParameters": {},
"queryParameters": {}
};
if(matches[0].pathParams){
ctx.pathParameters = matches[0].pathParams;
}
ctx.queryParameters = queryParams;
var noop = function(){};
var _before, _serve, _catch, _finally;
_before = resourceHandler.before || noop;
_serve = resourceHandler.handler || resourceHandler.serve || noop;
_catch = resourceHandler.catch || catchErrorHandler.bind(self, {
path: resourcePath,
method: method.toUpperCase(),
contentType: contentTypeHeader,
accepts: acceptsHeader
});
_finally = resourceHandler.finally || noop;
var callbackArgs = [ctx, request, response, resourceHandler, this];
try{
self.logger.trace('Before serving request for Resource[{}], Method[{}], Content-Type[{}], Accept[{}]', resourcePath, method.toUpperCase(), contentTypeHeader, acceptsHeader);
_before.apply(self, callbackArgs);
if(!response.isCommitted()){
self.logger.trace('Serving request for Resource[{}], Method[{}], Content-Type[{}], Accept[{}]', resourcePath, method.toUpperCase(), contentTypeHeader, acceptsHeader);
_serve.apply(this, callbackArgs);
self.logger.trace('Serving request for Resource[{}], Method[{}], Content-Type[{}], Accept[{}] finished', resourcePath, method.toUpperCase(), contentTypeHeader, acceptsHeader);
}
} catch(err){
try{
callbackArgs.splice(1, 0, err);
_catch.apply(self, callbackArgs);
} catch(_catchErr){
self.logger.error('Serving request for Resource[{}], Method[{}], Content-Type[{}], Accept[{}] error handler threw error', _catchErr);
throw _catchErr;
}
} finally{
HttpController.prototype.closeResponse.call(this);
try{
_finally.apply(self, []);
} catch(_finallyErr){
self.logger.error('Serving request for Resource[{}], Method[{}], Content-Type[{}], Accept[{}] post handler threw error', _finallyErr);
}
}
} else {
self.logger.error('No suitable resource handler for Resource[' + resourcePath + '], Method['+method.toUpperCase()+'], Content-Type['+contentTypeHeader+'], Accept['+acceptsHeader+'] found');
self.sendError(response.BAD_REQUEST, undefined, 'Bad Request', 'No suitable processor for this request.');
}
};
if(oMappings instanceof ResourceMappings){
this.resourceMappings = oMappings;
} else if(typeof oMappings === 'object' || 'undefined') {
this.resourceMappings = new ResourceMappings(oMappings, this);
}
this.resource = this.resourcePath = this.resourceMappings.resourcePath.bind(this.resourceMappings);
//weave-in HTTP method-based factory functions - shortcut for service().resource(sPath).method
['get','post','put','delete','remove','method'].forEach(function(sMethodName){
this[sMethodName] = function(sPath, sVerb, arrConsumes, arrProduces){
if(arguments.length < 1)
throw Error('Insufficient arguments provided to HttpController method ' + sMethodName + '.');
if(sPath === undefined)
sPath = "";
var resource = this.resourceMappings.find(sPath, sVerb, arrConsumes, arrProduces) || this.resourceMappings.resource(sPath);
resource[sMethodName]['apply'](resource, Array.prototype.slice.call(arguments, 1));
return this;
}.bind(this);
}.bind(this));
};
HttpController.prototype.mappings = function() {
return this.resourceMappings;
};
HttpController.prototype.sendError = function(httpErrorCode, applicationErrorCode, errorName, errorDetails) {
var request = require("http/v3/request");
var clientAcceptMediaTypes = this.normalizeMediaTypeHeaderValue(request.getHeader('Accept')) || ['application/json'];
var isHtml = clientAcceptMediaTypes.some(function(acceptMediaType){
return this.isMimeTypeCompatible( '*/html', acceptMediaType);
}.bind(this));
var response = require("http/v3/response");
response.setStatus(httpErrorCode || response.INTERNAL_SERVER_ERROR);
if(isHtml){
var message = errorName + (applicationErrorCode!==undefined ? '['+applicationErrorCode+']' : '') + (errorDetails ? ': ' + errorDetails : '');
response.sendError(httpErrorCode || response.INTERNAL_SERVER_ERROR, message);
} else {
var body = {
"code": applicationErrorCode,
"error": errorName,
"details": errorDetails
};
response.setHeader("Content-Type", "application/json");
response.print(JSON.stringify(body, null, 2));
}
this.closeResponse();
};
HttpController.prototype.closeResponse = function(){
var response = require("http/v3/response");
response.flush();
response.close();
};
/****************************
* http/v3/rs Module API *
****************************/
/**
* Creates a service, optionally initialized with oMappings
*
* @param {Object|ResourceMappings} [oMappings] configuration object or configuration builder with configuration() getter function
*
*/
exports.service = function(oConfig){
var config;
if(oConfig!==undefined){
if(typeof oConfig === 'object' || oConfig instanceof ResourceMappings){
config = oConfig;
} else {
throw Error('Illegal argument type: oConfig['+(typeof oConfig)+']');
}
}
var controller = new HttpController(config);
return controller;
};
© 2015 - 2025 Weber Informatics LLC | Privacy Policy