1 
  2 /******************************************************************************************************************************
  3  *  (c) ITSV GmbH - http://www.itsv.at/
  4  *
  5  *  Software Operations
  6  *    Zentrale Daten und Services
  7  *    Wolfgang Scherer
  8  *
  9  *  CCDB - Competence Center Controlling Database
 10  *
 11  *	MODULE:		ccdb.js
 12  *
 13  *	SEE ALSO: 	http://zpvwiki.sozvers.at/wiki/ZPV_Controlling_Datenbank#App
 14  *
 15  *	HISTORY:
 16  *	VERSION	DATE		CHANGES
 17  *	0.1		08.08.2015	created with RANGE 
 18  *	0.2		15.08.2015	multi-database with selection page
 19  *						/show_cpu graphically with D3-Library
 20  *						generic /query and /sequence
 21  *	0.3		09.09.2015	replace /show_cpu with query configuration
 22  *						load_xls with XLSX library
 23  *  0.4		06.10.2015	session management, LOGIN/password management, authorization rights check
 24  *	0.5		28.10.2015	remove /sequence, replaced by querytype='sequence'
 25  *						query result format	'hhlist'
 26  *	0.6		04.11.2015	DBrequest new attribute 'reqid', identifies the request in order to be able to substitute 
 27  *						other SQL statements for other databases (current SQL assumes oracle RDBMS with oracledb nodejs driver)
 28  *						relocate auxiliary functions to aux/auxiliary.js
 29  *						relocate modular business functions to functions/functions.js
 30  *	0.7		11.11.2015	re-factor load_csv and load_xls to a common load_file
 31  *  0.71	16.11.2015	allow options in SEQUENCE steps, sparated from QUERY-Name of step by ':'
 32  *						clean-up DSEQUENCE by processing all steps in the same state and by the same code
 33  *  0.8.099 20.11.2015	make /dquery a POST action
 34  *						start changing to query parameter processing 0.8:
 35  *							each query has a 'paramtype', no more 'paramnames' or 'attributenames'
 36  *							the paramtype can have multiple attributes, there go the query parameters !!
 37  *							param form processing will benefit from this 
 38  *
 39  ******************************************************************************************************************************/
 40 
 41  
 42  
 43 
 44 var script_version = "0.8.102";
 45 var st = new Date();								// force Date.prototype to be created
 46 
 47 // format date as YYYYMMDDhhmmss.ttt to create a unique sortable time string up to 1 msec in precision
 48 function dateTimStr(theDate) {
 49    var yyyy = theDate.getFullYear().toString();
 50    var mm = "00"+(theDate.getMonth()+1).toString(); // getMonth() is zero-based
 51    var dd  = "00"+theDate.getDate().toString();
 52    var hh = "00"+theDate.getHours().toString();
 53    var min = "00"+theDate.getMinutes().toString();
 54    var sec = "00"+theDate.getSeconds().toString();
 55    var mil = "000"+theDate.getMilliseconds().toString();
 56    return yyyy + "-" + mm.substring(mm.length-2) + "-" + dd.substring(dd.length-2) + "_" +
 57           hh.substring(hh.length-2) + ":" + min.substring(min.length-2) + ":" + sec.substring(sec.length-2) + "." + mil.substring(mil.length-3); // padding
 58   };
 59 // enhance Date by .timstr()
 60 Date.prototype.timstr = function() {
 61    var yyyy = this.getFullYear().toString();
 62    var mm = "00"+(this.getMonth()+1).toString(); // getMonth() is zero-based
 63    var dd  = "00"+this.getDate().toString();
 64    var hh = "00"+this.getHours().toString();
 65    var min = "00"+this.getMinutes().toString();
 66    var sec = "00"+this.getSeconds().toString();
 67    var mil = "000"+this.getMilliseconds().toString();
 68    return yyyy + mm.substring(mm.length-2) + dd.substring(dd.length-2) + 
 69           hh.substring(hh.length-2) + min.substring(min.length-2) + sec.substring(sec.length-2) +mil.substring(mil.length-3); // padding
 70   };
 71 // node.js internal modules
 72 
 73 var https = require('https');
 74 var http = require('http');
 75 var util = require('util');
 76 var crypto = require('crypto');
 77 var path = require('path');
 78 var os = require('os');
 79 
 80 // additional modules installed via NPM
 81 
 82 var querystring = require('querystring');
 83 var express = require('express');
 84 var express = require('express');
 85 var session = require('express-session');
 86 // var oracledb = require("oracledb");
 87 var mysqldb = require("mysql");
 88 var favicon = require('serve-favicon');
 89 var multer = require('multer');
 90 var bodyParser = require('body-parser');
 91 var formidable = require('formidable');
 92 var flow = require('flow');
 93 var fs = require('fs');
 94 var XLSX = require('xlsx');
 95 var winston = require('winston');
 96 
 97 // setup central logger instance using winston
 98 
 99   // require('winston-riak').Riak;
