/*
 * dbsetup.c  -  Configure database module
 *
 * Copyright (C) 2003-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: dbsetup.c,v 1.18 2007/01/06 18:31:38 gkminix Exp $
 */

#include <common.h>
#include <nblib.h>
#include "privlib.h"
#include "privdb.h"
#include "parsefile.h"



/*
 * Default SQL table names
 */
#define DEFAULT_SYSTABLE	"systems"
#define DEFAULT_PARAMTABLE	"parameters"



/*
 * Private variables
 */
static struct dbconfig *conflist = NULL;	/* list of db configurations */
static struct dbhandle *handlelist = NULL;	/* list of db handles */
static int isinit = FALSE;			/* initialization flag */



/*
 * Forward declarations
 */
static char *db_sect_start __P((const char *recname, struct sectdef **cursect));
static char *db_sect_end __P((const char *recname, struct sectdef **cursect));




/*
 *********************************************************************************
 *
 *		Database configuration definitions
 *
 *********************************************************************************
 */


/* Enumeration value for database mode */
static struct enumdef mtable[] = {
  { "read-only",	(int)dbmode_ro },
  { "read-write",	(int)dbmode_rw },
  { NULL,		(int)dbmode_none }
};


/* Enumeration value for database type */
static struct enumdef ttable[] = {
  { "text file",	(int)dbtype_text },
  { "berkeley db",	(int)dbtype_bdb },
  { "sql",		(int)dbtype_sql },
  { NULL,		(int)dbtype_none }
};


/* Structure holding parameters read from configuration file */
static struct {
	int        dbtype;		/* dbtype parameter */
	int        dbmode;		/* dbmode parameter */
	char      *fname;		/* filename parameter */
	char      *lockfile;		/* name of lock file */
	char      *systable;		/* SQL system table name */
	char      *paramtable;		/* SQL parameter table name */
	char      *database;		/* SQL database name (DSN) */
	char      *username;		/* user name */
	char      *password;		/* user password */
	int        logintimeout;	/* SQL login timeout */
	int        conntimeout;		/* SQL connection timeout */
} dbvars;


/* Definition of db:* sections in configuration file */
static struct paramdef dbparams[] = {
  { "dbmode",		par_enum,   mtable,	{&dbvars.dbmode}},
  { "dbtype",		par_enum,   ttable,	{&dbvars.dbtype}},
  { "filename",		par_file,   NULL,	{&dbvars.fname}},
  { "lockfile",		par_file,   NULL,	{&dbvars.lockfile}},
  { "systable",		par_string, NULL,	{&dbvars.systable}},
  { "paramtable",	par_string, NULL,	{&dbvars.paramtable}},
  { "database",		par_string, NULL,	{&dbvars.database}},
  { "username",		par_string, NULL,	{&dbvars.username}},
  { "password",		par_string, NULL,	{&dbvars.password}},
  { "logintimeout",	par_int,    NULL,	{&dbvars.logintimeout}},
  { "conntimeout",	par_int,    NULL,	{&dbvars.conntimeout}},
  { NULL,		par_null,   NULL,	{NULL}}
};



/*
 * Configuration file sections.
 */
struct sectdef dbsects[] = {
  { "db:*",		dbparams,	db_sect_start,	db_sect_end },
  { NULL,		NULL,		NULL,	NULL }
};




/*
 *********************************************************************************
 *
 *		Routines to handle database configuration sections
 *
 *********************************************************************************
 */


/*
 * Assign a new string to a string pointer
 */
static inline void assignstr __F((old, new), char **old AND char **new)
{
  if (*new != NULL) {
	if (*old != NULL)
		free(*old);
	*old = *new;
	*new = NULL;
  }
}



/*
 * Start reading a db:* section.
 */
static char *db_sect_start __F((recname, cursect),
				const char *recname AND
				struct sectdef **cursect)
{
  memzero(&dbvars, sizeof(dbvars));
  dbvars.dbtype = dbtype_none;
  dbvars.dbmode = dbmode_rw;
  dbvars.logintimeout = -1;
  dbvars.conntimeout = -1;
  return(NULL);
}



