single file format for multiple .ini files (MBC #1208)

This patch adds the infrastructure for reading a single file
which contains multiple separate .ini files. Writing is not
implemented.

The Ini*ConfigNode classes are derived from the File*ConfigNode
classes and could replace those. The only reason for not doing
this right away is that the master branch is in code freeze in
preparation for 1.0. Removing File*ConfigNode and replacing
their usages should be done after the release.

The SingleFileConfigTree implements the splitting of the single
file into independent pieces. It instantiates IniFileConfigNodes
with in-memory access to the actual data, using the new classes
introduced for this case in the previous patches.

SingleFileConfigTree could also use other ConfigNodes, but that kind
of flexibility is not needed yet and also would require rethinking the
way how the single file is split. Right now the separators ("===
.... ===") are known to not occur in the individual pieces.

Like the FileConfigTree, this class must be able to preserve
nodes which were created without flushing the tree afterwards.
This is done when reading from a read-only template, which may
add new nodes or modify existing ones in memory.
This commit is contained in:
Patrick Ohly 2010-05-03 14:54:58 +02:00
parent 663939976a
commit 006bcf2603
5 changed files with 846 additions and 0 deletions

View File

@ -0,0 +1,375 @@
/*
* Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
* Copyright (C) 2009 Intel Corporation
*
* This library 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 2.1 of the License, or (at your option) version 3.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#include <syncevo/IniConfigNode.h>
#include <syncevo/FileDataBlob.h>
#include <syncevo/SyncConfig.h>
#include <syncevo/util.h>
#include <boost/scoped_array.hpp>
#include <boost/foreach.hpp>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
IniBaseConfigNode::IniBaseConfigNode(const boost::shared_ptr<DataBlob> &data) :
m_data(data)
{
}
void IniBaseConfigNode::flush()
{
if (!m_modified) {
return;
}
if (m_data->isReadonly()) {
throw std::runtime_error(m_data->getName() + ": internal error: flushing read-only config node not allowed");
}
boost::shared_ptr<std::ostream> file = m_data->write();
toFile(*file);
m_modified = false;
}
IniFileConfigNode::IniFileConfigNode(const boost::shared_ptr<DataBlob> &data) :
IniBaseConfigNode(data)
{
read();
}
IniFileConfigNode::IniFileConfigNode(const string &path, const string &fileName, bool readonly) :
IniBaseConfigNode(boost::shared_ptr<DataBlob>(new FileDataBlob(path, fileName, readonly)))
{
read();
}
void IniFileConfigNode::toFile(std::ostream &file) {
BOOST_FOREACH(const string &line, m_lines) {
file << line << std::endl;
}
}
void IniFileConfigNode::read()
{
boost::shared_ptr<std::istream> file(m_data->read());
std::string line;
while (getline(*file, line)) {
m_lines.push_back(line);
}
m_modified = false;
}
/**
* get property and value from line, if any present
*/
static bool getContent(const string &line,
string &property,
string &value,
bool &isComment,
bool fuzzyComments)
{
size_t start = 0;
while (start < line.size() &&
isspace(line[start])) {
start++;
}
// empty line?
if (start == line.size()) {
return false;
}
// Comment? Potentially keep reading, might be commented out assignment.
isComment = false;
if (line[start] == '#') {
if (!fuzzyComments) {
return false;
}
isComment = true;
}
// recognize # <word> = <value> as commented out (= default) value
if (isComment) {
start++;
while (start < line.size() &&
isspace(line[start])) {
start++;
}
}
// extract property
size_t end = start;
while (end < line.size() &&
!isspace(line[end])) {
end++;
}
property = line.substr(start, end - start);
// skip assignment
start = end;
while (start < line.size() &&
isspace(line[start])) {
start++;
}
if (start == line.size() ||
line[start] != '=') {
// invalid syntax or we tried to read a comment as assignment
return false;
}
// extract value
start++;
while (start < line.size() &&
isspace(line[start])) {
start++;
}
value = line.substr(start);
// remove trailing white space: usually it is
// added accidentally by users
size_t numspaces = 0;
while (numspaces < value.size() &&
isspace(value[value.size() - 1 - numspaces])) {
numspaces++;
}
value.erase(value.size() - numspaces);
// @TODO: strip quotation marks around value?!
return true;
}
/**
* check whether the line contains the property and if so, extract its value
*/
static bool getValue(const string &line,
const string &property,
string &value,
bool &isComment,
bool fuzzyComments)
{
string curProp;
return getContent(line, curProp, value, isComment, fuzzyComments) &&
!strcasecmp(curProp.c_str(), property.c_str());
}
string IniFileConfigNode::readProperty(const string &property) const {
string value;
BOOST_FOREACH(const string &line, m_lines) {
bool isComment;
if (getValue(line, property, value, isComment, false)) {
return value;
}
}
return "";
}
void IniFileConfigNode::readProperties(ConfigProps &props) const {
map<string, string> res;
string value, property;
BOOST_FOREACH(const string &line, m_lines) {
bool isComment;
if (getContent(line, property, value, isComment, false)) {
// don't care about the result: only the first instance
// of the property counts, so it doesn't matter when
// inserting it again later fails
props.insert(pair<string, string>(property, value));
}
}
}
void IniFileConfigNode::removeProperty(const string &property)
{
string value;
list<string>::iterator it = m_lines.begin();
while (it != m_lines.end()) {
const string &line = *it;
bool isComment;
if (getValue(line, property, value, isComment, false)) {
it = m_lines.erase(it);
m_modified = true;
} else {
it++;
}
}
}
void IniFileConfigNode::setProperty(const string &property,
const string &newvalue,
const string &comment,
const string *defValue) {
string newstr;
string oldvalue;
bool isDefault = false;
if (defValue &&
*defValue == newvalue) {
newstr += "# ";
isDefault = true;
}
newstr += property + " = " + newvalue;
BOOST_FOREACH(string &line, m_lines) {
bool isComment;
if (getValue(line, property, oldvalue, isComment, true)) {
if (newvalue != oldvalue ||
(isComment && !isDefault)) {
line = newstr;
m_modified = true;
}
return;
}
}
// add each line of the comment as separate line in .ini file
if (comment.size()) {
list<string> commentLines;
ConfigProperty::splitComment(comment, commentLines);
if (m_lines.size()) {
m_lines.push_back("");
}
BOOST_FOREACH(const string &comment, commentLines) {
m_lines.push_back(string("# ") + comment);
}
}
m_lines.push_back(newstr);
m_modified = true;
}
void IniFileConfigNode::clear()
{
m_lines.clear();
m_modified = true;
}
IniHashConfigNode::IniHashConfigNode(const boost::shared_ptr<DataBlob> &data) :
IniBaseConfigNode(data)
{
read();
}
IniHashConfigNode::IniHashConfigNode(const string &path, const string &fileName, bool readonly) :
IniBaseConfigNode(boost::shared_ptr<DataBlob>(new FileDataBlob(path, fileName, readonly)))
{
read();
}
void IniHashConfigNode::read()
{
boost::shared_ptr<std::istream> file(m_data->read());
std::string line;
while (std::getline(*file, line)) {
string property, value;
bool isComment;
if (getContent(line, property, value, isComment, false)) {
m_props.insert(StringPair(property, value));
}
}
m_modified = false;
}
void IniHashConfigNode::toFile(std::ostream &file)
{
BOOST_FOREACH(const StringPair &prop, m_props) {
file << prop.first << " = " << prop.second << std::endl;
}
}
void IniHashConfigNode::readProperties(ConfigProps &props) const
{
BOOST_FOREACH(const StringPair &prop, m_props) {
props.insert(prop);
}
}
void IniHashConfigNode::writeProperties(const ConfigProps &props)
{
if (!props.empty()) {
m_props.insert(props.begin(), props.end());
m_modified = true;
}
}
string IniHashConfigNode::readProperty(const string &property) const
{
std::map<std::string, std::string>::const_iterator it = m_props.find(property);
if (it != m_props.end()) {
return it->second;
} else {
return "";
}
}
void IniHashConfigNode::removeProperty(const string &property) {
map<string, string>::iterator it = m_props.find(property);
if(it != m_props.end()) {
m_props.erase(it);
m_modified = true;
}
}
void IniHashConfigNode::clear()
{
if (!m_props.empty()) {
m_props.clear();
m_modified = true;
}
}
void IniHashConfigNode::setProperty(const string &property,
const string &newvalue,
const string &comment,
const string *defValue)
{
/** we don't support property comments here. Also, we ignore comment*/
if (defValue &&
*defValue == newvalue) {
removeProperty(property);
return;
}
map<string, string>::iterator it = m_props.find(property);
if(it != m_props.end()) {
string oldvalue = it->second;
if(oldvalue != newvalue) {
m_props.erase(it);
m_props.insert(StringPair(property, newvalue));
m_modified = true;
}
} else {
m_props.insert(StringPair(property, newvalue));
m_modified = true;
}
}
SE_END_CXX

130
src/syncevo/IniConfigNode.h Normal file
View File

@ -0,0 +1,130 @@
/*
* Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
*
* This library 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 2.1 of the License, or (at your option) version 3.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#ifndef INCL_EVOLUTION_INI_CONFIG_NODE
# define INCL_EVOLUTION_INI_CONFIG_NODE
#include <syncevo/ConfigNode.h>
#include <syncevo/DataBlob.h>
#include <string>
#include <list>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
using namespace std;
/**
* A base class for .ini style data blobs.
*/
class IniBaseConfigNode: public ConfigNode {
protected:
boost::shared_ptr<DataBlob> m_data;
bool m_modified;
/**
* Open or create a new blob. The blob will be read (if it exists)
* but not created or written to unless flush() is called explicitly.
*/
IniBaseConfigNode(const boost::shared_ptr<DataBlob> &data);
/**
* a virtual method to serial data structure to the file
* It is used by flush function to flush memory into disk file
*/
virtual void toFile(std::ostream &file) = 0;
public:
virtual void flush();
virtual string getName() const { return m_data->getName(); }
virtual bool exists() const { return m_data->exists(); }
};
/**
* This class started its life as the Posix implementation of the
* ManagementNode in the Funambol C++ client library. Nowadays it is
* part of the SyncEvolution ConfigTree (see there for details).
*
* Each node is mapped to one file whose location is determined by
* the ConfigTree when the node gets created. Each node represents
* one .ini file with entries of the type
* <property>\s*=\s*<value>\s*\n
*
* Comments look like:
* \s*# <comment>
*
*/
class IniFileConfigNode : public IniBaseConfigNode {
list<string> m_lines;
void read();
protected:
virtual void toFile(std::ostream &file);
public:
IniFileConfigNode(const boost::shared_ptr<DataBlob> &data);
IniFileConfigNode(const string &path, const string &fileName, bool readonly);
/* keep underlying methods visible; our own setProperty() would hide them */
using ConfigNode::setProperty;
virtual string readProperty(const string &property) const;
virtual void setProperty(const string &property,
const string &value,
const string &comment = "",
const string *defValue = NULL);
virtual void readProperties(ConfigProps &props) const;
virtual void removeProperty(const string &property);
virtual void clear();
};
/**
* The main difference from FileConfigNode is to store pair of 'property-value'
* in a map to avoid O(n^2) string comparison
* Here comments for property default value are discarded.
*/
class IniHashConfigNode: public IniBaseConfigNode {
map<std::string, std::string> m_props;
/**
* Map used to store pairs
*/
void read();
protected:
virtual void toFile(std::ostream & file);
public:
IniHashConfigNode(const boost::shared_ptr<DataBlob> &data);
IniHashConfigNode(const string &path, const string &fileName, bool readonly);
virtual string readProperty(const string &property) const;
virtual void setProperty(const string &property,
const string &value,
const string &comment = "",
const string *defValue = NULL);
virtual void readProperties(ConfigProps &props) const;
virtual void writeProperties(const ConfigProps &props);
virtual void removeProperty(const string &property);
virtual void clear();
};
SE_END_CXX
#endif // INCL_EVOLUTION_INI_CONFIG_NODE

View File

@ -84,6 +84,11 @@ SYNCEVOLUTION_SOURCES = \
FileConfigNode.h \
FileConfigNode.cpp \
\
IniConfigNode.h \
IniConfigNode.cpp \
SingleFileConfigTree.h \
SingleFileConfigTree.cpp \
\
DataBlob.h \
FileDataBlob.h \
FileDataBlob.cpp \

View File

@ -0,0 +1,242 @@
/*
* Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
*
* This library 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 2.1 of the License, or (at your option) version 3.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#include <config.h>
#include "test.h"
#include <syncevo/SingleFileConfigTree.h>
#include <syncevo/StringDataBlob.h>
#include <syncevo/FileDataBlob.h>
#include <syncevo/IniConfigNode.h>
#include <syncevo/util.h>
#include <boost/foreach.hpp>
#include <boost/algorithm/string.hpp>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
using namespace std;
SingleFileConfigTree::SingleFileConfigTree(const boost::shared_ptr<DataBlob> &data) :
m_data(data)
{
readFile();
}
SingleFileConfigTree::SingleFileConfigTree(const string &fullpath) :
m_data(new FileDataBlob(fullpath, true))
{
readFile();
}
boost::shared_ptr<ConfigNode> SingleFileConfigTree::open(const string &filename)
{
string normalized = normalizePath(string("/") + filename);
boost::shared_ptr<ConfigNode> &entry = m_nodes[normalized];
if (entry) {
return entry;
}
string name = getRootPath() + " - " + normalized;
boost::shared_ptr<DataBlob> data;
BOOST_FOREACH(const FileContent_t::value_type &file, m_content) {
if (file.first == normalized) {
data.reset(new StringDataBlob(name, file.second, true));
break;
}
}
if (!data) {
/*
* creating new files not supported, would need support for detecting
* StringDataBlob::write()
*/
data.reset(new StringDataBlob(name, boost::shared_ptr<std::string>(), true));
}
entry.reset(new IniFileConfigNode(data));
return entry;
}
void SingleFileConfigTree::flush()
{
// not implemented, cannot write anyway
}
void SingleFileConfigTree::remove(const string &path)
{
SE_THROW("internal error: SingleFileConfigTree::remove() called");
}
void SingleFileConfigTree::reset()
{
m_nodes.clear();
readFile();
}
boost::shared_ptr<ConfigNode> SingleFileConfigTree::open(const string &path,
PropertyType type,
const string &otherId)
{
string fullpath = path;
if (!fullpath.empty()) {
fullpath += "/";
}
switch (type) {
case visible:
fullpath += "config.ini";
break;
case hidden:
fullpath += ".internal.ini";
break;
case other:
fullpath += ".other.ini";
break;
case server:
fullpath += ".server.ini";
break;
}
return open(fullpath);
}
static void checkChild(const string &normalized,
const string &node,
set<string> &subdirs)
{
if (boost::starts_with(node, normalized)) {
string remainder = node.substr(normalized.size());
size_t offset = remainder.find('/');
if (offset != remainder.npos) {
// only directories underneath path matter
subdirs.insert(remainder.substr(0, offset));
}
}
}
list<string> SingleFileConfigTree::getChildren(const string &path)
{
set<string> subdirs;
string normalized = normalizePath(string("/") + path);
if (normalized != "/") {
normalized += "/";
}
// must check both actual files as well as unsaved nodes
BOOST_FOREACH(const FileContent_t::value_type &file, m_content) {
checkChild(normalized, file.first, subdirs);
}
BOOST_FOREACH(const NodeCache_t::value_type &file, m_nodes) {
checkChild(normalized, file.first, subdirs);
}
list<string> result;
BOOST_FOREACH(const string &dir, subdirs) {
result.push_back(dir);
}
return result;
}
void SingleFileConfigTree::readFile()
{
boost::shared_ptr<istream> in(m_data->read());
boost::shared_ptr<string> content;
string line;
m_content.clear();
while (getline(*in, line)) {
if (boost::starts_with(line, "=== ") &&
boost::ends_with(line, " ===")) {
string name = line.substr(4, line.size() - 8);
name = normalizePath(string("/") + name);
content.reset(new string);
m_content[name] = content;
} else if (content) {
(*content) += line;
(*content) += "\n";
}
}
}
#ifdef ENABLE_UNIT_TESTS
class SingleIniTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(SingleIniTest);
CPPUNIT_TEST(simple);
CPPUNIT_TEST_SUITE_END();
void simple() {
boost::shared_ptr<string> data(new string);
data->assign("# comment\n"
"# foo\n"
"=== foo/config.ini ===\n"
"foo = bar\n"
"foo2 = bar2\n"
"=== foo/.config.ini ===\n"
"foo_internal = bar_internal\n"
"foo2_internal = bar2_internal\n"
"=== /bar/.internal.ini ===\n"
"bar = foo\n"
"=== sources/addressbook/config.ini ===\n"
"=== sources/calendar/config.ini ===\n"
"evolutionsource = Personal\n");
boost::shared_ptr<DataBlob> blob(new StringDataBlob("test", data, true));
SingleFileConfigTree tree(blob);
boost::shared_ptr<ConfigNode> node;
node = tree.open("foo/config.ini");
CPPUNIT_ASSERT(node);
CPPUNIT_ASSERT(node->exists());
CPPUNIT_ASSERT_EQUAL(string("test - /foo/config.ini"), node->getName());
CPPUNIT_ASSERT_EQUAL(string("bar"), node->readProperty("foo"));
CPPUNIT_ASSERT_EQUAL(string("bar2"), node->readProperty("foo2"));
node = tree.open("/foo/config.ini");
CPPUNIT_ASSERT(node);
CPPUNIT_ASSERT(node->exists());
node = tree.open("foo//.config.ini");
CPPUNIT_ASSERT(node);
CPPUNIT_ASSERT(node->exists());
CPPUNIT_ASSERT_EQUAL(string("bar_internal"), node->readProperty("foo_internal"));
CPPUNIT_ASSERT_EQUAL(string("bar2_internal"), node->readProperty("foo2_internal"));
node = tree.open("bar///./.internal.ini");
CPPUNIT_ASSERT(node);
CPPUNIT_ASSERT(node->exists());
CPPUNIT_ASSERT_EQUAL(string("foo"), node->readProperty("bar"));
node = tree.open("sources/addressbook/config.ini");
CPPUNIT_ASSERT(node);
CPPUNIT_ASSERT(node->exists());
node = tree.open("sources/calendar/config.ini");
CPPUNIT_ASSERT(node);
CPPUNIT_ASSERT(node->exists());
CPPUNIT_ASSERT_EQUAL(string("Personal"), node->readProperty("evolutionsource"));
node = tree.open("no-such-source/config.ini");
CPPUNIT_ASSERT(node);
CPPUNIT_ASSERT(!node->exists());
list<string> dirs = tree.getChildren("");
CPPUNIT_ASSERT_EQUAL(string("bar|foo|no-such-source|sources"), boost::join(dirs, "|"));
dirs = tree.getChildren("sources/");
CPPUNIT_ASSERT_EQUAL(string("addressbook|calendar"), boost::join(dirs, "|"));
}
};
SYNCEVOLUTION_TEST_SUITE_REGISTRATION(SingleIniTest);
#endif // ENABLE_UNIT_TESTS
SE_END_CXX

