/*
PIP - Platform Independent Primitives
Config parser
Ivan Pelipenko peri4ko@yandex.ru
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see .
*/
#include "piconfig.h"
#include "pifile.h"
#include "piiostring.h"
#ifdef PIP_STD_IOSTREAM
# include "pistring_std.h"
# include
#endif
/*! \class PIConfig
* \brief Configuration file
* \details This class provide handle access to configuration file.
*
* \section PIConfig_sec0 Synopsis
* PIConfig reads configuration file and create internal dendritic
* representation of all entries of this file. You can easily read
* some values and write new.
* \image html piconfig.png
*
* %PIConfig supports also INI-style files with sections "[section]".
* In this case line with section name interpret as prefix to the next
* lines. For example, these configs are equal:
* \code
* ser.device = /dev/ttyS0
* ser.speed = 115200
* debug = true
* \endcode
* \code
* [ser]
* device = /dev/ttyS0
* speed = 115200
* []
* debug = true
* \endcode
*
* You can use multiline values ends with " \"
* \code
* value = start \ #s comment
* _mid \
* _end
* \endcode
* In this example value = "start_mid_end"
*
* \section PIConfig_sec1 Concepts
* Each node of internal tree has type PIConfig::Entry. %PIConfig
* has one root element \a rootEntry(). Any entry of configuration file is a
* child of this element.
*
*/
/*! \class PIConfig::Entry
* \brief %Entry of configuration file
* \details This class is node of internal PIConfig tree.
* %Entry provide access to elements of PIConfig. Each entry has
* children or next properties:
* * name
* * value
* * type
* * comment
*
* Each property is a PIString. These properties forms from text line with
* format: \code{.cpp} = # \endcode
* Type and comment are optional fields. Type is a single letter immediately
* after comment symbol "#". \n \n
* %Entry has many implicit convertions to common types: boolean, integers,
* float, double, PIString, PIStringList. \n \n
* Generally there is no need to create instance of %PIConfig::Entry manually,
* it returns by functions \a getValue() of \a PIConfig, \a PIConfig::Entry or
* \a PIConfig::Branch. If there is no suitable entry to return, reference to
* internal instance of %PIConfig::Entry with "default" value will be returned.
* \snippet piconfig.cpp PIConfig::Entry
*
*/
/*! \class PIConfig::Branch
* \brief %Branch is a list of entries of configuration file
* \details %Branch provides some features to get entries lists.
* \snippet piconfig.cpp PIConfig::Branch
*
*/
PIConfig::Entry PIConfig::Branch::_empty;
PIConfig::Entry PIConfig::Entry::_empty;
PIConfig::Branch PIConfig::Branch::allLeaves() {
Branch b;
b.delim = delim;
piForeach(Entry * i, *this) {
if (i->isLeaf())
b << i;
else
allLeaves(b, i);
}
return b;
}
PIConfig::Entry & PIConfig::Branch::getValue(const PIString & vname, const PIString & def, bool * exist) {
if (vname.isEmpty()) {
_empty.clear();
_empty.delim = delim;
if (exist != 0) *exist = false;
return _empty;
}
PIStringList tree = vname.split(delim);
PIString name = tree.front();
tree.pop_front();
Entry * ce = 0;
piForeach(Entry * i, *this)
if (i->_name == name) {
ce = i;
break;
}
if (ce == 0) {
_empty._name = vname;
_empty._value = def;
_empty.delim = delim;
if (exist != 0) *exist = false;
return _empty;
}
piForeach(PIString & i, tree) {
ce = ce->findChild(i);
if (ce == 0) {
_empty._name = vname;
_empty._value = def;
_empty.delim = delim;
if (exist != 0) *exist = false;
return _empty;
}
}
if (exist != 0) *exist = true;
return *ce;
}
PIConfig::Branch PIConfig::Branch::getValues(const PIString & name) {
Branch b;
b.delim = delim;
piForeach(Entry * i, *this) {
if (i->isLeaf()) {
if (i->_name.find(name) >= 0) b << i;
} else {
piForeach(Entry * j, i->_children)
if (j->_name.find(name) >= 0) b << j;
}
}
return b;
}
PIConfig::Branch PIConfig::Branch::getLeaves() {
Branch b;
b.delim = delim;
piForeach(Entry * i, *this)
if (i->isLeaf()) b << i;
return b;
}
PIConfig::Branch PIConfig::Branch::getBranches() {
Branch b;
b.delim = delim;
piForeach(Entry * i, *this)
if (!i->isLeaf()) b << i;
return b;
}
PIConfig::Branch & PIConfig::Branch::filter(const PIString & f) {
for (int i = 0; i < size_s(); ++i) {
if (at(i)->_name.find(f) < 0) {
remove(i);
--i;
}
}
return *this;
}
bool PIConfig::Branch::entryExists(const Entry * e, const PIString & name) const {
if (e->_children.isEmpty()) {
return (e->_name == name);
}
piForeachC(Entry * i, e->_children)
if (entryExists(i, name)) return true;
return false;
}
PIConfig::Entry & PIConfig::Entry::getValue(const PIString & vname, const PIString & def, bool * exist) {
PIStringList tree = vname.split(delim);
Entry * ce = this;
piForeach(PIString & i, tree) {
ce = ce->findChild(i);
if (ce == 0) {
_empty._name = vname;
_empty._value = def;
_empty.delim = delim;
if (exist != 0) *exist = false;
return _empty;
}
}
if (exist != 0) *exist = true;
return *ce;
}
PIConfig::Branch PIConfig::Entry::getValues(const PIString & vname) {
Branch b;
b.delim = delim;
piForeach(Entry * i, _children)
if (i->_name.find(vname) >= 0) b << i;
return b;
}
bool PIConfig::Entry::entryExists(const Entry * e, const PIString & name) const {
if (e->_children.isEmpty()) {
return (e->_name == name);
}
piForeachC(Entry * i, e->_children)
if (entryExists(i, name)) return true;
return false;
}
#ifdef PIP_STD_IOSTREAM
void PIConfig::Entry::coutt(std::ostream & s, const PIString & p) const {
PIString nl = p + " ";
if (!_value.isEmpty())
s << p << _name << " = " << _value << std::endl;
else
std::cout << p << _name << std::endl;
piForeachC(Entry * i, _children)
i->coutt(s, nl);
}
#endif
void PIConfig::Entry::piCoutt(PICout s, const PIString & p) const {
PIString nl = p + " ";
if (!_value.isEmpty())
s << p << _name << " = " << _value << " (" << _type << " " << _comment << ")" << PICoutManipulators::NewLine;
else
s << p << _name << PICoutManipulators::NewLine;
piForeachC(Entry * i, _children)
i->piCoutt(s, nl);
}
PIConfig::PIConfig(const PIString & path, PIIODevice::DeviceMode mode) {
_init();
open(path, mode);
}
PIConfig::PIConfig(PIString * string, PIIODevice::DeviceMode mode) {
_init();
open(string, mode);
}
PIConfig::PIConfig(PIIODevice * device, PIIODevice::DeviceMode mode) {
_init();
open(device, mode);
}
PIConfig::PIConfig(const PIString & path, PIStringList dirs) {
_init();
internal = true;
own_dev = true;
dev = new PIFile(path, PIIODevice::ReadOnly);
incdirs = dirs;
incdirs << PIFile::fileInfo(path).dir();
while (!dev->isOpened()) {
if (dirs.isEmpty()) break;
PIString cp = dirs.back();
if (cp.endsWith("/") || cp.endsWith("\\")) cp.pop_back();
cp += "/" + path;
dev->open(cp, PIIODevice::ReadOnly);
dirs.pop_back();
}
if (!dev->isOpened()) {
delete dev;
dev = 0;
return;
}
_setupDev();
parse();
}
PIConfig::~PIConfig() {
root.deleteBranch();
_destroy();
}
bool PIConfig::open(const PIString & path, PIIODevice::DeviceMode mode) {
_destroy();
incdirs << PIFile::fileInfo(path).dir();
own_dev = true;
dev = new PIFile(path, mode);
if (!dev->isOpened()) dev->open(path, mode);
_setupDev();
parse();
return dev->isOpened();
}
bool PIConfig::open(PIString * string, PIIODevice::DeviceMode mode) {
_destroy();
own_dev = true;
dev = new PIIOString(string, mode);
_setupDev();
parse();
return true;
}
bool PIConfig::open(PIIODevice * device, PIIODevice::DeviceMode mode) {
_destroy();
own_dev = false;
dev = device;
if (dev) {
dev->open(mode);
if (dev->isTypeOf()) incdirs << PIFile::fileInfo(((PIFile *)dev)->path()).dir();
}
_setupDev();
parse();
if (device) return device->isOpened();
return false;
}
void PIConfig::_init() {
delim = PIStringAscii(".");
root.delim = delim;
empty.delim = delim;
empty._parent = 0;
}
void PIConfig::_destroy() {
if (stream) {
delete stream;
stream = nullptr;
}
if (own_dev && dev) delete dev;
dev = nullptr;
piForeach(PIConfig * c, inc_devs)
delete c;
inc_devs.clear();
}
void PIConfig::_setupDev() {
if (!dev) return;
stream = new PIIOTextStream(dev);
stream->setEncoding(PIIOTextStream::UTF8);
}
void PIConfig::_clearDev() {
if (!dev) return;
if (PIString(dev->className()) == "PIFile") {
((PIFile *)dev)->clear();
return;
}
if (PIString(dev->className()) == "PIIOString") {
((PIIOString *)dev)->clear();
return;
}
}
void PIConfig::_flushDev() {
if (!dev) return;
if (PIString(dev->className()) == "PIFile") {
((PIFile *)dev)->flush();
}
}
bool PIConfig::_isEndDev() {
if (!stream) return true;
return stream->isEnd();
}
void PIConfig::_seekToBeginDev() {
if (!dev) return;
if (PIString(dev->className()) == "PIFile") {
((PIFile *)dev)->seekToBegin();
return;
}
if (PIString(dev->className()) == "PIIOString") {
((PIIOString *)dev)->seekToBegin();
return;
}
}
PIString PIConfig::_readLineDev() {
if (!stream) return PIString();
return stream->readLine();
}
void PIConfig::_writeDev(const PIString & l) {
// piCout << "write \"" << l << "\"";
if (!stream) return;
stream->append(l);
}
bool PIConfig::isOpened() const {
if (dev) return dev->isOpened();
return false;
}
PIConfig::Entry & PIConfig::getValue(const PIString & vname, const PIString & def, bool * exist) {
PIStringList tree = vname.split(delim);
Entry * ce = &root;
piForeach(PIString & i, tree) {
ce = ce->findChild(i);
if (ce == 0) {
if (exist != 0) *exist = false;
empty._name = vname;
empty._value = def;
empty.delim = delim;
return empty;
}
}
if (exist != 0) *exist = true;
return *ce;
}
PIConfig::Branch PIConfig::getValues(const PIString & vname) {
Branch b;
b.delim = delim;
piForeach(Entry * i, root._children)
if (i->_name.find(vname) >= 0) b << i;
return b;
};
void PIConfig::addEntry(const PIString & name, const PIString & value, const PIString & type, bool write) {
if (getValue(name)._parent != 0) return;
bool toRoot = false;
PIStringList tree = name.split(delim);
PIString ename = tree.back();
tree.pop_back();
Entry *te, *ce, *entry = &root;
if (tree.isEmpty()) toRoot = true;
piForeach(PIString & i, tree) {
te = entry->findChild(i);
if (te == 0) {
ce = new Entry();
ce->delim = delim;
ce->_tab = entry->_tab;
ce->_line = entry->_line;
ce->_name = i;
ce->_parent = entry;
entry->_children << ce;
entry = ce;
} else
entry = te;
}
PIConfig::Branch ch = entry->_children;
ch.sort(PIConfig::Entry::compare);
te = (entry->isLeaf() ? 0 : ch.back());
ce = new Entry();
ce->delim = delim;
ce->_name = ename;
ce->_value = value;
ce->_type = type;
if (te == 0) {
ce->_tab = entry->_tab;
if (toRoot)
ce->_line = other.size_s() - 1;
else
ce->_line = entry->_line;
} else {
ce->_tab = te->_tab;
if (toRoot)
ce->_line = other.size_s() - 1;
else {
ch = entry->_parent->_children;
ch.sort(PIConfig::Entry::compare);
ce->_line = ch.back()->_line + 1;
}
}
ce->_parent = entry;
entry->_children << ce;
other.insert(ce->_line, "");
Branch b = allLeaves();
bool found = false;
for (int i = 0; i < b.size_s(); ++i) {
if (found) {
b[i]->_line++;
continue;
}
if (b[i] == ce) {
found = true;
if (i > 0)
if (b[i - 1]->_line == b[i]->_line) b[i - 1]->_line++;
}
}
if (write) writeAll();
}
void PIConfig::setValue(const PIString & name, const PIString & value, const PIString & type, bool write) {
Entry & e(getValue(name));
if (&e == &empty) {
addEntry(name, value, type, write);
return;
}
e._value = value;
e._type = type;
if (write) writeAll();
}
int PIConfig::entryIndex(const PIString & name) {
PIStringList tree = name.split(delim);
Entry * ce = &root;
piForeach(PIString & i, tree) {
ce = ce->findChild(i);
if (ce == 0) return -1;
}
return allLeaves().indexOf(ce);
}
void PIConfig::setValue(uint number, const PIString & value, bool write) {
Entry & e(entryByIndex(number));
if (&e == &empty) return;
e._value = value;
if (write) writeAll();
}
void PIConfig::setName(uint number, const PIString & name, bool write) {
Entry & e(entryByIndex(number));
if (&e == &empty) return;
e._name = name;
if (write) writeAll();
}
void PIConfig::setType(uint number, const PIString & type, bool write) {
Entry & e(entryByIndex(number));
if (&e == &empty) return;
e._type = type;
if (write) writeAll();
}
void PIConfig::setComment(uint number, const PIString & comment, bool write) {
Entry & e(entryByIndex(number));
if (&e == &empty) return;
e._comment = comment;
if (write) writeAll();
}
void PIConfig::removeEntry(const PIString & name, bool write) {
Entry & e(getValue(name));
if (&e == &empty) return;
Branch b = allLeaves();
removeEntry(b, &e);
if (write) writeAll();
}
void PIConfig::removeEntry(uint number, bool write) {
Entry & e(entryByIndex(number));
if (&e == &empty) return;
Branch b = allLeaves();
removeEntry(b, &e);
if (write) writeAll();
}
void PIConfig::removeEntry(Branch & b, PIConfig::Entry * e) {
bool leaf = true;
if (e->isLeaf()) other.remove(e->_line);
if (!e->isLeaf() && !e->_value.isEmpty()) {
e->_value.clear();
leaf = false;
} else {
int cc = e->_children.size_s();
piForTimes(cc) removeEntry(b, e->_children.back());
}
bool found = false;
for (int i = 0; i < b.size_s(); ++i) {
if (found) {
b[i]->_line--;
continue;
}
if (b[i] == e) found = true;
}
if (!leaf) return;
e->_parent->_children.removeOne(e);
b.removeOne(e);
delete e;
}
PIString PIConfig::getPrefixFromLine(PIString line, bool * exists) {
line.trim();
if (line.left(1) == "#") {
if (exists) *exists = false;
return PIString();
}
int ci = line.find("#");
if (ci >= 0) line.cutRight(line.size() - ci);
if (line.find("=") >= 0) {
if (exists) *exists = false;
return PIString();
}
if (line.find("[") >= 0 && line.find("]") >= 0) {
if (exists) *exists = true;
return line.takeRange('[', ']').trim();
}
if (exists) *exists = false;
return PIString();
}
void PIConfig::writeAll() {
// cout << this << " write < " << size() << endl;
_clearDev();
buildFullNames(&root);
Branch b = allLeaves();
PIString prefix, tprefix;
bool isPrefix;
// for (int i = 0; i < b.size_s(); ++i)
// cout << b[i]->_name << " = " << b[i]->_value << endl;
int j = 0;
for (int i = 0; i < other.size_s(); ++i) {
// cout << j << endl;
if (j >= 0 && j < b.size_s()) {
if (b[j]->_line == i) {
b[j]->buildLine();
_writeDev((b[j]->_all).cutLeft(prefix.size()) + "\n");
// cout << this << " " << b[j]->_all << endl;
++j;
} else {
_writeDev(other[i]);
tprefix = getPrefixFromLine(other[i], &isPrefix);
if (isPrefix) {
prefix = tprefix;
if (!prefix.isEmpty()) prefix += delim;
}
if (i < other.size_s() - 1) _writeDev('\n');
// cout << this << " " << other[i] << endl;
}
} else {
_writeDev(other[i]);
tprefix = getPrefixFromLine(other[i], &isPrefix);
if (isPrefix) {
prefix = tprefix;
if (!prefix.isEmpty()) prefix += delim;
}
if (i < other.size_s() - 1) _writeDev('\n');
// cout << this << " " << other[i] << endl;
}
}
_flushDev();
readAll();
// cout << this << " write > " << size() << endl;
}
void PIConfig::clear() {
_clearDev();
parse();
}
void PIConfig::readAll() {
root.deleteBranch();
root.clear();
parse();
}
bool PIConfig::entryExists(const Entry * e, const PIString & name) const {
if (e->_children.isEmpty()) {
return (e->_name == name);
}
piForeachC(Entry * i, e->_children)
if (entryExists(i, name)) return true;
return false;
}
void PIConfig::updateIncludes() {
if (internal) return;
all_includes.clear();
piForeach(PIConfig * c, includes)
all_includes << c->allLeaves();
}
PIString PIConfig::parseLine(PIString v) {
int i = -1, l = 0;
while (1) {
i = v.find("${");
if (i < 0) break;
PIString w = v.mid(i + 1).takeRange('{', '}'), r;
l = w.length() + 3;
w = parseLine(w);
w.trim();
bool ex = false;
PIConfig::Entry & me = getValue(w, "", &ex);
if (ex) {
r = me._value;
} else {
piForeachC(PIConfig::Entry * e, all_includes)
if (e->_full_name == w) {
r = e->_value;
break;
}
}
v.replace(i, l, r);
}
return v;
}
void PIConfig::parse() {
// piCout << "[PIConfig] charset" << PIFile::defaultCharset();
PIString src, str, tab, comm, all, name, type, prefix, tprefix;
PIStringList tree;
Entry *entry = 0, *te = 0, *ce = 0;
int ind, sind;
bool isNew = false, isPrefix = false, wasMultiline = false, isMultiline = false;
piForeach(PIConfig * c, inc_devs)
delete c;
inc_devs.clear();
includes.clear();
if (!isOpened()) return;
_seekToBeginDev();
other.clear();
lines = 0;
while (!_isEndDev()) {
other.push_back(PIString());
src = str = parseLine(_readLineDev());
tprefix = getPrefixFromLine(src, &isPrefix);
if (isPrefix) {
prefix = tprefix;
if (!prefix.isEmpty()) prefix += delim;
}
// piCout << "line \"" << str << "\"";
tab = str.left(str.find(str.trimmed().left(1)));
str.trim();
all = str;
sind = str.find('#');
if (sind > 0) {
comm = str.mid(sind + 1).trimmed();
if (!comm.isEmpty()) {
type = comm[0];
comm.cutLeft(1).trim();
} else
type = "s";
str = str.left(sind).trim();
} else {
type = "s";
comm = "";
}
if (str.endsWith(" \\")) {
isMultiline = true;
str.cutRight(2).trim();
} else
isMultiline = false;
if (wasMultiline) {
wasMultiline = false;
if (ce) {
ce->_value += str;
ce->_all += " \\\n" + all;
}
str.clear();
} else
ce = 0;
wasMultiline = isMultiline;
// piCout << "[PIConfig] str" << str.size() << str << str.toUTF8();
ind = str.find('=');
if ((ind > 0) && (str[0] != '#')) {
tree = (prefix + str.left(ind).trimmed()).split(delim);
if (tree.front() == "include") {
name = str.mid(ind + 1).trimmed();
PIConfig * iconf = new PIConfig(name, incdirs);
// piCout << "include" << name << iconf->dev;
if (!iconf->dev) {
delete iconf;
} else {
inc_devs << iconf;
includes << iconf << iconf->includes;
updateIncludes();
}
other.back() = src;
} else {
name = tree.back();
tree.pop_back();
entry = &root;
piForeachC(PIString & i, tree) {
te = entry->findChild(i);
if (te == 0) {
ce = new Entry();
ce->delim = delim;
ce->_tab = tab;
ce->_line = lines;
ce->_name = i;
ce->_parent = entry;
entry->_children << ce;
entry = ce;
} else
entry = te;
}
isNew = false;
ce = entry->findChild(name);
if (ce == 0) {
ce = new Entry();
isNew = true;
}
ce->delim = delim;
ce->_tab = tab;
ce->_name = name;
ce->_value = str.mid(ind + 1).trimmed();
ce->_type = type;
ce->_comment = comm;
// piCout << "[PIConfig] comm" << comm.size() << comm << comm.toUTF8();
// piCout << "[PIConfig] type" << type.size() << type << type.toUTF8();
ce->_line = lines;
ce->_all = all;
if (isNew) {
ce->_parent = entry;
entry->_children << ce;
}
}
} else
other.back() = src;
lines++;
}
setEntryDelim(&root, delim);
buildFullNames(&root);
}
#ifdef PIP_STD_IOSTREAM
std::ostream & operator<<(std::ostream & s, const PIConfig::Entry & v) {
s << v.value();
return s;
}
std::ostream & operator<<(std::ostream & s, const PIConfig::Branch & v) {
v.coutt(s, "");
return s;
}
#endif