]> vaikene.ee Git - evaf/commitdiff
Implemented IniFile class that reads and writes parameters values in INI files.
authorEnar Väikene <enar@vaikene.net>
Tue, 4 Oct 2011 14:09:35 +0000 (17:09 +0300)
committerEnar Väikene <enar@vaikene.net>
Tue, 4 Oct 2011 14:09:35 +0000 (17:09 +0300)
src/libs/Common/CMakeLists.txt
src/libs/Common/inifile.cpp [new file with mode: 0644]
src/libs/Common/inifile.h
src/libs/Common/inifile_p.h [new file with mode: 0644]

index 31fc6347508b042b5ed2e560bde73675126bee52..bb87e9f8cfe9b827f9a39a36d083b4d6bbafbc49 100644 (file)
@@ -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 (file)
index 0000000..28e700f
--- /dev/null
@@ -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 <QtCore>
+
+
+// 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<QString, QExplicitlySharedDataPointer<IniFileSection> >::const_iterator it;
+    for (it = mCache.constBegin(); it != mCache.constEnd(); ++it) {
+        QExplicitlySharedDataPointer<IniFileSection> 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<QString, QExplicitlySharedDataPointer<IniFileValue> >::const_iterator it1;
+        for (it1 = sectionObject->values.constBegin(); it1 != sectionObject->values.constEnd(); ++it1) {
+            QExplicitlySharedDataPointer<IniFileValue> valueObject = *it1;
+            if (valueObject->filePos > pos)
+                valueObject->filePos += diff;
+        }
+    }
+}
+
+QExplicitlySharedDataPointer<IniFileSection> 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<QString, QExplicitlySharedDataPointer<IniFileSection> >::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<IniFileSection> 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<IniFileSection>();
+}
+
+QExplicitlySharedDataPointer<IniFileValue> IniFileImpl::getParameter(QFile & file, IniFileSection & section, QString const & paramName)
+{
+    // Look for the parameter in the cache first
+    QHash<QString, QExplicitlySharedDataPointer<IniFileValue> >::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<IniFileValue>();
+        }
+
+        // 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<IniFileValue> valueObject;
+        QHash<QString, QExplicitlySharedDataPointer<IniFileValue> >::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<IniFileValue>();
+}
+
+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<IniFileSection> sectionObject = getSection(f, section);
+    if (!sectionObject)
+        return defaultValue;
+
+    // Locate the parameter
+    QExplicitlySharedDataPointer<IniFileValue> 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<IniFileSection> 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<IniFileValue> 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<IniFileValue> 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;
+}
index 59f6fa0b1ff10fea94cace947395669be72e217c..b795e6a9435dc02dfc222c7af964094b93649801 100644 (file)
 
 #include "libcommon.h"
 
+#include <QString>
+#include <QVariant>
+#include <QIODevice>
+
 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 (file)
index 0000000..b782627
--- /dev/null
@@ -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 <QSharedData>
+#include <QExplicitlySharedDataPointer>
+#include <QHash>
+#include <QString>
+#include <QVariant>
+#include <QDateTime>
+#include <QFile>
+
+
+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<QString, QExplicitlySharedDataPointer<IniFileValue> > 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<QString, QExplicitlySharedDataPointer<IniFileSection> > 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<IniFileSection> 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<IniFileValue> getParameter(QFile & file, IniFileSection & section, QString const & paramName);
+
+};
+
+
+} // namespace eVaf::Common::Internal
+} // namespace eVaf::Common
+} // namespace eVaf
+
+#endif // inifile_p.h