/* * XML Backend for the OPIE-Contact Database. * * Copyright (c) 2002 by Stefan Eilers (Eilers.Stefan@epost.de) * * ===================================================================== * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * ===================================================================== * ToDo: XML-Backend: Automatic reload if something was changed... * * * ===================================================================== * Version: $Id$ * ===================================================================== * History: * $Log$ * Revision 1.8 2003/08/30 15:28:26 eilers * Removed some unimportant debug output which causes slow down.. * * Revision 1.7 2003/08/01 12:30:16 eilers * Merging changes from BRANCH_1_0 to HEAD * * Revision 1.6 2003/07/07 16:19:47 eilers * Fixing serious bug in hasQuerySettings() * * Revision 1.5 2003/04/13 18:07:10 zecke * More API doc * QString -> const QString& * QString = 0l -> QString::null * * Revision 1.4 2003/03/21 14:32:54 mickeyl * g++ compliance fix: default arguments belong into the declaration, but not the definition * * Revision 1.3 2003/03/21 12:26:28 eilers * Fixing small bug: If we search a birthday from today to today, it returned * every contact .. * * Revision 1.2 2003/03/21 10:33:09 eilers * Merged speed optimized xml backend for contacts to main. * Added QDateTime to querybyexample. For instance, it is now possible to get * all Birthdays/Anniversaries between two dates. This should be used * to show all birthdays in the datebook.. * This change is sourcecode backward compatible but you have to upgrade * the binaries for today-addressbook. * * Revision 1.1.2.2 2003/02/11 12:17:28 eilers * Speed optimization. Removed the sequential search loops. * * Revision 1.1.2.1 2003/02/10 15:31:38 eilers * Writing offsets to debug output.. * * Revision 1.1 2003/02/09 15:05:01 eilers * Nothing happened.. Just some cleanup before I will start.. * * Revision 1.12 2003/01/03 16:58:03 eilers * Reenable debug output * * Revision 1.11 2003/01/03 12:31:28 eilers * Bugfix for calculating data diffs.. * * Revision 1.10 2003/01/02 14:27:12 eilers * Improved query by example: Search by date is possible.. First step * for a today plugin for birthdays.. * * Revision 1.9 2002/12/08 12:48:57 eilers * Moved journal-enum from ocontact into i the xml-backend.. * * Revision 1.8 2002/11/14 17:04:24 eilers * Sorting will now work if fullname is identical on some entries * * Revision 1.7 2002/11/13 15:02:46 eilers * Small Bug in sorted fixed * * Revision 1.6 2002/11/13 14:14:51 eilers * Added sorted for Contacts.. * * Revision 1.5 2002/11/01 15:10:42 eilers * Added regExp-search in database for all fields in a contact. * * Revision 1.4 2002/10/16 10:52:40 eilers * Added some docu to the interface and now using the cache infrastucture by zecke.. :) * * Revision 1.3 2002/10/14 16:21:54 eilers * Some minor interface updates * * Revision 1.2 2002/10/07 17:34:24 eilers * added OBackendFactory for advanced backend access * * Revision 1.1 2002/09/27 17:11:44 eilers * Added API for accessing the Contact-Database ! It is compiling, but * please do not expect that anything is working ! * I will debug that stuff in the next time .. * Please read README_COMPILE for compiling ! * * */ #include "ocontactaccessbackend_xml.h" #include #include #include #include #include #include #include #include #include #include #include "ocontactaccessbackend.h" #include "ocontactaccess.h" #include #include using namespace Opie; OContactAccessBackend_XML::OContactAccessBackend_XML ( const QString& appname, const QString& filename ): m_changed( false ) { // Just m_contactlist should call delete if an entry // is removed. m_contactList.setAutoDelete( true ); m_uidToContact.setAutoDelete( false ); m_appName = appname; /* Set journalfile name ... */ m_journalName = getenv("HOME"); m_journalName +="/.abjournal" + appname; /* Expecting to access the default filename if nothing else is set */ if ( filename.isEmpty() ){ m_fileName = Global::applicationFileName( "addressbook","addressbook.xml" ); } else m_fileName = filename; /* Load Database now */ load (); } bool OContactAccessBackend_XML::save() { if ( !m_changed ) return true; QString strNewFile = m_fileName + ".new"; QFile f( strNewFile ); if ( !f.open( IO_WriteOnly|IO_Raw ) ) return false; int total_written; int idx_offset = 0; QString out; // Write Header out = "\n" " \n" " \n" " \n"; QCString cstr = out.utf8(); f.writeBlock( cstr.data(), cstr.length() ); idx_offset += cstr.length(); out = ""; // Write all contacts QListIterator it( m_contactList ); for ( ; it.current(); ++it ) { // qWarning(" Uid %d at Offset: %x", (*it)->uid(), idx_offset ); out += "save( out ); out += "/>\n"; cstr = out.utf8(); total_written = f.writeBlock( cstr.data(), cstr.length() ); idx_offset += cstr.length(); if ( total_written != int(cstr.length()) ) { f.close(); QFile::remove( strNewFile ); return false; } out = ""; } out += " \n\n"; // Write Footer cstr = out.utf8(); total_written = f.writeBlock( cstr.data(), cstr.length() ); if ( total_written != int( cstr.length() ) ) { f.close(); QFile::remove( strNewFile ); return false; } f.close(); // move the file over, I'm just going to use the system call // because, I don't feel like using QDir. if ( ::rename( strNewFile.latin1(), m_fileName.latin1() ) < 0 ) { qWarning( "problem renaming file %s to %s, errno: %d", strNewFile.latin1(), m_journalName.latin1(), errno ); // remove the tmp file... QFile::remove( strNewFile ); } /* The journalfile should be removed now... */ removeJournal(); m_changed = false; return true; } bool OContactAccessBackend_XML::load () { m_contactList.clear(); m_uidToContact.clear(); /* Load XML-File and journal if it exists */ if ( !load ( m_fileName, false ) ) return false; /* The returncode of the journalfile is ignored due to the * fact that it does not exist when this class is instantiated ! * But there may such a file exist, if the application crashed. * Therefore we try to load it to get the changes before the # * crash happened... */ load (m_journalName, true); return true; } void OContactAccessBackend_XML::clear () { m_contactList.clear(); m_uidToContact.clear(); m_changed = false; } bool OContactAccessBackend_XML::wasChangedExternally() { QFileInfo fi( m_fileName ); QDateTime lastmod = fi.lastModified (); return (lastmod != m_readtime); } QArray OContactAccessBackend_XML::allRecords() const { QArray uid_list( m_contactList.count() ); uint counter = 0; QListIterator it( m_contactList ); for( ; it.current(); ++it ){ uid_list[counter++] = (*it)->uid(); } return ( uid_list ); } OContact OContactAccessBackend_XML::find ( int uid ) const { OContact foundContact; //Create empty contact OContact* found = m_uidToContact.find( QString().setNum( uid ) ); if ( found ){ foundContact = *found; } return ( foundContact ); } QArray OContactAccessBackend_XML::queryByExample ( const OContact &query, int settings, const QDateTime& d ) { QArray m_currentQuery( m_contactList.count() ); QListIterator it( m_contactList ); uint arraycounter = 0; for( ; it.current(); ++it ){ /* Search all fields and compare them with query object. Store them into list * if all fields matches. */ QDate* queryDate = 0l; QDate* checkDate = 0l; bool allcorrect = true; for ( int i = 0; i < Qtopia::Groups; i++ ) { // Birthday and anniversary are special nonstring fields and should // be handled specially switch ( i ){ case Qtopia::Birthday: queryDate = new QDate( query.birthday() ); checkDate = new QDate( (*it)->birthday() ); case Qtopia::Anniversary: if ( queryDate == 0l ){ queryDate = new QDate( query.anniversary() ); checkDate = new QDate( (*it)->anniversary() ); } if ( queryDate->isValid() ){ if( checkDate->isValid() ){ if ( settings & OContactAccess::DateYear ){ if ( queryDate->year() != checkDate->year() ) allcorrect = false; } if ( settings & OContactAccess::DateMonth ){ if ( queryDate->month() != checkDate->month() ) allcorrect = false; } if ( settings & OContactAccess::DateDay ){ if ( queryDate->day() != checkDate->day() ) allcorrect = false; } if ( settings & OContactAccess::DateDiff ) { QDate current; // If we get an additional date, we // will take this date instead of // the current one.. if ( !d.date().isValid() ) current = QDate::currentDate(); else current = d.date(); // We have to equalize the year, otherwise // the search will fail.. checkDate->setYMD( current.year(), checkDate->month(), checkDate->day() ); if ( *checkDate < current ) checkDate->setYMD( current.year()+1, checkDate->month(), checkDate->day() ); // Check whether the birthday/anniversary date is between // the current/given date and the maximum date // ( maximum time range ) ! qWarning("Checking if %s is between %s and %s ! ", checkDate->toString().latin1(), current.toString().latin1(), queryDate->toString().latin1() ); if ( current.daysTo( *queryDate ) >= 0 ){ if ( !( ( *checkDate >= current ) && ( *checkDate <= *queryDate ) ) ){ allcorrect = false; qWarning (" Nope!.."); } } } } else{ // checkDate is invalid. Therefore this entry is always rejected allcorrect = false; } } delete queryDate; queryDate = 0l; delete checkDate; checkDate = 0l; break; default: /* Just compare fields which are not empty in the query object */ if ( !query.field(i).isEmpty() ){ switch ( settings & ~( OContactAccess::IgnoreCase | OContactAccess::DateDiff | OContactAccess::DateYear | OContactAccess::DateMonth | OContactAccess::DateDay | OContactAccess::MatchOne ) ){ case OContactAccess::RegExp:{ QRegExp expr ( query.field(i), !(settings & OContactAccess::IgnoreCase), false ); if ( expr.find ( (*it)->field(i), 0 ) == -1 ) allcorrect = false; } break; case OContactAccess::WildCards:{ QRegExp expr ( query.field(i), !(settings & OContactAccess::IgnoreCase), true ); if ( expr.find ( (*it)->field(i), 0 ) == -1 ) allcorrect = false; } break; case OContactAccess::ExactMatch:{ if (settings & OContactAccess::IgnoreCase){ if ( query.field(i).upper() != (*it)->field(i).upper() ) allcorrect = false; }else{ if ( query.field(i) != (*it)->field(i) ) allcorrect = false; } } break; } } } } if ( allcorrect ){ m_currentQuery[arraycounter++] = (*it)->uid(); } } // Shrink to fit.. m_currentQuery.resize(arraycounter); return m_currentQuery; } QArray OContactAccessBackend_XML::matchRegexp( const QRegExp &r ) const { QArray m_currentQuery( m_contactList.count() ); QListIterator it( m_contactList ); uint arraycounter = 0; for( ; it.current(); ++it ){ if ( (*it)->match( r ) ){ m_currentQuery[arraycounter++] = (*it)->uid(); } } // Shrink to fit.. m_currentQuery.resize(arraycounter); return m_currentQuery; } const uint OContactAccessBackend_XML::querySettings() { return ( OContactAccess::WildCards | OContactAccess::IgnoreCase | OContactAccess::RegExp | OContactAccess::ExactMatch | OContactAccess::DateDiff | OContactAccess::DateYear | OContactAccess::DateMonth | OContactAccess::DateDay ); } bool OContactAccessBackend_XML::hasQuerySettings (uint querySettings) const { /* OContactAccess::IgnoreCase, DateDiff, DateYear, DateMonth, DateDay * may be added with any of the other settings. IgnoreCase should never used alone. * Wildcards, RegExp, ExactMatch should never used at the same time... */ // Step 1: Check whether the given settings are supported by this backend if ( ( querySettings & ( OContactAccess::IgnoreCase | OContactAccess::WildCards | OContactAccess::DateDiff | OContactAccess::DateYear | OContactAccess::DateMonth | OContactAccess::DateDay | OContactAccess::RegExp | OContactAccess::ExactMatch ) ) != querySettings ) return false; // Step 2: Check whether the given combinations are ok.. // IngoreCase alone is invalid if ( querySettings == OContactAccess::IgnoreCase ) return false; // WildCards, RegExp and ExactMatch should never used at the same time switch ( querySettings & ~( OContactAccess::IgnoreCase | OContactAccess::DateDiff | OContactAccess::DateYear | OContactAccess::DateMonth | OContactAccess::DateDay ) ){ case OContactAccess::RegExp: return ( true ); case OContactAccess::WildCards: return ( true ); case OContactAccess::ExactMatch: return ( true ); case 0: // one of the upper removed bits were set.. return ( true ); default: return ( false ); } } // Currently only asc implemented.. QArray OContactAccessBackend_XML::sorted( bool asc, int , int , int ) { QMap nameToUid; QStringList names; QArray m_currentQuery( m_contactList.count() ); // First fill map and StringList with all Names // Afterwards sort namelist and use map to fill array to return.. QListIterator it( m_contactList ); for( ; it.current(); ++it ){ names.append( (*it)->fileAs() + QString::number( (*it)->uid() ) ); nameToUid.insert( (*it)->fileAs() + QString::number( (*it)->uid() ), (*it)->uid() ); } names.sort(); int i = 0; if ( asc ){ for ( QStringList::Iterator it = names.begin(); it != names.end(); ++it ) m_currentQuery[i++] = nameToUid[ (*it) ]; }else{ for ( QStringList::Iterator it = names.end(); it != names.begin(); --it ) m_currentQuery[i++] = nameToUid[ (*it) ]; } return m_currentQuery; } bool OContactAccessBackend_XML::add ( const OContact &newcontact ) { //qWarning("odefaultbackend: ACTION::ADD"); updateJournal (newcontact, ACTION_ADD); addContact_p( newcontact ); m_changed = true; return true; } bool OContactAccessBackend_XML::replace ( const OContact &contact ) { m_changed = true; OContact* found = m_uidToContact.find ( QString().setNum( contact.uid() ) ); if ( found ) { OContact* newCont = new OContact( contact ); updateJournal ( *newCont, ACTION_REPLACE); m_contactList.removeRef ( found ); m_contactList.append ( newCont ); m_uidToContact.remove( QString().setNum( contact.uid() ) ); m_uidToContact.insert( QString().setNum( newCont->uid() ), newCont ); qWarning("Nur zur Sicherheit: %d == %d ?",contact.uid(), newCont->uid()); return true; } else return false; } bool OContactAccessBackend_XML::remove ( int uid ) { m_changed = true; OContact* found = m_uidToContact.find ( QString().setNum( uid ) ); if ( found ) { updateJournal ( *found, ACTION_REMOVE); m_contactList.removeRef ( found ); m_uidToContact.remove( QString().setNum( uid ) ); return true; } else return false; } bool OContactAccessBackend_XML::reload(){ /* Reload is the same as load in this implementation */ return ( load() ); } void OContactAccessBackend_XML::addContact_p( const OContact &newcontact ) { OContact* contRef = new OContact( newcontact ); m_contactList.append ( contRef ); m_uidToContact.insert( QString().setNum( newcontact.uid() ), contRef ); } /* This function loads the xml-database and the journalfile */ bool OContactAccessBackend_XML::load( const QString filename, bool isJournal ) { /* We use the time of the last read to check if the file was * changed externally. */ if ( !isJournal ){ QFileInfo fi( filename ); m_readtime = fi.lastModified (); } const int JOURNALACTION = Qtopia::Notes + 1; const int JOURNALROW = JOURNALACTION + 1; bool foundAction = false; journal_action action = ACTION_ADD; int journalKey = 0; QMap contactMap; QMap customMap; QMap::Iterator customIt; QAsciiDict dict( 47 ); dict.setAutoDelete( TRUE ); dict.insert( "Uid", new int(Qtopia::AddressUid) ); dict.insert( "Title", new int(Qtopia::Title) ); dict.insert( "FirstName", new int(Qtopia::FirstName) ); dict.insert( "MiddleName", new int(Qtopia::MiddleName) ); dict.insert( "LastName", new int(Qtopia::LastName) ); dict.insert( "Suffix", new int(Qtopia::Suffix) ); dict.insert( "FileAs", new int(Qtopia::FileAs) ); dict.insert( "Categories", new int(Qtopia::AddressCategory) ); dict.insert( "DefaultEmail", new int(Qtopia::DefaultEmail) ); dict.insert( "Emails", new int(Qtopia::Emails) ); dict.insert( "HomeStreet", new int(Qtopia::HomeStreet) ); dict.insert( "HomeCity", new int(Qtopia::HomeCity) ); dict.insert( "HomeState", new int(Qtopia::HomeState) ); dict.insert( "HomeZip", new int(Qtopia::HomeZip) ); dict.insert( "HomeCountry", new int(Qtopia::HomeCountry) ); dict.insert( "HomePhone", new int(Qtopia::HomePhone) ); dict.insert( "HomeFax", new int(Qtopia::HomeFax) ); dict.insert( "HomeMobile", new int(Qtopia::HomeMobile) ); dict.insert( "HomeWebPage", new int(Qtopia::HomeWebPage) ); dict.insert( "Company", new int(Qtopia::Company) ); dict.insert( "BusinessStreet", new int(Qtopia::BusinessStreet) ); dict.insert( "BusinessCity", new int(Qtopia::BusinessCity) ); dict.insert( "BusinessState", new int(Qtopia::BusinessState) ); dict.insert( "BusinessZip", new int(Qtopia::BusinessZip) ); dict.insert( "BusinessCountry", new int(Qtopia::BusinessCountry) ); dict.insert( "BusinessWebPage", new int(Qtopia::BusinessWebPage) ); dict.insert( "JobTitle", new int(Qtopia::JobTitle) ); dict.insert( "Department", new int(Qtopia::Department) ); dict.insert( "Office", new int(Qtopia::Office) ); dict.insert( "BusinessPhone", new int(Qtopia::BusinessPhone) ); dict.insert( "BusinessFax", new int(Qtopia::BusinessFax) ); dict.insert( "BusinessMobile", new int(Qtopia::BusinessMobile) ); dict.insert( "BusinessPager", new int(Qtopia::BusinessPager) ); dict.insert( "Profession", new int(Qtopia::Profession) ); dict.insert( "Assistant", new int(Qtopia::Assistant) ); dict.insert( "Manager", new int(Qtopia::Manager) ); dict.insert( "Spouse", new int(Qtopia::Spouse) ); dict.insert( "Children", new int(Qtopia::Children) ); dict.insert( "Gender", new int(Qtopia::Gender) ); dict.insert( "Birthday", new int(Qtopia::Birthday) ); dict.insert( "Anniversary", new int(Qtopia::Anniversary) ); dict.insert( "Nickname", new int(Qtopia::Nickname) ); dict.insert( "Notes", new int(Qtopia::Notes) ); dict.insert( "action", new int(JOURNALACTION) ); dict.insert( "actionrow", new int(JOURNALROW) ); //qWarning( "OContactDefaultBackEnd::loading %s", filename.latin1() ); XMLElement *root = XMLElement::load( filename ); if(root != 0l ){ // start parsing /* Parse all XML-Elements and put the data into the * Contact-Class */ XMLElement *element = root->firstChild(); //qWarning("OContactAccess::load tagName(): %s", root->tagName().latin1() ); element = element->firstChild(); /* Search Tag "Contacts" which is the parent of all Contacts */ while( element && !isJournal ){ if( element->tagName() != QString::fromLatin1("Contacts") ){ //qWarning ("OContactDefBack::Searching for Tag \"Contacts\"! Found: %s", // element->tagName().latin1()); element = element->nextChild(); } else { element = element->firstChild(); break; } } /* Parse all Contacts and ignore unknown tags */ while( element ){ if( element->tagName() != QString::fromLatin1("Contact") ){ //qWarning ("OContactDefBack::Searching for Tag \"Contact\"! Found: %s", // element->tagName().latin1()); element = element->nextChild(); continue; } /* Found alement with tagname "contact", now parse and store all * attributes contained */ //qWarning("OContactDefBack::load element tagName() : %s", // element->tagName().latin1() ); QString dummy; foundAction = false; XMLElement::AttributeMap aMap = element->attributes(); XMLElement::AttributeMap::Iterator it; contactMap.clear(); customMap.clear(); for( it = aMap.begin(); it != aMap.end(); ++it ){ // qWarning ("Read Attribute: %s=%s", it.key().latin1(),it.data().latin1()); int *find = dict[ it.key() ]; /* Unknown attributes will be stored as "Custom" elements */ if ( !find ) { // qWarning("Attribute %s not known.", it.key().latin1()); //contact.setCustomField(it.key(), it.data()); customMap.insert( it.key(), it.data() ); continue; } /* Check if special conversion is needed and add attribute * into Contact class */ switch( *find ) { /* case Qtopia::AddressUid: contact.setUid( it.data().toInt() ); break; case Qtopia::AddressCategory: contact.setCategories( Qtopia::Record::idsFromString( it.data( ))); break; */ case JOURNALACTION: action = journal_action(it.data().toInt()); foundAction = true; qWarning ("ODefBack(journal)::ACTION found: %d", action); break; case JOURNALROW: journalKey = it.data().toInt(); break; default: // no conversion needed add them to the map contactMap.insert( *find, it.data() ); break; } } /* now generate the Contact contact */ OContact contact( contactMap ); for (customIt = customMap.begin(); customIt != customMap.end(); ++customIt ) { contact.setCustomField( customIt.key(), customIt.data() ); } if (foundAction){ foundAction = false; switch ( action ) { case ACTION_ADD: addContact_p (contact); break; case ACTION_REMOVE: if ( !remove (contact.uid()) ) qWarning ("ODefBack(journal)::Unable to remove uid: %d", contact.uid() ); break; case ACTION_REPLACE: if ( !replace ( contact ) ) qWarning ("ODefBack(journal)::Unable to replace uid: %d", contact.uid() ); break; default: qWarning ("Unknown action: ignored !"); break; } }else{ /* Add contact to list */ addContact_p (contact); } /* Move to next element */ element = element->nextChild(); } }else { qWarning("ODefBack::could not load"); } delete root; qWarning("returning from loading" ); return true; } void OContactAccessBackend_XML::updateJournal( const OContact& cnt, journal_action action ) { QFile f( m_journalName ); bool created = !f.exists(); if ( !f.open(IO_WriteOnly|IO_Append) ) return; QString buf; QCString str; // if the file was created, we have to set the Tag "" to // get a XML-File which is readable by our parser. // This is just a cheat, but better than rewrite the parser. if ( created ){ buf = ""; QCString cstr = buf.utf8(); f.writeBlock( cstr.data(), cstr.length() ); } buf = "