/*
 * Finish reading a db:* section and create a new entry in the database
 * configuration list
 */
static char *db_sect_end __F((recname, cursect),
				const char *recname AND
				struct sectdef **cursect)
{
  struct dbconfig *config = NULL;
  char *dbname;
  char *cp = NULL;

  /* Check for database name */
  if ((dbname = strchr(recname, ':')) == NULL || !*(++dbname)) {
	cp = "invalid database section name";
	goto dbsect_end;
  }

  /*
   * Check if database already defined. This is just a safety check.
   * The config file reading routine should merge all sections with
   * equal names. Do we trust?
   */
  for (config = conflist; config != NULL; config = config->next)
	if (!strcmp(config->name, dbname)) {
		cp = "redefined database section";
		goto dbsect_end;
	}

  /* Check for valid parameters */
  if (dbvars.dbtype == dbtype_none) {
	/* We have to have a database type in any case */
	cp = "missing database type";
  } else if (dbvars.dbtype == dbtype_text) {
	if (dbvars.fname == NULL)
		cp = "missing file name for text database";
	else if (dbvars.systable != NULL)
		cp = "system table name not allowed for text database";
	else if (dbvars.paramtable != NULL)
		cp = "parameter table name not allowed for text database";
	else if (dbvars.database != NULL)
		cp = "database name not allowed for text database";
	else if (dbvars.username != NULL)
		cp = "user name not allowed for text database";
	else if (dbvars.password != NULL)
		cp = "password not allowed for text database";
	else if (dbvars.logintimeout != -1)
		cp = "login timeout not allowed for text database";
	else if (dbvars.conntimeout != -1)
		cp = "connection timeout not allowed for text database";
  } else if (dbvars.dbtype == dbtype_bdb) {
	if (dbvars.fname == NULL)
		cp = "missing file name for Berkeley DB";
	else if (dbvars.lockfile != NULL)
		cp = "name of lock file not allowed for Berkeley DB";
	else if (dbvars.systable != NULL)
		cp = "system table name not allowed for Berkeley DB";
	else if (dbvars.paramtable != NULL)
		cp = "parameter table name not allowed for Berkeley DB";
	else if (dbvars.database != NULL)
		cp = "database name not allowed for Berkeley DB";
	else if (dbvars.username != NULL)
		cp = "user name not allowed for Berkeley DB";
	else if (dbvars.password != NULL)
		cp = "password not allowed for Berkeley DB";
	else if (dbvars.logintimeout != -1)
		cp = "login timeout not allowed for Berkeley DB";
	else if (dbvars.conntimeout != -1)
		cp = "connection timeout not allowed for Berkeley DB";
  } else if (dbvars.dbtype == dbtype_sql) {
	if (dbvars.fname != NULL)
		cp = "file name not allowed for SQL database";
	else if (dbvars.lockfile != NULL)
		cp = "name of lock file not allowed for SQL database";
	else if (dbvars.systable == NULL)
		copystr(&(dbvars.systable), DEFAULT_SYSTABLE);
	else if (dbvars.paramtable == NULL)
		copystr(&(dbvars.paramtable), DEFAULT_PARAMTABLE);
	else if (dbvars.database == NULL)
		cp = "missing SQL database name (DSN)";
	else if (dbvars.username == NULL)
		cp = "missing SQL user name";
	else if (dbvars.logintimeout != -1 &&
	         (dbvars.logintimeout < 0 || dbvars.logintimeout > 255))
		cp = "invalid login timeout for SQL database";
	else if (dbvars.conntimeout != -1 &&
	         (dbvars.conntimeout < 0 || dbvars.conntimeout > 255))
		cp = "invalid connection timeout for SQL database";
  } else {
	/* This should never happen */
	cp = "invalid database type";
  }

