
META-INF.dirigible.http.v4.rs.js Maven / Gradle / Ivy
/*
* Copyright (c) 2022 SAP SE or an SAP affiliate company and Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Eclipse Dirigible contributors
* SPDX-License-Identifier: EPL-2.0
*/
/**
* @module http/v4/rs
* @example
* ```js
* var rs = require("http/v4/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){
let 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(){
const _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]){
const 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");
const self = this;
const matchRequestUrl = function (requestPath, method, cfg) {
var pathDefs = Object.keys(cfg);
var matches = [];
for (var i = 0; i < pathDefs.length; i++) {
var pathDef = pathDefs[i];
var resolvedPath;
if (pathDef === requestPath) {
resolvedPath = pathDef;
matches.push({w: 1, p: resolvedPath, d: pathDef});
} else {
var pathDefSegments = pathDef.split('/');
var reqPathSegments;
if (requestPath.trim().length > 0)
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
const 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;
};
const 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
const 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;
};
const 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/v4/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/v4/response");
const queryParams = request.getQueryParametersMap() || {};
const acceptsHeader = normalizeMediaTypeHeaderValue(request.getHeader('Accept')) || '[]';
const contentTypeHeader = normalizeMediaTypeHeaderValue(request.getHeader('Content-Type')) || '[]';
const resourcePath = requestPath;
if(resourceHandler){
const ctx = {
"pathParameters": {},
"queryParameters": {}
};
if(matches[0].pathParams){
ctx.pathParameters = matches[0].pathParams;
}
ctx.queryParameters = queryParams;
const noop = function () {
};
let _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;
const 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 = "";
const 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) {
const request = require("http/v4/request");
const clientAcceptMediaTypes = this.normalizeMediaTypeHeaderValue(request.getHeader('Accept')) || ['application/json'];
const isHtml = clientAcceptMediaTypes.some(function (acceptMediaType) {
return this.isMimeTypeCompatible('*/html', acceptMediaType);
}.bind(this));
const response = require("http/v4/response");
response.setStatus(httpErrorCode || response.INTERNAL_SERVER_ERROR);
if(isHtml){
const message = errorName + (applicationErrorCode !== undefined ? '[' + applicationErrorCode + ']' : '') + (errorDetails ? ': ' + errorDetails : '');
response.sendError(httpErrorCode || response.INTERNAL_SERVER_ERROR, message);
} else {
const 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(){
const response = require("http/v4/response");
response.flush();
response.close();
};
/****************************
* http/v4/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){
let config;
if(oConfig!==undefined){
if(typeof oConfig === 'object' || oConfig instanceof ResourceMappings){
config = oConfig;
} else {
throw Error('Illegal argument type: oConfig['+(typeof oConfig)+']');
}
}
return new HttpController(config);
};
© 2015 - 2025 Weber Informatics LLC | Privacy Policy