100   // require('winston-mongo').Mongo;
101   var logger = new (winston.Logger)({
102     transports: [
103 	  new (winston.transports.Console)({		// console logger with custom formatter (according to https://github.com/winstonjs/winston#adding-custom-transports)
104 		timestamp: function() {
105 			return dateTimStr(new Date());
106 		},
107 		formatter: function(options) {
108 			// Return string will be passed to logger.
109 			return ' ' + options.timestamp() +' '+ options.level.toUpperCase() +' '+ (undefined !== options.message ? options.message : '') +
110 			(options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta,null,2) : '' );
111 		}
112 	  }),
113       // new winston.transports.Console(),
114       new winston.transports.File({ filename: __dirname+'/logs/all-logs.log' })
115       // new winston.transports.Couchdb({ 'host': 'localhost', 'db': 'logs' }),
116       // new winston.transports.Riak({ bucket: 'logs' }),
117       // new winston.transports.MongoDB({ db: 'db', level: 'info'}),
118     ],
119     exceptionHandlers: [
120 	  new (winston.transports.Console)({
121 		timestamp: function() {
122 			return dateTimStr(new Date());
123 		},
124 		formatter: function(options) {
125 			// Return string will be passed to logger.
126 			var rs = ' ' + options.timestamp() +' EXCEPTION '+ options.level.toUpperCase() +' '+ (undefined !== options.message ? options.message : '');
127 			if  (options.meta && Object.keys(options.meta).length) {
128 			  rs += '\n'+ JSON.stringify(options.meta,null,2);
129 			}
130 			return rs;
131 		}
132 	  }),
133       new winston.transports.File({ filename: __dirname+'/logs/exceptions.log' })
134     ]
135   });
136   
137 logger = expandErrors(logger);
138 
139 // Extend a winston by making it expand errors when passed in as the 
140 // second argument (the first argument is the log level).
141 function expandErrors(logger) {
142   var oldLogFunc = logger.log;
143   logger.log = function() {
144     var args = Array.prototype.slice.call(arguments, 0);
145     if (args.length >= 2 && args[1] instanceof Error) {
146       args[1] = args[1].stack;
147     }
148     return oldLogFunc.apply(this, args);
149   };
150   return logger;
151 }
152 
153 logger.info("Logging with winston enabled");
154 logger.debug("DEBUG Logging NOT enabled");
155 logger.level = 'debug';
156 logger.debug("DEBUG Logging enabled");
157 
158 require('../../JSON-js/cycle.js');
159 
160 var global_stored_results = {info: "CCDB global stored results"};
161 var global_active_actions = {info: "CCDB global active actions"};
162 
163 var prefs = {};									// global configurations preferences
164 prefs.restart_timeout_ms = 60000;				// restart timeout is 1 minute
165 prefs.restart_exit_code  = 92;					// exit code to request restart
166 prefs.shutdown_exit_code = 91;					// exit code to request permanent shutdown
167 prefs.halt_exit_code     = 99;					// exit code to request termination
168 prefs.do_not_catch_in = {};
169 prefs.do_not_catch_in['qexpression'] = true;		// control catching exceptions in query jsexpr expressions
170 prefs.do_not_catch_in['aexpression'] = true;		// control catching exceptions in query jsexpr expressions
171 
172 prefs.global_stored_results = global_stored_results;
173 prefs.global_active_actions = global_active_actions;
174 
175 prefs.appbase = __dirname;
176 prefs.uploaddir = prefs.appbase + "/uploads";
177 
178 // CCDB modules
179 
180 var aux = require('./auxiliary.js')(logger,"ccdb",prefs);
181 var db = require('./db.js')(logger,"ccdb",prefs);
182 var dbConfig = require('./dbConfig.js');
183 var dbconfig = db.getCurrentConfig();
184 var credentials = require('./credentials.js');
185 var functions = require('./functions.js')(logger,"ccdb",prefs);
186 
187 var storage = multer.diskStorage({
188   destination: function (req, file, cb) {
189     cb(null, prefs.uploaddir)
190   },
191   filename: function (req, file, cb) {
192     cb(null, file.fieldname + '-' + Date.now())
193   }
194 })
195  
196 var upload = multer({ storage: storage, dest: prefs.uploaddir })
197 
198 var app = express();
199 app.use(favicon(__dirname + '/html/favicon.ico'));    	// to insert FAVICON in responses
200 app.use(bodyParser.json());                           	// for parsing application/json
201 app.use(bodyParser.urlencoded({ extended: true }));   	// for parsing application/x-www-form-urlencoded
202 app.use(session({resave: false,
203 				saveUninitialized: false,
204 				secret: credentials.cookieSecret
205 				}));
206 app.use(function(req,res,next) {						// try middleware, TODO: does not work yet, find out why !!
207 				// logger.debug("CCDB_MW.BEGIN.REQ:");
208 				// logger.debug(req);
209 				if (!(req.session)) {					// if we don't have a session ...
210 					logger.error("REQ has no session");
211 					res.redirect(303,'/login');				//	... redirect the user to the login page
212 				} else {
213 					next();
214 				}
215 			});
216 
217 
218 
219 /*
220  * CLASS:	ManagedFile
221  */
222 function ManagedFile(id,path) {
223 	this.mfid = id;
224 	this.mfpath = path;
225 	this.getId = function() {
226 		return this.mfid;
227 	}
228 	this.getPath = function() {
229 		return this.mfpath;
230 	}
231 }
232 
233 
234 //
235 // load and activate handlebars templating engine 
236 //		   
237 var handlebars = require('express-handlebars')
238         .create({ defaultLayout: 'main', 
239 					helpers: {
240 						// the section helper, allows insertion of text into sections out of {{body}}
241 						section: function(name, options){
242 								if(!this._sections) this._sections = {};
243 								this._sections[name] = options.fn(this);
244 								return null;
245 								},
246 						// the 'group-by' helper, allows grouping in hierarchical lists
247 						group: function (list, options) {
248 								options = options || {};
249 								var fn = options.fn || aux.noop,
250 								inverse = options.inverse || aux.noop,
251 								hash = options.hash,
252 								prop = hash && hash.by,
253 								keys = [],
254 								groups = {};
255 								if (!prop || !list || !list.length) {
256 									return inverse(this);
257 								}
258 								function groupKey(item) {
259 									var key = aux.get(item, prop);
260 									if (keys.indexOf(key) === -1) { keys.push(key); }
261 									if (!groups[key]) { groups[key] = {value: key,items: []};}
262 									groups[key].items.push(item);
263 								}
264 								function renderGroup(buffer, key) {
265 									return buffer + fn(groups[key]);
266 								}
267 								list.forEach(groupKey);
268 								return keys.reduce(renderGroup, '');
269 								}
270 							}
271 				}	
272 		);
273 
274 app.engine('handlebars', handlebars.engine);
275 app.set('view engine', 'handlebars');
276 
277 //
278 // default port to listen on is TCP/3000
279 //	   
280 app.set('port', process.env.PORT || 3000);
281 
282 //
283 // provide static web pages in "html" subdirectory
284 //
285 app.use(express.static('html'));
286  
287 /* *****************************************************************************
288  *
289  *	ROUTE:	/
290  *
291  * the "home" page
292  */
293 app.get('/',
294 		function(req,res) {
295 			if (sessionIsLoggedIn(req.session)) {
296 				res.render('plain',{body: 'CCDB ist ein Management-Instrument für Competence-Centers'})
297 			} else {
298 				res.redirect(303,'/login');
299 			}
300 		});
301 
302 /* *****************************************************************************
303  *
304  *	ROUTE:	/shutdown
305  *
306  * initiate restarting the server
307  * to achieve this, the OS script invoking node.js must check the return status of this program
308  * this action will terminate the server with the return status code (exit code) taken from the 
309  * "restart_exit_code" <prefs.restart_exit_code> preference, defaulting to 942
310  * to somewhat lessen the disruptive impact of the termination, a restart will occur after a delay
311  * of <prefs.restart_timeout_ms>, defaulting to 60000 (1 Minute)
312  * the invoking OS script must detect the restart return status code and start the server again
313  */
314 app.get('/shutdown',
315 		function(req,res) {
316 			if (!(req.query.hasOwnProperty('mode'))) {
317 				logger.error("/shutdown without mode parameter");
318 				res.redirect(303,"/error");
319 				return;
320 			}
321 			if (!req.session) {
322 				res.render('error',{errmsg: "Shutdown "+req.query.mode+" kann nicht ausgeführt werden, keine Session verfügbar"});
323 				return;
324 			}	
325 			if (sessionIsLoggedIn(req.session)) {
326 				functions.check_authorization(req.session,'shutdown_'+req.query.mode,req,
327 					function(err,result) {
328 						if (err) {
329 							res.render('error',{errmsg: err.message});
330 							return;
331 						}
332 						prefs.server.close( function(err) {
333 									if (err) {
334 										logger.error("CCDR.ERROR_CLOSING.MESSAGE="+err.message);
335 									}
336 									logger.debug("CCDB: HTTPS server closed");
337 								}
338 									);
339 						var reason;
340 						switch (req.query.mode) {
341 							case "shutdown":
342 								reason = "um abgeschaltet zu werden";
343 								break;
344 							case "restart":
345 								reason = "um neu gestartet zu werden";
346 								break;
347 							default:
348 								reason = "aus unbekanntem Grund";
349 						}
350 						res.render('plain',{body: "Server wird in spätestens "+prefs.restart_timeout_ms+" ms "+
351 						                          " beendet, "+reason+". Bitte <a href=\"/logout\">abmelden</a>, um die Beendigung zu beschleunigen."});
352 						init_shutdown(req.query.mode);
353 					});
354 			} else {
355 				res.render('error',{errmsg: "nicht angemeldet"});
356 			}
357 		});			
358 			
359 /* *****************************************************************************
360  *
361  *	ROUTE:	/login
362  *
363  * 	DESCRIPTION:	the login page
364  */
365 app.get('/login',
366 		function(req,res) {
367 			if (sessionIsLoggedIn(req.session)) {
368 				res.redirect(303,'/logout');
369 			} else {
370 				res.render('loginform');
371 			}
372 		});
373 			
374 /* *****************************************************************************
375  *
376  *	ROUTE:	/do_login
377  *
378  * 	DESCRIPTION:	process the login request form
379  */
380 app.post('/do_login',function(req,res) {
381 				var form = new formidable.IncomingForm();
382 				form.keepExtensions = true;
383 				form.parse(req, function(err, fields, files){
384 									if (err) {
385 										var errmsg = "Error in parsing database select form: "+err.message;
386 										res.render('error',{errmsg: errmsg});
387 									}
388 									req.username = fields.username;
389 									req.password = fields.password;
390 									req.new_password = fields.new_password;
391 									req.new_password_repeat = fields.new_password_repeat;
392 									do_login(req,res,function(err,result) {
393 														if (err) {
394 															res.render('loginform',{errortext: err.message});
395 															return;
396 														}
397 														res.render('plain',{body: result.body});
398 													});
399 								});
400 		});
401 
402 /* *****************************************************************************
403  *	FUNCTION:		sessionLoggedIn
404  *	INPUT:			SESS	-	session object
405  *	RESULT:			TRUE if session is logged in, FALSE otherwise
406  *	DESCRIPTION:	checks if session is logged in. This is done by checking
407  *					if the session has a 'username' attribute
408  */
409  function sessionIsLoggedIn(sess) {
410 	 return sess.username?true:false;
411  }
412  
413 /* *****************************************************************************
414  *	FLOW:		do_login
415  *	INPUT:		req			-	request object
416  *	CALLBACK:	rfunc		- callback function to be called upon completion
417  *		PARAMETERS:
418  *				err			-	error object if an error occured, otherwise null
419  *				result		-	result object:
420  *									body	-	text with information for user logged in
421  */
422 var do_login = flow.define(
423 	// step 1: get user info from CCUS and rights list(s) from CCATT
424 	function(req,res,rfunc) {
425 		if (sessionIsLoggedIn(req.session)) {
426 			aux.error_callback(rfunc,null,'Bereits angemeldet (<a href="/logout">abmelden</a>)');
427 			return;
428 		}
429 		if (prefs.init_shutdown) {
430 			aux.error_callback(rfunc,null,"Server fährt herunter, Anmeldung nicht möglich, bitte später wieder <a href=\"/login\">Anmelden</a>");
431 			return;
432 		}
433 		this.req = req;
434 		this.callback = rfunc;
435 		this.req.session.username = this.req.username;
436 		this.usdbr = new db.Request({info:	"get user data for \""+this.req.session.username+"\"",
437 									reqid:	'RQID_GETUSRDATA',
438 									sql:	"SELECT us.USKURZZ, us.USVORNAME, us.USFAMILIENNAME, upw.ATTVALUE as HASHED_PASSWORD, ups.ATTVALUE as PASSWORD_SALT, "+
439 											"       wm_concat(DISTINCT ur.ATTVALUE) || ',' || wm_concat(DISTINCT gr.ATTVALUE) as RIGHTSLIST "+
440 											"FROM CCVUS us "+
441 											"       left outer join CCATT upw on upw.ATTPARKURZ=us.USKURZZ and upw.ATTARCHTYP='USPWD' and upw.ATTNAME='HASHED_PASSWORD' and upw.ATTPARTYPE='CCUS' "+
442 											"       left outer join CCATT ups on ups.ATTPARKURZ=us.USKURZZ and ups.ATTARCHTYP='USPWD' and ups.ATTNAME='PASSWORD_SALT' and ups.ATTPARTYPE='CCUS' "+
443 											"         left outer join CCUSAS ua on ua.USKURZZ=us.USKURZZ "+
444 											"       left outer join CCATT ur on ur.ATTARCHTYP='AUTHORIGHT' and ur.ATTPARTYPE='CCUS' and ur.ATTPARKURZ=us.USKURZZ and ur.ATTNAME='RIGHTSLIST' "+
445 											"           left outer join CCATT gr on gr.ATTARCHTYP='AUTHORIGHT' and gr.ATTPARTYPE='CCGRP' and gr.ATTPARKURZ=ua.GRP and gr.ATTNAME='RIGHTSLIST' "+
446 											" where us.USKURZZ=:USERNAME "+
447 											"  group by us.USKURZZ,us.USVORNAME,us.USFAMILIENNAME, upw.ATTVALUE, ups.ATTVALUE",
448 									params:	[this.req.session.username]});
449 		logger.debug("DO_LOGIN."+this.usdbr.info);
450 		db.select(this.usdbr,this);
451 	},
452 	// step 2: got user info, check password
453 	function(err,result) {
454 		if (err || !(result) || !(result.rows) || !(result.rows[0]) || (result.rows[0].length<6) || (result.rows[0][0]!=this.req.session.username)) {
455 			var nerr = new Error("LOGIN.ERROR.Illegal Username or Password");
456 			logger.error(nerr.message);
457 			logger.error(this.usdbr);
458 			this.callback(nerr);
459 			return;
460 		}
461 		this.uskurzz = result.rows[0][0]
462 		this.vorname = result.rows[0][1];
463 		this.familienname = result.rows[0][2];
464 		this.hashed_password = result.rows[0][3];
465 		this.password_salt = result.rows[0][4];
466 		this.rightslist = result.rows[0][5];
467 		logger.debug("DO_LOGIN.USER_RECORD=\""+this.vorname+" "+this.familienname+"\"");
468 		if (!(this.hashed_password) || this.hashed_password=='*') {
469 			// no password set, grant access
470 			this.have_to_change_password = true;
471 			logger.debug("DO_LOGIN.NO_PASSWORD_SET");
472 		} else {
473 			this.hashed_entered_password = crypto.pbkdf2Sync(this.req.password, this.password_salt, 4096, 20, 'sha256').toString('base64');
474 			logger.debug("DO_LOGIN.HASHED_PASSWORD=\""+this.hashed_password+"\".HASHED_ENTERED_PASSWORD=\""+this.hashed_entered_password+"\"");
475 			if (this.hashed_password!=this.hashed_entered_password) {
476 				var nerr = new Error("LOGIN.ERROR.Illegal Username or Password");
477 				logger.error(nerr.message);
478 				this.callback(nerr);
479 				return;
480 			}
481 		}
482 		this.req.session.vorname = this.vorname;
483 		this.req.session.familienname = this.familienname;
484 		this.req.session.rightslist = this.rightslist.split(",");
485 		// now we're logged in, check if user wanted to change the password
486 		if (this.req.new_password) {
487 			if (this.req.new_password!=this.req.new_password_repeat) {
488 				var nerr = new Error("LOGIN.ERROR.new passwords are not equal");
489 				logger.error(nerr.message);
490 				this.callback(nerr);
491 				return;
492 			}
493 			// make a new salt
494 			this.password_salt = crypto.randomBytes(20).toString('base64');
495 			this.new_hashed_password = crypto.pbkdf2Sync(this.req.new_password, this.password_salt, 4096, 20, 'sha256').toString('base64');
496 			this.pwdbr = new db.Request({info:	"Change password for "+this.req.session.username,
497 										reqid:	'RQID_CHGPWD',
498 										sql:	"MERGE INTO CCATT pws "+
499 												" USING (SELECT :USERNAME as ATTPARKURZ, :HASHED_PASSWORD as ATTVALUE from dual) pwu "+
500 												"    ON (pws.ATTARCHTYP='USPWD' and pws.ATTPARTYPE='CCUS' and pws.ATTPARKURZ=pwu.ATTPARKURZ and pws.ATTNAME='HASHED_PASSWORD') "+
501 												"  WHEN MATCHED THEN UPDATE SET pws.ATTVALUE = pwu.ATTVALUE "+
502 												"  WHEN NOT MATCHED THEN INSERT (ATTARCHTYP,ATTPARTYPE,ATTPARKURZ,ATTNAME,ATTVALUE) "+
503 												"                         VALUES('USPWD','CCUS',pwu.ATTPARKURZ,'HASHED_PASSWORD',:HASHED_PASSWORD)",
504 										params:	[this.req.session.username, this.new_hashed_password]});
505 			db.select(this.pwdbr,this);
506 		} else {
507 			if (this.have_to_change_password) {
508 				var nerr = new Error("Kennwort muss geändert werden");
509 				this.callback(nerr);
510 				return;
511 			}
512 			login_register_session(this.req.session);
513 			var result = {resulttype: 'string', body:this.req.session.vorname+" "+this.req.session.familienname+" erfolgreich angemeldet"};
514 			logger.debug("LOGIN.COMPLETE."+result.body);
515 			this.callback(null,result);
516 		}
517 	},
518 	// step 3: password changed, now store salt
519 	function(err,result) {
520 		if (err) {
521 			var nerr = new Error("LOGIN.ERROR.Error storing changed password: "+err.message);
522 			logger.error(nerr.message);
523 			this.callback(nerr,result);
524 			return;
525 		}
526 		this.sudbr = new db.Request({info:	"Store new password salt for "+this.req.session.username,
527 									reqid:	'RQID_STOPWDSALT',
528 									sql: 	"MERGE INTO CCATT pws "+
529 											" USING (SELECT :USERNAME as ATTPARKURZ, :PASSWORD_SALT as ATTVALUE from dual) pwu "+
530 											"    ON (pws.ATTARCHTYP='USPWD' and pws.ATTPARTYPE='CCUS' and pws.ATTPARKURZ=pwu.ATTPARKURZ and pws.ATTNAME='PASSWORD_SALT') "+
531 											"  WHEN MATCHED THEN UPDATE SET pws.ATTVALUE = pwu.ATTVALUE "+
532 											"  WHEN NOT MATCHED THEN INSERT (ATTARCHTYP,ATTPARTYPE,ATTPARKURZ,ATTNAME,ATTVALUE) "+
533 											"                         VALUES('USPWD','CCUS',pwu.ATTPARKURZ,'PASSWORD_SALT',:PASSWORD_SALT)",
534 									params:	[this.req.session.username, this.password_salt]});	
535 		db.select(this.sudbr,this);
536 	},
537 	// step 4: salt store complete
538 	function(err,result) {
539 		if (err) {
540 			var nerr = new Error("Error storing password salt: "+err.message);
541 			logger.error(nerr.message);
542 			this.callback(nerr,result);
543 			return;
544 		}
545 		var result = {resulttype: 'string', body:this.req.session.vorname+" "+this.req.session.familienname+" erfolgreich angemeldet, Kennwort gesetzt"}
546 		logger.debug("LOGIN.COMPLETE.PASSWORD_SET."+result.body);
547 		this.callback(null,result);
548 	}
549 );
550 
551 /* *****************************************************************************
552  *	FLOW:		do_logout
553  *	INPUT:		sess		-	session object
554  *	CALLBACK:	rfunc		- 	callback function to be called upon completion
555  *		PARAMETERS:
556  *				err			-	error object if an error occured, otherwise null
557  *				result		-	result object:
558  *									body	-	text with information for user logged out
559  */
560 var do_logout = flow.define(
561 // step 1: destroy credentials in session
562 function(sess,rfunc) {
563 	this.session = sess;
564 	this.callback = rfunc;
565 	if (!sessionIsLoggedIn(this.session)) {
566 		aux.error_callback(this.callback,null,'nicht angemeldet (<a href="/login">anmelden</a>)');
567 		return;
568 	}
569 	this.username = this.session.username;
570 	this.vorname = this.session.vorname;
571 	this.familienname = this.session.familienname;
572 	delete this.session.username;
573 	delete this.session.vorname;
574 	delete this.session.familienname;
575 	delete this.session.rightslist;
576 	this(null,{resulttype: 'string', body:'Session für Benutzername '+this.username+', '+this.vorname+' '+this.familienname+' abgemeldet'});
577 },
578 // step 2: complete, prepare success result object
579 function(err,result) {
580 	if (err) {
581 		aux.error_callback(this.callback,err,'Fehler bei Abmeldung',result);
582 		return;
583 	}
584 	logger.debug("LOGOUT.COMPLETE."+result.body);
585 	logout_deregister_session(this.session);
586 	this.callback(null,result);
587 });
588 
589 /* *****************************************************************************
590  *	ROUTE:		/logout
591  *	ACTION:		logs session out, destroys all session credentials
592  */
593  app.get("/logout",
594 	function(req,res) {
595 		if (sessionIsLoggedIn(req.session)) {
596 			do_logout(req.session,
597 						function(err,result) {
598 							if (err) {
599 								res.render('error',{errmsg: err.message});
600 								return;
601 							}
602 							res.redirect(303,'/login');
603 						});
604 		} else {
605 			res.redirect(303,'/login');
606 		}
607 	});
608 	
609 /* *****************************************************************************
610  *
611  *	ROUTE:	/dbconfig
612  *	ACTION:	presents database selection page
613  */
614 app.get("/dbconfig",
615 	function(req,res) {
616 		var rd = {};
617 		rd.dbnames = db.listConfigs();
618 		res.render('dbselect',rd);
619 	}
620 );
621 
622 /* *****************************************************************************
623  *
624  *	ROUTE:	/do_dbconfig
625  *	ACTION:	performs database selection upon input from /dbconfig
626  */
627  app.post('/do_dbconfig',function(req,res) {
628 	var form = new formidable.IncomingForm();
629 	form.keepExtensions = true;
630 	form.parse(req, 
631 				function(err, fields, files){
632 					if(err) {
633 						var nerr = new Error("Error in parsing database select form: "+err.message);
634 						logger.error(nerr.message);
635 						res.render('error',{errmsg: nerr.message});
636 					}
637 					var dbn = fields.dbname;
638 					do_dbconfig(req.session,dbn,
639 								function(err,result) {
640 									if (err) {
641 										var nerr = new Error("Error in DO_DBCONFIG(\""+dbn+"\"): "+err.message);
642 										logger.error(nerr.message);
643 										res.render('error',{errmsg: nerr.message});
644 										return;
645 									}
646 									res.redirect(303,'/about');
647 								});
648 				});
649 	});
650  
651  /* ****************************************************************************
652   *	FLOW:		do_dbconfig
653   *	INPUT:		SESS	-	session object for checking authorization credentials
654   *				DBN		-	name of new database configuration to use
655   *	CALLBACK:	RFUNC	-	called upon completion
656   *		PARAMETERS:
657   *				ERR		-	error object, undefined of everything OK
658   *				RESULT	-	result object 
659   */
660   
661  var do_dbconfig = flow.define(
662 	// step 1: check authorization
663 	function(sess,dbn,rfunc) {
664 		this.session = sess;
665 		this.dbn = dbn;
666 		this.callback = rfunc;
667 		functions.check_authorization(this.session,'do_dbconfig',this,this);
668 	},
669 	// step 2: perform database configuration switch
670 	function(err,result) {
671 		if (err) {
672 			var nerr = new Error("Authorization Error in DO_DBCONFIG(\""+this.dbn+"\"): "+err.message);
673 			logger.error(nerr.message);
674 			this.callback(nerr,result);
675 			return;
676 		}
677 		var restext = db.setConfig(dbn);
678 		this.callback(undefined,{info: restext});
679 	}
680 	// end of flow do_dbconfig
681 );
682 
683 /* *****************************************************************************
684  *
685  *	ROUTE:	/upload
686  *	ACTION:	present a form for file upload
687  */ 
688 app.get('/upload', function(req, res) {
689 			res.render('upload',{body: ""});
690 		});
691 
692 /* *****************************************************************************
693  * 	ROUTE:	/do_upload
694  *	ACTION:	perform upload
695  */
696 app.post('/do_upload',
697 		 function(req, res) {
698 			var form = new formidable.IncomingForm();
699 			form.uploadDir = prefs.uploaddir;
700 			form.keepExtensions = true;
701 			form.parse(req, function(err, fields, files){
702 				if(err) {
703 					var errmsg = "Error in parsing upload form: "+err.message;
704 					res.render('error',{errmsg: errmsg});
705 				}
706 				var rd = {};
707 				rd.filename = files.upload_file.name;
708 				if (files.upload_file.path.startsWith(prefs.appbase)) {										// uploaded to application tree directory?
709 					rd.filepath = files.upload_file.path.substring(prefs.appbase.length+path.sep.length);	// .. yes, store relative, (hopefully normalized) path
710 				} else {
711 					rd.filepath = files.upload_file.path;													// .. no, store absolute, complete (most probably platform-specific!) path
712 				}
713 				rd.mfileid  = fields.filetype+'_'+aux.nowstring();
714 				rd.mfiletype= fields.filetype;
715 				rd.mfileatt = fields.mfileatt;
716 				functions.check_authorization(req.session,'do_upload',req,
717 									function(err,result) {
718 										if (err) {
719 											var nerr = new Error("Authorization error in DO_UPLOAD: "+err.message);
720 											logger.error(nerr.message);
721 											res.render('error',{errmsg: nerr.message});
722 											return;
723 										}
724 										var dbr = new db.Request({info:		"insert new managed file with id "+rd.mfileid,
725 																reqid:		'RQID_INSMFILE',
726 																headertext:	"Neues Managed File "+rd.mfileid+" angelegt",
727 																oracle_sql:		"insert into CCMFILES(mfiletype, mfileid, mfilename, mfilepath, mfileattribs) "+
728 																			" values(:mfiletype,:mfileid,:mfilename,:mfilepath,:mfileattribs)",
729 																params:		[rd.mfiletype, rd.mfileid, rd.filename, rd.filepath, rd.mfileatt]});
730 										db.select(dbr,
731 												function(err,result) {
732 													if (err) {
733 														var errmsg = "Error in DO_UPLOAD: "+err.message;
734 														logger.error(errmsg);
735 														res.render('error',{errmsg: errmsg});
736 													}
737 													var rd = { mfiletype: this.params[0], mfileid: this.params[1], mfilepath: this.params[2] };
738 													res.render('upload_done',rd);
739 												}.bind(dbr));
740 									});
741 				});
742 		 });
743 
744 /* *****************************************************************************
745  *
746  *	ROUTE:	/mfiles
747  *	ACTION:	list managed file
748  */ 
749 app.get('/mfiles', function(req, res){
750 		var rd = {};
751 		rd.dbresults = new Array();
752 		var tdi;
753 		var tdf;
754 		functions.check_authorization(req.session,'mfiles',req,
755 							function(err,result) {
756 								if (err) {
757 									var nerr = new Error("Authorization error in MFILES: "+err.message);
758 									logger.error(nerr.message);
759 									res.render('error',{errmsg: nerr.message});
760 									return;
761 								}
762 								var dbr = new db.Request({	info:	"List Managed Files",
763 															reqid:	'RQID_LISTMFILES',
764 															headertext:	"Managed Files",
765 															sql:	'select mfileid as "Managed File ID",mfiletype as "Managed File Type",'+
766 																	'       mfilename as "Ursprünglicher Dateiname",mfilepath as "Speicherpfad",'+
767 																	'		mfileattribs as "Zusatz-Attribute (JSON-Format)" '+
768 																	'  from CCMFILES order by mfileid asc'});
769 								db.select(dbr,
770 										function(err,dbres) {
771 											if (err) {
772 												var errmsg = "Error in list managed files: "+err.message;
773 												res.render('error',{errmsg: errmsg});
774 												return;
775 											}
776 											rd.dbresults.push(copy_dbresult(dbres,dbr.headertext,dbr.desctext));
777 											res.render('mfiles',rd);
778 										}.bind(dbr));
779 							});
780 		});
781 
782 /* *****************************************************************************
783  *	ROUTE:		/showfile
784  *	ACTION:		show properties of one managed file
785  *	PARAMETERS:	MFILEID	-	managed file ID of the file to show
786  */
787 app.get("/showfile", function(req, res) {
788 		if (!req.query.hasOwnProperty('mfileid')) {
789 			var errmsg = 'Error in SHOWFILE: field "mfileid" not in HTTP-request'
790 			logger.error(errmsg);
791 			res.render('error',{errmsg: errmsg});
792 		} else {
793 			mfileinfo(req.session,req.query.mfileid,
794 					function(err,result) {
795 						if (err) {
796 							res.render('error',{errmsg: err.message});
797 							return;
798 						}
799 						res.render('showfile',result);
800 					})
801 		}
802 });
803 						
804 /* *****************************************************************************
805  *	FLOW:		mfileinfo
806  *	INPUT:		sess		- session object for checking settings and authorization
807  *				mfileid		- ID of managed file to get info for
808  *	CALLBACK:	rfunc
809  *					parameters:
810  *						err		-	indicating error, if no error, undefined
811  *						result	-	information object:
812  *							mfiletype	-	type of managed file
813  *							mfilename	-	original file name
814  *							mfilepath	-	full path in local FS
815  *							mfileatt	-	managed file attributes in stringified form (as stored in table CCMFILE)
816  *							mfileattribs	- 	object with MFILE attributes
817  *							filetype	-	MIME-Type of file (if available)
818  *							filext		- filename extension (of original file name)
819  *							actionurls	-	array of request URLS and display texts applicable to this file
820  *	DESCRIPTION:	information about the managed file <mfileid> is determined as follows:
821  *					MFILExxx attributes are got from the CCMFILE-entry
822  *					FILETYPE and FILEXT are got from the original file name and the contents of the file
823  *					ACTIONURLS are determined based on:
824  *					- RW MFTACTS, looking up MFILETYPE
825  *					- RW FTACTS, looking up FILETYPE and FILEXT
826  *					- contents of MFILEATTRIBS:
827  *					-- if MFILETYPE='ccdbtable':
828  *					--- MFILEATTRIBS.LOADCONTROLS:
829  *						each entry in LOADCONTROLS generates one ACTIONURL for loading the file or one part of the file
830  *						into the CCDB table with sub-attribute MFILEATTRIBS.LOADCONTROLS.<controlname>.TABLENAME
831  *					--- MFILEATTRIBS.TABLE:
832  *						generates one ACTIONURL to load the file or (if this file is a workbook and MFILEATTRIBS.SHEETNAME is present)
833  *						one part (worksheet) of the file into the CCDB table with the name from MFILEATTRIBS.TABLE
834  */
835  var mfileinfo = flow.define(
836  // step 1: check authorization
837  function (sess,mfileid,rfunc) {
838 	this.session = sess;
839 	this.mfileid = mfileid;
840 	this.callback = rfunc;
841 	functions.check_authorization(this.session,'mfileinfo',this,this);
842  },
843  // step 2: get MFILE information 
844  function(err,ares) {
845 	 if (err) {
846 		 var nerr = new Error("Authorization error in MFILEINFO: "+err.message);
847 		 logger.error(nerr.message);
848 		 this.callback(nerr,ares);
849 		 return;
850 	 }
851 	 functions.get_mfile_data(this.mfileid,this);
852  },
853  // step 3: got MFILE info, initialize structures and get file type
854  function(err,mfileinfo) {
855 	if (err) {
856 		aux.error_callback(this.callback,err,'Error in MFILEINFO reading MFILE data',mfileinfo);
857 		return;
858 	}
859 	this.rd = {};
860 	this.rd.mfileid   = mfileinfo.mfileid;
861 	this.rd.mfiletype = mfileinfo.mfiletype;
862 	this.rd.mfilename = mfileinfo.mfilename;
863 	this.rd.mfilepath = mfileinfo.mfilepath;
864 	this.rd.mfileatt  = mfileinfo.mfileattribs;
865 	if (this.rd.mfileatt) {
866 	  try {
867 		  this.rd.mfileattribs = JSON.parse(this.rd.mfileatt);
868 	  }
869 	  catch (e) {
870 		  aux.error_callback(this.callback,e,'MFILEINFO: Error parsing attributes',mfileinfo);
871 		  return;
872 	  }
873 	} else {
874 	  this.rd.mfileattribs = {};
875 	}
876 	aux.default_param(this.rd,'mfileattribs',{});
877 	this.rd.actionurls = [];
878 	aux.getFileType(this.rd.mfilepath,this);
879  },
880  // step 4:	got physical file type, determine possible action to be executed on this file
881  function(err,result) {
882 	if (err) {
883 		aux.error_callback(this.callback,err,"MFILEINFO: error determining file type of "+this.rd.mfilepath);
884 		return;
885 	}
886 	this.rd.filetype = result.mime;
887 	this.rd.filtypext = result.ext;
888 	this.rd.filnamext = result.namext;
889 	this.rd.filactstat = result.filactstat;
890 	this.rd.fexists = result.fexists;
891 	this.rd.statstring = JSON.stringify(result.stats,null,2); 
892 	if ((this.rd && this.rd.mfileattribs && this.rd.mfileattribs.extension)) {
893 		this.rd.filattext = this.rd.mfileattribs.extension;
894 	} else {
895 		this.rd.filattext = 'undef';
896 	}
897 	logger.debug("MFILEINFO.MFILEPATH=\""+this.mfilepath+"\".MIME_TYPE=\""+this.rd.filetype+"\".FILETYPEEXTENSION=\""+this.rd.filtypext+
898 	            "\".FILNAMEXTENSION=\""+this.rd.filnamext+"\".FILATTEXTENSION=\""+this.rd.filattext+"\"");
899 	this.rd.actionurls.push({url:'/delete_mfile?mfileid='+this.rd.mfileid, text:'Managed File löschen'});
900 	//	1) actions specified based upon MFILETYPE in RW MFTACTS or on FILETYPE in RW FTACTS
901 	this.fadbr = new db.Request(	{	info:		"Determine MFILE actions for MFILETYPE "+this.rd.mfiletype,
902 									reqid:		'RQID_GETMFTACT',
903 									headertext:	"MFILE actions, MFILETYPE="+this.rd.mfiletype,
904 									sql:		"select RWTYP,RWPARENT,RWNAME,RWVALUE,RWDESC from CCRW "+
905 												"where (RWTYP='MFTACT' and RWPARENT='MFTACTS' and RWNAME like '"+this.rd.mfiletype+"%') or "+
906 												"(RWTYP='FTACT' and RWPARENT='FTACTS' and RWNAME like '"+this.rd.filetype+"%') or "+
907 												"(RWTYP='FTACT' and RWPARENT='FTACTS' and UPPER(RWNAME) like '"+this.rd.filtypext.toUpperCase()+"%') or "+
908 												"(RWTYP='FTACT' and RWPARENT='FTACTS' and UPPER(RWNAME) like '"+this.rd.filnamext.toUpperCase()+"%') or "+
909 												"(RWTYP='FTACT' and RWPARENT='FTACTS' and UPPER(RWNAME) like '"+this.rd.filattext.toUpperCase()+"%')",
910 									params:		[]});
911 	db.select(this.fadbr,this);
912  },
913  // step 5: got actions for the MFILETYPE and the FILETYPE from CCRW, RW MFTACTS and FTACTS
914  function(err,result) {
915 	if (err) {
916 		var l3err = new Error("MFILEINFO: error getting MFACTIONS for type "+this.rd.mfiletype+": "+err.message);
917 		logger.error(l3err.message);
918 		this.callback(l3err,result);
919 		return;
920 	}	
921 	//	1) each row in the RW MFTACTS/FTACTS is an entry for an action:
922 	//	column 3 - RWVALUE contains an URL with placeholders
923 	// 	column 4 - RWDESC contains the text to be displayed for this action
924 	var url, text;
925 	for (var fai=0; fai<result.rows.length; fai++) {
926 		url = aux.populate(result.rows[fai][3],this.rd);
927 		text = aux.populate(result.rows[fai][4],this.rd);
928 		this.rd.actionurls.push({url:url,text:text});
929 	}
930 	//	another source for actions are the file attributes:
931 	//	2) the 'loadcontrols' specify a set of loading actions:
932 	//		'tablename' specifies a table to be loaded
933 	//		optionally 'sheetname' specifies the worksheet to be loaded
934 	//	3) alternatively, the file (or sheet) can be displayed
935 	if ((this.rd.mfileattribs) && (this.rd.mfileattribs.loadcontrols)) {
936 		for (var tabnam in this.rd.mfileattribs.loadcontrols) {
937 			if (this.rd.mfileattribs.loadcontrols[tabnam].tablename) {
938 				var url = '/dquery?dataname=LOADTABLE&mfileid='+this.rd.mfileid
939 						+'&tablename='+this.rd.mfileattribs.loadcontrols[tabnam].tablename;
940 				var text = 'Lade CCDB Tabelle '+this.rd.mfileattribs.loadcontrols[tabnam].tablename+" aus ";
941 				if (this.rd.mfileattribs.loadcontrols[tabnam].sheetname) {
942 					url += '&sheetname='+this.rd.mfileattribs.loadcontrols[tabnam].sheetname;
943 					text += 'dem Blatt '+this.rd.mfileattribs.loadcontrols[tabnam].sheetname+" ";
944 				}
945 				text += 'dieser Datei';
946 				this.rd.actionurls.push({url: url,text: text});
947 				url = 'dquery?dataname=SHOWXLS&mfileid='+this.rd.mfileid
948 					+'&tablename='+this.rd.mfileattribs.loadcontrols[tabnam].tablename;
949 				text = 'Zeige ';
950 				if (this.rd.mfileattribs.loadcontrols[tabnam].sheetname) {
951 					url += '&sheetname='+this.rd.mfileattribs.loadcontrols[tabnam].sheetname;
952 					text += 'das Blatt '+this.rd.mfileattribs.loadcontrols[tabnam].sheetname+' dieser Datei ';
953 				} else {
954 					 text += 'diese Datei ';
955 				}
956 					 text += 'an';
957 					 this.rd.actionurls.push({url: url, text: text});
958 			}
959 		}
960 	}
961 	//	4) if the MFILE has a 'table' attribute, an action for loading the table form the file is shown
962 	//		optionally, a 'sheetname' can be specified as MFILE-attribute
963 	if ((this.rd.mfileattribs) && (this.rd.mfileattribs.table)) {
964 		var url = '/dquery?dataname=LOADTABLE&mfileid='+this.rd.mfileid+'&tablename='+this.rd.mfileattribs.table;
965 		var text = 'Lade CCDB Tabelle '+this.rd.mfileattribs.table+' aus ';
966 		if (this.rd.mfileattribs.sheetname) {
967 			url += '&sheetname='+this.rd.mfileattribs.sheetname;
968 			text += 'dem Blatt '+this.rd.mfileattribs.sheetname+' ';
969 		}
970 		text += 'dieser Datei';
971 		this.rd.actionurls.push({url: url, text: text});
972 	}
973 	this.callback(undefined,this.rd);
974  });
975  
976 /* *****************************************************************************
977  *	ROUTE:		/delete_mfile
978  *	ACTION:		removes the selected managed file
979  */
980  app.get('/delete_mfile', 
981 		function (req,res) {
982 			if (!req.query.hasOwnProperty('mfileid')) {
983 				var errmsg = 'DELETE_MFILE: field "mfileid" not in HTTP-request';
984 				res.render('error',{errmsg: errmsg});
985 				return;
986 			} else {
987 				delete_mfile(req.session,req.query.mfileid,
988 					function(err,result) {
989 						if (err) {
990 							var errmsg = 'Error in DELETE_MFILE: '+err.message;
991 							logger.error(errmsg);
992 							res.render('error',{errmsg: errmsg});
993 							return;
994 						}
995 						res.render('info',result);
996 					});
997 			}
998 		});
999 
1000 /* *****************************************************************************
1001  *	FLOW:		delete_mfile
1002  *	INPUT:		MFILE		-	managed file ID to delete
1003  *				SESS		-	session object to determine authorization
1004  *	CALLBACK:	RFUNC		-	called upon completion
1005  *					PARAMETERS:
1006  *						ERR		-	error object, null if everything OK
1007  *						RESULT	-	result information
1008  */
1009  var delete_mfile = flow.define(
1010 	// step 1: check authorization
1011 	function(sess,mfileid,rfunc) {
1012 		this.session = sess;
1013 		this.mfileid = mfileid;
1014 		this.callback = rfunc;
1015 		functions.check_authorization(this.session,'delete_mfile',this,this);
1016 	},
1017 	// step 2: get MFILE data
1018 	function(err,result) {
1019 		if (err) {
1020 			aux.error_callback(this.callback,err,'Authorization error in DELETE_MFILE',result);
1021 			return;
1022 		}
1023 		functions.get_mfile_data(this.mfileid,this);
1024 	},
1025 	// step 2: delete CCMFILES entry
1026 	function(err,mfileinfo) {
1027 		if (err) {
1028 			aux.error_callback(this.callback,err,'Error getting MFILE data for '+this.mfileid,mfileinfo);
1029 			return;
1030 		}
1031 		this.mfileattr = mfileinfo.mfileid;
1032 		this.mfiletype = mfileinfo.mfiletype;
1033 		this.mfilename = mfileinfo.mfilename;
1034 		this.mfilepath = mfileinfo.mfilepath;
1035 		this.mfileatt  = mfileinfo.mfileattribs; 
1036 		var dbr = new db.Request({	info:	'delete managed file '+this.mfileid,
1037 									reqid:	'RQID_DELMFILE',
1038 									sql:	'delete from CCMFILES where mfileid=:mfileid',
1039 									params:	[this.mfileid]});
1040 		db.select(dbr,this);
1041 	},
1042 	// step 3: delete physical file
1043 	function(err,dbres) {
1044 		if (err) {
1045 			aux.error_callback(this.callback,err,'Error deleting MFILE data for '+this.mfileid,dbres);
1046 			return;
1047 		}
1048 		fs.unlink(this.mfilepath,this);
1049 	},
1050 	// step 4: complete
1051 	function(err) {
1052 		if (err) {
1053 			aux.error_callback(this.callback,err,'Error in deleting managed file '+this.mfileid+', physical file '+this.mfilepath);
1054 			return;
1055 		}
1056 		this.callback(undefined,{	title:	'Managed File löschen',
1057 									info: 	'Managed File <code>'+this.mfileid+'</code> gelöscht'});
1058 	});
1059 
1060 function isNumericDigit(d) {
1061 	return (d>='0' && d<='9');
1062 }
1063 
1064 /* *****************************************************************************
1065  *	FUNCTION:	fold_form_fields_to_query
1066  *	INPUT:		qry		-	query object to fold the form fields into
1067  *				flds	-	object with attributes like <dataname>_form.<parameter>
1068  *
1069  *	DESCRIPTION:	copies all form data fields from <flds> as attributes to <qry>
1070  *					the fields are expected to be named like <dataname>_form.<parameter>
1071  *					if form fields are more complex, they will have multi-segment <parameter>-names
1072  *					in this case, hierarchical objects will be built in <qry>
1073  *					as a special case, if <parameter> or segment names are numeric, an array
1074  *					will be built
1075  *					field names are checked, if they belong to the same form, they must have the same <dataname>
1076  *					errors are logged, if any checks are violated
1077  */
1078 function fold_form_fields_to_query(qry,flds) {
1079 	var parts, d, bo;
1080 	var p1 = null;
1081 	var fc = 0;
1082 	for (var fn in flds) {
1083 		fc++;
1084 		parts = fn.split('.');
1085 		if (p1) {
1086 			if (parts[0]!=p1) {
1087 				logger.error("Form Field "+fn+" has other header field \""+parts[0]+"\" than previous ones (\""+p1+"\")"); 
1088 			}
1089 		} else {
1090 			p1 = parts[0];
1091 		}
1092 		if (parts.length>1) {		// must at least have 2 parts: "<dataname>_form.<parameter>", others are ignored
1093 			if (parts.length>2) {   // <parameter> has more than one part, build hierarchical object from it
1094 				d = 1;
1095 				bo = qry;			// the current base object, lowest level base object is the query itself
1096 				for (d=1;d<(parts.length-1);d++) {	// build the path for this very field
1097 					if (!bo[parts[d]]) {			// current branch already exists ? No, create it
1098 						if (isNumericDigit(parts[d+1].charAt(0))) {	// if segment name is numeric, ...
1099 							bo[parts[d]] = new Array();		// ... create an array ...
1100 						} else {
1101 							bo[parts[d]] = {};				// ... otherwise, create an object
1102 						}
1103 					}
1104 					bo = bo[parts[d]];				// climb on level up the tree
1105 				}
1106 													// now add the new leaf
1107 				if (util.isArray(bo)) {
1108 					bo.push(flds[fn]);						// ... either as an array element ...
1109 				} else {
1110 					bo[parts[parts.length-1]] = flds[fn];	// ... or as a new attribute on the highest branch
1111 				}
1112 			} else {
1113 				qry[parts[1]] = flds[fn];	// if no hierarchical object, simply ann a new attribute to the query
1114 			}
1115 		} else {
1116 			logger.error("Form field "+fn+" has no header field");
1117 		}
1118 	}
1119 	// make some plausibility checks
1120 	if (fc<1) return;					// .. but only if any fields were there to be processed
1121 	if (!qry.dataname) {
1122 		logger.error("query form data does not have dataname parameter");
1123 	} else {
1124 		if (!p1) {
1125 			logger.error("param form does not have any header field:");
1126 			console.log("QUERY: ",qry);
1127 			console.log("FIELDS: ",flds);
1128 		} else {
1129 			var hfp = p1.split("_");
1130 			if (hfp[0]!=qry.dataname) {
1131 				logger.error("query header field dataname \""+qry.dataname+"\" is not equal form data name of \""+hfp[0]+"\"");
1132 			}
1133 			if (hfp[1]!="form") {
1134 				logger.error("form data header field name \""+p1+"\" does not end with _form");
1135 			}
1136 		}
1137 	}
1138 }
1139 	
1140 /* *****************************************************************************
1141  *
1142  *	ROUTE:		/dquery
1143  *	ACTION:		executes a query according to the query descriptor, processed by flow DQUERY
1144  */
1145 app.all("/dquery",function(req,res) {
1146 		var form = new formidable.IncomingForm();
1147 		form.uploadDir = __dirname + "/temp_uploads";
1148 		form.keepExtensions = true;
1149 		form.parse(req,
1150 			function(err,fields,files) {
1151 				if (err) {
1152 					var nerr = new Error("Error parsing form: "+err.message);
1153 					res.render('error',{errmsg: errmsg});
1154 					return;
1155 				}
1156 				fold_form_fields_to_query(req.query,fields);
1157 				aux.default_param(req.query,'dataname','NO_DATANAME_SPECIFIED');
1158 				aux.default_param(req.query,'puth',20);					// per default, push action to the background after 20 progress events 
1159 				var pucnt = 0;
1160 				var action_stored = false;
1161 				var action_handle = null;
1162 				// logger.debug("/DQUERY.FIELDS:",fields);
1163 				dquery(req.session,req.query,
1164 					// completion callback
1165 					function(err,result) {
1166 						if (err) {
1167 							if ((result) && (result.resulttype) && (result.resulttype=='paramspec')) {	// special 'error': need user input, result is a paramspec
1168 								result.url = '/dquery';
1169 								result.reqtype = 'Aktion';
1170 								prepare_param_form(result,
1171 												   function(err,formdata) {
1172 														if (err) {
1173 														   var errmsg = "Error in preparing query form: "+err.message;
1174 														   logger.error(errmsg);
1175 														   res.render('error',{errmsg: errmsg, data: formdata});
1176 														   return;
1177 														}
1178 														aux.escapeSingleQuoteAllStrings(formdata);
1179 														aux.escapeDoubleQuoteAllStrings(formdata);
1180 														aux.replaceControlWhitespaceAllStrings(formdata);
1181 														var jsondata 		= JSON.stringify(JSON.decycle(formdata));
1182 														var jsonundecycled	= JSON.stringify(formdata)
1183 														// formdata.jsondata = jsondata;
1184 														formdata.jsondata = jsonundecycled;
1185 														aux.default_param(formdata,'formview','queryparamform');
1186 														res.render(formdata.formview,formdata);
1187 														return;
1188 												   });
1189 								return;
1190 							}
1191 							var nerr = new Error("Error in DQUERY: "+err.message);
1192 							logger.error(nerr.message);
1193 							if (action_stored) {
1194 								global_active_actions[action_handle].result = result;
1195 								global_active_actions[action_handle].error  = nerr;
1196 								global_active_actions[action_handle].status = 'error';
1197 								return;
1198 							} else {
1199 								res.render('error',{errmsg: nerr.message, result: result});
1200 								return;
1201 							}
1202 						}
1203 						// action is complete, no error
1204 						aux.default_param(result,'disposition','view');
1205 						if (result.decoration) {
1206 							// logger.debug("/DQUERY: result decoration specified:");
1207 							// logger.debug(result.decoration);
1208 							decorate(result);
1209 						}
1210 						store_result(req.session,result);
1211 						if (action_stored) {											// action is stored away, update its status there
1212 							global_active_actions[action_handle].result = result;		// keep result, is already decorated, but do not yet render it
1213 							global_active_actions[action_handle].status = 'complete';
1214 						} else {												// action still in foreground of session, render its result to the client
1215 							if (result.disposition=='view') {						// configured to render result with a view
1216 								aux.default_param(result,'view','plain');				// default view is 'plain' (only has one placeholder for attribute 'body')
1217 								res.render(result.view,result);
1218 							} else {												// all other dispositions lead here, result attribute 'data' is directly sent to client
1219 								aux.default_param(result,'contentType','text/html');
1220 								res.type(result.contentType);
1221 								res.status(200);
1222 								res.send(result.data);					   
1223 							}
1224 						}
1225 				   },
1226 				   // progress status callback
1227 				   function(progstat) {
1228 						var pstat = {action:	'/dquery',
1229 									stage:		'1/1 - dquery',
1230 									subaction:	progstat};
1231 						// logger.debug("/dquery.PROGRESS:");
1232 						// logger.debug(pstat);
1233 						if (action_stored) {
1234 							update_object(global_active_actions[action_handle],pstat);
1235 							return;
1236 						}
1237 						pucnt++;
1238 						if (pucnt>req.query.puth) {							// if update cycle threshhold exceeded ...
1239 							store_active_action(req.session,pstat);			// ... push to background: save the action status ...
1240 							action_handle = pstat.actid;					// keep reference to the stored action so that the completion callback can use it
1241 							action_stored = true;
1242 							res.redirect(303,'/show_active_action?actid='+pstat.actid); 	// ... and redirect user to action status page
1243 						} else {
1244 							res.connection.setTimeout(0); // this could take a while, so do not let browser time out
1245 						}
1246 					});
1247 		});
1248 	});
1249 
1250 /* *****************************************************************************
1251  *	ROUTE:		/load_dcresweekly
1252  *	ACTION:		load a managed file containing weekly datacenter resource usage data into table CCDCRESL and then CCDCRES after checks
1253  *	PARAMETERS:	MFILEID	- managed file ID of a CSV file to load
1254  */
1255 app.get("/load_dcresweekly", 
1256 		function(req, res) {
1257 			if (!req.query.hasOwnProperty('mfileid')) {
1258 				logger.error('field "mfileid" not in HTTP-request');
1259 				res.redirect(303,'/error');
1260 			} else {
1261 				functions.load_dcresweekly(req.session,req.query.mfileid,
1262 								 function(err,result) {
1263 									if (err) {
1264 										logger.error(err.message);
1265 										res.render('error',{errmsg: err.message, result: result});
1266 										return;
1267 									}
1268 									res.render('table',result);
1269 								 });
1270 			}
1271 		});
1272 
1273 /* *****************************************************************************
1274  *	FUNCTION:		store_result
1275  *	INPUT:			session		-	session object
1276  *					result		-	result object
1277  *	DESCRIPTION:	stores the <result> in GLOBAL_STORED_RESULTS with a unique ID and assigns attribute saveparams.resid
1278  *					the ID is also stored in <session>.RESULTS
1279  *					
1280  */
1281  function store_result(session,result) {
1282 	 var resid = "GSR_"+aux.nowstring();
1283 	 result.saveparams = {resid: resid};
1284 	 if (session.last_result) {									// unlink the previous result from the global store if not yet saved
1285 		 delete global_stored_results[session.last_result];
1286 	 }
1287 	 if (!global_stored_results) global_stored_results = {};
1288 	 global_stored_results[resid] = {};
1289 	 global_stored_results[resid].resid  = resid 
1290 	 global_stored_results[resid].result = result;
1291 	 if (!session.results) session.results = {};
1292 	 session.last_result = resid;
1293 	 session.results[resid] = {resid: resid};
1294  }
1295  
1296 /* *****************************************************************************
1297  *	ROUTE:	save_result
1298  *	ACTION:	saves a result semi-permanentely in the session
1299  */
1300 app.get('/save_result',
1301 		function(req,res) {
1302 			if (!req.query.hasOwnProperty('resid')) {
1303 				logger.error("Request-Feld \"resid\" nicht im HTTP-request");
1304 				res.redirect(303,"/error");
1305 				return;
1306 			}
1307 			if (!req.session) {
1308 				res.render('error',{errmsg: "Ergebnis \""+req.query.resid+"\" kann nicht gespeichert werden, keine Session verfügbar"});
1309 				return;
1310 			}
1311 			if (!req.session.saved_results) {
1312 				req.session.saved_results = {};
1313 			}
1314 			if (req.session.last_result && req.session.last_result==req.query.resid) {	// unlink the session from the saved result
1315 				delete req.session.last_result;
1316 			}
1317 			req.session.saved_results[req.query.resid] = {resid: global_stored_results[req.query.resid].resid};
1318 			res.render('plain',{body: "Ergebnis \""+req.query.resid+"\" <a href=\"/saved_results\">gespeichert</a>"});
1319 		});
1320 
1321 /* *****************************************************************************
1322  *	ROUTE:	saved_results
1323  *	ACTION:	list saved results for this session
1324  */
1325 app.get('/saved_results',
1326 		function(req,res) {
1327 			if (req.session && req.session.saved_results) {
1328 				result = new db.Result();
1329 				for (rn in req.session.saved_results) {
1330 					result.rows.push(['<a href="/show_result?resid='+rn+'">'+rn+' - '+global_stored_results[rn].result.title+'</a> - '+
1331 					                  '<a href="/export_result?resid='+rn+'">exportieren</a> - '+
1332 									  '<a href="/export_result?resid='+rn+'&mfile">in Managed File exportieren</a> - '+
1333 									  '<a href="/show_result?resid='+rn+'&format=json">JSON anzeigen</a>']);
1334 				}
1335 				result.title='Gespeicherte Abfrage-Ergebnisse';
1336 				res.render('slist',result);
1337 			} else {
1338 				res.render('plain',{body: 'keine gespeicherten Abfrage-Ergebnisse'});
1339 			}
1340 		}
1341 		);
1342 
1343 /* *****************************************************************************
1344  *	ROUTE:	all_saved_results
1345  *	ACTION:	list all saved results for all sessions
1346  */
1347 app.get('/all_saved_results',
1348 		function(req,res) {
1349 			if (global_stored_results) {
1350 				result = new db.Result();
1351 				for (rn in global_stored_results) {
1352 					result.rows.push(['<a href="/show_result?resid='+rn+'">'+rn+' - '+global_stored_results[rn].result.title+'</a> - '+
1353 					                  '<a href="/export_result?resid='+rn+'">exportieren</a> - '+
1354 									  '<a href="/export_result?resid='+rn+'&mfile">in Managed File exportieren</a>']);
1355 				}
1356 				result.title='Gespeicherte Abfrage-Ergebnisse';
1357 				res.render('slist',result);
1358 			} else {
1359 				res.render('plain',{body: 'keine gespeicherten Abfrage-Ergebnisse'});
1360 			}
1361 		}
1362 		);
1363 
1364 /* *****************************************************************************
1365  *	ROUTE:	show_result
1366  *	ACTION:	render the saved result <resid> again
1367  */
1368 app.get('/show_result',
1369 		function(req,res) {
1370 			if (!req.query.hasOwnProperty('resid')) {
1371 				logger.error("Request-Feld \"resid\" nicht im HTTP-request");
1372 				res.redirect(303,"/error");
1373 				return;
1374 			}
1375 			if (req.query.hasOwnProperty('format') && req.query.format=='json') {
1376 				res.render('plain',{body: '<code>'+JSON.stringify(JSON.decycle(global_stored_results[req.query.resid].result))+'</code>'});
1377 			} else {
1378 				res.render(global_stored_results[req.query.resid].result.view,global_stored_results[req.query.resid].result);
1379 			}
1380 		}
1381 		);
1382 
1383 /* *****************************************************************************
1384  *	ROUTE:	export_result
1385  *	ACTION:	export the saved result <resid> to a JSON file
1386  */
1387 app.get('/export_result',
1388 		function(req,res) {
1389 			if (!req.query.hasOwnProperty('resid')) {
1390 				logger.error("Request-Feld \"resid\" nicht im HTTP-request");
1391 				res.redirect(303,"/error");
1392 				return;
1393 			}
1394 			var resobj = JSON.decycle(global_stored_results[req.query.resid].result);
1395 			var resdata = JSON.stringify(resobj);
1396 			if (req.query.hasOwnProperty('mfile')) {
1397 				functions.save_to_mfile('savedres',resdata,{	extension:	'json', 
1398 													resid: 		req.query.resid,
1399 													title:		resobj.title},
1400 								function(err,result) {
1401 									if (err) {
1402 										var nerr = 'Error saving result '+resid+' to managed file: '+err.message;
1403 										res.render('error',{errmsg: nerr.message});
1404 										return;
1405 									}
1406 									res.render('nvlist',object2nvlist(result,
1407 																		{	title:	'Ergebnis als Managed File gespeichert', 
1408 																			info:	'Ergebnis '+req.query.resid+' in managed file '+result.mfileid+' gespeichert'}));
1409 								});
1410 			} else {
1411 				aux.default_param(global_stored_results[req.query.resid].result,'contentType','text/json-decycled');
1412 				res.type(global_stored_results[req.query.resid].result.contentType);
1413 				res.status(200);
1414 				res.send(resdata);
1415 			}
1416 		});
1417 
1418 /* ******************************************************************************
1419  *	ROUTE:	active_actions
1420  *	ACTION:	list all active actions for this session
1421  */
1422 app.get('/active_actions',
1423 		function(req,res) {
1424 			if (req.session && req.session.active_actions) {
1425 				result = new db.Result();
1426 				for (rn in req.session.active_actions) {
1427 					if (global_active_actions[rn]) {
1428 					result.rows.push(['<a href="/show_active_action?actid='+rn+'">'+rn+' - '+global_active_actions[rn].title+'</a> - '+
1429 										'<a href="/show_active_action?actid='+rn+'&status">Status</a>'])
1430 					} else {
1431 						logger.error("DATA-STRUCTURE-ERROR: Session-local active action with ACTID=\""+rn+"\" not present in global active actions");
1432 					}
1433 				}
1434 				result.title='Laufende Aktionen';
1435 				res.render('slist',result);
1436 			} else {
1437 				res.render('plain',{body: 'keine laufenden Aktionen'});
1438 			}
1439 		}
1440 		);
1441 
1442 /* *****************************************************************************
1443  *	FUNCTION:		store_active_action
1444  *	INPUT:			session		-	the session the action is to be stored for
1445  *					action		-	the action object to store
1446  *	DESCRIPTION:	stores <action> in the global list of active actions
1447  *					with a reference in <session>
1448  */
1449 function store_active_action(session,action) {
1450 	 var actid = "ACTA_"+aux.nowstring();
1451 	 action.actid = actid;
1452 	 action.session = session;
1453 	 action.status = 'stored';
1454 	 if (session.last_action) {									// unlink the previous action from the global store if not yet saved
1455 		 delete global_active_actions[session.last_action];
1456 	 }
1457 	 if (!global_active_actions) global_active_actions = {};
1458 	 global_active_actions[actid] = {};
1459 	 global_active_actions[actid].actid  = actid;
1460 	 global_active_actions[actid].session = session;
1461 	 global_active_actions[actid].action = action;
1462 	 if (!session.active_actions) session.active_actions = {};
1463 	 session.last_action = actid;
1464 	 session.active_actions[actid] = {actid: actid};
1465  }
1466  
1467 /* *****************************************************************************
1468  *	ROUTE:	show_active_action
1469  *	ACTION:	show the current state of active action <actid>
1470  */
1471 app.get('/show_active_action',
1472 		function(req,res) {
1473 			if (!req.query.hasOwnProperty('actid')) {
1474 				if ((session) && (session.last_active_action)) {
1475 					req.query.actid = session.last_active_action;
1476 				} else {
1477 					logger.error("Request-Feld \"actid\" nicht im HTTP-request und auch keine letzte Aktion in der Session");
1478 					res.redirect(303,"/error");
1479 					return;
1480 				}
1481 			}
1482 			if (!global_active_actions[req.query.actid]) {
1483 				res.render('error',{errmsg: 'Aktion mit ACTID="'+req.query.actid+'" nicht vorhanden'});
1484 				return;
1485 			}
1486 			var act = global_active_actions[req.query.actid];
1487 			if ((act.status=='complete') &&										// action is complete, present final result to client
1488 				(!(req.query.hasOwnProperty('status'))))	{													// ... except user requests status explicitly
1489 				if (act.result.disposition=='view') {							// configured to render result with a view
1490 					aux.default_param(act.result,'view','plain');				// default view is 'plain' (only has one place holder for attribute 'body')
1491 					res.render(act.result.view,
1492 					           act.result);
1493 				} else {														// all other dispositions lead here, result attribute 'data' is directly sent to client
1494 					aux.default_param(act.result,'contentType','text/html');
1495 					res.type(act.result.contentType);
1496 					res.status(200);
1497 					res.send(act.result.data);					   
1498 				}
1499 			} else if (act.status=='error') {									// action has completed with error
1500 				res.render('error',{errmsg: act.error.message, 
1501 									result: act.result});						// always render with error view					
1502 			} else {															// action not yet complete, present status to client
1503 				var mo = {	actid: act.actid, status: act.status, 
1504 							action: act.action, stage: act.stage, 
1505 							subaction: act.subaction}; 							// make a copy of the action object to only show specific attributes
1506 				if (act.stateunit) mo.stateunit = act.stateunit;				// show stateunit only if present in original object
1507 				if (act.state) mo.state = act.state;							// the same for state
1508 				var fact = flatten_object(mo,6,null,							// convert to flat NVP object
1509 										  function(oref) {													
1510 												var rf;
1511 											  if (oref.name=="password") {									// obscure password values
1512 												  oref.value = "******";
1513 												  rf = true;
1514 											  } else if ((typeof(oref.value)==='string') && (oref.value.startsWith("[hidden"))) {				// suppress hidden values
1515 												  rf = false;
1516 											  } else {
1517 												rf = true;
1518 											  }
1519 											  // logger.debug("SAAFOF: "+oref.name+"=\""+oref.value+"\" - RESULT = "+rf);
1520 											  return rf;
1521 										  });
1522 				var farr = object2NVPArray(fact);
1523 				res.render('active_action',{actid: fact.actid, actiondata: farr});
1524 			}
1525 		}
1526 		);
1527 		
1528 app.get('/error',
1529         function(req,res) {
1530 			var rd = {};
1531 			rd.body='<font size="+5" color=red>Ein Fehler ist aufgetreten</font>';
1532 			res.render('plain',rd);
1533 		});
1534 
1535 app.get('/about', 
1536 		function(req, res){
1537 			var dbcfg = db.getCurrentConfig();
1538 			var rh = "<h2>About CCDB</h2>\n";
1539 			rh += "<table>\n";
1540 			rh += "<tr><td>Script Path:</td><td><code>"+__filename+"</code></td></tr>\n";
1541 			rh += "<tr><td>Script Version:</td><td><code>"+script_version+"</code></td></tr>\n";
1542 			rh += "<tr><td>Database Connect String:</td><td><code>"+dbcfg.getConnectString()+"</code></td></tr>\n";
1543 			rh += "<tr><td>Database User:</td><td><code>"+dbcfg.getUser()+"</code></td></tr>\n";
1544 			rh += "</table>";
1545 			if (req.session) {
1546 				rh += "<h2>Session-Information</h2>\n";
1547 				rh += "<table>\n";
1548 				for (so in req.session) {
1549 					if (req.session.hasOwnProperty(so)) {
1550 						if (typeof req.session[so] === 'object') {
1551 							rh += "<tr><td>"+so+"</td><td><code>"+JSON.stringify(JSON.decycle(req.session[so]))+"</code></td></tr>\n";
1552 						} else {							
1553 							rh += "<tr><td>"+so+"</td><td><code>"+req.session[so]+"</code></td></tr>\n";
1554 						}
1555 					}
1556 				}
1557 				rh += "</table>";
1558 			}
1559 			rh += "<br/><a href=\"/dbconfig\">Datenbank auswählen</a>";
1560 			rh += "<br/><a href=\"/shutdown?mode=restart\">CCDB Server neu starten</a>";
1561 			rh += "<br/><a href=\"/shutdown?mode=halt\">CCDB Server beenden</a>";
1562 			rh += "<br/><a href=\"/shutdown?mode=shutdown\">CCDB Server herunterfahren, Neustart verhindern</a>";
1563 			var rd = {body: rh};
1564 			res.render('plain',rd);
1565 		});
1566 
1567 // custom 404 page
1568 app.use(function(req, res){
1569         res.type('text/plain');
1570         res.status(404);
1571         res.send('404 - Not Found');
1572 });
1573 
1574 // custom 500 page
1575 app.use(function(err, req, res, next){
1576         logger.error(err.stack);
1577         res.type('text/plain');
1578         res.status(500);
1579         res.send('500 - Server Error');
1580 });
1581 
1582 /* *****************************************************************************
1583  *
1584  *	FLOW:			prepare_param_form
1585  *	INPUT:			paramspec	-	parameter specification object
1586  *						dataname	-	name of the query the parameter form is for
1587  *										sub-form-name if called recursively
1588  *						paramdata[]	-	array with one <paraminfo> per parameter:
1589  *								name	
1590  *								type
1591  *								value
1592  *								attributes
1593  *								source
1594  *	CALLBACK:		rfunc		-	called upon completion of preparation
1595  *		CALLBACKPARAMS:
1596  *					err			- 	error object or undefined if everything OK 
1597  *					formdata	-	parameter data for user form presentation:
1598  *						substructure:
1599  *							paramdata[]	- one <paraminfo> entry for each parameter:
1600  *								name	- name of the parameter
1601  *								type 	- type of the parameter
1602  *								value	- pre-defined value of the parameter
1603  *								desc	- description text for the parameter
1604  *								vlist	-	array of available values for this parameter,
1605  *									each value has the properties:
1606  *									value	-	value to be passed when selected
1607  *									text	-	text displayed to the user for this value
1608  *									desc	-	description for this value
1609  *												( this attribute only exists for scalar.enum structured parameters )
1610  *	DESCRIPTION:	prepare all necessary data to present an input form for all
1611  *					required input to the user
1612  *					<paramspec> is used to find out which input form fields to prepare
1613  *					upon completion, <paramdata> has all elements to render the HTML form
1614  *		paramspec has the following structure:
1615  *			dataname: 	name of the query to be performed, 
1616  *			paramdata:	a ROWS object with one row per required input parameter:
1617  *							name,type,value,source
1618  *			reqtype: 	name of the query request type:	
1619  *							usually: 'Abfrage' 
1620  *			url: 		the base URL for the query to be executed upon forms completion 
1621  *							usually: '/dquery'
1622  */
1623  var prepare_param_form = flow.define(
1624 	// step 1: preparations
1625 	function(paramspec,rfunc) {
1626 		this.paramspec = paramspec;
1627 		logger.debug("PREPARE_PARAM_FORM.BEGIN.PARAMSPEC: ",this.paramspec);
1628 		this.callback = rfunc;
1629 		this.curparnum = 0;					// start with 1st parameter
1630 		this.types = {};					// collect all types' type-information here
1631 		this.paramdata = new Array();
1632 		this.formdata = {};
1633 		for (var psi in this.paramspec) {					// copy all input from <paramspec> to <formdata> ...
1634 			if (psi!='paramdata') {							// ... except <paramspec.paramdata>, which will be ...
1635 				this.formdata[psi] = this.paramspec[psi];	// ... prepared by this flow
1636 			}
1637 		}
1638 		// now iterate over all entries in <paramspec.paramdata>, iterator is <curparnum>
1639 		this();
1640 	},
1641 	// step 2: process data for parameter <n>, prepare request for parameter <n+1>
1642 	function(err,paraminfo) {
1643 		if (err) {
1644 			var nerr = new Error("Error in preparing form parameter "+this.curparnum+" - \""+this.paramspec.paramdata[this.curparnum][0]+"\": "+err.message);
1645 			logger.error(nerr.message);
1646 			this.callback(nerr,paraminfo);
1647 			return;
1648 		}
1649 		if (paraminfo) {													// process result of parameter <curparnum>
1650 			this.paramdata.push(paraminfo);
1651 			this.curparnum++;
1652 		}
1653 		if (this.curparnum<this.paramspec.paramdata.length) {		// get info for parameter <curparnum+1>
1654 			logger.debug("PREPARE_PARAM_FORM.ITERATE.PARAMSPEC.PARAMDATA["+this.curparnum+"]: ",this.paramspec.paramdata[this.curparnum]);
1655 			this.REWIND();												// stay at this step
1656 			param_form_query(this.paramspec.paramdata[this.curparnum],
1657 							this);										// callback from param_form_query() will continue flow
1658 			return;														// simply return here, flow will be nudged on by callback from param_form_query()
1659 		}
1660 		this.formdata.paramdata = this.paramdata;
1661 		this.callback(null,this.formdata);
1662 		return;
1663 	}
1664 	// end of flow PREPARE_PARAM_FORM
1665 	);
1666 
1667 /* *****************************************************************************
1668  *	FLOW:				param_form_query
1669  *	INPUT:				pinfo	-	paraminfo object:
1670  *							name 	-	name of the parameter to prepare paraminfo for
1671  *							type	-	type of the parameter
1672  *							value	-	pre-supplied value for the parameter
1673  *							attribs	-	additional attributes object
1674  *							src		-	source of the requirement for the parameter:
1675  *								'param'		-	required as DB-query parameter
1676  *								'attrib'	-	required as general attribute for the query
1677  *								'ptype'		-	required by <paramtype> query attribute
1678  *	CALLBACK:			rfunc	-	called upon completion
1679  *							PARAMETERS:
1680  *								err			-	error, if any, otherwise NULL
1681  *								paraminfo	-	a paraminfo object:
1682  *									name	-	name of the query parameter
1683  *									type	-	name of the type
1684  *									value	-	pre-supplied value
1685  *										if <type>='array' this is an array of paraminfo
1686  *										if <type>='object' this is an object with the attributes (also paraminfos) as defined in the object type definition
1687  *									attribs	-	attributes, as passed from caller
1688  *									src		-	source, as passed from caller
1689  *												if <type>='array' this is 'recurse_array'
1690  +												IF <type>='object' this is 'recurse_object'
1691  *
1692  * 	DESCRIPTION:		prepares form input information for
1693  *						the query parameter in referred to by <name>/<type>:
1694  *						1) get type information from RW TYPES
1695  *
1696  */
1697 var	param_form_query = flow.define(
1698 	function (pinfo,rfunc,dep) {
1699 		this.callback = rfunc;
1700 		this.paraminfo = pinfo;
1701 		this.curname  = this.paraminfo.name;
1702 		this.curtype  = this.paraminfo.type;
1703 		this.curvalue = this.paraminfo.value;
1704 		this.attributes = this.paraminfo.attribs;
1705 		this.cursrc   = this.paraminfo.src;
1706 		this.curdep = dep || 100;				// keep track of recursion depth, starting at default of 100
1707 		logger.debug("PARAM_FORM_QUERY.BEGIN.DEPTH="+this.curdep+".PINFO: ",pinfo);
1708 		this.typdbr = new db.Request({info:		"get type info for \""+this.curtype+"\"",
1709 									reqid:		'RQID_GETYPINF',
1710 									headertext:	"type info for \""+this.curtype+"\"",
1711 									params:	[this.curtype]});
1712 		if (this.typdbr.getDBtype()=="mysql") {								// mySQL needs one param for each placeholder in SQL, even if they're the same
1713 			this.typdbr.setParams([this.curtype,this.curtype,this.curtype]);
1714 		}
1715 		db.select(this.typdbr,this);
1716 	},
1717 	// step 2: process result of querying type information
1718 	function(err,result) {
1719 		logger.debug("PARAM_FORM_QUERY.STEP_2.DEPTH="+this.curdep+".PARAMNAME=\""+this.curname+"\".STATE="+this.state+"/"+this.substate);
1720 		if (err) {
1721 			aux.error_callback(this.callback,err,"Error in param_form_query/GETYPINF",result);
1722 			return;
1723 		}
1724 		if (!(result.rows) || (result.rows.length<1) || (result.rows[0].length<3)) {
1725 			aux.error_callback(this.callback,null,"too few or wrong type information data about "+this.curtype+" in param_form_query/GETYPINF",result);
1726 			return;
1727 		}
1728 		// transfer the type attributes to the paraminfo
1729 		for (var i=0; i<result.rows.length; i++) {
1730 			if (result.rows[i].length<3) {
1731 				aux.error_callback(this.callback,null,"too few fields in type definition for \""+this.curtype+"\" row "+i+" in param_form_query/GETYPINF",result);
1732 				return;
1733 			}
1734 			this.paraminfo[result.rows[i][0]] = result.rows[i][1];
1735 			// this.paraminfo[result.rows[i][0]+'_desc'] = result.rows[i][2];
1736 		}
1737 		// fold attributes into paraminfo base object, allows overwriting of base type attributes by attributes from query
1738 		if (typeof this.attributes !== 'object') {
1739 			this.paraminfo.attribs = this.attributes;
1740 		} else {
1741 			for (var att in this.attributes) {
1742 				// logger.debug("...copying attribute "+i+":");
1743 				// logger.debug(this.attributes[i]);
1744 				this.paraminfo[att] = this.attributes[att];
1745 			}
1746 		}
1747 		logger.debug("PARAM_FORM_QUERY.STEP_2.FOLDED_TYPEINFO("+this.curtype+").PARAMINFO: ",this.paraminfo);
1748 		aux.default_param(this.paraminfo,'structure','string');			// default structure is 'string'
1749 		if (this.paraminfo.structure.indexOf('.')>=0) {
1750 			this.paraminfo.substructpath = this.paraminfo.structure.split('.');
1751 			this.paraminfo.structure = this.paraminfo.substructpath[0];
1752 		}
1753 		switch (this.paraminfo.structure) {
1754 			case 'string':											// structure 'string': simple free text string, no more information
1755 				this.paraminfo.inputString = true;
1756 				aux.default_param(this.paraminfo,'maxlen',40);
1757 				aux.default_param(this.paraminfo,'fieldlen',this.paraminfo.maxlen);
1758 				this.state = 'got';
1759 				this.substate = 'string';
1760 				this();												// go on to next step, no further query
1761 				break;
1762 			case 'scalar':
1763 				aux.default_param(this.paraminfo,'scalarsubtype','number');
1764 				if (this.paraminfo.substructpath[1]) this.paraminfo.scalarsubtype = this.paraminfo.substructpath[1];
1765 				switch (this.paraminfo.scalarsubtype) {
1766 					case 'number':
1767 						this.paraminfo.inputNumber = true;
1768 						this.paraminfo.paramIsNumber = true;
1769 						aux.default_param(this.paraminfo,'minvalue',0);
1770 						aux.default_param(this.paraminfo,'maxvalue',-1);
1771 						this.state = 'got';
1772 						this.substate = 'number';
1773 						this();
1774 						break;
1775 					case 'enum':
1776 					case 'presets':
1777 						this.dbtype = db.getCurrentConfigDBtype();
1778 						this.paraminfo.inputEnum = (this.paraminfo.scalarsubtype=='enum');
1779 						this.paraminfo.paramIsEnum = (this.paraminfo.scalarsubtype=='enum');
1780 						this.paraminfo.inputPresets = (this.paraminfo.scalarsubtype=='presets');
1781 						this.paraminfo.paramIsPresets = (this.paraminfo.scalarsubtype=='presets');
1782 						if (this.paraminfo.vlistexpr) {				// vlistexpr must deliver an Array of objects with attributes value,text,desc
1783 							try {
1784 								this.paraminfo.vlist = eval(this.paraminfo.vlistexpr);
1785 							}
1786 							catch (e) {
1787 								aux.error_callback(this.callback,e,"Error processing vlistexpr",{resulttype: 'object', resultobject: this.paraminfo});
1788 								return;
1789 							}
1790 							for (var vi=0; vi<this.paraminfo.vlist.length; vi++) {
1791 								aux.default_param(this.paraminfo.vlist[vi],'value',vi);
1792 								aux.default_param(this.paraminfo.vlist[vi],'text',this.paraminfo.vlist[vi].value);
1793 								aux.default_param(this.paraminfo.vlist[vi],'desc',this.paraminfo.vlist[vi].text);
1794 							}
1795 							this.state = 'got';
1796 							this.substate = 'vlist';
1797 							this();
1798 							return;
1799 						} else if (this.paraminfo["vlistquery_"+this.dbtype]) { // DB-type specific vlistquery must deliver a result with n rows each having 3 columns: value,text,desc
1800 							this.dbr = new db.Request({	info:	'VLIST query for parameter name "'+this.curname+'" of type "'+this.curtype+'" on DB-type "'+this.dbtype+'"',
1801 														reqid:	'RQID_VLIQRY_'+this.curtype,
1802 														sql:	this.paraminfo["vlistquery_"+this.dbtype],
1803 														params:	[]});
1804 							this.state = 'wait';
1805 							this.substate = 'vlist';
1806 							db.select(this.dbr,this);
1807 						} else if (this.paraminfo.vlistquery) {					// general vlistquery must deliver a result with n rows each having 3 columns: value,text,desc
1808 							this.dbr = new db.Request({	info:	'VLIST query for parameter name "'+this.curname+'" of type "'+this.curtype+'"',
1809 														reqid:	'RQID_VLIQRY_'+this.curtype,
1810 														sql:	this.paraminfo.vlistquery,
1811 														params:	[]});
1812 							this.state = 'wait';
1813 							this.substate = 'vlist';
1814 							db.select(this.dbr,this);
1815 						}
1816 						break;
1817 					default:
1818 						aux.error_callback(this.callback,null,"Error: illegal scalar subtype \""+this.paraminfo.scalarsubtype+"\"",{resulttype: 'object', resultobject: this.paraminfo});
1819 						return;
1820 				}
1821 				break;
1822 			case 'array':											// structure 'array': array of <arraytype>
1823 				this.paraminfo.inputArray = true;
1824 				this.paraminfo.paramIsArray = true;
1825 				aux.default_param(this.paraminfo,'arraytype','string');
1826 				aux.default_param(this.paraminfo,'minlength','1');
1827 				aux.default_param(this.paraminfo,'maxlength','50');
1828 				this.paraminfo.value = new Array();
1829 				this.state = 'wait';
1830 				this.substate = 'arraytype'
1831 				if (this.paraminfo.paramdata) {
1832 					logger.error("PARAMINFO for Array type has paramdata[], this is strange!!");
1833 				}
1834 				this.paraminfo.paramdata = new Array();
1835 				param_form_query({	name:	this.curname+'_array_row',	// get paraminfo for entry type in array, this name must not stay !!
1836 									type:	this.paraminfo.arraytype,	// type of entry
1837 									value:	this.curvalue,				// value of entry, TODO: implement!!
1838 									attribs:	null,					// attributes: not yet known
1839 									src:	'recurse_array'},			// source
1840 									this,								// callback: next step
1841 									this.curdep+1);
1842 				break;
1843 			case 'object':
1844 				this.paraminfo.inputObject = true;
1845 				this.paraminfo.paramIsObject = true;
1846 				aux.default_param(this.paraminfo,'objectattributes','a');
1847 				this.state = 'prep';
1848 				this.substate = 'object';
1849 				this.objectattributes = this.paraminfo.objectattributes.split(',');
1850 				if (this.paraminfo.paramdata) {
1851 					logger.error("PARAMINFO for Object type has paramdata[], this is strange!!");
1852 				}
1853 				this.paraminfo.paramdata = new Array();
1854 				this.curobjattnum = 0;
1855 				delete this.paraminfo.value;					// object itself must not have value, paramdata[] must carry the field values
1856 				this();											// real queries for all attributes are done in next step
1857 				break;
1858 			default:
1859 				aux.error_callback(this.callback,null,"Error: illegal structure name \""+this.paraminfo.structure+"\"",{resulttype: 'object', resultobject: this.paraminfo});
1860 				return;
1861 		}
1862 	},
1863 	// step 3: got param info query result
1864 	function(err,result) {
1865 		if (err) {
1866 			aux.error_callback(this.callback,err,"Error in param_form_query()/state="+this.state+"/"+this.substate+" for type \""+this.curtype+"\"",result);
1867 			return;
1868 		}
1869 		logger.debug("PARAM_FORM_QUERY.STEP_3.DEPTH="+this.curdep+".PARAMNAME=\""+this.curname+"\".STATE="+this.state+"/"+this.substate);
1870 		switch (this.state) {
1871 			case 'got':							// nothing expected, terminate flow here
1872 				this.callback(null,this.paraminfo);
1873 				break;
1874 			case 'wait':						// we're waiting for one of possibly more param query results
1875 				if (result) {							// if no result, any result is already prepared in this.paraminfo
1876 					switch (this.substate) {	// what are we waiting for?
1877 						case 'vlist':			// waiting for paraminfo VLIST
1878 							var vle;
1879 							this.paraminfo.vlist = new Array();
1880 							for (var vi = 0; vi<result.rows.length; vi++) {
1881 								vle = {};
1882 								if (result.rows[vi].length>2) vle.desc  = result.rows[vi][2];
1883 								if (result.rows[vi].length>1) vle.text  = result.rows[vi][1];
1884 								if (result.rows[vi].length>0) vle.value = result.rows[vi][0];
1885 								aux.default_param(vle,'value',vi);
1886 								aux.default_param(vle,'text',vle.value);
1887 								aux.default_param(vle,'desc',vle.text);
1888 								if (vle.value == this.paraminfo.value) {
1889 									vle.selected = true;
1890 								}
1891 								this.paraminfo.vlist.push(vle);
1892 							}
1893 							this.callback(null,this.paraminfo);
1894 							break;
1895 						case 'arraytype':		// waiting for type info on array entry type
1896 												// result is assumed to be paraminfo
1897 							for (var afi = 0; afi<result.paramdata.length; afi++) {
1898 								this.paraminfo.paramdata.push(result.paramdata[afi]);	// merge the result paramdata[]
1899 							}
1900 							this.callback(null,this.paraminfo);
1901 							break;
1902 						case 'object':			// waiting for next object attribute paraminfo
1903 												// result is assumed to be paraminfo
1904 							this.paraminfo.paramdata.push(result);
1905 							this.curobjattnum++;									// move on to next attribute
1906 							this.state = 'prep';
1907 							this.substate = 'object';
1908 							this.REWIND();
1909 							this();
1910 							break;
1911 						default:
1912 							aux.error_callback(this.callback,null,"Illegal state/substate="+this.state+"/"+this.substate+" in param_form_query()/3/wait");
1913 							return;
1914 					}
1915 				} else {
1916 					aux.error_callback(this.callback,null,"Error in param_form_query().state="+this.state+"/"+this.substate+": got no result");
1917 					return;
1918 				}
1919 				break;
1920 			case 'prep':
1921 				switch (this.substate) {
1922 					case 'object':
1923 						if (this.curobjattnum>=this.objectattributes.length) {	// no more object attributes
1924 							this.callback(null,this.paraminfo);
1925 						} else {
1926 							logger.debug("..curobjatt["+this.curobjattnum+"]=\""+this.objectattributes[this.curobjattnum]+"\"");
1927 							this.curobjattfields = this.objectattributes[this.curobjattnum].split(':');
1928 							this.curobjattname = this.curobjattfields[0] || this.curobjattnum;
1929 							this.curobjatttype = this.curobjattfields[1] || this.curobjattname;
1930 							logger.debug("PARAM_FORM_QUERY.STEP_3.CUROBJATTNUM="+this.curobjattnum+".CUROBJATTFIELDS[2]="+this.curobjattfields[2]);
1931 							this.curobjattoptstr = aux.populate(this.curobjattfields[2] || '',this.paraminfo,this);
1932 							logger.debug("PARAM_FORM_QUERY.STEP_3.CUROBJATTNUM="+this.curobjattnum+".CUROBJATTOPTSTR=\"",this.curobjattoptstr+"\"");
1933 							if (this.curobjattoptstr) {
1934 								try {
1935 									this.curobjattoptions = JSON.parse(this.curobjattoptstr);
1936 								}
1937 								catch (e) {
1938 									aux.error_callback(this.callback,e,"Error JSON-parsing object attributes options for attribute "+this.curobjattname);
1939 									return;
1940 								}
1941 							} else {
1942 								this.curobjattoptions = null;
1943 							}
1944 							logger.debug("PARAM_FORM_QUERY.STEP_3.CUROBJATTNUM="+this.curobjattnum+".CUROBJATTOPTIONS:",this.curobjattoptions);
1945 							this.state = 'wait';
1946 							this.substate = 'object';
1947 							this.REWIND();								// stay at this step in another state
1948 							param_form_query({name:	this.curobjattname,
1949 												type:	this.curobjatttype,
1950 												value:	null,			// value: TODO: implement!!
1951 												attribs:	this.curobjattoptions,
1952 												src:	'recurse_object_attribute'
1953 											},
1954 											this,this.curdep+1);
1955 						}
1956 						break;
1957 					default:
1958 						aux.error_callback(this.callback,null,"Illegal state/substate="+this.state+"/"+this.substate+" in param_form_query()/3/prep");
1959 						return;
1960 				}
1961 				break;
1962 			default:
1963 				aux.error_callback(this.callback,null,"Illegal state/substate="+this.state+"/"+this.substate+" in param_form_query()");
1964 				return;
1965 		}
1966 		logger.debug("PARAM_FORM_QUERY.STEP_3.END.PARAM_NAME=\""+this.curname+"\".DEPTH="+this.curdep+".STATE="+this.state+"/"+this.substate);
1967 	}
1968 	// end of flow PARAM_FORM_QUERY
1969 );
1970  
1971  /* *****************************************************************************
1972  *	FUNCTION:		process_sequence_step
1973  *	INPUT:			that	-	the sequence execution context
1974  *					phase	-	the name of the phase of the sequence to be processed
1975  *	RETURNS:		result of the processed expression
1976  *	DESCRIPTION:	performs pre- or post-processing for the sequence step <that.stepnum>
1977  *					This is done by processing a javascript-expression
1978  *					The expression is taken:
1979  *					- either from the context attribute named <phase>_step_<stepnumber>
1980  *					- or from the context attribute named query.<phase>_step_<stepnumber>
1981  *						(if first does not exist)
1982  *					If neither attributes exist, nothing is done
1983  *					The content of the attributes is pre-processed by replacing any placeholders 
1984  *					delimited by {{..}} (see also aux.populate() )
1985  *					
1986  *	
1987  */
1988  function process_sequence_step(that,phase) {
1989 	var procparname = phase+"_step_"+that.stepnum;
1990 	var procexpr = '';
1991 	var procres;
1992 	var attctx
1993 	if (that[procparname]) {
1994 		procexpr = aux.populate(''+that[procparname],that);
1995 		attctx = 'sequence';
1996 	} else {
1997 		if ((that.query) && (that.query[procparname])) {
1998 			procexpr = aux.populate(''+that.query[procparname],that);
1999 			attctx = 'sequence.query';
2000 		} else {
2001 			procexpr = '';
2002 			// logger.debug('SEQUENCE: no JS-expression '+procparname+' defined');
2003 			return;
2004 		 }
2005 	}
2006 	logger.debug("SEQUENCE.PROCPAR."+procparname+"(populated)=\""+procexpr+"\"");
2007 	var procres;
2008 	try {
2009 		procres = eval(procexpr);
2010 	}
2011 	catch(e) {
2012 		procres = "*** ERROR processing expression \""+procexpr+"\" from \""+attctx+"."+procparname+"\" ***";
2013 		logger.error(procres);
2014 	}
2015 	logger.debug("SEQUENCE.RESULT_OF_"+procparname+":");
2016 	logger.debug(procres);
2017  }
2018 
2019 /* *****************************************************************************
2020  *
2021  *	FLOW:			dsequence
2022  *	INPUT:			query	-	query object
2023  *						attributes:
2024  *					seqname		-	name of the sequence to execute
2025  * CALLBACK:		rfunc	-	called upon completion of sequence, successsful or in error
2026  *	CALLBACKPARAMS:	err		-	error objects, undefined if no error
2027  *					result	-	result object
2028  *								attributes:
2029  *						rows	-	result table rows
2030  *						data	-	csv-formatted result, if query.format was 'csv'
2031  * CALLBACK:		pfunc		-	called upon updated progress status
2032  *	CALLBACKPARAMS:	progstat	-	arbitrary object describing the progress of the sequence
2033  *
2034  * *****************************************************************************/
2035  
2036  var dsequence = flow.define(
2037 	// step 1: check authorization 
2038 	function(sess,query,rfunc,pfunc) {
2039 		this.calldepth = 0;
2040 		this.callback = rfunc;
2041 		this.pcallback = pfunc;
2042 		this.query = query;
2043 		this.session = sess;
2044 		this.lastresult = null;
2045 		this.dbresults = new Array();
2046 		aux.default_param(this.query,'dataname','***UNKNOWN***');
2047 		logger.debug("SEQUENCE.BEGIN.DATANAME=\""+this.query.dataname+"\".FORMAT=\""+this.query.format+"\"");
2048 		functions.check_authorization(this.session,'dsequence',this,this);
2049 	},
2050 	// step 1a: get sequence descriptor data
2051 	function(err,result) {
2052 		if (err) {
2053 			aux.error_callback(this.callback,err,"Authorization error in SEQUENCE(\""+this.dataname+"\")",result);
2054 			return;
2055 		}
2056 		this.qddbr = new db.Request({sql:    "SELECT RWNAME,RWVALUE,RWDESC FROM CCRW WHERE RWTYP='DSEQPARAM' and RWPARENT=:DATANAME",
2057 									info:   "Get sequence descriptor data for DATANAME=\""+this.query.dataname+"\"",
2058 									reqid:	'RQID_GETSEQDESC',
2059 									params: [this.query.dataname]});
2060 		db.select(this.qddbr,
2061 				this,
2062 				function(progstat) {
2063 					// logger.debug("DSEQUENCE.GETSEQDESC.PROGRESS:");
2064 					// logger.debug(util.inspect(progstat,false,null));
2065 					this.calldepth++;
2066 					var pstat = {action: 'dsequence', stage: 'getquerydesc', calldepth: this.calldepth, subaction: progstat};
2067 					if (this.pcallback) this.pcallback(pstat);
2068 					this.calldepth--;
2069 				}.bind(this));
2070 	},
2071 	// step 2: query for sequence descriptor completed, check, prepare and execute
2072 	function(err,result) {
2073 		if (err) {
2074 			aux.error_callback(this.callback,err,"Error in DSEQUENCE/GetSeqDesc",result);
2075 			return;
2076 		}
2077 		prepare_dquery_params(this,result);				// populate the sequence's parameters, possibly from the sequence descriptor
2078 		aux.default_param(this.query,'sequence','EXIT');
2079 		this.steps = this.query.sequence.split(",");	// list of steps in the sequence
2080 		this.seqlen = this.steps.length;				// number of steps in sequence
2081 		this.stepnum = 0;								// start with 1st step
2082 		this();											// proceed to step-processing
2083 	},
2084 	// step 3: sequence step completed (if result there), check and then execute next (or first, if no result there) step or terminate (if no (more) steps)
2085 	function(err,result) {
2086 		if (err) {
2087 			aux.error_callback(this.callback,err,"Error in DSEQUENCE/2",result);
2088 			return;
2089 		}
2090 		if (result) {									// if result is there, we came from the same state and already processed a step
2091 			result.prior = this.lastresult;					// build a stack-like chain of results 
2092 			this.lastresult = result;						// attach result to sequence context
2093 			this.dbresults.push(result);					// collect all results
2094 			this.lastresult.dbresults = this.dbresults;		// keep a reference to all collected results in each partial result
2095 			process_sequence_step(this,'post');				// apply any post-processing defined in 'post_step_<N>'
2096 			delete this.stepquery;
2097 			this.stepnum++;									// proceed with next step
2098 		}
2099 		this.stepquery = {};
2100 		this.stepquery.sequencecontext = this;			// link stepquery back to sequence context to let expressions access it
2101 		if (this.stepnum>=this.seqlen) {				// sequence is complete, terminate
2102 			logger.debug("SEQUENCE.NAME=\""+this.query.dataname+"\".COMPLETE");
2103 			this.callback(undefined,this.lastresult);	// the result of the last step is the result of the sequence (post-processing may have amended it)
2104 			return;
2105 		} else {										// sequence not complete, proceed
2106 			this.REWIND();								// tell flow to stay at this step
2107 			this.curstep = {};
2108 			this.curstep.parts = this.steps[this.stepnum].split(':');	// strip off optional information
2109 			this.stepquery.dataname = this.curstep.parts[0];			// get next query dataname (1st part in step string)
2110 			this.stepquery.optionstring = (this.curstep.parts.length>1)?this.curstep.parts[1]:'';
2111 			if (this.stepquery.optionstring) {
2112 				this.stepquery.optionstring = aux.populate(this.stepquery.optionstring,this.query,this);
2113 				if (this.stepquery.optionstring.charAt(0)=='{') {
2114 					try {
2115 						this.stepquery.options = JSON.parse(this.stepquery.optionstring);
2116 					} catch (e) {
2117 						aux.error_callback(this.callback,e,
2118 						  "Error JSON-parsing sequence(\""+this.query.dataname+"\")-step("+this.stepnum+",\""+this.stepquer.dataname+"\")-options \""+this.stepquery.optionstring+"\"");
2119 						return;
2120 					}
2121 				}
2122 			}
2123 			logger.debug("SEQUENCE.NAME=\""+this.query.dataname+"\".STEPNUM="+this.stepnum+".STEP=\""+this.stepquery.dataname+"\"");
2124 			process_sequence_step(this,'pre');			// apply any pre-processing defined in 'pre_step_<N+1>'
2125 			dquery(this.session,this.stepquery,this,
2126 				function(progstat) {
2127 					var pstat = {action:	'dsequence',
2128 								stage:		'execstep',
2129 								stateunit:	'seqstep',
2130 								state:		this.stepnum,
2131 								seqdataname: 	this.query.dataname,
2132 								seqstepquery:	this.stepquery,
2133 								subaction:	progstat
2134 								};
2135 					if (this.pcallback) this.pcallback(pstat);
2136 				}.bind(this));							// execute next step
2137 		}
2138 	}
2139 	// end of flow dsequence
2140  );
2141  
2142 /* *****************************************************************************
2143  *	FUNCTION:		find_name
2144  *	INPUT:			nvpa	- 	either array of arrays or array of objects
2145  *					nam		-	name to find in <nvpa>
2146  *					npn		- 	name of property containing the name (if <nvpa> is an object
2147  *								DEFAULT:	'name'
2148  *	DESCRIPTION:	checks, if an entry with name <nam> already exists in <nvpa>
2149  *					if <nvpa> is an array of arrays, the name is assumed to be entry [0] of each sub-array
2150  *					if <nvpa> is an array of objects, the name is assumed to be the attribute <npn> of each object
2151  */
2152 function find_name(nvpa,nam,npn) {
2153 	npn = npn || 'name';
2154 	// logger.debug("FIND_NAME.ARRAY:");
2155 	// logger.debug(nvpa);
2156 	// logger.debug("FIND_NAME.NAM=\""+nam+"\"");
2157 	for (var i=0; i<nvpa.length; i++) {
2158 		if (nvpa[i] && util.isArray(nvpa[i])) {
2159 			if ((nvpa[i]) && (nvpa[i][0]) && (nvpa[i][0]==nam)) {
2160 				return true;
2161 			}
2162 		} else {
2163 			if ((nvpa[i]) && nvpa[i].hasOwnProperty(npn) && nvpa[i][npn]==nam) {
2164 				return true;
2165 			}
2166 		}
2167 	}
2168 	return false;
2169 }
2170 
2171 /* *****************************************************************************
2172  *	FUNCTION:		update_object
2173  *	INPUT:			obj		-	target object
2174  *					updobj	-	object to update <obj> with
2175  *	DESCRIPTION:	updates all attributes in <obj> from <updobj>
2176  *					if <updobj> has an attribute that <obj> does not have, <obj> will be extended
2177  *					if the attribute already exists in <obj>, its value is overwritten from <updobj>
2178  */
2179  function update_object(obj,updobj) {
2180 	 for (ua in updobj) {
2181 		 obj[ua] = updobj[ua];
2182 	 }
2183  }
2184  
2185  /* *****************************************************************************
2186  *	FUNCTION:		flatten_object
2187  *	INPUT:			obj			-	hierarchical object
2188  *					maxdepth	-	maximal hierarchical depth to be investigated
2189  *									DEFAULT:	1
2190  *					nameprefix	-	prefix to be put in front of each attributes' name in <fobj>
2191  *					filterfunc	-	optional filtering callback function:
2192  *									called for each leaf object
2193  *									input:	objref	-	pointer to leaf object, possibly modified:
2194  *										nameprefix	-	passed from flatten_object() - invocation
2195  *										name		-	leaf name
2196  *										value		-	leaf value
2197  *									result:	true - include leaf object in output
2198  *											false - omit leaf object from output
2199  *									filterfunc may modify nameprefix, name and value of objref
2200  *	RETURNS:		fobj	-	flattened object
2201  *	DESCRIPTION:	makes <fobj> a copy of <obj>, but with all sub-objects folded in
2202  *					if an attribute in <obj> is an object itself, its attributes
2203  *					are copied down into <fobj> with dotted name
2204  *					Example:
2205  *						{ 	a: 1,
2206  *							b:	{	b1:	2.1,
2207  *									b2:	2.2 }
2208  *							c: "maxi"	}
2209  *						is flattened to:
2210  *						{	a: 		1,
2211  *							b.b1: 	2.1,
2212  *							b.b2:	2.2,
2213  *							c:		"maxi" }
2214  */
2215  function flatten_object(obj,maxdepth,nameprefix,filterfunc) {
2216 	var fobj = {};
2217 	var tobj = {};
2218 	var tval = null;
2219 	for (anam in obj) {
2220 		tval = obj[anam];
2221 		try {
2222 			aty = typeof tval;
2223 		}
2224 		catch (e) {
2225 			 logger.error("FLATTTEN_OBJECT: Error determining type of attribute "+anam+": "+e.message);
2226 			 aty = 'undetermined'
2227 		}
2228 		// logger.debug("FLATTEN_OBJECT.MAXDEPTH=\""+maxdepth+"\".NAMEPREFIX=\""+nameprefix+"\".ANAM=\""+anam+"\".ATYPE=\""+aty+"\"");
2229 		if (aty === 'object') {										// attribute is an object itself, recurse down
2230 			if (maxdepth && maxdepth>1) {								// but only until <maxdepth>
2231 				update_object(fobj,flatten_object(obj[anam],maxdepth-1,''+((nameprefix)?nameprefix:'')+anam+'.',filterfunc));
2232 				continue;
2233 			} else {
2234 				tval = "[hiddenObject]";	// hide lower hierarchies
2235 			}
2236 		}
2237 		if (aty === 'undetermined') {
2238 			tval = "[undetermined]";		// can't grab it ...
2239 		} else if (aty === 'function') {
2240 			tval = "[hiddenFunction]";		// don't detail functions
2241 		}
2242 		if (filterfunc) {
2243 			tobj.nameprefix = nameprefix;
2244 			tobj.name = anam;
2245 			tobj.value = tval;
2246 			if (filterfunc(tobj)) {
2247 				fobj[(tobj.nameprefix)?tobj.nameprefix+tobj.name:tobj.name] = tobj.value;
2248 			}
2249 		} else {
2250 			// logger.debug("FOBJ: no filterfunc - "+nameprefix+"."+anam+" = \""+obj[anam]+"\"");
2251 			fobj[(nameprefix)?nameprefix+anam:anam] = tval;
2252 		}
2253 	}
2254 	// logger.debug("FLATTEN_OBJECT.FOBJ:");
2255 	// logger.debug(fobj);
2256 	return fobj;
2257  }
2258  
2259  /* *****************************************************************************
2260  *	FUNCTION:		object2NVPArray
2261  *	INPUT:			obj		- flat object
2262  *	RETURNS:		array with one entry for each attribute of <obj>
2263  *					each entry is an array of 2 elements: 
2264  *						0 .. name of the attribute
2265  *						1 .. value of the attribute
2266  */
2267  function object2NVPArray(obj) {
2268 	 nvpa = new Array();
2269 	 for (anam in obj) {
2270 		nvpa.push([anam,obj[anam]]);
2271 	 }
2272 	 return nvpa;
2273  }
2274 
2275 /* *****************************************************************************
2276  *	FUNCTION:			prepare_dquery_params
2277  *	INPUT:				that	-	dquery/dsequence context object to be populated from <result>
2278  *						result	-	result object from reading dquery/dsequence descriptor:
2279  *									rows[x][0]	-	query attribute name
2280  *									rows[x][1]	-	query attribute value
2281  *	OUTPUT:				that.need_user_input	-	true if user input needed
2282  *							if true:
2283  *								that.paramdata	-	a paramdata object containing information about parameters to be entered:
2284  *									paramdata is an array of paraminfo objects:
2285  *										name	-	name of the parameter
2286  *										type	-	name of the type of the parameter
2287  *										value	-	optionally already present value
2288  *										attributes	-	optional additional attributes concerning the entry of this parameter
2289  *										dtype		-	source of the request for this parameter: 'param'/'attr'/'ptype'
2290  *										index		-	index in the respective request structure
2291  *								that.names_needed	-	a paramdata object containing only the needed parameters (no value supplied)
2292  *	DESCRIPTION:		populate <that> query's parameters with data from <result> (possibly read from RW DQUERY/<dataname>) 
2293  */
2294 function prepare_dquery_params(that,result) {
2295 	delete that.query.querytype;
2296 	delete that.query.sql;
2297 	delete that.query.paramnames;
2298 	delete that.query.attributenames;
2299 	delete that.query.headertext;
2300 	delete that.query.info;
2301 	delete that.query.desctext;
2302 	delete that.query.title;
2303 	var pnam, pvar, pnames, bo, pns;
2304 	var pdata = new Array();											// pdata collects parameter data with current value to be edited by user if some are missing
2305 	//
2306 	// phase 1: transfer query attributes from query descriptor to query context (that.query)
2307 	//
2308 	for (var qi=0; qi<result.rows.length; qi++) {
2309 		pnam = result.rows[qi][0];
2310 		pval = result.rows[qi][1];
2311 		if (!pnam) continue;											// ignore config items with no name
2312 		if (pnam.indexOf(".")>=0) {										// is this a multi-level name ?
2313 			pnames = pnam.split(".");									// yes, split it into its segments
2314 			bo = that.query;											
2315 			pns = "";
2316 			for (var pi=0; pi<pnames.length; pi++) {					// traverse the path described by the segments, starting with the query object
2317 				// logger.debug("level "+pnames[pi]);
2318 				if (pi<pnames.length-1) {
2319 					if (!bo.hasOwnProperty(pnames[pi])) {
2320 						bo[pnames[pi]] = {};
2321 						// logger.debug("created level "+pnames[pi]);
2322 					}
2323 					bo = bo[pnames[pi]];
2324 					pns += (pns.length==0?"":".")+pnames[pi];
2325 				} else {
2326 					bo[pnames[pi]] = pval;
2327 				}
2328 			}
2329 		} else {
2330 			that.query[pnam] = pval;
2331 		}
2332 	}
2333 	//
2334 	// phase 2: get required parameters and check if all are present
2335 	//			required parameters are specified in:
2336 	//			[paramnames]
2337 	//			[attributenames]
2338 	//			[paramtype]
2339 	aux.default_param(that.query,'querytype','dbselect');
2340 	var sppn = 'paramnames_'+db.getCurrentConfigDBtype();
2341 	var specparamnames = null;
2342 	if (that.query.hasOwnProperty(sppn)) {
2343 	  specparamnames = that.query[sppn];		// let [paramnames_<db-type>] override [paramnames]
2344 	  that.query.paramnames = specparamnames;
2345 	}
2346 	aux.default_param(that.query,'paramnames',"");
2347 	aux.default_param(that.query,'attributenames',"");
2348 	aux.default_param(that.query,'paramtype',"");
2349 	that.query.params = new Array();								// collect query params here
2350 	that.need_user_input = false;
2351 	that.parameters_needed = new Array();							// collect needed params here
2352 	that.names_needed = {};
2353 	var namcomps, curname, curvalue, curtype, curatt;
2354 	//
2355 	// phase 2.a: gather from [paramnames], these are required for dbselect type queries and bound to bind-variables
2356 	//
2357 	if (that.query.paramnames.length>0) {												// parameters are necessary
2358 		that.paramnames = that.query.paramnames.split(",");
2359 		for (var pi = 0; pi<that.paramnames.length; pi++) {			// check all necessary bind-variable-parameters
2360 			curtype = "string";
2361 			curname = that.paramnames[pi];
2362 			curvalue = '';
2363 			if (curname.indexOf(":")>=0) {
2364 				namcomps = curname.split(":");
2365 				if (namcomps.length>0) curname = namcomps[0];
2366 				if (namcomps.length>1) curtype = namcomps[1];		// name has a type attached
2367 				if (namcomps.length>2) curatt  = namcomps[2];		// name has additional attributes attached
2368 			}
2369 			if (that.query.hasOwnProperty(curname)) {  											// do we have the parameter already as a query attribute?
2370 				curvalue = that.query[curname];
2371 				that.query.params.push(curvalue);												// yes, take it from query
2372 			} else {																			// no, not specified in query
2373 				if (!(that.names_needed[curname])) {											// have we already requested it?
2374 					that.parameters_needed.push(curname);										// don't have it, signal it
2375 					that.names_needed[curname] = {dtype: 'param', type: curtype, attributes: curatt, index: pi};	// memorize it as already requested as parameter
2376 				}
2377 				that.need_user_input = true;													// no, signal that we need user input
2378 			}
2379 			if (!find_name(pdata,curname)) {
2380 				pdata.push({name: curname, type: curtype, value: curvalue, attributes: curatt, dtype: 'param'});
2381 			}
2382 		}
2383 	}
2384 	//
2385 	// phase 2.b: gather from [attributenames]
2386 	//
2387 	that.query.attributes = {};
2388 	if (that.query.attributenames.length>0) {													// required attributes from query descriptor
2389 		that.attributenames = that.query.attributenames.split(",");
2390 		for (var ai = 0; ai<that.attributenames.length; ai++) {									// check all required attributes' names
2391 			curtype = 'string';
2392 			curname = that.attributenames[ai];
2393 			curvalue = '';
2394 			if (curname.indexOf(":")>=0) {
2395 				namcomps = curname.split(":");
2396 				if (namcomps.length>0) curname = namcomps[0];
2397 				if (namcomps.length>1) curtype = namcomps[1];		// name has a type attached
2398 				if (namcomps.length>2) curatt  = namcomps[2];		// name has additional options attached
2399 			}
2400 			if (that.query.hasOwnProperty(curname)) {											// do we have a query attribute with this name?
2401 				curvalue = that.query[curname];													// yes, get its value from the query
2402 				that.query.attributes[curname] = curvalue;										// use it
2403 			} else {																			// we don't have a query attribute with this name
2404 				if (!(that.names_needed[curname])) {											// have we already requested it?
2405 					that.parameters_needed.push(curname);										// don't have it, signal it
2406 					that.names_needed[curname] = {dtype: 'attr', type: curtype, attributes: curatt, index: ai};		// memorize as already requested as attribute
2407 				}
2408 				that.need_user_input = true;
2409 			}
2410 			if (!find_name(pdata,curname)) {
2411 				pdata.push({name: curname, type: curtype, value: curvalue, attributes: curatt, dtype: 'attr'});
2412 			}
2413 		}
2414 	}
2415 	//
2416 	// phase 2.c: gather from [paramtype]
2417 	//
2418 	if (that.query.paramtype) {
2419 		curtype = that.query.paramtype;
2420 		// TODO: UNDER CONSTRUCTION !!!
2421 		curname = 'paramobject'
2422 		curvalue = {};
2423 		curatt = '';
2424 		if (curname.indexOf(':')>=0) {
2425 			namcomps = curname.split(":"); 
2426 			if (namcomps.length>0) curname = namcomps[0];
2427 			if (namcomps.length>1) curtype = namcomps[1];		// name has a type attached
2428 			if (namcomps.length>2) curatt  = namcomps[2];		// name has additional options attached
2429 		}
2430 		if (that.query.hasOwnProperty(curname)) {							// do we have a query attribute 'paramobject' already?
2431 			curvalue = that.query[curname];
2432 			that.query.attributes[curname] = curvalue;
2433 		} else {															// don't have a query attribute with this name
2434 			if (!(that.names_needed[curname])) {							// do we alread have requested it ?
2435 				that.parameters_needed.push(curname);						// don't have it, signal it
2436 				that.names_needed[curname] = {dtype: 'ptype', type: curtype, attributes: curatt, index: -1};	// memorize as already requested by paramtype
2437 			}
2438 			that.need_user_input = true;
2439 		}
2440 		if (!find_name(pdata,curname)) {
2441 			pdata.push({name: curname, type: curtype, value: curvalue, attributes: curatt, dtype: 'ptype'});
2442 		}
2443 	}
2444 	// 
2445 	// phase 3: now resolve placeholders in value fields and attributes value fields
2446 	//
2447 	for (var i = 0; i<pdata.length; i++) {
2448 		if (pdata[i].value) {
2449 			if (typeof pdata[i].value === 'string') {
2450 				pdata[i].value = aux.populate(pdata[i].value,that.query,that);
2451 				if (pdata[i].value.charAt(0)=='{') {
2452 					try {
2453 						pdata[i].value = JSON.parse(pdata[i].value);
2454 					}
2455 					catch (e) {
2456 						var errmsg = "Error JSON-parsing param-data value of \""+pdata[i].name+"\": "+e.message
2457 						logger.error(errmsg);
2458 						pdata[i].value = {error: errmsg, input: pdata[i].value};
2459 					}
2460 				}
2461 			}
2462 		}
2463 		if (pdata[i].attributes) {
2464 			if (typeof pdata[i].attributes === 'string') {
2465 				pdata[i].attributes = aux.populate(pdata[i].attributes,that.query,that);
2466 				// logger.debug("PREPARE_DQUERY_PARAMS.3.INDEX="+i+".ATTRIBUTES=\""+pdata[i].attributes+"\"");
2467 				if (pdata[i].attributes.charAt(0)=='{') {
2468 					try {
2469 						pdata[i].attributes = JSON.parse(pdata[i].attributes);
2470 					}
2471 					catch (e) {
2472 						var errmsg = "Error JSON-parsing param-data attributes of \""+pdata[i].name+"\": "+e.message
2473 						logger.error(errmsg);
2474 						pdata[i].attributes = {error: errmsg, input: pdata[i].attributes};
2475 					}
2476 				}
2477 			}
2478 		}
2479 	}
2480 	that.paramdata = pdata;
2481 	// logger.debug("PDATA:");
2482 	// logger.debug(pdata);
2483 }
2484 
2485 /* *****************************************************************************
2486  *	FUNCTION:		process_query
2487  *	INPUT:			that	-	query execution context
2488  *					phase	-	name of extension hook to be invoked
2489  *					resattnam	-	OPTIONAL: 	name of attribute of <that> to which the 
2490  *												result of the expression is stored (if any)
2491  *	OUTPUT:			that	-	possible changes by executed expression
2492  *	DESCRIPTION:	processes query extension hook javascript expression
2493  *					it is assumed that <that> has an attribute named <phase>
2494  *					this attribute shall contain a javascript-expression
2495  *					this expression is executed
2496  *					if the expression produces a return value, this value
2497  *					is logged but not further used or kept
2498  *					before execution (with javascript eval()-function), placeholders
2499  *					delimited by {{..}} are replaced (using aux.populate()-function)
2500  *					If the expression returns a result and <resattnam> is specified,
2501  *					the result is stored to the attribute <resattnam> of <that>
2502  */ 
2503 
2504 function process_query(that,phase,resattnam) {
2505 	var procparname = phase;
2506 	var donotcatch = prefs.do_not_catch_in[procparname] || false;
2507 	var procexpr = '';
2508 	if (that[procparname]) {
2509 		procexpr = aux.populate(''+that[procparname],that);
2510 	} else {
2511 		if ((that.query) && (that.query[procparname])) {
2512 			procexpr = aux.populate(''+that.query[procparname],that);
2513 		} else {
2514 			procexpr = '';
2515 			// logger.debug('DQUERY: no JS-expression '+procparname+' defined');
2516 			return;
2517 		 }
2518 	}
2519 	// logger.debug("PROCESS_QUERY.PROCPAR["+procparname+"(populated)]=\""+procexpr+"\"");
2520 	var procres;
2521 	if (donotcatch) {
2522 		procres = eval(procexpr);
2523 	} else {
2524 		try {
2525 			procres = eval(procexpr);			// now really execute the JS-expression
2526 		}
2527 		catch (e) {
2528 			logger.error(e);
2529 			var nerr = new Error("Error in PROCESS_QUERY.PROCPAR["+procparname+"(populated)]=\""+procexpr+"\": "+e.message);
2530 			logger.error(nerr.message);
2531 			procres = {error: nerr};
2532 		}
2533 	}
2534 	if (procres) {
2535 		if (resattnam) {
2536 			// logger.debug("PROCESS_QUERY.RESULT_TO_THAT[\""+resattnam+"\"]");
2537 			that[resattnam] = procres;
2538 		}
2539 	} else {
2540 		// logger.debug("PROCESS_QUERY.RESULT["+procparname+"]=undefined");
2541 	}
2542  }
2543  
2544  /* *****************************************************************************
2545  *		REPORT CREATION
2546  *	a report is created out of an already existing [dbresult] structure
2547  * 	a report is hierarchical, it can have an arbitrary number of grouping levels
2548  *	groups are groups of data records sharing a common data value in the discriminator fields
2549  *	the discriminator fields are similar to the arguments to a GROUP BY clause in SQL
2550  *	each grouping level has:
2551  *		- a discriminator, specifying the data fields to determine the group change from
2552  *		- a list of data fields listed at this grouping level
2553  *		- a format template for group headers
2554  *		- a format template for group footers
2555  *		- a format template for a group label
2556  */
2557  // create the grouping control structure: grpctl is an array with one entry per grouping level:
2558  //		levelname	-	name of the level
2559  //		dkey	-	current reference data key used for group change detection
2560  //		nkey	-	data key value for currently processed input data row
2561  //		dicnames-	array of names of the columns to be included in group change checks
2562  //		dacnames-	array of names of the columns to be included in data rows on this level
2563  function rep_init_grpctl(ctx) {
2564 	var cgln;
2565 	var cgl;
2566 	ctx.grpnum = 0;
2567 	ctx.grlnum = 0;
2568 	ctx.maxgrl = -1;
2569 	aux.default_param(ctx.query,"grouplevels","");									// per default, the report is single-level
2570 	if (ctx.query.grouplevels.length<1) {
2571 		ctx.grplvlnames = [""];
2572 	} else {
2573 		ctx.grplvlnames = ctx.query.grouplevels.split(",");
2574 	}
2575 	ctx.grpctl = new Array();
2576     for (var gli=0; gli<ctx.grplvlnames.length; gli++) {								// group levels start with 0 as the highest/coarsest and upwards growing finer
2577 		cgln = ctx.grplvlnames[gli];		 
2578 		cgl =	{	level: gli, levelname:	cgln};										// level number and level name
2579 		aux.default_param(ctx.query,cgln+'repqrydisc',ctx.metaData[0].name);			// default for repqrydisc is first column of dbselect result
2580 		cgl.dicnames = ctx.query[cgln+'repqrydisc'].split(',');							// list of columns to be contained in group change discriminator
2581 		aux.default_param(ctx.query,cgln+'datacolumns',									// default for datacolumns are all columns except the ones in repqrydisc
2582 						function(g) {
2583 							var dl = new Array();
2584 							var cmn;
2585 							for (var di=0; di<ctx.metaData.length; di++) {
2586 							   cmn = ctx.metaData[di].name;
2587 							   if (!(aux.find_in_array(g.dicnames,cmn))) {
2588 								  dl.push(cmn);
2589 							   }
2590 							}
2591 							return dl.join(',');
2592 						},cgl);
2593 		cgl.dacnames = ctx.query[cgln+'datacolumns'].split(',');							// list of datacolumns to be listed in report data
2594 		aux.default_param(ctx.query,cgln+'grphead',										// default for grphead is "Gruppe: ", followed by list of discriminator field values
2595 	                   function(g) {
2596 						   var ght = "Gruppe: ";
2597 						   for (var di=0; di<g.dicnames.length; di++) {
2598 								ght += "[["+g.dicnames[di]+"]]";
2599 								if (di<(g.dicnames.length-1)) {
2600 									ght += " - ";
2601 								}
2602 						   }
2603 						   return ght;
2604 					   },cgl);
2605 		cgl.grphead = ctx.query[cgln+'grphead'];
2606 		aux.default_param(ctx.query,cgln+'grpfoot',"Ende von "+cgl.grphead);				// default for grpfoot   is "Ende von Gruppe: ",followed by group header
2607 		cgl.grpfoot = ctx.query[cgln+'grpfoot'];
2608 		aux.default_param(ctx.query,cgln+'grplabel',"[["+cgl.dicnames[cgl.dicnames.length-1]+"]] ([[DATARECNUM]]) - default");
2609 		cgl.grplabel = ctx.query[cgln+'grplabel'];
2610 		aux.default_param(ctx.query,cgln+'grplabeltitle',"[["+cgl.dicnames[cgl.dicnames.length-1]+"]]");
2611 		cgl.grplabeltitle = ctx.query[cgln+'grplabeltitle'];
2612 		aux.default_param(ctx.query,cgln+'addparams',
2613 							function(gl) {
2614 								var gap = "{";
2615 								gap += "\"div_left_margin\":\""+(cgl.level*2)+"em\""
2616 								gap += "}";
2617 								return gap;
2618 							},cgl);
2619 		try {
2620 			cgl.addparams = JSON.parse(ctx.query[cgln+"addparams"]);
2621 		} catch (e) {
2622 			logger.error();
2623 			cgl.addparams = {status: "ERROR", errortext: "REP_INIT_GRPCTL.ERROR_PARSING_JSON["+cgln+"addparams]=\""+ctx.query[cgln+"addparams"]+"\": "+e.message, error: e};
2624 			logger.error(cgl.addparams.errortext);
2625 		}
2626 		aux.default_param(ctx.query,cgln+"grprowtitle",null);
2627 		cgl.grprowtitle = ctx.query[cgln+"grprowtitle"];
2628 		aux.default_param(ctx.query,cgln+"grpcoltitle",null);
2629 		cgl.grprowtitle = ctx.query[cgln+"grpcoltitle"];
2630 		ctx.grpctl.push(cgl);
2631 	}
2632 	ctx.grlnum = ctx.grpctl.length;														// number of grouping levels
2633 	ctx.maxgrl = ctx.grpctl.length - 1;													// highest (=finest) grouping level
2634  }
2635  
2636  // add a sub-groups structure to the group GRP 
2637  function rep_addgroups(ctx,grp,options) {
2638 	 var nocascade = false;
2639 	 if (options) {
2640 		 nocascade = options.nocascade || false;
2641 	 }
2642 	 if (!(grp)) {
2643 		 logger.error("REP_ADDGROUPS.GRP_IS_NULL_OR_UNDEF!!");
2644 		 return;
2645 	 }
2646 	 grp.groups = new Array();
2647 	 if (!nocascade) {
2648 		rep_grps_addgroup(ctx,grp);
2649 	 }
2650  }
2651  
2652  // create a new group of level LVL and all its sub-groups to cover all grouping levels
2653  function rep_newgroup(ctx,lvl,options) {
2654 	 var ng = {status: "fresh"};
2655 	 ng.ctx = ctx;
2656 	 ctx.grpnum++;
2657 	 ng.grpid = ctx.upfx+"grp"+ctx.grpnum;
2658 	 ng.level = lvl;
2659 	 // logger.debug("REP_NEWGROUP.START.LEVEL="+ng.level+".GRPID="+ng.grpid);
2660 	 ng.colvalues = {};
2661 	 aux.copyObject(ng.colvalues,ctx.colvalues);
2662 	 ng.colvalues.end = {};
2663 	 aux.copyObject(ng.colvalues.end,ctx.colvalues);
2664 	 if ((lvl>(ctx.maxgrl+1)) || (lvl<0)) {
2665 			 logger.error("REP_NEWGROUP.LEVEL="+lvl+".LEVEL_OUT_OF_RANGE!!");
2666 			 logger.error((new Error()).stack);
2667 			 return ng;
2668 	}
2669 	ng.levelname	= (ng.level==(ctx.maxgrl+1))?"data_detail":ctx.grpctl[ng.level].levelname;
2670 	ng.grphead		= (ng.level==0)?ctx.query.totalhead:ctx.grpctl[ng.level-1].grphead;
2671 	ng.grpfoot		= (ng.level==0)?ctx.query.totalfoot:ctx.grpctl[ng.level-1].grpfoot;
2672 	ng.grplabel		= (ng.level==0)?"<NO_LABEL_AT_ROOTGROUP>":ctx.grpctl[ng.level-1].grplabel;
2673 	ng.grplabeltitle= (ng.level==0)?"<NO_LABELTITLE_AT_ROOTGROUP>":ctx.grpctl[ng.level-1].grplabeltitle;
2674 	ng.head = aux.populate({	template: ng.grphead,
2675 								rex:      ctx.phrex,
2676 								data:     ng.colvalues,
2677 								adata:    ng});
2678 	if (ng.level==(ctx.maxgrl+1)) {
2679 		ng.result = {resulttype: "dbresult"};
2680 		ng.result.metaData = new Array();
2681 		for (var ci=0; ci<ctx.grpctl[ctx.maxgrl].dacnames.length; ci++) {
2682 			ng.result.metaData.push({name: ctx.grpctl[ctx.maxgrl].dacnames[ci]});
2683 		}
2684 		ng.result.rows = new Array();
2685 	 } else {
2686 		ng.dkey = rep_build_key(ctx,ng.level);
2687 		rep_addgroups(ctx,ng,options);
2688 	 }
2689 	 aux.copyObject(ng,(ng.level>ctx.maxgrl)?{}:ctx.grpctl[ng.level].addparams);
2690 	 return ng;
2691  }
2692  
2693  // close GRP 
2694  // before closing, write terminating group footer and group label
2695  function rep_closegroup(ctx,grp,options) {
2696 	 var alertclose = false;
2697 	 if (options) {
2698 		 alertclose = options.alertclose || false;
2699 	 }
2700 	 if (!grp) {
2701 		 return;
2702 	 }
2703 	 if (grp.status=="closed") {						// already closed, do nothing
2704 		return;
2705 	 }
2706 	 if (alertclose) {
2707 		 logger.error("REP_CLOSEGROUP.CLOSING."+rep_grpinfo(ctx,grp)+".HAS_TO_BE_CLOSED_UNEXPECTEDLY");
2708 	 }
2709 	grp.datarecnum = rep_grp_cnt_datarecs(ctx,grp);
2710 	grp.colvalues["DATARECNUM"] = grp.datarecnum;
2711 	grp.colvalues.end = {};
2712 	aux.copyObject(grp.colvalues.end,ctx.colvalues.prev);
2713 	if (grp.level>(ctx.maxgrl+1)) {
2714 		logger.error("REP_CLOSEGROUP."+rep_grpinfo(ctx,grp)+".LEVEL="+grp.level+".TOO_HIGH");
2715 		grp.foot = "---";
2716 		grp.label = ">>>";
2717 		grp.labeltitle = "Daten";
2718 	} else {
2719 		if (grp.grpfoot)
2720 			grp.foot = aux.populate({	template: grp.grpfoot,
2721 										rex:      ctx.phrex,
2722 										data:     grp.colvalues,
2723 										adata:    grp});
2724 		if (grp.grplabel)
2725 			grp.label = aux.populate({	template:	grp.grplabel,
2726 										rex:      	ctx.phrex,
2727 										data:     	grp.colvalues,
2728 										adata:    	grp});
2729 		if (grp.grplabeltitle)
2730 			grp.labeltitle = aux.populate({	template:	grp.grplabeltitle,
2731 											rex:      	ctx.phrex,
2732 											data:     	grp.colvalues,
2733 											adata:    	grp});
2734 	}
2735 	delete grp.ctx;						// unlink grp from ctx (don't need it after closing)
2736 	grp.status = "closed";
2737  }
2738  
2739  // close GRP and all its sub-groups
2740  function rep_closeallgroups(ctx,grp,options) {
2741 	if (!grp) {
2742 		return;
2743 	}
2744 	if (grp.groups) {
2745 		for (var i=0; i<grp.groups.length; i++) {
2746 			rep_closeallgroups(ctx,grp.groups[i],options);
2747 		}
2748 	}
2749 	rep_closegroup(ctx,grp,options);
2750  }
2751  
2752  // add a new subgroup to GRP
2753  function rep_grps_addgroup(ctx,grp) {
2754 	 if (grp) {
2755 		 if (!(grp.groups)) {
2756 			rep_addgroups(ctx,grp);
2757 		 } else {
2758 			grp.groups.push(rep_newgroup(ctx,grp.level+1));
2759 		 }
2760 		 grp.status = "data";
2761 		 return grp.groups[grp.groups.length-1];
2762 	 } else {
2763 		 logger.error("REP_GRPS_ADDGROUP.GRP_IS_NULL_UNDEF!!");
2764 		 return null;
2765 	 }
2766  }				
2767  
2768  // determine if a group has data (sub-groups or data records). This includes its sub-groups at finer levels
2769  function rep_grp_has_data(ctx,grp) {
2770 	 if (!grp.groups) {
2771 		 if (!grp.result) {
2772 			 return false;
2773 		 } else {
2774 			 if ((grp.result.rows) && (grp.result.rows.length>0)) {
2775 				 return true;
2776 			 } else {
2777 				 return false;
2778 			 }
2779 		 }
2780 	 } else {
2781 		 var i = 0;
2782 		 while (i<grp.groups.length) {
2783 			 if (rep_grp_has_data(ctx,grp.groups[i])) {
2784 				 return true;
2785 			 } else {
2786 				 i += 1;
2787 			 }
2788 		 }
2789 		 return false;
2790 	 }
2791  }
2792  
2793  // count all data records in the group GRP and all its descendent groups at finer levels
2794  function rep_grp_cnt_datarecs(ctx,grp) {
2795 	 if (!grp) {
2796 		 return 0;
2797 	 } else {
2798 		 var cnt = 0;
2799 		 if (grp.groups) {
2800 			 for (var i=0; i<grp.groups.length; i++) {
2801 				 cnt += rep_grp_cnt_datarecs(ctx,grp.groups[i]);
2802 			 }
2803 		 }
2804 		 if ((grp.result) && (grp.result.rows)) {
2805 			 cnt += grp.result.rows.length;
2806 		 }
2807 		 return cnt;
2808 	 }
2809  }
2810  
2811  function rep_grpinfo(ctx,grp) {
2812 	if (grp) {
2813 		return "{GRPID="+((grp.grpid)?grp.grpid:"<UNKNOWN>")+".LEVEL="+((grp.level)?grp.level:"<UNKNOWN>")+".LEVELNAME="+grp.levelname+".GROUPS["+((grp.groups)?grp.groups.length:"NONE")+"]}";
2814 	} else {
2815 		return "<NOT_DEFINED>";
2816 	}
2817  }
2818  
2819  // perform a group change in CTX for group GRP
2820  // this is done by closing the current group and opening a new empty one (including all sub-levels)
2821  // a special case is when the current group has no data (subgroups or data records) yet. Then it is re-used to avoid empty groups
2822  function rep_lg_chggroup(ctx,rlg) {
2823 	 if (!(rlg)) {
2824 		 logger.error("REP_GL_CHGGROUP.NO_LEVELGROUP!!");
2825 		 return;
2826 	 }
2827 	 if (!(rlg.groups)) {
2828 		 logger.warn("REP_GL_CHGGROUP.NO_GROUPS");
2829 		 rep_addgroups(ctx,rlg);
2830 		 rlg.dkey = ctx.nkey;
2831 		 return rep_grps_addgroup(ctx,rlg.groups,rlg.level+1);
2832 	 }
2833 	 if (rlg.groups.length<1) {
2834 		 logger.warn("REP_GL_CHGGROUP.GROUPS_EMPTY");
2835 		 rlg.dkey = ctx.nkey;
2836 		 return rep_grps_addgroup(ctx,rlg.groups,rlg.level+1);
2837 	 }
2838 	 var cg = rlg.groups[rlg.groups.length-1];
2839 	 // logger.debug("REP_LG_CHGGROUP.CG="+rep_grpinfo(ctx,cg));
2840 	 if (rep_grp_has_data(ctx,cg)) {
2841 		rep_closeallgroups(ctx,cg);
2842 		cg = rep_grps_addgroup(ctx,rlg);
2843 	 }
2844 	 rlg.dkey = ctx.nkey;
2845 	 return cg;
2846  }
2847  
2848  // find the group in CTX data is to be written to
2849  // per definitionem the last group in the finest grouping level
2850  // if <grp> is supplied, find the data group starting from <grp>
2851  // if <grp> is not supplied, start from <ctx>'s root group
2852  function rep_find_data_group(ctx,grp) {
2853 	var cdgp = grp || ctx.rootgroup;
2854 	// logger.debug("REP_FIND_DATA_GROUP_START.CDGP:\r\n--- BEGIN_CDGP ---\n"+JSON.stringify(cdgp,null,4)+"\r\n--- END_CDGP ---");
2855 	while (true) {
2856 		// logger.debug("REP_FIND_DATA_GROUP_BODYSTART.CDGP.GRPID="+cdgp.grpid);
2857 		if (!cdgp.groups) {
2858 			return cdgp;
2859 		}
2860 		// logger.debug("REP_FIND_DATA_GROUP_GROUPS_THERE.CDGP.GRPID="+cdgp.grpid+".GROUPS.LENGTH="+cdgp.groups.length);
2861 		if (cdgp.groups.length<1) {
2862 			// logger.error("REP_FIND_DATAGROUP.CDGP.GRPID="+cdgp.grpid+".GROUPS_IS_EMPTY!!");
2863 			cdgp.groups.push({status: "new"});
2864 			grpnum++;
2865 			cdgp.groups[cdgp.groups.length-1].grpid = upfx+"grp"+grpnum;
2866 			// logger.debug("REP_FIND_DATA_GROUP.NEW_WORKAROUND_SUBGROUP.GRPID="+cdgp.groups[cdgp.groups.length-1].grpid);
2867 		}
2868 		cdgp = cdgp.groups[cdgp.groups.length-1];
2869 		// logger.debug("REP_FIND_DATA_GROUP_AFTER_STEP.GRPID="+cdgp.grpid+".GROUPS_THERE="+((cdgp.groups)?"TRUE":"FALSE"));
2870 	}
2871  }
2872  
2873  // write the current data record in CTX to the current data group
2874  function rep_writedata(ctx) {
2875 	var cdgp = rep_find_data_group(ctx);
2876 	if (!(cdgp)) {
2877 		logger.error("REP_WRITEDATA.DATAGROUP_NOT_FOUND!!");
2878 		return;
2879 	}
2880 	var newrow = new Array();
2881 	var cgl = ctx.grpctl[ctx.maxgrl];													// data row always conforms to data record specification of finest level
2882 	for (var ci=0; ci<cgl.dacnames.length; ci++) {
2883 		newrow.push(ctx.colvalues[cgl.dacnames[ci]]);
2884 	}
2885 	if (cgl.grprowtitle) {
2886 		newrow["rowtitle"] = aux.populate({	template:	cgl.grprowtitle,
2887 											rex:      	ctx.phrex,
2888 											data:     	ctx.colvalues,
2889 											adata:    	ctx});
2890 	}
2891 	cdgp.result.rows.push(newrow);
2892 	cdgp.status = "data";
2893 	// logger.debug("REP_WRITEDATA.RI="+ctx.ri+".ROWWRITTEN.DATAGROUP="+rep_grpinfo(ctx,cdgp)+".ROWS["+cdgp.result.rows.length+"]");
2894  }
2895  
2896  function rep_update_colvalues(ctx) {
2897 	if (!(ctx.colvalues)) {
2898 		ctx.colvalues = {prev: {}};
2899 	}
2900 	delete ctx.colvalues['prev'];											// remove old previous values
2901 	var pprev = {};
2902 	for (var pa in ctx.colvalues) {											// update previous values to current (i.e. the previous rows') colvalues
2903 		pprev[pa] = ctx.colvalues[pa];
2904 	}
2905 	ctx.colvalues = { prev: pprev };										// move previous rows' colvalues to colvalues.prev
2906 	for (ci=0; ci<ctx.metaData.length; ci++) {								// colvalues holds the current row's column values plus the previous ones in .prev
2907 		ctx.colvalues[ctx.metaData[ci].name] = ctx.rows[ctx.ri][ci];
2908 			}
2909 	ctx.colvalues["ROWNUM"] = ctx.ri;
2910 	if (ctx.query.datarecext) {
2911 		ctx.colvalues["RECEXT"] = aux.populate({	template: ctx.query.datarecext,
2912 													rex:      ctx.phrex,
2913 													data:     ctx.colvalues,
2914 													adata:    ctx});
2915 		if (typeof(ctx.colvalues["RECEXT"])==='object') {
2916 			for (var oan in ctx.colvalues["RECEXT"]) {
2917 				ctx.colvalues[oan] = ctx.colvalues["RECEXT"][oan];
2918 			}
2919 		}
2920 	}
2921  }
2922   
2923  function rep_build_key(ctx,lvl) {
2924 	 if (lvl>(ctx.grpctl.length-1)) {
2925 		 logger.error("REP_BUILD_KEY.LVL_TOO_HIGH="+lvl+".MAX="+ctx.maxgrl+"/"+(ctx.grpctl.length-1));
2926 		 logger.error((new Error()).stack);
2927 		 return "";
2928 	 }
2929 	var key = "";
2930 	var cgl = ctx.grpctl[lvl];
2931 	for (var ci=0; ci<cgl.dicnames.length; ci++) {
2932 		key += ctx.colvalues[cgl.dicnames[ci]];
2933 	}
2934 	return key;
2935  }
2936 
2937  // =============== customization support functions =============================
2938 
2939 function rep_datasum(ctx,grp,colname) {
2940 	var dgp = rep_find_data_group(ctx,grp);
2941 	var sum = 0;
2942 	var cr;
2943 	var dci = -1;
2944 	// find the column containing the column name <colname>
2945 	for (var ci=0; ci<dgp.result.metaData.length; ci++) {
2946 		if (dgp.result.metaData[ci].name==colname) {
2947 			dci = ci;
2948 			break;
2949 		}
2950 	}
2951 	if (dci<0) {			// if not found, sum defaults to 0
2952 		return 0;
2953 	}
2954 	for (var dri=0; dri<dgp.result.rows.length; dri++) {
2955 		cr = dgp.result.rows[dri];
2956 		if (dci<cr.length) {
2957 			sum += cr[dci];
2958 		}
2959 	}
2960 	return sum;
2961 }
2962 		
2963  
2964  /* *****************************************************************************
2965  *
2966  *  FUNCTION:	dbreport_postprocess
2967  *	INPUT:		that		-	dquery-context of the current dbreport-query
2968  *				that.query	-	query parameters configuring the behaviour:
2969  *					repgrpdisc		- comma-separated list of .result-columns to be used as group discriminator
2970  *									  a change in the values of these columns is detected as group-change
2971  *					grphead			- a populate()d pattern to be output at the head (before) of each group 
2972  *					grpfoot			- a populate()d pattern to be output at the foot (after)  of each group
2973  *					totalhead		- a populate()d pattern to be output at 
2974  *					totalfoot		- a populate()d pattern to be output at 
2975  *					datacolumns		- comma-separated list of .result-columns to be output in each group, 
2976  *										this is usually the other .result-columns apart from the group discriminator
2977  *	OUTPUT:		that.result	-	result object modified to formatted report
2978  *	DESCRIPTION:		post-processes a 'dbresult' result object into a 'dbreport' result object
2979  */
2980  function dbreport_postprocess(that) {
2981 	 // logger.debug("DBREPORT_POSTPROCESS.DEBUG="+that.query.debug);
2982 	 var dbg = (that)?(that.result)?(that.query.debug)?(that.query.debug=="true"):false:false:false;
2983 	 if (dbg) {
2984 		 aux.logJSON(that.query,"DBREPORT_POSTPROCESS.QUERY");
2985 		 aux.logJSON(that.result,"DBREPORT_POSTPROCESS.RESULT");
2986 	 }
2987 	 if (!that.result) {
2988 		 throw new Error("dbreport_postprocess: query context object does not have a result attribute");
2989 		 return;
2990 	 }
2991 	 if (!(that.result.resulttype=='dbresult')) {
2992 		 throw new Error("dbreport_postprocess: input result type shall be 'dbresult' but is \""+that.result.resulttype+"\"");
2993 		 return;
2994 	 }
2995 	var ctx = that.result;
2996 	ctx.query = that.query;									// let ctx point to query so that placeholders can refer to it
2997 	ctx.prefs = prefs;										// let ctx point also to global prefs so that placeholders can refer to it
2998 	var oldresult = {};
2999 	for (var oro in that.result) {
3000 		oldresult[oro] = that.result[oro];
3001 	 }
3002 	 ctx.oldresult = oldresult;
3003 	ctx.phrex = /\[\[([\w\.\(\)\#"':;\?@]*)\]\]/g				// placeholders must be surrounded by "[[" and "]]", this regex detects them in aux.populate()
3004 	ctx.upfx = aux.uniqueNowID();							// unique prefix for this very report instance, in order to produce unique report element identifiers
3005 	rep_init_grpctl(ctx);																// create the grouping control structure from query parameters
3006 	aux.default_param(ctx.query,'totalhead',"Bericht \""+ctx.query.dataname+"\"");		// default for totalhead is "Bericht <dataname>"
3007 	aux.default_param(ctx.query,'totalfoot',"Ende von "+ctx.query.totalhead);			// default for totalfoot is "Ende von <totalhead>"
3008 	// now create the dbreport structure
3009 	ctx.resulttype = 'dbreport';
3010 	if (ctx.rows.length<1) {
3011 		ctx.head = "Keine Daten im Bericht";
3012 	} else {
3013 		ctx.ri = 0;																		// current data record number
3014 		rep_update_colvalues(ctx);														// populate colvalues from 1st data row for initializing the tree
3015 		ctx.repid = ctx.upfx;
3016 		ctx.rootgroup = rep_newgroup(ctx,0);											// this creates the starting tree implicitely
3017 		// create the report heading using values from the first data row
3018 		ctx.head = aux.populate({	template: ctx.query.totalhead,
3019 			                        rex:      ctx.phrex,
3020 			                        data:     ctx.colvalues,
3021 			                        adata:    ctx});
3022 		// now entering report processing proper
3023 		for (ctx.ri=0; ctx.ri<that.result.rows.length; ctx.ri++) {									// iterate over all rows in the flat input result
3024 			ctx.curreprow = ctx.ri;
3025 			rep_update_colvalues(ctx);																// populate colvalues from teh current data row
3026 			// now check if any group level detects group change
3027 			ctx.cgp = ctx.rootgroup;																// start group change detection at root level
3028 			while ((ctx.cgp) && (ctx.cgp.level<=ctx.maxgrl)) {
3029 				ctx.nkey = rep_build_key(ctx,ctx.cgp.level);
3030 				// logger.debug("DBREPORT_POSTPROCESS.CHKGRPCHG.RI="+ctx.ri+".LVL="+ctx.cgp.level+".LEVELNAME="+ctx.cgp.levelname+".GRPID="+ctx.cgp.grpid+
3031 				// 				".DKEY=\""+ctx.cgp.dkey+"\".NKEY=\""+ctx.nkey+"\"");
3032 				if (ctx.nkey!=ctx.cgp.dkey) {
3033 					// logger.debug("DBREPORT.POSTPROCESS.GRPCHGDETECTED.DKEY=\""+ctx.cgp.dkey+"\".NKEY=\""+ctx.nkey+"\"");
3034 					ctx.cgp = rep_lg_chggroup(ctx,ctx.cgp);
3035 				}
3036 				ctx.cgp = (ctx.cgp.groups)?ctx.cgp.groups[ctx.cgp.groups.length-1]:null;			// iterate to next level ( last member of current groups' groups-array)
3037 			}				
3038 			// now write the current data row to the result output of the finest level's result data
3039 			rep_writedata(ctx);
3040 			// logger.debug("DBREPORT_POSTPROCESS.ROW_COMPLETE="+ri);
3041 		} // end for result rows
3042 		// have written last row of all, close all groups and the whole report
3043 		rep_closeallgroups(ctx,ctx.rootgroup);
3044 		// create to report footer, based on the data values of the last row in the input
3045 		ctx.colvalues["TOTALDATARECS"] = rep_grp_cnt_datarecs(ctx,ctx.rootgroup);
3046 	    ctx.foot = aux.populate({	template: ctx.query.totalfoot,
3047 									rex:      ctx.phrex,
3048 									data:     ctx.colvalues,
3049 									adata:    ctx});
3050 	} // end if result has rows
3051 	delete ctx.cgp;
3052 	delete that.result['rows'];													// clean out dbresult remainders
3053 	delete that.result['metaData'];
3054 	delete that.result['oldresult'];
3055 	delete that.result["prefs"];
3056 	delete that.result["query"];
3057 	if (dbg) aux.logJSON(that.result,"DBREPORT_POSTPROCESS.PROCESSED_RESULT");
3058  } // end of dbreport_postprocess()	 
3059 	 
3060  
3061 /* *****************************************************************************
3062  *
3063  *  FLOW:			dquery
3064  *	INPUT:			SESS	-	session object for checking authorization credentials
3065  *					QUERY	- query object
3066  *								ATTRIBUTES:
3067  *							dataname 	- name of a query descriptor in RW DQUERIES
3068  *							format		- a format specifier:
3069  *								CSV 	- tabular data in CSV format
3070  *								HTAB	- HTML table
3071  * CALLBACK:		rfunc				- called upon successful or failed completion
3072  *	CALLBACKPARAMS:	ERR		- error Object, UNDEFINED if no error
3073  *					RESULT	- result object
3074  *								ATTRIBUTES:
3075  *							disposition	- 'view' .. shall be rendered in a view
3076  *										- 'download' .. plain data to be downloaded
3077  *							view		- if disposition='view', this is the name of the view to use
3078  *							contentType	- the MIME-type to be set in the HTTP response
3079  *							data		- if present, to be passed to return document if disposition != 'view'
3080  * CALLBACK:		pfunc				-	called upon updated progress status
3081  *  CALLBACKPARAMS:	 PROGSTAT	-	an arbitrary object with attributes reflecting the progress of the query
3082  *	DESCRIPTION:	gets query parameters from RW DQUERIES
3083  *						the query descriptor needs the following rows in table CCRW:
3084  *							- one with RWTYPE='DQUERY' an RWPARENT='QUERIES' and RWNAME=<dataname>
3085  *							- one or more with RWTYP='DQUERYPARAM' and RWPARENT=<dataname>:
3086  *								RWNAME			RWVALUE
3087  *								querytype	 	type of the query to be executed:
3088  *												'dbselect' ... perform a SELECT in the database
3089  *												'showview' ... show a web view
3090  *													renders query-object in handlebars-view with the name from QUERYPARAM 'view'
3091  *								view			the name of the view to render the result (or the query in case of 'showview' )
3092  *												'jsexpr' ..... execute the javascript-expression in QUERYPARAM 'aexpression' or 'qexpression':
3093  *								aexpression			asynchronous javascript expression to execute, must callback <that> upon completion
3094  *								qexpression			synchronous javascript expression to execute
3095  *												'REST' ....... perform a REST-query defined by following attributes:
3096  *								host				host name or IP-address to execute REST query at
3097  *								endpoint			HTTP endpoint for REST-query
3098  *								method				HTTP method for REST-query:		GET,POST, ...
3099  *								data				data to send with REST-query
3100  *														for method='GET' this is appended to the endpoint (HTTP query-string)
3101  *														for all other methods it is sent as POST-headers
3102  *												DEFAULT: 'dbselect'
3103  *								sql				the SQL statement to be executed for the query
3104  *												DEFAULT: "SELECT 'no SQL provided to PREPARE_DQUERY_PARAMS' as ERROR from DUAL"
3105  *								paramnames		comma-separated list of names of query parameters to replace bind variables
3106  *								attributenames	comma-separated list of names of additional query attributes needed
3107  *								title			text possibly shown in result ahead of the query result data
3108  *								info			short descriptive text to be logged with query
3109  *												DEFAULT: "No info provided"
3110  *								desctext		descriptive text possibly shown to user
3111  *												DEFAULT: ""
3112  *								format			format in which the result data shall be presented to user (or caller)
3113  *												'htab' ...... tabular HTML format
3114  *												'csv' ....... comma-separated text string
3115  *								fieldsep		string separating fields in CSV format
3116  *												DEFAULT: ',' (comma)
3117  *								linesep			string separating lines in CSV format
3118  *												DEFAULT: '\r\n' (CR+LF)
3119  *								pre_query		a javascript expression to be processed before the query is executed
3120  *												this expression can rely on following 
3121  *								<others>		query-configuration specific attributes used in placeholder replacement
3122  *					executes query in database
3123  *					formats data according to FORMAT query parameter
3124  */
3125  
3126  var dquery = flow.define(
3127 	// step 1: check authorization
3128 	function(sess,query,rfunc,pfunc) {
3129 		this.session = sess;
3130 		this.callback = rfunc;
3131 		this.pcallback = pfunc;
3132 		this.query = query;
3133 		aux.default_param(this.query,"donotquerydescriptor","NO");
3134 		// logger.debug("DQUERY.QUERY: ",this.query);
3135 		logger.debug("DQUERY.BEGIN.DATANAME=\""+this.query.dataname+"\".FORMAT=\""+this.query.format+"\"");
3136 		// logger.debug("SESSION:");
3137 		// logger.debug(this.session);
3138 		functions.check_authorization(this.session,'dquery',this,this);
3139 	},
3140 	// step 1a: get query descriptor data
3141 	function(err,result) {
3142 		if (err) {
3143 			aux.error_callback(this.callback,err,"Authorization error in DQUERY",result);
3144 			return;
3145 		}
3146 		if (this.query.donotquerydescriptor!="YES") {
3147 			db.select({	sql:    "SELECT RWNAME,RWVALUE,RWDESC FROM CCRW WHERE RWTYP='DQUERYPARAM' and RWPARENT=:DATANAME",
3148 						reqid:	'RQID_GETQRYDESC',
3149 						info:   "Get query descriptor data for DATANAME=\""+this.query.dataname+"\"",
3150 						params: [this.query.dataname]},this,
3151 					function(progstat) {
3152 						// logger.debug("DQUERY.GETQRYDESC.PROGRESS:");
3153 						// logger.debug(progstat);
3154 						var pstat = {action:	'dquery',
3155 									stage:		'1/2 - getquerydesc',
3156 									subaction: progstat};
3157 						if (this.pcallback) this.pcallback(pstat);
3158 					}.bind(this));
3159 		} else {
3160 			this();
3161 		}
3162 	},
3163 	// step 2: SELECT for query descriptor completed, check, prepare and execute query
3164 	function(err,result) {
3165 		if (err) {
3166 			aux.error_callback(this.callback,err,"Error in DQUERY/GetQryDesc",result);
3167 			return;
3168 		}
3169 		if (this.query.donotquerydescriptor!="YES") { 
3170 			if (result.rows.length<1) {
3171 				aux.error_callback(this.callback,null,"Error in DQUERY/GetQryDesc: no query descriptor data for dataname=\""+this.query.dataname+"\" - no data returned from database",result);
3172 				return;
3173 			}
3174 			aux.default_param(this.query,'options',{});
3175 			prepare_dquery_params(this,result);									// check if all input parameters (as specified in <result>(query descriptor)) are there and get them into this.query
3176 		} 
3177 		if (this.need_user_input) {												// cannot execute query, user input needed
3178 			this.paramspec = {};
3179 			this.paramspec.resulttype = 'paramspec';
3180 			this.paramspec.dataname = this.query.dataname;
3181 			this.paramspec.paramdata = this.paramdata;
3182 			var errmsg = "Need input for query "+this.paramspec.dataname+" parameters";
3183 			var err = new Error(errmsg);
3184 			aux.error_callback(this.callback,err,errmsg,this.paramspec);
3185 			return;
3186 		} else {																						// all parameters there, execute query
3187 			process_query(this,'pre_query');															// apply any preprocessing
3188 			if (this.query.hasOwnProperty('sqlexpr')) {													// "sqlexpr" provides an SQL string built by a javascript expression and overrides "sql"
3189 				that = this;
3190 				this.query.sql = eval(''+this.query.sqlexpr);
3191 			}
3192 			if (this.query.hasOwnProperty('sql_'+db.getCurrentConfigDBtype())) {						// "sql_<db-type>" provides database-specific SQL and overrides "sql" 
3193 				this.query.sql = this.query['sql_'+db.getCurrentConfigDBtype()];
3194 			}
3195 			aux.populate_all_strings(this.query, this.query.attributes, this.query);
3196 			if (this.query.title) this.query.title = aux.populate(this.query.title,this.query.attributes);
3197 			this.isSequence = false;
3198 			switch (this.query.querytype) {
3199 				case 'dbselect':													// query type is select in database
3200 				case 'dbreport':													// query type is report, but the initial step is the same as dbselect
3201 					aux.default_param(this.query,'info','DQUERY.'+this.query.querytype+'.DATANAME="'+this.query.dataname+'"');
3202 					aux.default_param(this.query,'title','Datenbank-Abfrage-Ergebnis von '+this.query.dataname+'('+this.query.params.join(',')+')');
3203 					aux.default_param(this.query,'sql',"SELECT 'no SQL provided to DQUERY' as ERROR from DUAL");
3204 					// aux.default_param(this.query,'sql_mysql',"SELECT 'no SQL provided to DQUERY \""+this.query.dataname+"\"' as ERROR");
3205 					aux.default_param(this.query,'paramnames',"");
3206 					this.query.dbr = new db.Request({sql: 		this.query.sql,
3207 													// sql_mysql:	this.query.sql_mysql,
3208 													reqid:		'RQID_SQLQRY_'+this.query.dataname,
3209 													info: 		this.query.info,
3210 													headertext:	this.query.headertext,
3211 													desctext:	this.query.desctext,
3212 													params:		this.query.params,
3213 													options:	this.query.options});
3214 					// logger.debug("DQUERY.'+this.query.querytype+'.DBR");
3215 					// logger.debug(this.query.dbr);
3216 					db.select(this.query.dbr,
3217 							this,
3218 							function(progstat) {
3219 								// logger.debug("DQUERY.DOQUERY.PROGRESS:");
3220 								// logger.debug(progstat);
3221 								var pstat = {action:	'dquery',
3222 											stage:		'2/2 - exec',
3223 											subaction: progstat};
3224 								if (this.pcallback) this.pcallback(pstat);
3225 							}.bind(this));
3226 					break;
3227 				case 'REST':														// query type REST is invoking a RESTful Web-Service
3228 					aux.default_param(this.query,'info','DQUERY.REST.DATANAME="'+this.query.dataname+'"');
3229 					aux.default_param(this.query,'title','REST-Abfrage-Ergebnis von '+this.query.dataname+'('+this.query.params.join(',')+')');
3230 					if (this.query.hasOwnProperty('dataspecexpression')) {
3231 						process_query(this,'dataspecexpression');
3232 					}
3233 					aux.default_param(this.query,'data',{});
3234 					aux.default_param(this.query,'host','localhost');
3235 					aux.default_param(this.query,'endpoint','/');
3236 					aux.default_param(this.query,'method','GET');
3237 					if ((typeof this.query.data === "string") && (this.query.data.charAt(0) == "{")) {
3238 						try {
3239 							var qdata = JSON.parse(this.query.data);
3240 							this.query.data = qdata;
3241 						}
3242 						catch (e) {
3243 							logger.error("RESTquery Error parsing JSON data attribute "+e.message);
3244 						}
3245 					}
3246 					RESTquery(this.query.host,this.query.endpoint,this.query.method,this.query.data,this,
3247 								function(progstat) {
3248 									var pstat = {action:	'dquery',
3249 												stage:		'2/2 - exec[RESTquery]',
3250 												subaction:	progstat};
3251 									if (this.pcallback) this.pcallback(pstat);
3252 								}.bind(this));
3253 					break;
3254 				case 'showview':													// query type "showview" is simply showing (possible calculated) parameters in a view
3255 					aux.default_param(this.query,'view','plain');						// need a view, default is 'plain' (has only {{body}} placeholder
3256 					// now resolve all placeholders in the query parameters
3257 					for (att in this.query) {
3258 						// logger.debug("SHOWVIEW.PARAM["+att+"]");
3259 						if (typeof this.query[att] === "string") {
3260 							this.query[att] = aux.populate(this.query[att],this.query);
3261 						}
3262 					}
3263 					this.query.resulttype = 'object';
3264 					this.callback(undefined,this.query);											// delegate view rendering to caller
3265 					return;
3266 					break;
3267 				case 'jsexpr':														// query type "jsexpr": execute a JS-expression
3268 					aux.default_param(this.query,'qexpression','throw new Error("no qexpression given");');
3269 					if (this.query.aexpression) {					// we have an aexpression == "Async Expression" to be processed
3270 						process_query(this,'aexpression','ares');			// must lead to completion in step 3 !! Error is signalled via this.ares
3271 						if ((this.ares) && (this.ares.error)) {
3272 							aux.error_callback(this.callback,this.ares.error,'Error processing AEXPRESSION',this.ares);
3273 							return;
3274 						}
3275 					} else {
3276 						process_query(this,'qexpression','qres');				// process the expression
3277 						if ((this.qres) && (this.qres.error)) {
3278 							aux.error_callback(this.callback,this.qres.error,'Error processing QEXPRESSION',this.qres);
3279 							return;
3280 						}						
3281 						aux.default_param(this,'result',{title: 'query expression completed'});
3282 						aux.default_param(this.result,'resulttype','object');
3283 						aux.default_param(this,'go_on',false);					// if not set, signal not staying in query
3284 						aux.default_param(this,'do_rewind',false);
3285 						if (this.do_rewind) {
3286 							this.REWIND();
3287 						}
3288 						if (this.go_on) {									// if expression set GO_ON then it is complete
3289 							this(this.err,this.result);						// expression has to have set this.err and this.result when signalling GO_ON !!
3290 						}
3291 					}
3292 					break;
3293 				case 'sequence':													// query type 'sequence': process sequence
3294 					aux.default_param(this.query,'sequence','NOSEQUENCE');
3295 					this.isSequence = true;
3296 					dsequence(this.session,this.query,this,this.pcallback);
3297 					break;
3298 				default:
3299 					this.callback(new Error('Error in DQUERY/1: illegal query type "'+this.query.querytype),this.query+'"');
3300 					return;
3301 			}
3302 		}
3303 	},
3304 	// step 3: query executed, process result
3305 	function(err,result) {
3306 		if (err) {
3307 			aux.error_callback(this.callback,err,"Error in DQUERY/2",result);
3308 			return;
3309 		}
3310 		this.result = result;
3311 		// logger.debug("DQUERY.EXECUTED.RESULT_BEFORE_COPY:");
3312 		// logger.debug(this.result);
3313 		switch (this.query.querytype) {										// determine default post-processing configuration
3314 			case 'dbreport':																// for 'dbreport', ...
3315 				logger.debug("DQUERY: querytpe=dbreport, set default post_query processing");
3316 				aux.default_param(this.query,'post_query','dbreport_postprocess(that)');	// post-process with dbreport_postprocess()
3317 				break;
3318 		}
3319 		process_query(this,'post_query','postqueryres');									// apply any post-processing
3320 		if (this.postqueryres && this.postqueryres.error) {
3321 			this.result.postqueryres = this.postqueryres;
3322 			aux.error_callback(this.callback,this.postqueryres.error,"Error in executing query post-processing",this.result);
3323 			return;
3324 		}
3325 		aux.default_param(this,'result',{});		
3326 		for (var qi in this.query) {										// copy everything from query to result in case we need it
3327 			if ((qi=='dbr')) continue;										// ... except: dbr
3328 			this.result[qi] = this.query[qi];								
3329 		}
3330 		if (!this.result.resulttype) {
3331 			aux.error_callback(this.callback,null,"Consistency Error: Query Result has no result type:",result);
3332 			return;
3333 		}
3334 		aux.default_param(this.result,'resulttype','string');
3335 		// logger.debug("DQUERY.RESULT.FORMAT="+this.result.format+".VIEW="+this.result.view+".QUERY:");
3336 		// logger.debug(this.query);
3337 		if (!this.result.format) {											// determine default result format from resulttype
3338 			switch (this.result.resulttype) {
3339 				case 'editdata':
3340 					this.result.format = 'JSON';
3341 					aux.default_param(this.result.view,'editform');
3342 					break;
3343 				case 'object':
3344 					this.result.format = 'JSON';
3345 					break;
3346 				case 'string':
3347 					this.result.format = 'plain';
3348 					break;
3349 				case 'dbresult':
3350 					this.result.format = 'htab';
3351 					break;
3352 				case 'dbreport':
3353 					this.result.format = 'hgreport';
3354 					break;
3355 				case 'plain':
3356 					this.result.format = 'plain';
3357 					break;
3358 				default:
3359 					logger.warn("Unknown result type \""+this.result.resulttype+"\", defaulting format to 'htab'");
3360 					this.result.format = 'htab';
3361 			}
3362 		}
3363 		logger.debug("DQUERY.RESULT.FORMAT=\""+this.result.format+"\".RESULTTYPE=\""+this.result.resulttype+"\"");
3364 		switch (this.result.format) {
3365 			case 'csv':														// present query result in CSV format, 1st line is headers, field separator is ";", line separator is CRLF
3366 				if (this.result.resulttype!='dbresult') {
3367 					aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+
3368 					                                      " must be 'dbresult' to produce CSV",
3369 					                   this.result);
3370 					return;
3371 				}
3372 				var qrl = new Array();										// array of result lines already joined 
3373 				var rhn = new Array();										// array of result header names
3374 				aux.default_param(this.result,'linesep','\r\n');				// default for line separator is CRLF
3375 				aux.default_param(this.result,'fieldsep',',');					// default for field separator is COMMA
3376 				aux.default_param(this.result,'headersInFirstLine',true);
3377 				if (this.result.headersInFirstLine) {
3378 					for (var fi = 0; fi<this.result.metaData.length; fi++) {
3379 						rhn.push(this.result.metaData[fi].name);				// assemble header line
3380 					}
3381 					qrl.push(rhn.join(this.result.fieldsep));					// put header name line as first in data (if configured by option "headersInFirstLine")
3382 				}
3383 				for (var di = 0; di<this.result.rows.length; di++) {
3384 					qrl.push(this.result.rows[di].join(this.result.fieldsep));
3385 				}
3386 				this.result.data = qrl.join(this.result.linesep);
3387 				aux.default_param(this.result,'contentType','text/csv');
3388 				aux.default_param(this.result,'disposition','download');
3389 				break;
3390 			case 'htab':														// present query result as HTML table, rendered per default with view 'tables' or 'table'
3391 				aux.default_param(this.result,'disposition','view');			// if not otherwise specified, disposition is to view the result
3392 				if (this.result.resulttype!='dbresult') {
3393 					aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+
3394 					                                      ", must be 'dbresult' to produce HTML-table",
3395 					                   this.result);
3396 					return;
3397 				}
3398 				if (this.isSequence) {											// if we're a sequence ...
3399 					// logger.debug("DQUERY.htab.SEQUENCE.query:");
3400 					// logger.debug(this.query);
3401 					if (!(this.query.view)) {									// ... and view was not set by top level query descriptor ...
3402 						this.result.view = null;								// ... then undefine a possibly present view from a detail query ...
3403 					}															// ... in order to re-evaluate the default view in the next step
3404 				}
3405 				if (!(this.result.view)) {													// if this result has no (explicitely defined or defaulted) view ...
3406 					// logger.debug("VIEW not set, will default");							// ... then derive a default view from the result:
3407 					if ((this.result.dbresults) && (this.result.dbresults.length>1)) {		//     if there is more than one result in the result ...
3408 						this.result.view = 'tables'											//		... then use the 'tables' view (presenting all results)
3409 					} else {																//		... if there is only 1 result ...
3410 						this.result.view = 'table';											//		... then use the 'table' view (presenting only the base result)
3411 					}
3412 					// logger.debug("... default view is "+this.result.view);
3413 				}
3414 				aux.default_param(this.result,'view','tables');					// anyway, the default view is 'tables', if no other defaulting algorithm succeeded
3415 				break;
3416 			case 'hhlist':														// present query result as hierarchical HTML list
3417 				if (this.result.resulttype!='object') {
3418 					aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+
3419 					                                      ", must be 'object' to produce hierarchical HTML-List",
3420 					                   this.result);
3421 					return;
3422 				}
3423 				// logger.debug("DQUERY.COMPLETE.HHLIST.RESOBJECT:\n"+JSON.stringify(this.result.resultobject,null,2));
3424 				this.result.body = object2hierHtmlList(this.result.resultobject,true);
3425 				// logger.debug("DQUERY.COMPLETE.HHLIST.BODY=\""+this.result.body+"\"");
3426 				aux.default_param(this.result,'disposition','view');
3427 				aux.default_param(this.result,'view','plain');
3428 				break;
3429 			case 'hgreport':
3430 				if (this.result.resulttype!='dbreport') {
3431 					aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+
3432 					                                      ", must be 'dbreport' to produce grouped database report",
3433 					                   this.result);
3434 					return;
3435 				}
3436 				aux.default_param(this.result,'disposition','view');
3437 				aux.default_param(this.result,'view','hgreport');
3438 				break;
3439 			case 'JSON':
3440 				switch (this.result.resulttype) {
3441 					case 'object':
3442 						// this.result.body = JSON.stringify(JSON.decycle(this.result.resultobject),null,4).replace(/(?:\r\n|\r|\n)/g,"<br/>");
3443 						this.result.body = "<pre>"+JSON.stringify(JSON.decycle(this.result.resultobject),null,4)+"</pre>";
3444 						aux.default_param(this.result,'disposition','view');
3445 						aux.default_param(this.result,'view','plain');
3446 						break;
3447 					case 'editdata':
3448 						this.result.form_jsondata = JSON.stringify(JSON.decycle(this.result));
3449 						aux.default_param(this.result,'disposition','view');
3450 						aux.default_param(this.result,'view','editform');
3451 						break;
3452 					default:
3453 						aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+
3454 						                                      this.result.resulttype+", must be 'object' or 'editdata' to produce JSON output",this.result);
3455 						return;
3456 				}
3457 				break;
3458 			case 'plain':
3459 			    if (!this.result.body) {
3460 					aux.error_callback(this.callback,null,"DQUERY-result-Error: result of query does not contain a 'body' attribute",this.result);
3461 					return;
3462 				}
3463 				aux.default_param(this.result,'disposition','view');
3464 				aux.default_param(this.result,'view','plain');
3465 				break;
3466 			default:
3467 				aux.error_callback(this.callback,null,"illegal result format \""+this.result.format+"\"",this.result);
3468 				return;
3469 		}
3470 		logger.debug("DQUERY.COMPLETE.DATANAME=\""+this.query.dataname+"\".FORMAT="+this.result.format+".DISPOSITION=\""+this.result.disposition+"\".VIEW=\""+this.result.view+"\""+
3471 					".CONTENT_TYPE=\""+this.result.contentType+"\".RESULT_TYPE="+this.result.resulttype+"\"");
3472 		// logger.debug(this.result);
3473 		this.callback(undefined,this.result);
3474 	}
3475 	// end of flow dquery
3476 	);
3477 
3478 	
3479 /* *****************************************************************************
3480  *	FLOW:			RESTquery
3481  *	INPUT:			host	-	IP-Address or DNS-name of host to contact
3482  *					endpoint	-	endpoint address in the host
3483  *					method	-	HTTP-Method to use:
3484  *						'GET'
3485  *						'POST'
3486  *					data	-	an object containing the request data
3487  *					rfunc	-	the completion callback function
3488  *					pfunc	-	optional: the progress status reporting function
3489  *	CALLBACK:		rfunc	-	called upon completion
3490  *						PARAMETERS:
3491  *							err		-	error object if anything bad happened
3492  *							result	-	a result object	
3493  *					PFUNC	- callback function to be called for progress info
3494  *						parameters:
3495  *							PROG	-	a hierarchical object describing the progress of the operation
3496  *	DESCRIPTION:	performs a REST query:
3497  *						query URL string is taken from <query.request>
3498  *						result contains the (JSON-)decoded response
3499  *
3500  *		TODO: as of 29.10.2015, only GET-requests and only JSON-responses are supported
3501  */
3502  var RESTquery = flow.define(
3503  // step 1:	issue query
3504  function (host,endpoint,method,data,rfunc,pfunc) {
3505 	 this.host = host;
3506 	 this.endpoint = endpoint;
3507 	 this.method = method;
3508 	 this.data = data;
3509 	 this.port = 80;
3510 	 this.proxy      = prefs.http_proxy_host || "proxy.sozvers.at";
3511 	 this.proxy_port = prefs.http_proxy_port || 8080;
3512 	 this.callback = rfunc;
3513 	 this.pcallback = pfunc;
3514 	 this.starttime=new Date();
3515 	 this.headers = {};
3516 	 logger.debug("RESTquery.BEGIN.HOST=\""+this.host+"\".ENDPOINT=\""+this.endpoint+"\".METHOD=\""+this.method+"\".DATA:",this.data);
3517 	 this.dataString = JSON.stringify(data);
3518 	if (method == 'GET') {
3519 		this.endpoint += '?' + querystring.stringify(this.data);
3520 	} else {
3521 		this.headers = {
3522 			'Content-Type': 'application/json',
3523 			'Content-Length': this.dataString.length
3524 			};
3525 	}
3526 	this.options = {
3527 		host: 	this.host,
3528 		port:	this.port,
3529 		path: 	this.endpoint,
3530 		method:	this.method,
3531 		headers:	this.headers
3532 	};
3533 	logger.debug("RESTquery.OPTIONS:",this.options);
3534 	this.req = http.request(this.options,this);
3535 	logger.debug("RESTquery.SEND.DATA=\""+aux.cutString(this.dataString)+"\"");
3536 	this.req.write(this.dataString);
3537 	this.req.end();
3538  },
3539  // step 2: process response
3540 function(res) {
3541 	logger.debug("RESTquery.PROCESS_RESPONSE");
3542     res.setEncoding('utf-8');
3543     this.responseString = '';
3544 
3545     res.on('data', function(data) {
3546 		logger.debug("RESTquery.PROCESS_RESPONSE.DATA");
3547 		this.responseString += data;
3548 		if (this.pcallback) this.pcallback({	action: 'RESTquery', stage: 'getResponseData', 
3549 											stateunit: 'gotChars', state: this.responseString.length});
3550     }.bind(this));
3551 
3552     res.on('end', function() {
3553       logger.debug("RESTquery.END.RESPONSE=\""+(this.responseString.length>40?this.responseString.substring(0,40)+"...":this.responseString)+"\"");
3554 	  if (this.responseString.charAt(0)=='{') {				// response is JSON-formatted
3555 		  try {
3556 			this.responseObject = JSON.retrocycle(JSON.parse(this.responseString));
3557 			this.callback(null,{resulttype: 'object', resultobject: this.responseObject});
3558 		  }
3559 		  catch (e) {
3560 			  this.callback(new Error("Error decoding JSON-response: "+e.message));
3561 		  }
3562 	  } else { 												// no JSON
3563 		this.callback(null,{resulttype: 'string', body: this.responseString});
3564 	  }
3565     }.bind(this));
3566   });
3567 
3568 /* *****************************************************************************
3569  *
3570  *  FUNCTION:		copy_dbresult
3571  *	INPUT:			dbres 	- result structure from a DBSelect database query. Must have:
3572  *						metaData	-	array with objects, column names in .name attribute
3573  *						rows		- array (of rows) of arrays (of columns)
3574  *					rtitle			- title of result display
3575  *					rdescription	- descriptive text displayed aside the result table
3576  *	RETURNS:		DBRESULT structure with the contents from <dbres>:
3577  *						title
3578  *						description
3579  *						metaData
3580  *						rows
3581  *  DESCRIPTION:	copies the result of a database query into a dbresults structure to be rendered afterwards
3582  *
3583  *
3584  */ 
3585 function copy_dbresult(dbres,rtitle,rdescription) {
3586 	var dbr = {};
3587 	dbr.title = rtitle;
3588 	dbr.description = rdescription;
3589 	dbr.metaData = new Array();
3590 	for (var mdi = 0; mdi<dbres.metaData.length; mdi++) {
3591 		dbr.metaData.push({name: dbres.metaData[mdi].name});
3592 		}
3593 	dbr.rows = new Array();
3594 	for (tdi = 0;tdi<dbres.rows.length;tdi++) {
3595 		dbr.rows[tdi] = new Array();
3596 		for (tdf = 0; tdf<dbres.rows[tdi].length; tdf++) {
3597 			dbr.rows[tdi][tdf] = dbres.rows[tdi][tdf];
3598 		}
3599 	}
3600 	return dbr;
3601 }
3602 
3603 
3604 
3605 /* *****************************************************************************
3606  *	FUNCTION:	read_file_line_by_line
3607  *	INPUT:		path		-	full path to file in local file system
3608  *				options		-	an object containing options
3609  *	CALLBACK:	rfunc		-	called upon completion
3610  *					PARAMETERS:
3611  *						err			-	present if an error occured
3612  *						filedata	-	array of strings, one per line in the file
3613  *				pfunc		-	called for progress report
3614  *					PARAMETERS:
3615  *						action		-	an action state object describing the state of affairs
3616  */
3617  var read_file_line_by_line = flow.define(
3618  // step 1: read the file into memory
3619  function(path,options,rfunc,pfunc) {
3620 	 this.path = path;
3621 	 this.options = options;
3622 	 this.callback = rfunc;
3623 	 this.pcallback = pfunc;
3624 	 this.delimiter = (this.options.linesep || '\r\n');
3625 	 this.encoding = (this.options.encoding || 'utf8');
3626 	 this.readflags = (this.options.readflags || 'r');
3627 	 this.readoptions = {};
3628 	 if (this.encoding) this.readoptions.encoding = this.encoding;
3629 	 if (this.readflags) this.readoptions.flags = this.readflags;
3630 	 logger.debug("READ_FILE_LINE_BY_LINE.PATH="+this.path);
3631 	 fs.readFile(path,this.readoptions,this);
3632  },
3633  //step 2: split file data into lines
3634  function(err,resobj) {
3635 	 if (err) {
3636 		 aux.error_callback(this.callback,err,"Error Reading file "+this.path,resobj);
3637 		 return;
3638 	 }
3639 	if (!util.isString(resobj)) {
3640 		aux.error_callback(this.callback,null,"Data Error in READ_FILE_LINE_BY_LINE: file data is not string, check encoding!",resobj);
3641 		return;
3642 	}
3643 	this.filelines = resobj.split(this.delimiter);
3644 	logger.debug("READ_FILE_LINE_BY_LINE("+this.path+").LINES_GOT="+this.filelines.length);
3645 	this.callback(null,this.filelines);
3646  }
3647  );
3648 
3649 /* *****************************************************************************
3650  *	FUNCTION:		convert_buffer_to_hex_dump_result
3651  *	INPUT:			buffer			-	Buffer object containing raw bytes
3652  *					hdr				-	result object to be filled with type 'dbresult' hex dump rows
3653  *					rowsize			-	bytes to be dumped per row, default is 32
3654  *	OUTPUT:			hdr				-	DBRESULT object filled with hex dump component strings:
3655  *					  metaData		- row heading texts
3656  *					  rows			- array of arrays
3657  *						each row contains data for <rowsize> bytes in the file
3658  *						column 0	-	offset in file
3659  *						column 1	-	hex dump of <rowsize> bytes starting from that offset 
3660  *						column 2	-	ASCII characters for the same <rowsize> bytes: non-printables are shown as '.'
3661  */
3662  function convert_buffer_to_hex_dump_result(buf,hdr,rowsize) {
3663 	 logger.debug("CONVBUF2HEXDRES.BEGIN.NUM_BYTES="+buf.length);
3664 	 var aa = 0;
3665 	 var xs, as, cb, cc; 
3666 	 hdr.resulttype = 'dbresult';
3667 	 hdr.rows = new Array();
3668 	 hdr.metaData = [{name: 'Offset (Hex)'},{name: 'Hex Dump (Hex Bytes)'},{name: 'ASCII'}];
3669 	 rowsize = (rowsize || 32);									// default for rowsize is 32 bytes
3670 	 for (ofs = 0; ofs<buf.length; ofs+=rowsize) {
3671 		 arow = new Array();
3672 		 arow.push(HEX(ofs,16));								// column 0: relative offset in HEX representation, fixed width 16 digits
3673 		 xs = '';
3674 		 as = '';
3675 		 for (ci = 0; ci<rowsize; ci++) {
3676 			 aa = ofs+ci;
3677 			 if (aa>=buf.length) {
3678 				xs += '   ';
3679 				as += ' ';
3680 			 } else {
3681 				 cb = buf.readUInt8(aa,true);
3682 				 xs += HEX(cb,2) + ' ';
3683 				 if ((cb>=32) && (cb<=126)) {					// only printable characters
3684 					 cc = String.fromCharCode(cb);
3685 					 switch (cc) {								// perform HTML escaping
3686 						 case '>':	cc = '>';	break;
3687 						 case '<':  cc = '<';	break;
3688 						 case '&':  cc = '&';	break;
3689 					 }
3690 					 as += cc;
3691 				 } else {
3692 					 as += '.';									// non-printable characters represented by "."
3693 				 }
3694 			 }
3695 		 }
3696 		 arow.push(xs);											// column 1: bytewise HEX dump
3697 		 arow.push(as);											// column 2: ASCII representation (HTML-escaped)
3698 		 hdr.rows.push(arow);
3699 	 }
3700 	logger.debug("CONVBUF2HEXDRES.COMPLETE");
3701  }
3702 
3703 /* *****************************************************************************
3704  *
3705  *	FLOW:			process_mfile
3706  *	INPUT:			flowctx			-	flow context in which the action is running
3707  *					sess			-	session object to check authorization
3708  *					mfileid			-	managed file id to process
3709  *					options			-	object containing options controlling behaviour
3710  *	CALLBACK:		rfunc			-	called upon completion
3711  *						PARAMETERS:
3712  *							err		-	error object in case of malfunction
3713  *							result	-	a DBresult object with the output of processing
3714  *					pfunc			-	called to report progress
3715  *						PARAMETERS:
3716  *							action	-	an action state object describing the state of affairs
3717  *	DESCRIPTION:	processes the data in managed file <mfileid>
3718  */
3719  var process_mfile = flow.define(
3720 	// step 1: check authorization
3721 	function(flowctx,sess,mfileid,options,rfunc,pfunc) {
3722 		logger.debug("PROCESS_FILE.MFILEID=\""+mfileid+"\".BEGIN");
3723 		this.flowctx = flowctx;
3724 		this.callback = rfunc;
3725 		this.pcallback = pfunc;
3726 		this.options = options;
3727 		this.mfileid = mfileid;
3728 		this.session = sess;
3729 		functions.check_authorization(this.session,'process_file',this,this);
3730 	},
3731 	// step 2: get MFILE data
3732 	function(err,result) {
3733 		if (err) {
3734 			aux.error_callback(this.callback,err,"Authorization Error in PROCESS_MFILE",result);
3735 			return;
3736 		}
3737 		functions.get_mfile_data(this.mfileid,this,this.pcallback);
3738 	},
3739 	// step 3: read file
3740 	function(err,result) {
3741 		if (err) {
3742 			aux.error_callback(this.callback,err,"Error getting MFILE-info",result);
3743 			return;
3744 		}
3745 		this.mfileinfo = result;
3746 		this.open_flags = (this.options.open_flags || 'r');
3747 		this.read_mode  = (this.options.read_mode || 'whole_file');
3748 		logger.debug("PROCESS_MFILE.READ_MODE="+this.read_mode);
3749 		if (this.read_mode=='whole_file') {
3750 			fs.readFile(this.mfileinfo.mfilepath,this.options,this);
3751 		} else if (this.read_mode='line_by_line') {
3752 			read_file_line_by_line(this.mfileinfo.mfilepath,this,this,this.pcallback);
3753 		} else {
3754 			this.read_status = 'step_03';
3755 			process_query(flowctx,'start_read');
3756 			this();
3757 		}
3758 	},
3759 	// step 4:	process file
3760 	function(err,resobj) {
3761 		if (err) {
3762 			aux.error_callback(this.callback,err,"Error processing file step 3"+this.mfileinfo.mfilepath);
3763 			return;
3764 		}
3765 		if (this.read_mode=='whole_file') {				// file completely read in one string <resobj>
3766 			this.result = {	resulttype: 'dbresult', 
3767 							title: "Inhalt von "+this.mfileinfo.mfilepath, 
3768 							metaData: [{name:'filedata'}], 
3769 							rows: [[resobj]]}
3770 			if (this.options.process_result) {
3771 				this.flowctx.exectx = this;
3772 				this.filedata = resobj;
3773 				logger.debug("PROCESS_MFILE: process_result is: \""+this.flowctx.query[this.options.process_result]+"\"");
3774 				process_query(this.flowctx,this.options.process_result);
3775 			}
3776 			this.callback(null,this.result);
3777 		} else if (this.read_mode=='line_by_line') { // file read complete, <resobj> is an array of strings, representing lines
3778 			this.result = {resulttype: 'dbresult', 
3779 			               title: "Inhalt von "+this.mfileinfo.mfilepath, 
3780 						   metaData: [{name: 'linenumber'},{name: 'linedata'}]};
3781 			this.result.rows = new Array();
3782 			for (var i = 0; i<resobj.length; i++) {
3783 				this.result.rows.push([i+1,resobj[i]]);
3784 			}
3785 			this.callback(null,this.result);
3786 		} else {
3787 			process_query(this.flowctx,'next_read');
3788 			if (this.read_status!='complete') {
3789 				this.REWIND();
3790 				this();
3791 			} else {
3792 				this.callback(null,this);
3793 			}
3794 		}
3795 	}
3796 	);
3797 		
3798 /*
3799  * *****************************************************************************************************
3800  *                         Utilities
3801  * *****************************************************************************************************
3802  */
3803  
3804  /* ****************************************************************************
3805   *
3806   *	Decoration - formatting etc. for query results
3807   *
3808   * decoration modifies the result object of a query before showing it to the user 
3809   * this can be used for formatting (bold text, ...)
3810   * but also for adding hyperlinks
3811   *
3812   * decoration is controlled by query-attributes.
3813   * Depending on the result type, varying query attributes are checked and,
3814   * if present, applied.
3815   * Applying means that the respective data field is replaced by the result of 
3816   * placeholder-resolution by using aux.populate()
3817   */
3818   
3819  // helper functions for decorate()
3820  
3821  // decorate a dbresult
3822  //	INPUT:	dbres		-	the dbresult object to be decorated
3823  //			result		-	the result object being the complete query result and carrying the query attributes
3824  //			ctxobj		-	the query context where other parameters can be taken from
3825  //	decoration query attributes:
3826  //		decoration.decorate.metadata.<column>, where <column> is the column number, starting from 0
3827  //		decoration.decorate_colcell.<column>, where <column is the column number, starting from 0
3828  //	available placeholder names:
3829  //		placeholder names are resolved in the context of <dbres>, the dbresult object to be decorated. If not found in <dbres>, fallback goes to <ctxobj>
3830  //		colnames - an array of the names of the columns, copied from metaData, index starting from 0
3831  //		<colname> - the content of the current row's column named <colname> (as taken from metaData)
3832  //
3833 function decorate_dbresult(dbres,result,ctxobj,options) {
3834 	var dbg = (options)?((options.dbg)?options.dbg:false):false;
3835 	if (dbg) logger.debug("DECORATE_DBRESULT");
3836 	dbres.base_result = result;													// place a link to the base result into dbres so that populate() can use it
3837 	dbres.colnames = new Array();
3838 	for (var mi=0; mi<dbres.metaData.length; mi++) {
3839 		dbres.colnames.push(dbres.metaData[mi].name);
3840 	}
3841 	if (result.decoration.decorate_metadata) {
3842 		  for (var mi=0; mi<dbres.metaData.length; mi++) {
3843 			  if (result.decoration.decorate_metadata.hasOwnProperty(mi)) {
3844 				  dbres.colname = dbres.metaData[mi].name;
3845 				  dbres.metaData[mi].name = aux.populate(result.decoration.decorate_metadata[mi],dbres,ctxobj);
3846 			  }
3847 		  }
3848 	}
3849 	if (result.decoration.decorate_colcell) {
3850 		var ci, ri, tpl;
3851 		for (ri=0; ri<dbres.rows.length; ri++) {
3852 			for (ci=0; ci<dbres.metaData.length; ci++) {
3853 				dbres[dbres.colnames[ci]] = dbres.rows[ri][ci];
3854 				if (dbg) logger.debug("DECORATE_DBRESULT.SET_COLVALS."+dbres.colnames[ci]+"="+dbres[dbres.colnames[ci]]);
3855 			}
3856 			for (ci=0; ci<dbres.metaData.length; ci++) {
3857 				if (result.decoration.decorate_colcell.hasOwnProperty(ci)) {
3858 					tpl = result.decoration.decorate_colcell[ci];
3859 					if (ci<dbres.rows[ri].length) {
3860 						dbres.celldata = dbres.rows[ri][ci];					// provide "celldata" for populate()
3861 						dbres.rows[ri][ci] = aux.populate(tpl,dbres,ctxobj);
3862 					} else {
3863 						logger.error("DECORATE_DBRESULT.CANNOT_DECORATE.RI="+ri+".CI="+ci+".CI>LEN="+dbres.rows[ri].length);
3864 					}
3865 				}
3866 			}
3867 		}
3868 	}
3869 	if (result.decoration.decorate_dbresult) {
3870 		var rdt = result.decoration.decorate_dbresult;
3871 		var tatt;
3872 		if (rdt.target) {
3873 			tatt = rdt.target;
3874 		} else {
3875 			tatt = "decoration_target";
3876 		}
3877 		if (rdt.expression) {
3878 			if (dbg) aux.logJSON(rdt,"DECORATE_DBRESULT");
3879 			dbres[tatt] = aux.populate({template: rdt.expression, data: dbres, adata: ctxobj, dbg: dbg});
3880 			if (dbg) aux.logJSON(dbres[tatt],"DECORATE_DBRESULT."+tatt);
3881 		}
3882 	}
3883 }
3884 
3885 // decorate a report group
3886 // decoration attributes:
3887  //		decoration.decorate_dbreport - marker attribute, if this is present and not null, each data group (being a dbresult) 
3888  //										is decorated as a dbresult using the decoration attributes as in dbresult:
3889  //		decoration.decorate_metadata.<column>, where <column> is the column number, starting from 0
3890  //		decoration.decorate_colcell.<column>, where <column is the column number, starting from 0
3891 // ATTENTION: this decoration function is only invoked if the query attribute decoration.decorate_dbreport is defined
3892 function decorate_repgroup(grp,result,ctxobj,options) {
3893 	var dbg = (options)?((options.dbg)?options.dbg:false):false;
3894 	if (dbg) logger.debug("DECORATE_REPGROUP.GRPID="+grp.grpid);
3895 	if (grp.result) {
3896 		decorate_dbresult(grp.result,result,ctxobj,options);
3897 	}
3898 	if (grp.groups) {
3899 		for (var gi=0; gi<grp.groups.length; gi++) {
3900 			decorate_repgroup(grp.groups[gi],result,ctxobj,options);
3901 		}
3902 	}
3903 }
3904 	
3905 
3906 // decorate a dbreport
3907  //	decoration query attributes:
3908  //		decoration.decorate_dbreport - marker attribute, if this is present and not null, each data group (being a dbresult) 
3909  //										is decorated as a dbresult using the decoration attributes as in dbresult:
3910  //		decoration.decorate_metadata.<column>, where <column> is the column number, starting from 0
3911  //		decoration.decorate_colcell.<column>, where <column is the column number, starting from 0
3912  //	available placeholder names:
3913  //		placeholder names are resolved in the context of <dbres>, the dbresult object to be decorated
3914  //		colnames - an array of the names of the columns, copied from metaData, index starting from 0
3915  //		<colname> - the content of the current row's column named <colname> (as taken from metaData)
3916  //
3917 function decorate_dbreport(result,ctxobj,options) {
3918 	var dbg = (options)?((options.dbg)?options.dbg:false):false;
3919 	var rd = result.decoration.decorate_dbreport;
3920 	if (dbg) logger.debug("DECORATE_DBREPORT."+rd);
3921 	if (rd && result.rootgroup) {
3922 		decorate_repgroup(result.rootgroup,result,ctxobj,options);
3923 	}
3924 }
3925  
3926  /* ****************************************************************************
3927   *	FUNCTION:		decorate
3928   *	INPUT:			result		-	a RESULT object to be decorated
3929   *						result.decoration	-	a decoration specifier object:
3930   *							.decorate_metadata	-	decoration specifier for metadata / column headers:
3931   *								.<number>			-	decoration for column header of column <number>
3932   *							.decorate_colcell	-	decoration specifier for data cells, by columns:
3933   *								.<number>			-	decoration template for all data cells in column <number>
3934   *							.decorate_dpreport	-	decoration for a 'dbreport' result
3935   *					ctxobj		-	a context object for additional decoration information to be taken from
3936   *	OUTPUT:			result		- 	decorated
3937   *	DESCRIPTION:	the contents of <result> are decorated controlled by the result.decoration specification
3938   *					decoration is defined to be a visual enhancement of the data in result
3939   *					result.decoration is expected to have attributes that specify how the contents of the metaData and rows objects
3940   *					shall be modified to become decorated
3941   *					sub-objects of result.decoration may be:
3942   *					- decorate_metadata[colnum]	-	a template applied to the metadata object for column colnum
3943   *					- decorate_colcell[colnum]	-	a template applied to each cell in column data in column colnum
3944   */
3945   function decorate(result,ctxobj) {
3946 	  if (!(result.decoration)) return;
3947 	  var dbg = false;
3948 	  if (result.decoration.debug) dbg = true;
3949 	  if (dbg) aux.logJSON(result.decoration,"DECORATE.DECORATION");
3950 	  result.context = ctxobj;
3951 	  if ((result.decoration.decorate_dbreport) && (result.resulttype=='dbreport')) {
3952 		  decorate_dbreport(result,ctxobj,{dbg: dbg});
3953 	  }
3954 	  if ((result.resulttype=='dbresult') && ((result.decoration.decorate_colcell) || (result.decoration.decorate_metadata) || (result.decoration.decorate_dbresult))) {
3955 		  decorate_dbresult(result,result,ctxobj,{dbg: dbg});
3956 	  }
3957   }
3958 
3959 /* ****************************************************************************
3960  *
3961  *	FUNCTION:	object2hierHtmlList
3962  *	INPUT:		obj		-	object to be converted
3963  *				dd		-	if true, decycle the object first
3964  *	RESULT:		HTML text containing a unordered list enumerating the object <obj>s' attributes
3965  *				if an attribute is an object itself, a sub-list is recursively created
3966  */
3967  function object2hierHtmlList(obj, dd) {
3968 	 // logger.debug("OBJ2HHL.BEGIN");	 
3969 	 var ht = "";
3970 	 var o;
3971 	 if (dd) { 
3972 		o = JSON.decycle(obj)
3973 	 } else {
3974 		 o = obj;
3975 	 }
3976 	 // logger.debug("OBJ2HHL.DECYCLED");
3977 	 var an;
3978 	 var at;
3979 	 at = (typeof o);
3980 	 logger.debug("OBJ2HHL.AT=\""+at+"\"");
3981 	 switch (at) {
3982 		 case 'object':
3983 			ht += '<ul>\n'
3984 			for (an in o) {
3985 				logger.debug("OBJ2HHL.AN=\""+an+"\"");
3986 				ht += '<li>'+an+': '+object2hierHtmlList(o[an],false)+'</li>\n';
3987 			}				
3988 			ht += '</ul>\n'
3989 			break;
3990 		 case 'function':
3991 			ht += '[function]';
3992 			break;
3993 		case 'number':
3994 		case 'boolean':
3995 			ht += o.toString();
3996 			break;
3997 		case 'string':
3998 			ht += o;
3999 			break;
4000 		default:
4001 			ht += '['+at+']';
4002 	 }
4003 	 return ht;
4004  }
4005   
4006 /* ****************************************************************************
4007  *	FUNCTION:		object2nvlist
4008  *  INPUT:			o		- object reference
4009  *					options	-	additional object to add additional attributes into NVlist
4010  *	RESULT:			NVlist object derived from <o>
4011  *	DESCRIPTION:	each attribute of <o> is added into the result in the form:
4012  *					{ name: <attribute-name>, value: "<attribute-value" }
4013  *					additionally, all attributes of <options> are copied to the result
4014  */
4015  function object2nvlist(o,options) {
4016 	 var res = {};
4017 	 var i = 1;
4018 	 res.nvlist = new Array();
4019 	 for (a in o) {
4020 		 res.nvlist.push({name: a, value: o[a]});
4021 	 }
4022 	 for (a in options) {
4023 		 res[a] = options[a];
4024 	 }
4025 	 return res;
4026  }
4027 
4028 /* ****************************************************************************
4029  *	FUNCTION:		DEC
4030  *	DESCRIPTION:	format number as decimal string
4031  *					string is padded with leading zeroes to <ndig> places
4032  *					if numeric string would be longer than <ndig> the string is
4033  *					cut down right-aligned to <ndig> places, higher order places
4034  *					to the left are cut off
4035  */  
4036 function DEC(num,ndig) {
4037 	var s = num.toString();
4038 	while (s.length<ndig) {
4039 		s = '0'+s;
4040 	}
4041 	if (s.length>ndig) {
4042 		s = s.slice(0-ndig);
4043 	}
4044 	return s;
4045 }
4046 
4047 /* ****************************************************************************
4048  *	FUNCTION:		HEX
4049  *	INPUT:			num		-	number to be formatted
4050  *					ndig	-	number of digits to be produced
4051  *	DESCRIPTION:	format number as hexadecimal string
4052  *					string is padded with leading zeroes to <ndig> places
4053  *					if numeric string would be longer than <ndig> the string is
4054  *					cut down right-aligned to <ndig> places, higher order places
4055  *					to the left are cut off
4056  */
4057 function HEX(num,ndig) {
4058 		var s = num.toString(16);
4059 		while (s.length<ndig) {
4060 			s = '0'+s;
4061 		}
4062 		if (s.length>ndig) {
4063 			s = s.slice(0-ndig);
4064 		}
4065 		return s;
4066 }
4067 
4068 /* ****************************************************************************
4069  *	FUNCTION:	init_shutdown		
4070  */
4071 function init_shutdown(mode) {
4072 	if (!prefs.init_shutdown) {
4073 		prefs.init_shutdown = true;
4074 	}
4075 	logger.debug("INIT_SHUTDOWN, shutdown with mode \""+mode+"\" in "+prefs.restart_timeout_ms+" ms");
4076 	prefs.shutdown_mode = mode;
4077 	setTimeout(function() {
4078 				do_exit(mode);
4079 			   },
4080 				prefs.restart_timeout_ms);
4081 	logger.debug("CCDB: shutdown/"+mode+" scheduled in "+prefs.restart_timeout_ms+" ms");
4082 }
4083 
4084 /* ****************************************************************************
4085  *	FUNCTION:	do_exit		
4086  */	
4087 function do_exit(mode) {
4088 	var ecod;
4089 	switch (mode) {
4090 		case "restart":
4091 			ecod = prefs.restart_exit_code; 
4092 			break;
4093 		case "halt":
4094 			ecod = prefs.halt_exit_code;
4095 			break;
4096 		case "shutdown":
4097 			ecod = prefs.shutdown_exit_code;
4098 			break;
4099 		default:
4100 			ecod = prefs.halt_exit_code;
4101 	}
4102 	logger.debug("DO_EXIT.MODE=\""+mode+"\".EXIT_CODE="+ecod);
4103 	setTimeout(function() {
4104 				logger.debug("CCDB: will terminate with exit code "+ecod+" with mode "+mode);
4105 				process.exit(ecod);
4106 			   },
4107 				2000);
4108 	logger.debug("CCDB: shutdown/"+mode+" with exit code "+ecod+" scheduled in "+2000+" ms");
4109 }
4110 
4111 /* ****************************************************************************
4112  *	FUNCTION:	login_register_session		
4113  */
4114 function login_register_session(sess) {
4115 	if (!(prefs.session_management)) {
4116 		prefs.session_management = { actual_session_count: 0 };
4117 	}
4118 	if (!(prefs.session_management.actual_session_count)) {
4119 		prefs.session_management.actual_session_count = 0;
4120 	}
4121 	prefs.session_management.actual_session_count++;
4122 	logger.debug("REGISTER_SESSION: now "+prefs.session_management.actual_session_count+" sessions registered.");
4123 }
4124 
4125 /* ****************************************************************************
4126  *	FUNCTION:		
4127  */
4128  
4129 function logout_deregister_session(sess) {
4130 	if (!(prefs.session_management)) {
4131 		prefs.session_management = { actual_session_count: 0 };
4132 	}
4133 	if (prefs.session_management.actual_session_count>0) {
4134 		prefs.session_management.actual_session_count--;
4135 	}
4136 	logger.debug("DEREGISTER_SESSION: now "+prefs.session_management.actual_session_count+" sessions registered.");
4137 	if (prefs.session_management.actual_session_count==0 && prefs.init_shutdown) {
4138 		do_exit(prefs.shutdown_mode);
4139 	}
4140 }
4141 
4142 /* ****************************************************************************
4143  *	FUNCTION:		processSessionAction
4144  *	INPUT:			sess	-	session in which's context the action shall be performed
4145  *								this is needed for credentials
4146  *					action	-	action to be performed
4147  *						necessary fields in <action>:
4148  *						cmd		-	a string identifying the action to be performed:
4149  *									"dquery"	:	perform a query
4150  *						query	-	a [query] object passed to the execution function
4151  *	DESCRIPTION:	performs the action and logs the result					 
4152  */
4153 
4154  function processSessionAction(sess,action) {
4155 	 var cmd = action.cmd;
4156 	 var query = action.query;
4157 	 logger.debug("PROCSESSACT.CMD=\""+cmd+"\"");
4158 	 switch (cmd) {
4159 		 case "dquery":
4160 			dquery(sess,query,
4161 			       function(err,result) {
4162 					   if (err) logger.error("SESSACTCOMPLERR.Error completing session action: ",err);
4163 					   if (result) {
4164 						   logger.debug("SESSACTCOMPLRES.Session Action completion result: ");
4165 						   switch (result.resulttype) {
4166 							   case 'string':
4167 									logger.debug("-- "+result.body);
4168 									break;
4169 								case 'dbresult':
4170 									if (result.metaData) {
4171 										var ls = '';
4172 										for (var i = 0; i<result.metaData.length; i++) {
4173 											if (i>0) ls += ' | ';
4174 											ls += aux.fixString(result.metaData[i].name,40);
4175 										}
4176 										logger.debug(ls);
4177 										ls = '';
4178 										for (var i = 0; i<result.metaData.length; i++) {
4179 											if (i>0) ls += '-+-';
4180 											ls += aux.fixString('-',40,'-');
4181 										}
4182 										logger.debug(ls);
4183 									}
4184 									if (result.rows) {
4185 										ls = '';
4186 										for (var r = 0; r<result.rows.length; r++) {
4187 											for (var i = 0; i<result.rows[r].length; i++) {
4188 												if (i>0) ls += ' | ';
4189 												ls += aux.fixString(result.rows[r][i],40);
4190 											}
4191 											logger.debug(ls);
4192 										}
4193 									}
4194 									// fall through to log other attributes, too
4195 								default:
4196 									logger.debug("session action result type is \""+result.resulttype+"\", attributes are:");
4197 									for (var a in result) {
4198 										logger.debug(aux.fixString(a,32)+': '+aux.cutString(result[a],40));
4199 									}
4200 									logger.debug("----------- end of session action result --------------------");
4201 						   }
4202 					   } else {
4203 							logger.debug("SESSACTCOMPL, session action "+cmd+" complete");
4204 					   }
4205 				   },
4206 				   function(pstat) {
4207 					   // logger.debug("SESSACTPROG, session action progress: ",pstat);
4208 				   });
4209 			break;
4210 		default:
4211 			logger.error("Undefined session action command \""+cmd+"\"");
4212 	 }
4213  }
4214  
4215  /* ****************************************************************************
4216   *		FUNCTION:		adminSessionTick
4217   *		DESCRIPTION:	this function is called periodically every second
4218   *						is processes the next action in the sessions queue
4219   */
4220  function adminSessionTick(asess) {
4221 	 if (asess.busy) {
4222 		 return;
4223 	 }
4224 	 // logger.debug("ADMINSESSIONTICK.ASESS: ",asess);
4225 	 asess.busy = true;
4226 	 var entact = asess.queue.get();
4227 	 if (entact) {
4228 		 processSessionAction(asess,entact);
4229 	 } else {
4230 		 asess.busy = false;
4231 	 }
4232  }
4233 	 
4234  /* ****************************************************************************
4235   *		FUNCTION:		startAdminSession
4236   *		DESCRIPTION:	starts the ADMIN session
4237   *						creates interval timer to 
4238   */
4239  function startAdminSession() {
4240 	 var asq = aux.createQueue("ADMIN");
4241 	 var asess = {queue: asq};
4242 	 aux.createGlobalNamedObject("ADMIN_SESSION",asess);
4243 	 aux.copyObject(asess,prefs.adminsession);
4244 	 asess.tickTimeout = setInterval(adminSessionTick,1000,asess);
4245 	 asess.busy = false;
4246  }
4247 	 
4248 	 
4249 /* ***********************************************************************************************
4250  *                                    MAIN PROGRAM 
4251  * ***********************************************************************************************/
4252  
4253 hostname = '0.0.0.0';									// listen on all interfaces
4254 logger.debug('Current directory: ' + process.cwd());
4255 
4256 /*
4257 startup_db = process.argv[2];
4258 if (!startup_db) startup_db = '';
4259 logger.debug(db.setConfig(startup_db));
4260 */
4261 
4262 var prefs_initializer = dbConfig.prefs_initializer;
4263 
4264 if (prefs_initializer) {
4265 	aux.copyObject(prefs,prefs_initializer);
4266 	// aux.logPretty(prefs,"PREFS");
4267 }
4268 
4269 prefs.modulerefs = prefs.modulerefs || {};
4270 
4271 aux.createGlobalNamedObject("PREFS",prefs);
4272 
4273 logger.debug("PROCESS.ARGV: ",process.argv);
4274 var startup_db = process.argv[2] || os.hostname();
4275 var startup_action_desc = process.argv[3];
4276 if (!startup_db) startup_db = '';
4277 logger.debug(db.setConfig(startup_db));
4278 
4279 var ssl_options = {
4280         key: fs.readFileSync(__dirname + '/ssl/ccdb.pem'),
4281         cert: fs.readFileSync(__dirname + '/ssl/ccdb.crt'),
4282 };
4283 
4284 prefs.server = https.createServer(ssl_options, app);
4285 prefs.server.listen(app.get('port'), hostname, 
4286 				function(){
4287 					logger.debug('CCDB.Express.JS_in_node.JS.STARTED_HTTPS.HOSTNAME='+hostname+'.ENV='+app.get('env')+'.PORT='+app.get('port')+'.PRESS_CTRL-C_TO_TERMINATE' );
4288 				});
4289 //
4290 // create the background ADMIN-Session and issue an optional startup action to be executed by the ADMIN-Session
4291 //	
4292 
4293 startAdminSession();
4294 if (startup_action_desc) {
4295 	logger.debug("STARTUP_ACTION: ",startup_action_desc);
4296 	functions.doInAdminSession(new functions.Action(startup_action_desc));
4297 }
4298 
4299 logger.debug("CCDB.END_MAIN")