Added CSV parsing and export of Symbian-format Event logs that have had their tables...
[qwerkisync] / EventParsers / CSVSymbianEventLogParser.cpp
index 43142f1..91855e1 100644 (file)
  * <http://www.gnu.org/licenses/>.
  */
 
+#include "CSV.h"
 #include "CSVSymbianEventLogParser.h"
 #include "EventTypes/PhoneCall.h"
 #include "Settings.h"
 
 #include <QDebug>
 
+#include <QDir>
 #include <QFile>
+#include <QFileInfo>
 #include <QString>
 #include <QStringList>
 
@@ -51,170 +54,136 @@ iEventParser *CSVSymbianEventLogParser::IsValid(const Settings &currentSettings,
 {
        qDebug() << "Checking if a CSV call log file...";
 
-       QTextStream stream(&eventFile);
+       CSV eventsReader;
+       eventsReader.Open(eventFile);
 
-       QString firstLineContent(stream.readLine());
-       eventFile.seek(0);
-       if(firstLineContent.length() > 0)
-       {
-               // Count the non-alphanumeric characters used
-               QHash<QChar, uint> counts;
-               foreach(const QChar c, firstLineContent)
-                       ++counts[c];
-
-               QList<QPair<QChar, uint> > orderedCounts;
-               orderedCounts.reserve(counts.size());
-               foreach(const QChar c, counts.keys())
-                       if(!QChar(c).isLetterOrNumber())
-                               orderedCounts.append(QPair<QChar, uint>(c, counts.value(c)));
-
-               qSort(orderedCounts.begin(), orderedCounts.end(), SortByValueDesc());
-
-               // Work around Q_FOREACH macro limitation when dealing with
-               // multi-typed templates (comma issue)
-               typedef QPair<QChar, uint> bodge;
-               foreach(bodge count, orderedCounts)
-                       qDebug() << count.first << " = " << count.second;
-
-               QChar delim;
-               // No-one would be mad enough to use quotation marks or apostrophes
-               // as their delimiter,but just in case, check the second most
-               // frequent character is present thr right number of times for
-               // the qutation marks to be present on every column heading (two
-               // per heading, less one as they're seperators)
-               if((orderedCounts.value(0).first == '"' || orderedCounts.value(0).first == '\'')
-                       && ((orderedCounts.value(0).second / 2) - 1 == orderedCounts.value(1).second ))
-               {
-                       // We're good.
-                       delim = orderedCounts.value(1).first;
-               }
-               else
-                       delim = orderedCounts.value(0).first;
+       // If we found all of the required headings, continue
+       if(eventsReader.HasRequiredHeadings(QStringList() << "EType" << "ETime"<< "Remote" << "Direction" << "Status" << "Duration" << "Number" << "Data").count() == 0)
+               return new EventParsers::CSVSymbianEventLogParser(currentSettings, eventFile.fileName()/*, delim, numColumnsPerRecord, headingPositions*/);
+       else
+               return NULL;
+}
 
-               // Check we have the essential fields we need, and grab their
-               // column ordering
-               QStringList requiredHeadings;
-               requiredHeadings << "etype" << "etime" << "remote" << "direction"
-                                                << "duration" << "number" << "data";
+CSVSymbianEventLogParser::CSVSymbianEventLogParser(const Settings &settings, const QString &filename/*, const QChar delimiter, const int numColumnsPerRecord, const ColumnIndicesHash &headingIndices*/)
+       : m_Settings(settings)/*, m_Delimiter(delimiter), m_NumColumnsPerRecord(numColumnsPerRecord), m_HeadingIndices(headingIndices)*/
+{
+}
 
-               EventParsers::CSVSymbianEventLogParser::ColumnIndicesHash headingPositions;
-               headingPositions.reserve(requiredHeadings.count());
+EventTypes::EventFromFileList CSVSymbianEventLogParser::ParseFile(QFile &eventFile, const QList<uint> &recordsToReturn)
+{
+       EventTypes::EventFromFileList fileEvents;
 
-               QStringList headings(QString(firstLineContent).split(delim, QString::KeepEmptyParts, Qt::CaseSensitive));
-               int numColumnsPerRecord(headings.count());
-               for(QStringList::size_type i(0); i < headings.count(); ++i)
-               {
-                       QString heading(ExtractString(headings.value(i)));
-                       qDebug() << headings.value(i) << " : " << heading;
+       // Parse secondary table for string values
+       {
+               // Determine filename
+               QFileInfo eventFileInfo(eventFile);
+               QString filePath(eventFileInfo.absolutePath() + "/string.csv");
 
-                       // Check over the required headings
-                       foreach(QString requiredHeading, requiredHeadings)
+               QFile stringsFile(filePath);
+               if(stringsFile.open(QFile::ReadOnly))
+               {
+                       CSV stringsReader;
+                       stringsReader.Open(stringsFile);
+                       if(stringsReader.HasRequiredHeadings(QStringList() << "TEXT" << "Id").count() == 0)
                        {
-                               if(heading.toLower() == requiredHeading)
+                               while(!stringsReader.AtEnd())
                                {
-                                       headingPositions[requiredHeading] = i;
-                                       requiredHeadings.removeOne(requiredHeading);
+                                       QHash<QString, QString> values(stringsReader.ReadRecord());
+                                       Strings().insert(values.value("text").toLower(), values.value("id").toUInt());
                                }
                        }
                }
-
-               // If we found all of the required headings, continue
-               if(requiredHeadings.count() == 0)
-                       return new EventParsers::CSVSymbianEventLogParser(currentSettings, eventFile.fileName(), delim, numColumnsPerRecord, headingPositions);
+               else
+                       throw std::runtime_error(QString("Unable to open: '%1'. The error was: %2").arg(filePath).arg(stringsFile.error()).toStdString());
        }
 
-       return NULL;
-}
-
-CSVSymbianEventLogParser::CSVSymbianEventLogParser(const Settings &settings, const QString &filename, const QChar delimiter, const int numColumnsPerRecord, const ColumnIndicesHash &headingIndices)
-       : m_Settings(settings), m_Delimiter(delimiter), m_NumColumnsPerRecord(numColumnsPerRecord), m_HeadingIndices(headingIndices)
-{
-}
-
-EventTypes::EventFromFileList CSVSymbianEventLogParser::ParseFile(QFile &eventFile, const QList<uint> &recordsToReturn)
-{
-       qDebug() << "CSV Parsing NYI!";
-       EventTypes::EventFromFileList fileEvents;
-       //return fileEvents;
-
        QSet<uint> recordsToReturnSet(QSet<uint>::fromList(recordsToReturn));
 
-       uint lineNumber(0);
-       uint recordNumber(0);
-       eventFile.seek(0);
-
-       // Read the first line
-       QTextStream stream(&eventFile);
-       QString firstLineContent(stream.readLine());
-       QStringList firstLineValues(QString(firstLineContent).split(m_Delimiter));
-       if(firstLineValues.count() != m_NumColumnsPerRecord)
-               throw new std::runtime_error(QString("Unexpected number of columns (%1, expected %2) on line %3 of %4")
-                       .arg(firstLineValues.count())
-                       .arg(m_NumColumnsPerRecord)
-                       .arg(lineNumber)
-                       .arg(eventFile.fileName()).toStdString());
-       ++lineNumber;
-
-       // Read the main body of the file
-       while(!stream.atEnd())
+       CSV eventsReader;
+       eventsReader.Open(eventFile);
+       if(eventsReader.HasRequiredHeadings(QStringList() << "EType" << "ETime"
+                                                                               << "Remote" << "Direction" << "Status"
+                                                                               << "Duration" << "Number" << "Data").count() == 0)
        {
-               QStringList lineValues(QString(stream.readLine()).split(m_Delimiter));
-               ++lineNumber;
-               // Make sure we have enough columns (i.e. handle newlines in values)
-               while(lineValues.count() < m_NumColumnsPerRecord)
+               while(!eventsReader.AtEnd())
                {
-                       lineValues.append(QString(stream.readLine()).split(m_Delimiter));
-                       ++lineNumber;
-               }
+                       QHash<QString, QString> recordValues(eventsReader.ReadRecord());
 
-               if(recordsToReturnSet.count() == 0 || recordsToReturnSet.contains(recordNumber))
-               {
-                       bool bOK(false);
-                       int eType(lineValues.at(m_HeadingIndices.value("etype")).toUInt(&bOK));
-                       // We're only interested in phone calls
-                       if(bOK && eType == 0)
+                       if(recordsToReturnSet.count() == 0 || recordsToReturnSet.contains(eventsReader.CurrentRecordNumber()))
                        {
-                               qDebug() << "Parsing event from line #" << lineNumber << ". Values: " << lineValues;
-
-                               QDateTime eTime(QDateTime::fromString(lineValues.at(m_HeadingIndices.value("etime")), "dd/MM/yyyy h:mm:ss ap"));
-                               Settings::eDirection direction(lineValues.at(m_HeadingIndices.value("direction")) == "0"
-                                       ? Settings::INCOMING
-                                       : Settings::OUTGOING);
-
-                               // We only care about the requested directions...
-                               if(CurrentSettings().ShouldProcess(direction, EventTypes::EVENT_TYPE_CALL))
+                               bool bOK(false);
+                               int eType(recordValues.value("etype").toUInt(&bOK));
+                               // We're only interested in phone calls
+                               if(bOK && eType == 0)
                                {
-                                       int duration(lineValues.at(m_HeadingIndices.value("duration")).toInt(&bOK));
-                                       if(!bOK)
-                                       {
-                                               qDebug() << QString("Unable to parse '%1' as a duration. Skipping record.")
-                                                                               .arg(lineValues.at(m_HeadingIndices.value("duration")));
-                                               continue;
-                                       }
-                                       QString number(ExtractString(lineValues.at(m_HeadingIndices.value("number"))));
-                                       // TODO: Not currently used...but almost certainly contains SIP call data
-                                       QString data(ExtractString(lineValues.at(m_HeadingIndices.value("data"))));
+                                       qDebug() << "Parsing event from record #" << eventsReader.CurrentRecordNumber() << ". Values: " << recordValues;
+
+                                       QDateTime eTime(QDateTime::fromString(recordValues.value("etime"), "dd/MM/yyyy h:mm:ss ap"));
+                                       Settings::eDirection direction(ReadDirection(recordValues.value("direction").toUInt()));
 
-                                       if(number.trimmed().length() == 0)
+                                       // We only care about the requested directions...
+                                       if(CurrentSettings().ShouldProcess(direction, EventTypes::EVENT_TYPE_CALL))
                                        {
-                                               qDebug() << "Empty tel!";
+                                               int duration(recordValues.value("duration").toInt(&bOK));
+                                               if(!bOK)
+                                               {
+                                                       qDebug() << QString("Unable to parse '%1' as a duration. Skipping record.")
+                                                                                       .arg(recordValues.value("duration"));
+                                                       continue;
+                                               }
+
+                                               QString number(ExtractString(recordValues.value("number")));
+
+                                               bool isMissedCall(ExtractString(recordValues.value("direction")).toUInt() == Strings().value("missed call"));
+
+                                               uint test = ExtractString(recordValues.value("direction")).toUInt();
+                                               if(test != Strings().value("incoming") && test != Strings().value("outgoing"))
+                                                       qDebug() << eventFile.fileName() << ", line #" << eventsReader.CurrentRecordNumber() << "Direction? : " << test;
+
+                                               // TODO: Not currently used...but almost certainly contains SIP call data
+                                               QString data(ExtractString(recordValues.value("data")));
+
+                                               if(number.trimmed().length() == 0)
+                                                       qDebug() << "Empty tel!";
+
+                                               QSharedPointer<EventTypes::iEvent> newEvent(new PhoneCall(
+                                                       CurrentSettings(),
+                                                       direction,
+                                                       eTime,
+                                                       number,
+                                                       duration,
+                                                       isMissedCall));
+                                               fileEvents.append(EventTypes::EventFromFile(newEvent, eventsReader.CurrentRecordNumber()));
                                        }
-
-                                       QSharedPointer<EventTypes::iEvent> newEvent(new PhoneCall(
-                                               CurrentSettings(),
-                                               direction,
-                                               eTime,
-                                               number,
-                                               duration));
-                                       fileEvents.append(EventTypes::EventFromFile(newEvent, recordNumber));
                                }
                        }
                }
-
-               ++recordNumber;
        }
 
        qDebug() << QString("File pos: %1, bAvail: %2, canReadLine: %3").arg(eventFile.pos()).arg(eventFile.bytesAvailable()).arg(eventFile.canReadLine());
        qDebug() << fileEvents.count() << " events loaded from file";
        return fileEvents;
 }
+
+Settings::eDirection CSVSymbianEventLogParser::ReadDirection(const uint value)
+{
+       // Missed calls are
+       if(value == Strings().value("incoming") || value == Strings().value("missed call"))
+               return Settings::INCOMING;
+       else if(value == Strings().value("outgoing"))
+               return Settings::OUTGOING;
+       else
+       {
+               // Determine which string this value corresponds to...
+               QString unexpectedString("");
+               foreach(QString string, Strings().keys())
+                       if(Strings().value(string) == value)
+                               unexpectedString = string;
+
+               throw std::runtime_error(
+                               QString("Unexpected value found: \"%1\" (maps to \"%2\")")
+                               .arg(value)
+                                       .arg(unexpectedString != "" ? unexpectedString : "<no known string>")
+                               .toStdString());
+       }
+}