View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
*
* This library 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 2.1 of the License, or (at your option) version 3.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#ifndef INCL_EVOLUTION_SINGLE_FILE_CONFIG_TREE
# define INCL_EVOLUTION_SINGLE_FILE_CONFIG_TREE
#include <ConfigTree.h>
#include <DataBlob.h>
#include <util.h>
#include <string>
#include <map>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
/**
* This class handles data blobs which contain multiple .ini files, using
* the following format:
* @verbatim
# comment
# ...
=== <first path>/[.internal.ini|config.ini|template.ini|...] ===
<file content>
=== <second file name> ===
...
* @endverbatim
*
* This is based on the assumption that the === ... === file separator
* is not part of valid .ini file content.
*
* Right now, only reading such a single data blob is implemented.
*/
class SingleFileConfigTree : public ConfigTree {
public:
/**
* @param data access to complete file data
*/
SingleFileConfigTree(const boost::shared_ptr<DataBlob> &data);
SingleFileConfigTree(const std::string &fullpath);
/**
* same as open(), with full file name (like sources/addressbook/config.ini)
* instead of path + type
*/
boost::shared_ptr<ConfigNode> open(const std::string &filename);
/* ConfigTree API */
virtual string getRootPath() const { return m_data->getName(); }
virtual void flush();
virtual void remove(const std::string &path);
virtual void reset();
virtual boost::shared_ptr<ConfigNode> open(const std::string &path,
PropertyType type,
const std::string &otherId = std::string(""));
list<string> getChildren(const std::string &path);
private:
boost::shared_ptr<DataBlob> m_data;
/**
* maps from normalized file name (see normalizePath()) to content for that name
*/
typedef std::map<std::string, boost::shared_ptr<std::string> > FileContent_t;
FileContent_t m_content;
/** cache of all nodes ever accessed */
typedef map< string, boost::shared_ptr<ConfigNode> > NodeCache_t;
NodeCache_t m_nodes;
/**
* populate m_content from m_data
*/
void readFile();
};
SE_END_CXX
#endif