  /* Create new database list entry */
  if (cp == NULL) {
	config = (struct dbconfig *)nbmalloc(sizeof(struct dbconfig));
	config->type = dbvars.dbtype;
	config->mode = dbvars.dbmode;
	config->next = conflist;
	conflist = config;
	copystr(&(config->name), dbname);
	if (dbvars.dbtype == dbtype_text) {
		assignstr(&(config->config.text.fname), &(dbvars.fname));
		assignstr(&(config->config.text.lockfile), &(dbvars.lockfile));
	} else if (dbvars.dbtype == dbtype_bdb) {
		assignstr(&(config->config.bdb.fname), &(dbvars.fname));
	} else if (dbvars.dbtype == dbtype_sql) {
		assignstr(&(config->config.sql.dsn), &(dbvars.database));
		assignstr(&(config->config.sql.systable), &(dbvars.systable));
		assignstr(&(config->config.sql.paramtable), &(dbvars.paramtable));
		assignstr(&(config->config.sql.username), &(dbvars.username));
		assignstr(&(config->config.sql.password), &(dbvars.password));
		config->config.sql.logintimeout = dbvars.logintimeout;
		config->config.sql.conntimeout = dbvars.conntimeout;
	}
  }

  /* Clear old variables */
dbsect_end:
  if (dbvars.fname != NULL)
	free(dbvars.fname);
  if (dbvars.lockfile != NULL)
	free(dbvars.lockfile);
  if (dbvars.systable != NULL)
	free(dbvars.systable);
  if (dbvars.paramtable != NULL)
	free(dbvars.paramtable);
  if (dbvars.database != NULL)
	free(dbvars.database);
  if (dbvars.username != NULL)
	free(dbvars.username);
  if (dbvars.password != NULL)
	free(dbvars.password);
  return(cp);
}




/*
 *********************************************************************************
 *
 *		Initialization and termination routines
 *
 *********************************************************************************
 */


/*
 * Function to be called upon program termination
 */
static void termdb __F_NOARGS
{
  struct dbhandle *hdl, *curhdl = handlelist;

  while (curhdl != NULL) {
	hdl = curhdl;
	curhdl = curhdl->next;
	if (hdl->termhandler != NULL)
		(hdl->termhandler)((DBHDL)hdl);
	if (hdl->lastfind != NULL)
		free(hdl->lastfind);
	free(hdl);
  }
}



/*
 * Initialize module
 */
static inline int initmodule __F_NOARGS
{
  int ret;

  if (!isinit) {
	if ((ret = nbatexit(&termdb)) != 0) {
		nberror(EXIT_INTERNAL, "unable to set DB exit function");
		return(FALSE);
	}
	isinit = TRUE;
  }
  return(TRUE);
}




/*
 *********************************************************************************
 *
 *		Routines to create and destroy database handles
 *
 *********************************************************************************
 */


/*
 * Find a named configuration. If the name in the returned structure
 * is NULL, the structure has to be freed by the caller after being
 * used.
 */
static struct dbconfig *findconfig __F((name), const char *name)
{
  struct dbconfig *p = conflist;

  /* Check if we have a configuration from the config file */
  if (!strncmp(name, "db@", 3)) {
	while (p != NULL) {
		if (!strcmp(p->name, &(name[3])))
			return(p);
		p = p->next;
	}
	return(NULL);
  }

  /* Otherwise we treat the database name as a file name */
  p = (struct dbconfig *)nbmalloc(sizeof(struct dbconfig));
  p->name = NULL;
  p->type = dbtype_text;
  copystr(&(p->config.text.fname), name);
  return(p);
}



/*
 * Parse database flags
 */
static int parseflags __F((flags), char *flags)
{
  int len, ret = 0;
  char *cp1, *cp2;

  /* Parse option string */
  cp2 = flags;
  while (*cp2 != ']') {
	for (cp1 = ++cp2; *cp2 && *cp2 != ',' && *cp2 != ']'; cp2++)
		;
	len = (int)(cp2 - cp1);
	if (len == 0 || !*cp2)
		return(-1);
	else if (len == 3 && !strncmp(cp1, "add", 3))
		ret |= DBFLAGS_ADD;
	else if (len == 3 && !strncmp(cp1, "opt", 3))
		ret |= DBFLAGS_OPT;
	else if (len == 2 && !strncmp(cp1, "ro", 2))
		ret |= DBFLAGS_RO;
	else
		return(-1);
  }

  /* Check if string terminates properly */
  if (cp2[1] != '\0')
	return(-1);

  /* Return with flags */
  return(ret);
}



