1 /***************************************************************************
2 KraftDB.cpp -
3 -------------------
4 begin : Die Feb 3 2004
5 copyright : (C) 2004 by Klaas Freitag
6 email : freitag@kde.org
7 ***************************************************************************/
8
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17 #include <QDebug>
18
19 #include <QFile>
20 #include <QSqlQuery>
21 #include <QStringList>
22 #include <QRegExp>
23 #include <QTextStream>
24 #include <QSqlError>
25 #include <QDir>
26 #include <QDebug>
27 #include <QDomDocument>
28 #include <QDomElement>
29 #include <QTimer>
30
31 #include "version.h"
32 #include "kraftdb.h"
33 #include "doctype.h"
34 #include "dbids.h"
35 #include "defaultprovider.h"
36 #include "archiveman.h"
37 #include "documentsaverdb.h"
38
Q_GLOBAL_STATIC(KraftDB,mSelf)39 Q_GLOBAL_STATIC(KraftDB, mSelf)
40
41
42 SqlCommand::SqlCommand()
43 {
44
45 }
46
SqlCommand(const QString & cmd,const QString & msg,bool mayfail)47 SqlCommand::SqlCommand(const QString& cmd, const QString& msg, bool mayfail)
48 : mSql(cmd),
49 mMessage(msg),
50 mMayFail(mayfail)
51 {
52 if( !mMessage.isEmpty() && !mMessage.endsWith(';') ) {
53 mMessage.append(';');
54 }
55 if( !mSql.isEmpty() && !mSql.endsWith(';') ) {
56 mSql.append(';');
57 }
58 }
59
message()60 QString SqlCommand::message()
61 {
62 return mMessage;
63 }
64
command()65 QString SqlCommand::command()
66 {
67 return mSql;
68 }
69
mayfail()70 bool SqlCommand::mayfail()
71 {
72 return mMayFail;
73 }
74
75 // ============================
76
SqlCommandList()77 SqlCommandList::SqlCommandList()
78 :QList<SqlCommand>(),
79 _number(0)
80 {
81
82 }
83
setNumber(int no)84 void SqlCommandList::setNumber(int no)
85 {
86 _number = no;
87 }
88
number()89 int SqlCommandList::number()
90 {
91 return _number;
92 }
93
setMetaAddDocTypeList(QList<MetaDocTypeAdd> list)94 void SqlCommandList::setMetaAddDocTypeList( QList<MetaDocTypeAdd> list )
95 {
96 _docTypeMetaList = list;
97 }
98
metaAddDocTypeList() const99 QList<MetaDocTypeAdd> SqlCommandList::metaAddDocTypeList() const
100 {
101 return _docTypeMetaList;
102 }
103 // ==========================================================================
104
KraftDB()105 KraftDB::KraftDB()
106 :QObject (), mParent(nullptr),
107 mSuccess( true ),
108 EuroTag( QString::fromLatin1( "%EURO" ) ),
109 mInitDialog(nullptr),
110 _amountOfDocs(-1),
111 _amountOfArchs(-1),
112 _emitDBChangeSignal(true)
113 {
114 // Attention: Before setup assistant rewrite, dbConnect() was called here.
115 // Keep that in mind, maybe the auto connect to the DB now misses somewhere.
116 // dbConnect();
117 }
118
dbConnect(const QString & driver,const QString & dbName,const QString & dbUser,const QString & dbHost,const QString & dbPasswd)119 bool KraftDB::dbConnect( const QString& driver, const QString& dbName,
120 const QString& dbUser, const QString& dbHost,
121 const QString& dbPasswd )
122 {
123 mSuccess = true;
124
125 mDatabaseDriver = driver;
126 mDatabaseName = dbName;
127
128 if( mDatabaseDriver.isEmpty() ) {
129 // qDebug () << "Database Driver is not specified, check katalog settings";
130 mSuccess = false;
131 return false;
132 } else {
133 // qDebug () << "Using database Driver " << mDatabaseDriver;
134 }
135
136 QStringList list = QSqlDatabase::drivers();
137 if( list.size() == 0 ) {
138 // qDebug () << "Database Drivers could not be loaded." << endl;
139 mSuccess = false ;
140 } else {
141 if( list.indexOf( mDatabaseDriver ) == -1 ) {
142 // qDebug () << "Database Driver " << mDatabaseDriver << " could not be loaded!" << endl;
143 mSuccess = false;
144 }
145 }
146
147 if( mSuccess && m_db.isValid() ) {
148 m_db.close();
149 }
150
151 if( mSuccess ) {
152 m_db = QSqlDatabase::addDatabase( mDatabaseDriver );
153
154 if ( ! m_db.isValid() || m_db.isOpenError() )
155 {
156 qDebug() << "Failed to connect to the database driver: "
157 << m_db.lastError().text() << endl;
158 mSuccess = false;
159 }
160 }
161
162 if ( mSuccess ) {
163 int re = 0;
164 if(mDatabaseDriver == "QMYSQL") {
165 int port = -1; // use the default port so far
166 // FIXME: get port from user interface
167 // qDebug () << "Try to open MySQL database " << name << endl;
168 re = checkConnect( dbHost, dbName , dbUser, dbPasswd, port);
169 } else if(mDatabaseDriver == "QSQLITE") {
170 // SqlLite only requires a valid file name which comes in as Database Name
171 // qDebug () << "Try to open SqLite database " << name << endl;
172 re = checkConnect( QString(), dbName, QString(), QString(), -1);
173 }
174 if ( re == 0 ) {
175 // Database successfully opened; we can now issue SQL commands.
176 // qDebug () << "** Database opened successfully" << endl;
177 } else {
178 // qDebug () << "## Could not open database" << endl;
179 mSuccess = false;
180 }
181 }
182
183 bool detectChanges {true};
184
185 connect( &_timer, &QTimer::timeout, this, &KraftDB::slotCheckDocDatabaseChanged);
186
187 if (mSuccess && detectChanges) {
188 _timer.start(10*1000);
189 }
190 return mSuccess;
191 }
192
self()193 KraftDB *KraftDB::self()
194 {
195 return mSelf;
196 }
197
close()198 void KraftDB::close()
199 {
200 QString name = m_db.connectionName();
201 // qDebug () << "Database connection name to close: " << name;
202
203 m_db.close();
204 }
205
isSqlite()206 bool KraftDB::isSqlite()
207 {
208 const QString dbDriver = qtDriver().toUpper();
209
210 return (dbDriver.startsWith("QSQLITE"));
211 }
212
checkConnect(const QString & host,const QString & dbName,const QString & user,const QString & pwd,int port)213 int KraftDB::checkConnect( const QString& host, const QString& dbName,
214 const QString& user, const QString& pwd, int port )
215 {
216 mDatabaseName = dbName;
217 // works for both mysql and sqlite if the filename for sqlite comes in
218 // as parameter two
219 if ( dbName.isEmpty() || !(m_db.isValid()) ) return false;
220 m_db.setHostName( host );
221 m_db.setDatabaseName( dbName );
222 m_db.setUserName( user );
223 m_db.setPassword( pwd );
224 if( port > -1 ) {
225 m_db.setPort(port);
226 }
227 int re = 0;
228
229 m_db.open();
230 if ( m_db.isOpenError() ) {
231 qDebug () << "ERR opening the db: " << m_db.lastError().text() <<
232 ", type is " << m_db.lastError().type() << endl;
233 re = m_db.lastError().type();
234 }
235 return re;
236 }
237
lastError()238 QSqlError KraftDB::lastError()
239 {
240 return m_db.lastError();
241 }
242
getLastInsertID()243 dbID KraftDB::getLastInsertID()
244 {
245 if(! ( m_db.isValid()) ) return 0;
246
247 QSqlQuery query;
248 if( mDatabaseDriver.toLower() == "qmysql" ) {
249 query.prepare("SELECT LAST_INSERT_ID()");
250 query.exec();
251 } else if( mDatabaseDriver.toLower() == "qsqlite") {
252 query.prepare( "SELECT last_insert_rowid()");
253 query.exec();
254 } else {
255 // qDebug () << "############# FATAL ERROR: Unknown database driver " << mDatabaseDriver;
256 }
257 int id = -1;
258
259 if( query.next() ) {
260 id = query.value(0).toInt();
261 } else {
262 // qDebug () << "############# FATAL ERROR: Query for last insert id is invalid!";
263 }
264 // qDebug () << "Last Insert ID: " << id;
265 return dbID(id);
266 }
267
databaseName() const268 QString KraftDB::databaseName() const
269 {
270 return mDatabaseName;
271 }
272
databaseExists()273 bool KraftDB::databaseExists()
274 {
275 bool re = false;
276
277 if(!m_db.isOpen()) {
278 m_db.open();
279 }
280 if(m_db.isOpen()) {
281 const QStringList t = m_db.tables();
282 re = t.contains( "kraftsystem");
283 }
284 return re;
285 }
286
setSchemaVersion(const QString & versionStr)287 void KraftDB::setSchemaVersion( const QString& versionStr )
288 {
289 QSqlQuery q;
290 q.prepare( "UPDATE kraftsystem SET dbSchemaVersion=:id" );
291 q.bindValue(":id", versionStr );
292 q.exec();
293 }
294
parseCommandFile(int currentVersion)295 SqlCommandList KraftDB::parseCommandFile( int currentVersion )
296 {
297 SqlCommandList list;
298 const QString& file = QString("%1_dbmigrate.sql").arg(currentVersion);
299 list = parseCommandFile(file);
300 list.setMetaAddDocTypeList( parseMetaFile(currentVersion) );
301 list.setNumber(currentVersion);
302 return list;
303 }
304
parseCommandFile(const QString & file)305 SqlCommandList KraftDB::parseCommandFile( const QString& file )
306 {
307 QString sqlFile;
308 QString env = QString::fromUtf8( qgetenv( "KRAFT_HOME" ) );
309 if( !env.isEmpty() && env.right(1) != QDir::separator () ) {
310 env += QDir::separator ();
311 }
312
313 QString driverPrefix = "mysql"; // Default on mysql
314 if( mDatabaseDriver.toLower() == "qsqlite") {
315 driverPrefix = "sqlite3";
316 }
317
318 // qDebug() << "XXXXXXXXXX: " << stdDirs.resourceDirs("data");
319
320 if( env.isEmpty() ) {
321 // Environment-Variable is empty, search in KDE paths
322 QString fragment = QString("kraft/dbmigrate/%1/%2").arg(driverPrefix).arg(file );
323 sqlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, fragment );
324 // qDebug () << "Searching for this fragment: " << fragment;
325 // search in dbcreate as well.
326 if ( sqlFile.isEmpty() ) {
327 fragment = QString("kraft/dbinit/%1/%2").arg(driverPrefix).arg(file );
328 // qDebug () << "Also searching in " << fragment;
329 sqlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, fragment );
330 }
331 } else {
332 // read from environment variable path
333 QString envPath = QString( "%1/database/%2/%3").arg(env).arg(driverPrefix).arg(file);
334 // qDebug () << "Environment variable KRAFT_HOME set, searching for DB setup files in " << envPath;
335 if( QFile::exists( envPath ) ) {
336 sqlFile = envPath;
337 } else if( QFile::exists( QString( "%1/database/%2/migration/%3").arg(env).arg(driverPrefix).arg(file ) ) ){
338 sqlFile = QString( "%1/database/%2/migration/%3").arg(env).arg(driverPrefix).arg(file );
339 }
340 }
341
342 SqlCommandList retList;
343
344 if ( ! sqlFile.isEmpty() ) {
345 // qDebug () << "Opening migration file " << sqlFile << endl;
346
347 QFile f( sqlFile );
348 if ( !f.exists() ) {
349 qDebug() << "FATAL: File" << sqlFile << "does not exist!";
350 }
351 if ( !f.open( QIODevice::ReadOnly ) ) {
352 qDebug () << "FATAL: Could not open " << sqlFile << endl;
353 } else {
354 QTextStream ts( &f );
355 ts.setCodec("UTF-8");
356
357 QString allSql = ts.readAll(); //Not sure of this one!
358 QStringList sqlList = allSql.split(";");
359
360 QRegExp reg( "\\s*(#|--)\\s*message:? ?(.*)\\s*\\n" );
361 QRegExp failreg( "\\s*(#|--)\\s*mayfail\\s*\\n" );
362 reg.setMinimal( true );
363
364 QListIterator<QString> it(sqlList);
365
366 while( it.hasNext() ) {
367 QString msg, command;
368
369 QString sqlFragment = it.next().trimmed();
370
371 int pos = reg.indexIn( sqlFragment.toLower(), 0 );
372 if ( pos > -1 ) {
373 msg = reg.cap( 2 );
374 // qDebug() << "SQL-Commands-Parser: Msg: >" << msg << "<" << endl;
375 }
376
377 bool mayfail = false;
378 pos = failreg.indexIn( sqlFragment.toLower(), 0 );
379 if( pos > -1 ) {
380 mayfail = true;
381 }
382 bool clean = false;
383 while( ! clean ) {
384 if( sqlFragment.startsWith("#") || sqlFragment.startsWith("--") ) {
385 // remove the comment line.
386 int newLinePos = sqlFragment.indexOf('\n');
387 // qDebug() << "Found newline in <" << sqlFragment << ">:" << newLinePos;
388 if(newLinePos > 0) {
389 sqlFragment = sqlFragment.remove( 0, 1+sqlFragment.indexOf('\n') );
390 } else {
391 sqlFragment = QString();
392 }
393 // qDebug() << "Left over SQL Fragment:" << sqlFragment;
394 } else {
395 clean = true;
396 }
397 }
398
399 if( !sqlFragment.isEmpty() ) {
400
401 if( sqlFragment.startsWith( "CREATE TRIGGER", Qt::CaseInsensitive )) {
402 // Triggers contain a ; which scares the parser. In case of triggers we pull
403 // the next item in the list which should be the END; keyword.
404 command = sqlFragment + ";";
405 if( it.hasNext())
406 command += it.next();
407 } else {
408 // ordinary command, we take it as it is.
409 command = sqlFragment;
410 }
411 if( !command.isEmpty() ) {
412 retList.append( SqlCommand( command, msg, mayfail ) );
413 }
414 }
415 }
416 }
417 } else {
418 qDebug () << "ERR: Can not find sql file " << file;
419 }
420
421
422
423 return retList;
424 }
425
parseMetaFile(int currentVersion)426 QList<MetaDocTypeAdd> KraftDB::parseMetaFile( int currentVersion )
427 {
428 const QString fileName = QString("%1_meta.xml").arg(currentVersion);
429
430 QString env = QString::fromUtf8( qgetenv( "KRAFT_HOME" ) );
431 if( !env.isEmpty() && env.right(1) != QDir::separator () ) {
432 env += QDir::separator ();
433 }
434 QString xmlFile;
435 if( !env.isEmpty() ) {
436 xmlFile = env + QLatin1String("database/meta/") + fileName;
437 } else {
438 const QString fragment = QString("kraft/meta/") + fileName;
439 xmlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, fragment );
440 }
441 QFile f( xmlFile );
442 MetaXMLParser parser;
443 if( f.exists() ) {
444 if ( !f.open( QIODevice::ReadOnly ) ) {
445 qDebug () << "FATAL: Could not open " << xmlFile << endl;
446 } else {
447 QTextStream ts( &f );
448 ts.setCodec("UTF-8");
449 parser.parse( &f );
450 }
451 } else {
452 qDebug() << "XML Metafile" << xmlFile << "does not exist!";
453 }
454
455 return parser.metaDocTypeAddList();
456 }
457
processSqlCommands(const SqlCommandList & commands)458 int KraftDB::processSqlCommands( const SqlCommandList& commands )
459 {
460 int cnt = 0;
461
462 // first do the doctype definitions
463 QList<MetaDocTypeAdd> newDocTypes = commands.metaAddDocTypeList();
464
465 // loop over all doctypes first, later loop again to create the followers.
466 // The followers might reference each other and thus must exist.
467 for( auto newDocType : newDocTypes ) {
468 const QString name = newDocType.name();
469 DocType type(name, true);
470
471 for( QString attr : newDocType._attribs.keys() ) {
472 type.setAttribute(attr, newDocType._attribs[attr]);
473 }
474 type.save();
475 }
476
477 // now loop again to process the followers
478 for( auto newDocType : newDocTypes ) {
479 const QString name = newDocType.name();
480 if( newDocType._follower.count() > 0 ) {
481 DocType type(name, true);
482 type.setAllFollowers(newDocType._follower);
483 type.save();
484 }
485 }
486
487 foreach( SqlCommand cmd, commands ) {
488 if( !cmd.message().isEmpty() ) {
489 emit statusMessage( cmd.message() );
490 }
491
492 if( !cmd.command().isEmpty() ) {
493 bool res = true;
494 QSqlQuery q;
495 q.clear();
496 res = q.exec(cmd.command()) || cmd.mayfail();
497
498 if ( res ) {
499 // qDebug () << "Successful SQL Command: " << cmd.command() << endl;
500 cnt ++;
501 } else {
502 QSqlError err = q.lastError();
503 res = false;
504 qDebug () << "###### Failed SQL Command " << cmd.command() << ": " << err.text() << endl;
505 }
506 q.clear();
507 emit processedSqlCommand( res );
508
509 }
510 }
511 return cnt;
512 }
513
requiredSchemaVersion()514 int KraftDB::requiredSchemaVersion()
515 {
516 return KRAFT_REQUIRED_SCHEMA_VERSION;
517 }
518
currentSchemaVersion()519 int KraftDB::currentSchemaVersion()
520 {
521 QSqlQuery query;
522
523 query.exec("SELECT dbschemaversion FROM kraftsystem"); //We'll retrieve every record
524
525 int re = -1;
526 if ( query.next() ) {
527 re = query.value(0).toInt();
528 }
529 return re;
530 }
531
qtDriver()532 QString KraftDB::qtDriver()
533 {
534 return mDatabaseDriver;
535 }
536
currentTimeStamp(const QDateTime & dt)537 QString KraftDB::currentTimeStamp( const QDateTime& dt )
538 {
539 QString dateStr;
540 if( dt.isValid() ) {
541 dateStr = dt.toString(Qt::ISODate);
542 } else {
543 dateStr = QDateTime::currentDateTime().toString(Qt::ISODate);
544 }
545 return dateStr;
546 }
547
mysqlEuroEncode(const QString & str) const548 QString KraftDB::mysqlEuroEncode( const QString& str ) const
549 {
550 QChar euro( 0x20ac );
551 QString restr( str );
552 return restr.replace( euro, EuroTag );
553 }
554
mysqlEuroDecode(const QString & str) const555 QString KraftDB::mysqlEuroDecode( const QString& str ) const
556 {
557 QChar euro( 0x20ac );
558 QString restr( str );
559 return restr.replace( EuroTag, euro );
560 }
561
wordList(const QString & selector,StringMap replaceMap)562 QStringList KraftDB::wordList( const QString& selector, StringMap replaceMap )
563 {
564 QStringList re;
565 QSqlQuery query;
566
567 query.prepare("SELECT category, word FROM wordLists WHERE category=:cat");
568 query.bindValue(":cat", selector);
569 query.exec();
570 while ( query.next() ) {
571 re << replaceTagsInWord( query.value(1).toString(), replaceMap );
572 }
573 re.sort();
574 return re;
575 }
576
replaceTagsInWord(const QString & w,StringMap replaceMap) const577 QString KraftDB::replaceTagsInWord( const QString& w, StringMap replaceMap ) const
578 {
579 QString re( w );
580
581 QMap<int, QStringList> reMap;
582 StringMap::Iterator it;
583 for ( it = replaceMap.begin(); it != replaceMap.end(); ++it ) {
584 reMap[it.key().length()] << it.key();
585 }
586
587 QMap<int, QStringList>::Iterator reIt;
588 for ( reIt = reMap.end(); reIt != reMap.begin(); ) {
589 --reIt;
590 QStringList keys = reIt.value();
591 // qDebug () << "PP: " << keys;
592 for ( QStringList::Iterator dtIt = keys.begin(); dtIt != keys.end(); ++dtIt ) {
593 QString repKey = *dtIt;
594 re.replace( repKey, replaceMap[repKey] );
595 }
596 }
597
598 // qDebug () << "Adding to wordlist <" << re << ">";
599
600 return re;
601 }
602
writeWordList(const QString & listName,const QStringList & list)603 void KraftDB::writeWordList( const QString& listName, const QStringList& list )
604 {
605 // qDebug () << "Saving " << list[0] << " into list " << listName << endl;
606 QSqlQuery qd;
607 qd.prepare( "DELETE FROM wordLists WHERE category=:catName" );
608 qd.bindValue( ":catName", listName );
609 qd.exec();
610
611 QSqlQuery qi;
612 qi.prepare( "INSERT INTO wordLists (category, word) VALUES( :category, :entry )" );
613
614 qi.bindValue( ":category", listName );
615 for ( QStringList::ConstIterator it = list.begin(); it != list.end(); ++it ) {
616 qi.bindValue( ":entry", *it );
617 qi.exec();
618 }
619 }
620
checkTableExistsSqlite(const QString & name,const QStringList & lookupCols)621 bool KraftDB::checkTableExistsSqlite(const QString& name, const QStringList& lookupCols)
622 {
623 const QString query = QString("PRAGMA table_info(%1)").arg(name);
624 QSqlQuery q(query);
625 QStringList cols = lookupCols;
626
627 q.exec();
628 QSqlError err = q.lastError();
629 if( err.isValid() ) {
630 qDebug() << "Error: " << err.text();
631 }
632
633 while( q.next() ) {
634 const QString colName = q.value(1).toString();
635 qDebug() << "checking colum" << colName;
636 cols.removeAll(colName);
637 }
638 return cols.isEmpty();
639 }
640
~KraftDB()641 KraftDB::~KraftDB()
642 {
643 }
644
slotCheckDocDatabaseChanged()645 void KraftDB::slotCheckDocDatabaseChanged()
646 {
647 bool changed{false};
648 {
649 QSqlQuery q("SELECT count(*) FROM document");
650
651 q.exec();
652 QSqlError err = q.lastError();
653 if( err.isValid() ) {
654 qDebug() << "Error: " << err.text();
655 return;
656 }
657
658 if ( q.next() ) {
659 bool ok;
660 int cnt = q.value(0).toInt(&ok);
661
662 if (_amountOfDocs != -1 && cnt != _amountOfDocs ) {
663 qDebug() << "Docs from" << _amountOfDocs << "to" << cnt;
664 changed = true;
665 }
666 _amountOfDocs = cnt;
667 }
668 }
669 {
670 QSqlQuery qArch("SELECT count(*) FROM archdoc");
671
672 qArch.exec();
673 QSqlError err = qArch.lastError();
674 if( err.isValid() ) {
675 qDebug() << "Error: " << err.text();
676 return;
677 }
678
679 if ( qArch.next() ) {
680 bool ok;
681 int cnt = qArch.value(0).toInt(&ok);
682
683 if (_amountOfArchs != -1 && cnt != _amountOfArchs) {
684 qDebug() << "Arched docs from" << _amountOfArchs << "to" << cnt;
685 changed = true;
686 }
687 _amountOfArchs = cnt;
688 }
689 }
690 if (changed && _emitDBChangeSignal) emit docDatabaseChanged();
691 }
692
archiveDocument(KraftDoc * docPtr)693 dbID KraftDB::archiveDocument( KraftDoc *docPtr )
694 {
695 dbID archID = ArchiveMan::self()->archiveDocument( docPtr );
696
697 if (archID.isOk()) {
698 _emitDBChangeSignal = false; // block sending of the signal
699 slotCheckDocDatabaseChanged();
700 _emitDBChangeSignal = true;
701 }
702
703 return archID;
704 }
705
loadDocument(const QString & id,KraftDoc * docPtr)706 void KraftDB::loadDocument(const QString& id, KraftDoc *docPtr)
707 {
708 DocumentSaverDB loader;
709
710 loader.load(id, docPtr);
711 }
712
saveDocument(KraftDoc * docPtr)713 bool KraftDB::saveDocument(KraftDoc *docPtr)
714 {
715 bool res {false};
716 DocumentSaverDB saver;
717
718 if (docPtr) {
719 res = saver.saveDocument(docPtr);
720 if (res) {
721 _emitDBChangeSignal = false; // block sending of the signal
722 slotCheckDocDatabaseChanged();
723 _emitDBChangeSignal = true;
724 }
725 }
726
727 return res;
728 }
729
730