// NeL - MMORPG Framework
// Copyright (C) 2010 Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
#include "nel/misc/config_file.h"
#include
#include
#include
#include "nel/misc/file.h"
#include "nel/misc/debug.h"
#include "nel/misc/path.h"
#include "nel/misc/i18n.h"
#include "nel/misc/mem_stream.h"
#include "locale.h"
using namespace std;
using namespace NLMISC;
extern void cfrestart (FILE *); // used to reinit the file
extern int cfparse (void *); // used to parse the file
//extern FILE *cfin;
extern int cf_CurrentLine;
extern char *cf_CurrentFile;
extern bool cf_Ignore;
extern bool cf_OverwriteExistingVariable;
extern CMemStream cf_ifile;
// put true if you want that the config file class check type when you call asFunctions
// (for example, check when you call asInt() that the variable is an int).
// when it's false, the function will convert to the wanted type (if he can)
const bool CheckType = false;
bool LoadRoot = false;
namespace NLMISC
{
const char *CConfigFile::CVar::TypeName[] = { "Integer", "String", "Float", "Boolean" };
int CConfigFile::CVar::asInt (int index) const
{
if (CheckType && Type != T_INT) throw EBadType (Name, Type, T_INT);
switch (Type)
{
case T_STRING:
{
if (index >= (int)StrValues.size () || index < 0) throw EBadSize (Name, (int)StrValues.size (), index);
int ret = 0;
NLMISC::fromString(StrValues[index], ret);
return ret;
}
case T_REAL:
if (index >= (int)RealValues.size () || index < 0) throw EBadSize (Name, (int)RealValues.size (), index);
return (int)RealValues[index];
default:
if (index >= (int)IntValues.size () || index < 0) throw EBadSize (Name, (int)IntValues.size (), index);
return IntValues[index];
}
}
double CConfigFile::CVar::asDouble (int index) const
{
if (CheckType && Type != T_REAL) throw EBadType (Name, Type, T_REAL);
switch (Type)
{
case T_INT:
if (index >= (int)IntValues.size () || index < 0) throw EBadSize (Name, (int)IntValues.size (), index);
return (double)IntValues[index];
case T_STRING:
if (index >= (int)StrValues.size () || index < 0) throw EBadSize (Name, (int)StrValues.size (), index);
return atof(StrValues[index].c_str());
default:
if (index >= (int)RealValues.size () || index < 0) throw EBadSize (Name, (int)RealValues.size (), index);
return RealValues[index];
}
}
float CConfigFile::CVar::asFloat (int index) const
{
return (float) asDouble (index);
}
std::string CConfigFile::CVar::asString (int index) const
{
if (CheckType && Type != T_STRING) throw EBadType (Name, Type, T_STRING);
switch (Type)
{
case T_INT:
if (index >= (int)IntValues.size () || index < 0) throw EBadSize (Name, (int)IntValues.size (), index);
return toString(IntValues[index]);
case T_REAL:
if (index >= (int)RealValues.size () || index < 0) throw EBadSize (Name, (int)RealValues.size (), index);
return toString(RealValues[index]);
default:
if (index >= (int)StrValues.size () || index < 0) throw EBadSize (Name, (int)StrValues.size (), index);
return StrValues[index];
}
}
bool CConfigFile::CVar::asBool (int index) const
{
switch (Type)
{
case T_STRING:
if (index >= (int)StrValues.size () || index < 0) throw EBadSize (Name, (int)StrValues.size (), index);
if(StrValues[index] == "true")
{
return true;
}
else
{
return false;
}
case T_REAL:
if (index >= (int)RealValues.size () || index < 0) throw EBadSize (Name, (int)RealValues.size (), index);
if ((int)RealValues[index] == 1)
{
return true;
}
else
{
return false;
}
default:
if (index >= (int)IntValues.size () || index < 0) throw EBadSize (Name, (int)IntValues.size (), index);
if (IntValues[index] == 1)
{
return true;
}
else
{
return false;
}
}
}
void CConfigFile::CVar::setAsInt (int val, int index)
{
if (Type != T_INT) throw EBadType (Name, Type, T_INT);
else if (index > (int)IntValues.size () || index < 0) throw EBadSize (Name, (int)IntValues.size (), index);
else if (index == (int)IntValues.size ()) IntValues.push_back(val);
else IntValues[index] = val;
Root = false;
}
void CConfigFile::CVar::setAsDouble (double val, int index)
{
if (Type != T_REAL) throw EBadType (Name, Type, T_REAL);
else if (index > (int)RealValues.size () || index < 0) throw EBadSize (Name, (int)RealValues.size (), index);
else if (index == (int)RealValues.size ()) RealValues.push_back(val);
else RealValues[index] = val;
Root = false;
}
void CConfigFile::CVar::setAsFloat (float val, int index)
{
setAsDouble (val, index);
}
void CConfigFile::CVar::setAsString (const std::string &val, int index)
{
if (Type != T_STRING) throw EBadType (Name, Type, T_STRING);
else if (index > (int)StrValues.size () || index < 0) throw EBadSize (Name, (int)StrValues.size (), index);
else if (index == (int)StrValues.size ()) StrValues.push_back(val);
else StrValues[index] = val;
Root = false;
}
void CConfigFile::CVar::forceAsInt (int val)
{
Type= T_INT;
IntValues.resize(1);
RealValues.clear();
StrValues.clear();
IntValues[0]= val;
Root = false;
}
void CConfigFile::CVar::forceAsDouble (double val)
{
Type= T_REAL;
IntValues.clear();
RealValues.resize(1);
StrValues.clear();
RealValues[0]= val;
Root = false;
}
void CConfigFile::CVar::forceAsString (const std::string &val)
{
Type= T_STRING;
IntValues.clear();
RealValues.clear();
StrValues.resize(1);
StrValues[0]= val;
Root = false;
}
void CConfigFile::CVar::setAsInt (const std::vector &vals)
{
if (Type != T_INT) throw EBadType (Name, Type, T_INT);
else IntValues = vals;
Root = false;
}
void CConfigFile::CVar::setAsDouble (const std::vector &vals)
{
if (Type != T_REAL) throw EBadType (Name, Type, T_REAL);
else RealValues = vals;
Root = false;
}
void CConfigFile::CVar::setAsFloat (const std::vector &vals)
{
if (Type != T_REAL) throw EBadType (Name, Type, T_REAL);
else
{
RealValues.clear ();
RealValues.resize (vals.size ());
for (uint i = 0; i < vals.size (); i++)
RealValues[i] = (double)vals[i];
}
Root = false;
}
void CConfigFile::CVar::setAsString (const std::vector &vals)
{
if (Type != T_STRING) throw EBadType (Name, Type, T_STRING);
else StrValues = vals;
Root = false;
}
bool CConfigFile::CVar::operator== (const CVar& var) const
{
if (Type == var.Type)
{
switch (Type)
{
case T_INT: return IntValues == var.IntValues; break;
case T_REAL: return RealValues == var.RealValues; break;
case T_STRING: return StrValues == var.StrValues; break;
default: break;
}
}
return false;
}
bool CConfigFile::CVar::operator!= (const CVar& var) const
{
return !(*this==var);
}
void CConfigFile::CVar::add (const CVar &var)
{
if (Type == var.Type)
{
switch (Type)
{
case T_INT: IntValues.insert (IntValues.end(), var.IntValues.begin(), var.IntValues.end()); break;
case T_REAL: RealValues.insert (RealValues.end(), var.RealValues.begin(), var.RealValues.end()); break;
case T_STRING: StrValues.insert (StrValues.end(), var.StrValues.begin(), var.StrValues.end()); break;
default: break;
}
}
}
uint CConfigFile::CVar::size () const
{
switch (Type)
{
case T_INT: return (uint)IntValues.size ();
case T_REAL: return (uint)RealValues.size ();
case T_STRING: return (uint)StrValues.size ();
default: return 0;
}
}
CConfigFile::~CConfigFile ()
{
if (_ConfigFiles == NULL || (*_ConfigFiles).empty ()) return;
vector::iterator it = find ((*_ConfigFiles).begin (), (*_ConfigFiles).end (), this);
if (it != (*_ConfigFiles).end ())
{
(*_ConfigFiles).erase (it);
}
if ((*_ConfigFiles).empty())
{
delete _ConfigFiles;
_ConfigFiles = NULL;
}
}
void CConfigFile::load (const string &fileName, bool lookupPaths )
{
if(fileName.empty())
{
nlwarning ("CF: Can't load a empty file name configfile");
return;
}
FileNames.clear ();
FileNames.push_back (fileName);
if (_ConfigFiles == NULL)
{
_ConfigFiles = new std::vector;
}
(*CConfigFile::_ConfigFiles).push_back (this);
reparse (lookupPaths);
/* _FileName.clear ();
_FileName.push_back (fileName);
if (_ConfigFiles == NULL)
{
_ConfigFiles = new std::vector;
}
(*CConfigFile::_ConfigFiles).push_back (this);
reparse ();
// If we find a linked config file, load it but don't overload already existing variable
CVar *var = getVarPtr ("RootConfigFilename");
if (var)
{
string RootConfigFilename = var->asString();
nlinfo ("RootConfigFilename variable found in the '%s' config file, parse it (%s)", fileName.c_str(), RootConfigFilename.c_str());
string path = CFile::getPath(fileName);
if (!path.empty())
path += "/";
path += RootConfigFilename;
reparse (path.c_str());
}
*/
// print ();
}
bool CConfigFile::loaded()
{
return !CConfigFile::FileNames.empty();
}
uint32 CConfigFile::getVarCount()
{
return (uint32)_Vars.size();
}
void CConfigFile::reparse (bool lookupPaths)
{
if (FileNames.empty())
{
nlwarning ("CF: Can't reparse config file because file name is empty");
return;
}
string fn = FileNames[0];
FileNames.clear ();
LastModified.clear ();
// clearVars ();
while (!fn.empty())
{
if (lookupPaths)
{
fn = CPath::lookup(fn, true);
}
else
{
fn = NLMISC::CPath::getFullPath(fn, false);
}
nldebug ("CF: Adding config file '%s' in the config file", fn.c_str());
FileNames.push_back (fn);
LastModified.push_back (CFile::getFileModificationDate(fn));
if (!CPath::lookup(fn, false).empty())
{
ucstring content;
CI18N::readTextFile(fn, content, true, true, true);
string utf8 = content.toUtf8();
CMemStream stream;
stream.serialBuffer((uint8*)(utf8.data()), (uint)utf8.size());
cf_ifile = stream;
if (!cf_ifile.isReading())
{
cf_ifile.invert();
}
cfrestart (NULL);
cf_CurrentLine = 0;
cf_CurrentFile = NULL;
cf_Ignore = false;
cf_OverwriteExistingVariable = (FileNames.size()==1);
LoadRoot = (FileNames.size()>1);
bool parsingOK = (cfparse (&(_Vars)) == 0);
// cf_ifile.close();
if (!parsingOK)
{
// write the result of preprocessing in a temp file
string debugFileName;
debugFileName += "debug_";
debugFileName += CFile::getFilename(fn);
CI18N::writeTextFile(debugFileName, content, true);
nlwarning ("CF: Parsing error in file %s line %d, look in '%s' for a preprocessed version of the config file",
cf_CurrentFile,
cf_CurrentLine,
debugFileName.c_str());
throw EParseError (fn, cf_CurrentLine);
}
if (cf_CurrentFile != NULL)
free(cf_CurrentFile);
// reset all 'FromLocalFile' flag on created vars before reading next root cfg
for (uint i=0; i<_Vars.size(); ++i)
{
_Vars[i].FromLocalFile = false;
}
}
else
{
nlwarning ("CF: Config file '%s' not found in the path '%s'", fn.c_str(), CPath::getCurrentPath().c_str());
throw EFileNotFound (fn);
}
// cf_ifile.close ();
cf_ifile.clear();
// If we find a linked config file, load it but don't overload already existing variable
CVar *var = getVarPtr ("RootConfigFilename");
if (var)
{
string RootConfigFilename = var->asString();
if (!NLMISC::CFile::fileExists(RootConfigFilename))
{
// file is not found, try with the path of the master cfg
string path = NLMISC::CPath::standardizePath (NLMISC::CFile::getPath(FileNames[0]));
RootConfigFilename = path + RootConfigFilename;
}
RootConfigFilename = NLMISC::CPath::getFullPath(RootConfigFilename, false);
if (RootConfigFilename != fn)
{
nlinfo ("CF: RootConfigFilename variable found in the '%s' config file, parse the root config file '%s'", fn.c_str(), RootConfigFilename.c_str());
fn = RootConfigFilename;
}
else
fn.clear ();
}
else
fn.clear ();
}
if (_Callback != NULL)
_Callback();
/* if (filename == NULL)
{
_LastModified = getLastModified ();
nlassert (!_FileName.empty());
if (cf_ifile.open (_FileName[0]))
{
// if we clear all the array, we'll lost the callback on variable and all information
// _Vars.clear();
cfrestart (NULL);
cf_CurrentLine = 1;
cf_Ignore = false;
cf_OverwriteExistingVariable = true;
LoadRoot = false;
bool parsingOK = (cfparse (&(_Vars)) == 0);
cf_ifile.close();
if (!parsingOK)
{
nlwarning ("Parsing error in file %s line %d", _FileName.c_str(), cf_CurrentLine);
throw EParseError (_FileName, cf_CurrentLine);
}
}
else
{
nlwarning ("ConfigFile '%s' not found in the path '%s'", _FileName.c_str(), CPath::getCurrentPath().c_str());
throw EFileNotFound (_FileName);
}
}
else
{
nlassert (strlen(filename)>0);
// load external config filename, don't overwrite existing variable
if (cf_ifile.open (filename))
{
cfrestart (NULL);
cf_CurrentLine = 1;
cf_Ignore = false;
cf_OverwriteExistingVariable = false;
LoadRoot = true;
bool parsingOK = (cfparse (&(_Vars)) == 0);
cf_ifile.close ();
if (!parsingOK)
{
nlwarning ("Parsing error in file %s line %d", filename, cf_CurrentLine);
throw EParseError (filename, cf_CurrentLine);
}
}
else
{
nlwarning ("RootConfigFilename '%s' not found", _FileName.c_str());
}
}
if (callingCallback)
{
if (_Callback != NULL)
_Callback();
}
*/
}
CConfigFile::CVar &CConfigFile::getVar (const std::string &varName)
{
CVar *var = getVarPtr (varName);
if (var == 0)
throw EUnknownVar (getFilename(), varName);
else
return *var;
}
CConfigFile::CVar *CConfigFile::getVarPtr (const std::string &varName)
{
uint i;
for (i = 0; i < _Vars.size(); i++)
{
// the type could be T_UNKNOWN if we add a callback on this name but this var is not in the config file
if (_Vars[i].Name == varName && (_Vars[i].Type != CVar::T_UNKNOWN || _Vars[i].Comp))
return &(_Vars[i]);
}
// if not found, add it in the array if necessary
for (i = 0; i < UnknownVariables.size(); i++)
if(UnknownVariables[i] == varName)
break;
if (i == UnknownVariables.size())
UnknownVariables.push_back(varName);
return NULL;
}
bool CConfigFile::exists (const std::string &varName)
{
for (uint i = 0; i < _Vars.size(); i++)
{
// the type could be T_UNKNOWN if we add a callback on this name but this var is not in the config file
if (_Vars[i].Name == varName && (_Vars[i].Type != CVar::T_UNKNOWN || _Vars[i].Comp))
{
return true;
}
}
return false;
}
void CConfigFile::save () const
{
// Avoid any problem, Force Locale to default
setlocale(LC_ALL, "C");
FILE *fp = fopen (getFilename().c_str (), "w");
if (fp == NULL)
{
nlwarning ("CF: Couldn't create %s file", getFilename().c_str ());
return;
}
// write the UTF-8 bom in order to be able to re-read a config file with
// unicode content.
/* ace: we need to test this before commit it
static char utf8Header[] = {char(0xef), char(0xbb), char(0xbf), 0};
fprintf(fp, utf8Header);
*/
for(int i = 0; i < (int)_Vars.size(); i++)
{
// Not a root value
if (!_Vars[i].Root)
{
if (_Vars[i].Comp)
{
fprintf(fp, "%-20s = {", _Vars[i].Name.c_str());
switch (_Vars[i].Type)
{
case CConfigFile::CVar::T_INT:
{
for (int it=0; it < (int)_Vars[i].IntValues.size(); it++)
{
if (it%_Vars[i].SaveWrap == 0)
{
fprintf(fp, "\n\t");
}
fprintf(fp, "%d%s", _Vars[i].IntValues[it], it<(int)_Vars[i].IntValues.size()-1?", ":" ");
}
break;
}
case CConfigFile::CVar::T_STRING:
{
for (int st=0; st < (int)_Vars[i].StrValues.size(); st++)
{
if (st%_Vars[i].SaveWrap == 0)
{
fprintf(fp, "\n\t");
}
fprintf(fp, "\"%s\"%s", _Vars[i].StrValues[st].c_str(), st<(int)_Vars[i].StrValues.size()-1?", ":" ");
}
break;
}
case CConfigFile::CVar::T_REAL:
{
for (int rt=0; rt < (int)_Vars[i].RealValues.size(); rt++)
{
if (rt%_Vars[i].SaveWrap == 0)
{
fprintf(fp, "\n\t");
}
fprintf(fp, "%.10f%s", _Vars[i].RealValues[rt], rt<(int)_Vars[i].RealValues.size()-1?", ":" ");
}
break;
}
default: break;
}
fprintf(fp, "\n};\n");
}
else
{
switch (_Vars[i].Type)
{
case CConfigFile::CVar::T_INT:
fprintf(fp, "%-20s = %d;\n", _Vars[i].Name.c_str(), _Vars[i].IntValues[0]);
break;
case CConfigFile::CVar::T_STRING:
fprintf(fp, "%-20s = \"%s\";\n", _Vars[i].Name.c_str(), _Vars[i].StrValues[0].c_str());
break;
case CConfigFile::CVar::T_REAL:
fprintf(fp, "%-20s = %.10f;\n", _Vars[i].Name.c_str(), _Vars[i].RealValues[0]);
break;
default: break;
}
}
}
}
fclose (fp);
}
void CConfigFile::display () const
{
display (InfoLog);
}
void CConfigFile::display (CLog *log) const
{
createDebug ();
log->displayRawNL ("Config file %s have %d variables and %d root config file:", getFilename().c_str(), _Vars.size(), FileNames.size()-1);
log->displayRaw ("Root config files: ");
for(int i = 1; i < (int)FileNames.size(); i++)
{
log->displayRaw (FileNames[i].c_str());
}
log->displayRawNL ("");
log->displayRawNL ("------------------------------------------------------");
for(int i = 0; i < (int)_Vars.size(); i++)
{
log->displayRaw ((_Vars[i].Callback==NULL)?" ":"CB ");
log->displayRaw ((_Vars[i].Root)?"Root ":" ");
if (_Vars[i].Comp)
{
switch (_Vars[i].Type)
{
case CConfigFile::CVar::T_INT:
{
log->displayRaw ("%-20s { ", _Vars[i].Name.c_str());
for (int it=0; it < (int)_Vars[i].IntValues.size(); it++)
{
log->displayRaw ("'%d' ", _Vars[i].IntValues[it]);
}
log->displayRawNL ("}");
break;
}
case CConfigFile::CVar::T_STRING:
{
log->displayRaw ("%-20s { ", _Vars[i].Name.c_str());
for (int st=0; st < (int)_Vars[i].StrValues.size(); st++)
{
log->displayRaw ("\"%s\" ", _Vars[i].StrValues[st].c_str());
}
log->displayRawNL ("}");
break;
}
case CConfigFile::CVar::T_REAL:
{
log->displayRaw ("%-20s { " , _Vars[i].Name.c_str());
for (int rt=0; rt < (int)_Vars[i].RealValues.size(); rt++)
{
log->displayRaw ("`%f` ", _Vars[i].RealValues[rt]);
}
log->displayRawNL ("}");
break;
}
case CConfigFile::CVar::T_UNKNOWN:
{
log->displayRawNL ("%-20s { }" , _Vars[i].Name.c_str());
break;
}
default:
{
log->displayRawNL ("%-20s (%d)" , _Vars[i].Name.c_str(), _Vars[i].Type);
break;
}
}
}
else
{
switch (_Vars[i].Type)
{
case CConfigFile::CVar::T_INT:
log->displayRawNL ("%-20s '%d'", _Vars[i].Name.c_str(), _Vars[i].IntValues[0]);
break;
case CConfigFile::CVar::T_STRING:
log->displayRawNL ("%-20s \"%s\"", _Vars[i].Name.c_str(), _Vars[i].StrValues[0].c_str());
break;
case CConfigFile::CVar::T_REAL:
log->displayRawNL ("%-20s `%f`", _Vars[i].Name.c_str(), _Vars[i].RealValues[0]);
break;
case CConfigFile::CVar::T_UNKNOWN:
log->displayRawNL ("%-20s ", _Vars[i].Name.c_str());
break;
default:
{
log->displayRawNL ("%-20s (%d)" , _Vars[i].Name.c_str(), _Vars[i].Type);
break;
}
}
}
}
}
void CConfigFile::setCallback (void (*cb)())
{
_Callback = cb;
if( !FileNames.empty() )
nlinfo ("CF: Setting callback to reload the file '%s' when modified externally", getFilename().c_str());
}
void CConfigFile::setCallback (const string &VarName, void (*cb)(CConfigFile::CVar &var))
{
for (vector::iterator it = _Vars.begin (); it != _Vars.end (); it++)
{
if (VarName == (*it).Name)
{
(*it).Callback = cb;
//nldebug("CF: Setting callback to reload the variable '%s' in the file '%s' when modified externally", VarName.c_str(), getFilename().c_str());
return;
}
}
// VarName doesn't exist, add it now for the future
CVar Var;
Var.Name = VarName;
Var.Callback = cb;
Var.Type = CVar::T_UNKNOWN;
Var.Comp = false;
_Vars.push_back (Var);
//nldebug("CF: Setting callback to reload the variable '%s' in the file '%s' when modified externally (currently unknown)", VarName.c_str(), getFilename().c_str());
}
// ***************************************************************************
vector *CConfigFile::_ConfigFiles = NULL;
uint32 CConfigFile::_Timeout = 1000;
void CConfigFile::checkConfigFiles ()
{
if (_ConfigFiles == NULL) return;
static time_t LastCheckTime = time (NULL);
if (_Timeout > 0 && (float)(time (NULL) - LastCheckTime)*1000.0f < (float)_Timeout) return;
LastCheckTime = time (NULL);
bool needReparse;
for (vector::iterator it = (*_ConfigFiles).begin (); it != (*_ConfigFiles).end (); it++)
{
needReparse = false;
nlassert ((*it)->FileNames.size() == (*it)->LastModified.size());
for (uint i = 0; i < (*it)->FileNames.size(); i++)
{
if ((*it)->LastModified[i] != CFile::getFileModificationDate((*it)->FileNames[i]))
{
needReparse = true;
(*it)->LastModified[i] = CFile::getFileModificationDate((*it)->FileNames[i]);
}
}
if (needReparse)
{
try
{
(*it)->reparse ();
}
catch (const EConfigFile &e)
{
nlwarning ("CF: Exception will re-read modified config file '%s': %s", (*it)->getFilename().c_str(), e.what ());
}
}
}
}
void CConfigFile::setTimeout (uint32 timeout)
{
_Timeout = timeout;
}
void CConfigFile::clear()
{
_Vars.clear ();
}
void CConfigFile::clearVars ()
{
for (vector::iterator it = _Vars.begin (); it != _Vars.end (); it++)
{
(*it).Type = CVar::T_UNKNOWN;
}
}
uint CConfigFile::getNumVar () const
{
return (uint)_Vars.size ();
}
CConfigFile::CVar *CConfigFile::getVar (uint varId)
{
return &(_Vars[varId]);
}
CConfigFile::CVar *CConfigFile::insertVar (const std::string &varName, const CVar &varToCopy)
{
// Get the var
CVar *var = getVarPtr (varName);
if (!var)
{
_Vars.push_back (varToCopy);
var = &(_Vars.back ());
var->Root = false;
var->Name = varName;
}
return var;
}
} // NLMISC