/*
 * Create a database handle
 */
DBHDL createdb __F((name, errorhandler),
				const char *name AND
				dberr errorhandler)
{
  struct dbhandle *hdl;
  struct dbconfig *config;
  char *cp, *dbname, *namebuf = NULL;
  int flags, lastwrite, addnum;

  /* Initialize module */
  if (!initmodule())
	return(DBHDL_NULL);

  /* Determine database specification to create a handle for */
  if (name != NULL)
	copystr(&namebuf, name);
  else if (sysdbname != NULL)
	copystr(&namebuf, sysdbname);
  else {
	nberror(EXIT_DB, "missing systems database name");
	return(DBHDL_NULL);
  }

  /* Create a new handle structure */
  hdl = (struct dbhandle *)nbmalloc(sizeof(struct dbhandle));
  hdl->errhandler = errorhandler;
  hdl->termhandler = NULL;
  hdl->lastfind = NULL;
  hdl->dbnum = 0;
  hdl->dbread = -1;
  hdl->dbmark = -1;

  /* Now scan through the database name and extract all sub-names */
  addnum = 0;
  lastwrite = -1;
  dbname = strtok(namebuf, ":");
  while (dbname != NULL) {
	flags = 0;
	if ((cp = strchr(dbname, '[')) != NULL) {
		flags = parseflags(cp);
		if (flags < 0)
			break;
		*cp = '\0';
	}
	if ((config = findconfig(dbname)) == NULL)
		break;
	if (config->mode == dbmode_ro)
		flags |= DBFLAGS_RO;
	if ((flags & (DBFLAGS_RO | DBFLAGS_ADD)) == (DBFLAGS_RO | DBFLAGS_ADD))
		break;
	else if ((flags & (DBFLAGS_RO | DBFLAGS_ADD)) == 0)
		lastwrite = hdl->dbnum;
	else if ((flags & (DBFLAGS_RO | DBFLAGS_ADD)) == DBFLAGS_ADD)
		addnum++;
	hdl->configs[hdl->dbnum] = config;
	hdl->dbids[hdl->dbnum] = DBID_NULL;
	hdl->flags[hdl->dbnum] = flags;
	hdl->dbnum++;
	dbname = strtok(NULL, ":");
  }

  /* Check for error */
  free(namebuf);
  if (dbname != NULL || hdl->dbnum == 0) {
	nberror(EXIT_DB, "invalid database \"%s\"", (name != NULL ? name : sysdbname));
	free(hdl);
	return(DBHDL_NULL);
  }

  /* Force ADD flag if none has been found and return database handle */
  if (addnum == 0 && lastwrite >= 0 && lastwrite < hdl->dbnum)
	hdl->flags[lastwrite] |= DBFLAGS_ADD;
  hdl->next = handlelist;
  handlelist = hdl;
  return((DBHDL)hdl);
}



/*
 * Check if a database handle is valid
 */
int checkdb __F((handle), DBHDL handle)
{
  struct dbhandle *hdl;

  for (hdl = handlelist; hdl != NULL; hdl = hdl->next)
	if ((DBHDL)hdl == handle)
		return(TRUE);
  return(FALSE);
}



/*
 * Free all memory associated with a database
 */
void freedb __F((handle), DBHDL handle)
{
  struct dbhandle *hdl, *prev = NULL;
  int i;

  /* Check for valid database handle */
  hdl = (struct dbhandle *)handle;
  if (handlelist != hdl) {
	prev = handlelist;
	while (prev != NULL) {
		if (prev->next == hdl)
			break;
		prev = prev->next;
	}
	if (prev == NULL)
		return;
  }

  /* Check that all databases within the handle have been closed */
  for (i = 0; i < hdl->dbnum; i++)
	assert(hdl->dbids[i] == DBID_NULL);

  /* Remove the database handle from handle list */
  if (prev == NULL)
	handlelist = hdl->next;
  else
	prev->next = hdl->next;
  if (hdl->lastfind != NULL)
	free(hdl->lastfind);
  free(hdl);
}

