From: Enar Väikene Date: Tue, 4 Oct 2011 14:09:35 +0000 (+0300) Subject: Implemented IniFile class that reads and writes parameters values in INI files. X-Git-Url: https://vaikene.ee/gitweb/gitweb.cgi?p=evaf;a=commitdiff_plain;h=6230ebd0a05134031e52001c15473c3c3e6c7c1b Implemented IniFile class that reads and writes parameters values in INI files. --- diff --git a/src/libs/Common/CMakeLists.txt b/src/libs/Common/CMakeLists.txt index 31fc634..bb87e9f 100644 --- a/src/libs/Common/CMakeLists.txt +++ b/src/libs/Common/CMakeLists.txt @@ -23,6 +23,7 @@ set(SRCS logger.cpp registry.cpp util.cpp + inifile.cpp ) # Header files for the meta-object compiler diff --git a/src/libs/Common/inifile.cpp b/src/libs/Common/inifile.cpp new file mode 100644 index 0000000..28e700f --- /dev/null +++ b/src/libs/Common/inifile.cpp @@ -0,0 +1,467 @@ +/** + * @file Common/inifile.cpp + * @brief Internal implementation of the class for reading and writing parameter values in INI files. + * @author Enar Vaikene + * + * Copyright (c) 2011 Enar Vaikene + * + * This file is part of the eVaf C++ cross-platform application development framework. + * + * This file can be used under the terms of the GNU General Public License + * version 3.0 as published by the Free Software Foundation and appearing in + * the file LICENSE included in the packaging of this file. Please review the + * the following information to ensure the GNU General Public License version + * 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html. + * + * Alternatively, this file may be used in accordance with the Commercial License + * Agreement provided with the Software. + */ + +#include "inifile.h" +#include "inifile_p.h" +#include "util.h" +#include "ilogger.h" + +#include + + +// End of line characters for Linux and Windows +#ifdef Q_OS_WIN32 +# define EOL "\r\n" +#else +# define EOL "\n" +#endif + +using namespace eVaf::Common; + +//------------------------------------------------------------------- + +IniFile::IniFile(QString const & fileName, QIODevice::OpenMode mode) +{ + d = new Internal::IniFileImpl(fileName, mode); +} + +IniFile::~IniFile() +{ + delete d; +} + +bool IniFile::isValid() const +{ + return d->isValid(); +} + +QString IniFile::errorString() const +{ + return d->errorString(); +} + +QVariant IniFile::getValue(QString const & paramName, QVariant const & defaultValue) +{ + return d->getValue(paramName, defaultValue); +} + +bool IniFile::setValue(QString const & paramName, QVariant const & value) +{ + return d->setValue(paramName, value); +} + + +//------------------------------------------------------------------- + +using namespace eVaf::Common::Internal; + +IniFileImpl::IniFileImpl(QString const & fileName, QIODevice::OpenMode mode) + : mValid(false) + , mFileName(fileName) + , mMode(mode) +{ + // Verify that we can open the file in the specified mode + QFile f(mFileName); + if (!f.open(mMode)) { + mErrorString = f.errorString(); + EVAF_ERROR("Failed to open INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + } + else { + // Get and store the last modified time + QFileInfo fi(f); + mLastModified = fi.lastModified(); + mValid = true; + f.close(); + } +} + +IniFileImpl::~IniFileImpl() +{ + mCache.clear(); +} + +void IniFileImpl::updateCache(quint64 pos, qint64 diff) +{ + // Walk through all the sections in the cache + QHash >::const_iterator it; + for (it = mCache.constBegin(); it != mCache.constEnd(); ++it) { + QExplicitlySharedDataPointer sectionObject = *it; + + // Update the section object if it comes after the current file offset + if (sectionObject->filePos > pos) + sectionObject->filePos += diff; + + // Update individual values in the section that come after the current file offset + QHash >::const_iterator it1; + for (it1 = sectionObject->values.constBegin(); it1 != sectionObject->values.constEnd(); ++it1) { + QExplicitlySharedDataPointer valueObject = *it1; + if (valueObject->filePos > pos) + valueObject->filePos += diff; + } + } +} + +QExplicitlySharedDataPointer IniFileImpl::getSection(QFile & file, QString const & sectionName) +{ + // Check for external modifications + QFileInfo fi(file); + if (fi.lastModified() != mLastModified) { + // The INI file was modified externally and our internal cache is probably invalid + mCache.clear(); + mLastModified = fi.lastModified(); + } + + // Look for the section in the cache first + QHash >::const_iterator it = mCache.constFind(sectionName.toLower()); + if (it != mCache.constEnd()) { + // Found in the cache + if (mValid) + file.seek((*it)->filePos); + return *it; + } + + // Read the INI file and look for the section + while (mValid && !file.atEnd()) { + QString line = file.readLine().trimmed(); + + // Ignore the line if it is empty, a comment or not a section name + if (line.isEmpty() || line.startsWith(';') || line.startsWith('#') || !line.startsWith('[')) + continue; + + // Position of the closing ']' + int idx = line.indexOf(']'); + if (idx == -1) + continue; + + // Is this the section that we are looking for? + if (line.mid(1, idx - 1).compare(sectionName, Qt::CaseInsensitive) == 0) { + // Create the section object and add to the cache + QExplicitlySharedDataPointer sectionObject(new IniFileSection(file.pos())); + sectionObject->name = sectionName.toLower(); + mCache.insert(sectionName.toLower(), sectionObject); + + // Returns the section object + return sectionObject; + } + + } + + // No such section found in the INI file -- return an invalid object; the file is already positioned at the end + return QExplicitlySharedDataPointer(); +} + +QExplicitlySharedDataPointer IniFileImpl::getParameter(QFile & file, IniFileSection & section, QString const & paramName) +{ + // Look for the parameter in the cache first + QHash >::const_iterator it = section.values.constFind(paramName.toLower()); + if (it != section.values.constEnd()) { + // Found it in the cache + if (mValid) + file.seek((*it)->filePos); + return *it; + } + + // Read the INI file and look for the parameter name + while (mValid && !file.atEnd()) { + + // Current file position + quint64 currentPos = file.pos(); + + QString line = file.readLine().trimmed(); + + // Ignore the line if it is empty or a comment + if (line.isEmpty() || line.startsWith(';') || line.startsWith('#')) + continue; + + // Returns an invalid object if we reach the beginning of another section + if (line.startsWith('[')) { + // Rewind to the stored position + file.seek(currentPos); + + return QExplicitlySharedDataPointer(); + } + + // Locate '=' in the line and get the name/value pair + int idx = line.indexOf('='); + if (idx == -1) + continue; + + QString name = line.mid(0, idx).trimmed().toLower(); + QString value = line.mid(idx + 1).trimmed(); + + // Check for the 'windows:' or 'linux:' prefix in the parameter name + bool thisOsOnly = false; +#ifdef Q_OS_LINUX + if (name.startsWith("windows:")) + continue; + if (name.startsWith("linux:")) { + name.remove(0, 6); + thisOsOnly = true; + } +#endif +#ifdef Q_OS_WIN32 + if (name.startsWith("linux:")) + continue; + if (name.startsWith("windows:")) { + name.remove(0, 8); + thisOsOnly = true; + } +#endif + + // If the parameter value is not in the cache, add it to the cache + QExplicitlySharedDataPointer valueObject; + QHash >::const_iterator it = section.values.constFind(name); + if (it == section.values.constEnd()) { + valueObject = new IniFileValue(currentPos); + valueObject->name = name; + valueObject->paramValue = value; + section.values.insert(name, valueObject); + } + else { + // We should not find it in the cache, but if we somehow managed to end up here, use and update the object from the cache + valueObject = *it; + valueObject->name = name; + valueObject->paramValue = value; + } + + // Is this the parameter vwe are looking for? + if (name.compare(paramName, Qt::CaseInsensitive) == 0) { + // Rewind to the beginning of the line + file.seek(currentPos); + + // Return the value object + return valueObject; + } + } + + // The parameter with this name was not found in the INI file -- return an invalid object; the file is already positioned at the next section or the end of the file + return QExplicitlySharedDataPointer(); +} + +QVariant IniFileImpl::getValue(QString const & paramName, QVariant const & defaultValue) +{ + // Locate the '/' character that separates section names from key names + int idx = paramName.indexOf('/'); + if (idx < 0) + return defaultValue; + + // Separate section and key names + QString section = paramName.left(idx); + QString key = paramName.mid(idx + 1); + + // Open the file + QFile f(mFileName); + if (!f.open(mMode)) { + mErrorString = f.errorString(); + mValid = false; + EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + return defaultValue; + } + + // Locate the section + QExplicitlySharedDataPointer sectionObject = getSection(f, section); + if (!sectionObject) + return defaultValue; + + // Locate the parameter + QExplicitlySharedDataPointer valueObject = getParameter(f, *sectionObject, key); + if (!valueObject) + return defaultValue; + + if (f.isOpen()) + f.close(); + + return toVariant(valueObject->paramValue, defaultValue); +} + +bool IniFileImpl::setValue(QString const & paramName, QVariant const & value) +{ + // Locate the '/' character that separates section names from key names + int idx = paramName.indexOf('/'); + if (idx < 0) + return false; + + // Separate section and key names + QString section = paramName.left(idx).toLower(); + QString key = paramName.mid(idx + 1).toLower(); + + // Format the value depending on the type + QString valueString; + switch (value.type()) { + case QVariant::UInt: + valueString = "0x" + QString::number(value.toUInt(), 16); + break; + case QVariant::Int: + valueString = QString::number(value.toInt()); + break; + case QVariant::Double: + valueString = QString::number(value.toDouble(), 'f'); + break; + case QVariant::Bool: + valueString = value.toBool() ? "true" : "false"; + break; + case QVariant::Char: + valueString = value.toChar(); + break; + default: + valueString = value.toString(); + } + + // Open the file + QFile f(mFileName); + if (!f.open(mMode)) { + mErrorString = f.errorString(); + mValid = false; + EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + return false; + } + mValid = true; + mErrorString.clear(); + + // Get the section object + QExplicitlySharedDataPointer sectionObject = getSection(f, section); + + // If the section is not found, add a new section to the end of the INI file + if (!sectionObject) { + + // Write the new section to the INI file (the file is already positioned at the end) + if (f.write(QString("[%1]" EOL).arg(section).toLocal8Bit()) == -1) { + mErrorString = f.errorString(); + mValid = false; + EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + return false; + } + + // Current file position + quint64 currentPos = f.pos(); + + // Add the new section to the internal cache + sectionObject = new IniFileSection(currentPos); + mCache.insert(section.toLower(), sectionObject); + + // Write the parameter value to the INI file + if (f.write(QString("%1 = %2" EOL).arg(key).arg(valueString).toLocal8Bit()) == -1) { + mErrorString = f.errorString(); + mValid = false; + EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + return false; + } + + // Add the parameter value to the internal cache + QExplicitlySharedDataPointer valueObject(new IniFileValue(currentPos)); + valueObject->name = key; + valueObject->paramValue = valueString; + sectionObject->values.insert(key, valueObject); + } + + // If the section is found, use the existing section object from the cache + else { + quint64 currentPos; + quint64 oldPos = f.pos(); + QString prefix; // Platform-specific prefix + + // Locate the parameter value + QExplicitlySharedDataPointer valueObject = getParameter(f, *sectionObject, key); + + // The file is now positioned either at the beginning of the parameter line or at the end of the section/file + currentPos = f.pos(); + + if (valueObject) { + // Parameter was found; skip the line with the current parameter value + f.readLine(); + oldPos = f.pos(); + + // Format the prefix string if necessary + if (valueObject->thisOsOnly) { +#ifdef Q_OS_LINUX + prefix = "linux:"; +#endif +#if Q_OS_WIN32 + prefix = "windows:"; +#endif + } + } + + else { + // Parameter was not found; create a new value object + valueObject = new IniFileValue(currentPos); + valueObject->name = key; + } + + // Store everything from the current position till the end of file in a temporary buffer + QBuffer tmp; + if (!tmp.open(QBuffer::ReadWrite)) { + mErrorString = tmp.errorString(); + mValid = false; + EVAF_ERROR("Failed to open the temporary buffer : %s", qPrintable(mErrorString)); + return false; + } + if (tmp.write(f.readAll()) == -1) { + mErrorString = tmp.errorString(); + mValid = false; + EVAF_ERROR("Failed to write to the temporary buffer : %s", qPrintable(mErrorString)); + return false; + } + + // Rewind to the original position and write the new parameter value + f.seek(currentPos); + if (f.write(QString("%1%2 = %3" EOL).arg(prefix).arg(key).arg(valueString).toLocal8Bit()) == -1) { + mErrorString = f.errorString(); + mValid = false; + EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + return false; + } + + // How much were sections and parameters shifted due to the new parameter value? + qint64 diff = qint64(f.pos() - oldPos); + + // Write everything back from the temporary buffer + tmp.seek(0); + if (f.write(tmp.readAll()) == -1) { + mErrorString = f.errorString(); + mValid = false; + EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + return false; + } + tmp.close(); + + // If the original file was larger than the new file, then truncate it + if (f.size() > f.pos()) { + if (!f.resize(f.pos())) { + mErrorString = f.errorString(); + mValid = false; + EVAF_ERROR("Failed to resize the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString)); + return false; + } + } + + // If the shift was not zero, update the cache + if (diff) + updateCache(currentPos, diff); + + } + + f.close(); + + // Update the time when the INI file was last modified + QFileInfo fi(mFileName); + mLastModified = fi.lastModified(); + + return true; +} diff --git a/src/libs/Common/inifile.h b/src/libs/Common/inifile.h index 59f6fa0..b795e6a 100644 --- a/src/libs/Common/inifile.h +++ b/src/libs/Common/inifile.h @@ -22,6 +22,10 @@ #include "libcommon.h" +#include +#include +#include + namespace eVaf { namespace Common { namespace Internal { @@ -71,11 +75,6 @@ public: * The isValid() method returns true if the INI file is can be used. Use this * method after creating the object to verify that opening the INI file in the specified * mode succeeded. - * - * If the object is not valid, then: - * @li Writing to the INI file always fails; the internal cache is still updated and reading the parameter returns - * the new value; - * @li Reading from the INI file returns the cached value or the default value if no values with this name are written. */ bool isValid() const; @@ -119,9 +118,6 @@ public: * * The method returns true if the parameter value was written into the INI file and false if not. Use the errorString() method * to get a human-readable error string if writing to the INI file fails. - * - * Writing to an invalid INI file always fails, but the value is still stored into the internal cache. Readin the same parameter - * value returns the new value even if it was actually not stored into the INI file. */ bool setValue(QString const & paramName, QVariant const & value); @@ -136,4 +132,4 @@ private: } // namespace eVaf::Common } // namespace eVaf -#endif // INIFILE_H +#endif // inifile.h diff --git a/src/libs/Common/inifile_p.h b/src/libs/Common/inifile_p.h new file mode 100644 index 0000000..b782627 --- /dev/null +++ b/src/libs/Common/inifile_p.h @@ -0,0 +1,207 @@ +/** + * @file Common/inifile_p.h + * @brief Internal implementation of the class for reading and writing parameter values in INI files. + * @author Enar Vaikene + * + * Copyright (c) 2011 Enar Vaikene + * + * This file is part of the eVaf C++ cross-platform application development framework. + * + * This file can be used under the terms of the GNU General Public License + * version 3.0 as published by the Free Software Foundation and appearing in + * the file LICENSE included in the packaging of this file. Please review the + * the following information to ensure the GNU General Public License version + * 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html. + * + * Alternatively, this file may be used in accordance with the Commercial License + * Agreement provided with the Software. + */ + +#ifndef __COMMON_INIFILE_P_H +#define __COMMON_INIFILE_P_H + +#include +#include +#include +#include +#include +#include +#include + + +namespace eVaf { +namespace Common { +namespace Internal { + +/** + * INI file value in the internal cache + */ +class IniFileValue : public QSharedData +{ +public: + + IniFileValue(quint64 pos) + : QSharedData() + , filePos(pos) + , thisOsOnly(false) + {} + + /** + * File position + * + * Offset of the parameter in the INI file. By seeking the file to this offset value, + * the next character read or written will be the beginning of the key name. + */ + quint64 filePos; + + /** + * Key name of the parameter + */ + QString name; + + /** + * Value from the INI file + */ + QString paramValue; + + /** + * Flag indicating that this value is valid on this OS only + */ + bool thisOsOnly; +}; + +/** + * INI file section in the internal cache + */ +class IniFileSection : public QSharedData +{ +public: + + IniFileSection(quint64 pos) + : QSharedData() + , filePos(pos) + {} + + /** + * File position + * + * Offset of the section in the INI file. By seeking the file to this offset value, + * the next character read or written will be the first character of the section. + */ + quint64 filePos; + + /** + * Name of the section + */ + QString name; + + /** + * List of all the known parameter values in this section + * + * The key to the hash table is the name of the key for the parameters value. + */ + QHash > values; + +}; + +/** + * Internal implementation of the IniFile class. + */ +class IniFileImpl +{ +public: + + IniFileImpl(QString const & fileName, QIODevice::OpenMode mode); + + ~IniFileImpl(); + + QVariant getValue(QString const & paramName, QVariant const & defaultValue); + + bool setValue(QString const & paramName, QVariant const & value); + + inline bool isValid() const { return mValid; } + + inline QString const & fileName() const { return mFileName; } + + inline QString const & errorString() const { return mErrorString; } + + +private: // Members + + /// Flag indicating that the INI file object is valid and can be used to access the INI file + bool mValid; + + /// Name of the INI file + QString mFileName; + + /// INI file opening mode + QIODevice::OpenMode mMode; + + /// Last human-readable error message if an operation with the INI file failed + QString mErrorString; + + /** + * Internal cache of sections and parameter values. + * + * The key to the hash table is the name of the section. + */ + QHash > mCache; + + /// When was the INI file modified. + QDateTime mLastModified; + + +private: /// Methods + + /** + * Updates items in the internal cache after changes to the INI file + * @param pos File offset from where the cache should be updated + * @param diff Difference between old and new file offsets + * + * When a parameter value is modified or a new value inserted, the length of the INI file changes. + * Sections and parameters that come after the modified parameter value are shifted and their file + * positions changed. This method updates items in the internal cache after changes to the INI file. + */ + void updateCache(quint64 pos, qint64 diff); + + /** + * Looks for a section in the INI file + * @param file The file object + * @param sectionName Name of the section + * @return The section object or an invalid object if not found + * + * This method reads the INI file and looks for the section with the given name. If found, returns the section + * object and seeks the file to the first character after the section name. + * + * If the section is not found, returns an invalid section object and seeks the file to the end of the file. + * + * The file object is expected to be opened if the mValid flag is true. If the mValid flag is false, looks + * only in the cache. + */ + QExplicitlySharedDataPointer getSection(QFile & file, QString const & sectionName); + + /** + * Looks for a parameter in the INI file + * @param file The file object + * @param section The section object + * @param paramName Name of the parameter + * @return The value object or an invalid object if not found + * + * This method reads the INI file and looks for the parameter with the given name. If found, returns the value + * object and seeks the file to the beginning of the line containing the parameter. + * + * If not found, returns an invalid value object and seeks the file to the end of the section. + * + * The file object is expected to be opened if the mValid flag is true. If the mValid flag is false, looks + * only in the cache. + */ + QExplicitlySharedDataPointer getParameter(QFile & file, IniFileSection & section, QString const & paramName); + +}; + + +} // namespace eVaf::Common::Internal +} // namespace eVaf::Common +} // namespace eVaf + +#endif // inifile_p.h