1 /***************************************************************************
2  * SPDX-FileCopyrightText: 2021 S. MANKOWSKI stephane@mankowski.fr
3  * SPDX-FileCopyrightText: 2021 G. DE BURE support@mankowski.fr
4  * SPDX-License-Identifier: GPL-3.0-or-later
5  ***************************************************************************/
6 /** @file
7  * This file defines classes SKGObjectBase.
8  *
9  * @author Stephane MANKOWSKI / Guillaume DE BURE
10  */
11 #include "skgobjectbase.h"
12 
13 #include <qsqldatabase.h>
14 #include "qregularexpression.h"
15 
16 #include <klocalizedstring.h>
17 #include <kstringhandler.h>
18 
19 #include <algorithm>
20 
21 #include "skgdocument.h"
22 #include "skgnamedobject.h"
23 #include "skgservices.h"
24 #include "skgtraces.h"
25 
26 /**
27  * This private class of SKGObjectBase
28  */
29 class SKGObjectBasePrivate
30 {
31 public:
32     /**
33      * internal id
34      */
35     int id = 0;
36 
37     /**
38      * internal table
39      */
40     QString table;
41 
42     /**
43      * internal document
44      */
45     SKGDocument* document = nullptr;
46 
47     /**
48      * internal attributes
49      */
50     SKGQStringQStringMap attributes;
51 
52     /**
53      * objects to save
54      */
55     SKGObjectBase::SKGListSKGObjectBase objects;
56 };
57 
SKGObjectBase()58 SKGObjectBase::SKGObjectBase() : SKGObjectBase(nullptr)
59 {}
60 
SKGObjectBase(SKGDocument * iDocument,const QString & iTable,int iID)61 SKGObjectBase::SKGObjectBase(SKGDocument* iDocument, const QString& iTable, int iID)
62     : d(new SKGObjectBasePrivate)
63 {
64     d->id = iID;
65     d->table = iTable;
66     d->document = iDocument;
67     if (Q_LIKELY(d->id != 0)) {
68         load();
69     }
70 }
71 
~SKGObjectBase()72 SKGObjectBase::~SKGObjectBase()
73 {
74     delete d;
75 }
76 
SKGObjectBase(const SKGObjectBase & iObject)77 SKGObjectBase::SKGObjectBase(const SKGObjectBase& iObject)
78     : d(new SKGObjectBasePrivate)
79 {
80     copyFrom(iObject);
81 }
82 
SKGObjectBase(SKGObjectBase && iObject)83 SKGObjectBase::SKGObjectBase(SKGObjectBase&& iObject) noexcept
84     : d(iObject.d)
85 {
86     iObject.d = nullptr;
87 }
88 
operator =(const SKGObjectBase & iObject)89 SKGObjectBase& SKGObjectBase::operator= (const SKGObjectBase& iObject)
90 {
91     copyFrom(iObject);
92     return *this;
93 }
94 
operator ==(const SKGObjectBase & iObject) const95 bool SKGObjectBase::operator==(const SKGObjectBase& iObject) const
96 {
97     return (getUniqueID() == iObject.getUniqueID());
98 }
99 
operator !=(const SKGObjectBase & iObject) const100 bool SKGObjectBase::operator!=(const SKGObjectBase& iObject) const
101 {
102     return !(*this == iObject);
103 }
104 
operator <(const SKGObjectBase & iObject) const105 bool SKGObjectBase::operator<(const SKGObjectBase& iObject) const
106 {
107     double d1 = SKGServices::stringToDouble(getAttribute(QStringLiteral("f_sortorder")));
108     double d2 = SKGServices::stringToDouble(iObject.getAttribute(QStringLiteral("f_sortorder")));
109     return (d1 < d2);
110 }
111 
operator >(const SKGObjectBase & iObject) const112 bool SKGObjectBase::operator>(const SKGObjectBase& iObject) const
113 {
114     double d1 = SKGServices::stringToDouble(getAttribute(QStringLiteral("f_sortorder")));
115     double d2 = SKGServices::stringToDouble(iObject.getAttribute(QStringLiteral("f_sortorder")));
116     return (d1 > d2);
117 }
118 
getUniqueID() const119 QString SKGObjectBase::getUniqueID() const
120 {
121     return SKGServices::intToString(d->id) % '-' % getRealTable();
122 }
123 
getID() const124 int SKGObjectBase::getID() const
125 {
126     return d->id;
127 }
128 
getAttributeFromView(const QString & iView,const QString & iName) const129 QString SKGObjectBase::getAttributeFromView(const QString& iView, const QString& iName) const
130 {
131     QString output;
132 
133     SKGStringListList result;
134     QString wc = getWhereclauseId();
135     if (wc.isEmpty()) {
136         wc = "id=" % SKGServices::intToString(d->id);
137     }
138     QString sql = "SELECT " % iName % " FROM " % iView % " WHERE " % wc;
139     if (getDocument() != nullptr) {
140         getDocument()->executeSelectSqliteOrder(sql, result, false);
141     }
142     if (result.count() == 2) {
143         output = result.at(1).at(0);
144     }
145 
146     return output;
147 }
148 
getDisplayName() const149 QString SKGObjectBase::getDisplayName() const
150 {
151     return getAttributeFromView("v_" % getRealTable() % "_displayname", QStringLiteral("t_displayname"));
152 }
153 
copyFrom(const SKGObjectBase & iObject)154 void SKGObjectBase::copyFrom(const SKGObjectBase& iObject)
155 {
156     if (d == nullptr) {
157         d = new SKGObjectBasePrivate;
158     }
159     d->id = iObject.d->id;
160     d->table = iObject.d->table;
161     d->document = iObject.d->document;
162     d->attributes = iObject.d->attributes;
163 }
164 
cloneInto()165 SKGObjectBase SKGObjectBase::cloneInto()
166 {
167     return cloneInto(nullptr);
168 }
169 
cloneInto(SKGDocument * iDocument)170 SKGObjectBase SKGObjectBase::cloneInto(SKGDocument* iDocument)
171 {
172     SKGDocument* targetDocument = iDocument;
173     if (targetDocument == nullptr) {
174         targetDocument = d->document;
175     }
176 
177     SKGObjectBase output;
178     output.copyFrom(*this);
179     output.d->id = 0;
180     output.d->document = targetDocument;
181     return output;
182 }
183 
resetID()184 SKGError SKGObjectBase::resetID()
185 {
186     d->id = 0;
187     return SKGError();
188 }
189 
getTable() const190 QString SKGObjectBase::getTable() const
191 {
192     return d->table;
193 }
194 
getRealTable() const195 QString SKGObjectBase::getRealTable() const
196 {
197     return SKGServices::getRealTable(d->table);
198 }
199 
getDocument() const200 SKGDocument* SKGObjectBase::getDocument() const
201 {
202     return d->document;
203 }
204 
getAttributes() const205 SKGQStringQStringMap SKGObjectBase::getAttributes() const
206 {
207     return d->attributes;
208 }
209 
getNbAttributes() const210 int SKGObjectBase::getNbAttributes() const
211 {
212     return d->attributes.count();
213 }
214 
setAttributes(const QStringList & iNames,const QStringList & iValues)215 SKGError SKGObjectBase::setAttributes(const QStringList& iNames, const QStringList& iValues)
216 {
217     SKGError err;
218     int nb = iNames.size();
219     for (int i = 0; !err && i < nb; ++i) {
220         const auto& att = iNames.at(i);
221         const auto& val = iValues.at(i);
222 
223         if (Q_LIKELY(att != QStringLiteral("id"))) {
224             err = setAttribute(att, val);
225         } else {
226             d->id = SKGServices::stringToInt(val);
227         }
228     }
229     return err;
230 }
231 
setAttribute(const QString & iName,const QString & iValue)232 SKGError SKGObjectBase::setAttribute(const QString& iName, const QString& iValue)
233 {
234     SKGError err;
235     if (Q_LIKELY(iValue != NOUPDATE)) {
236         QString val = iValue;
237 
238         // Case modification on pointed document
239         if (this->getRealTable() == this->getTable() && iName != iName.toLower()) {
240             QString realAttribute = d->document->getRealAttribute(iName);
241             if (!realAttribute.isEmpty()) {
242                 QStringList l = realAttribute.split('.');
243                 if (l.size() == 3) {
244                     SKGObjectBase obj;
245                     QString refId = getAttribute(l.at(1));
246                     if (!refId.isEmpty()) {
247                         err = getDocument()->getObject("v_" % l.at(0), "id=" % refId, obj);
248                         IFOK(err) {
249                             err = obj.setAttribute(l.at(2), iValue);
250                             d->objects.push_back(obj);
251                         }
252                     }
253                 }
254             }
255         } else if (iValue.startsWith(QLatin1String("="))) {
256             // Case modificator
257             QString op = iValue.right(iValue.length() - 1).toLower();
258             val = d->attributes[iName];
259             if (op == i18nc("Key word to modify a string into a field", "lower")) {
260                 val = val.toLower();
261             } else if (op == i18nc("Key word to modify a string into a field", "upper")) {
262                 val = val.toUpper();
263             } else if (op == i18nc("Key word to modify a string into a field", "capwords")) {
264                 val = KStringHandler::capwords(val.toLower());
265             } else if (op == i18nc("Key word to modify a string into a field", "capitalize")) {
266                 val = val.at(0).toUpper() % val.right(val.length() - 1).toLower();
267             } else if (op == i18nc("Key word to modify a string into a field", "trim")) {
268                 val = val.trimmed();
269             } else {
270                 val = iValue;
271             }
272         }
273         d->attributes[iName] = val;
274     }
275     return err;
276 }
277 
getAttribute(const QString & iName) const278 QString SKGObjectBase::getAttribute(const QString& iName) const
279 {
280     QString output;
281     if (Q_LIKELY(d->attributes.contains(iName))) {
282         output = d->attributes[iName];
283     } else if (iName == QStringLiteral("id")) {
284         output = SKGServices::intToString(getID());
285     }  if (iName.startsWith(QLatin1String("p_"))) {
286         output = getProperty(iName.right(iName.length() - 2));
287     } else {
288         // Is the iName a number ?
289         bool ok;
290         int pos = iName.toInt(&ok);
291         if (ok) {
292             // What is the key corresponding to this name ?
293             QStringList keys = d->attributes.keys();
294             std::sort(keys.begin(), keys.end());
295             if (pos >= 0 && pos < keys.count()) {
296                 output = d->attributes[keys.at(pos)];
297             }
298         } else {
299             // Case modification on pointed document iName1.(iTable2)iName2 or iName1.iName2
300             int pos = iName.indexOf('.');
301             if (pos != -1) {
302                 QString attributeref = iName.left(pos);
303                 QString refId = getAttribute(attributeref);
304                 if (!refId.isEmpty()) {
305                     QString rest = iName.right(iName.length() - pos - 1);
306 
307                     QString table;
308                     QString att;
309                     auto rx = QRegularExpression(QStringLiteral("^\\(([^\\)]*)\\)(.*)$")).match(rest);
310                     if (rx.hasMatch()) {
311                         // Case iName1.(iTable2)iName2
312                         table = rx.captured(1);
313                         att = rx.captured(2);
314                     } else {
315                         auto rx2 = QRegularExpression(QStringLiteral("^.*_(.*)_.*$")).match(attributeref);
316                         if (rx2.hasMatch()) {
317                             // Case iName1.iName2
318                             table = "v_" % rx2.captured(1);
319                             att = rest;
320                         }
321                     }
322 
323                     SKGObjectBase obj;
324                     SKGError err = getDocument()->getObject(table, "id=" % refId, obj);
325                     IFOK(err) {
326                         output = obj.getAttribute(att);
327                     }
328                 }
329             }
330         }
331     }
332 
333     return output;
334 }
335 
exist() const336 bool SKGObjectBase::exist() const
337 {
338     SKGTRACEINFUNC(20)
339 
340     SKGStringListList result;
341     QString wc = getWhereclauseId();
342     if (wc.isEmpty() && d->id != 0) {
343         wc = "id=" % SKGServices::intToString(d->id);
344     }
345     if (wc.isEmpty()) {
346         return false;
347     }
348 
349     QString sql = "SELECT count(1) FROM " % d->table % " WHERE " % wc;
350     if (getDocument() != nullptr) {
351         getDocument()->executeSelectSqliteOrder(sql, result, false);
352     }
353     return (result.size() >= 2 && result.at(1).at(0) != QStringLiteral("0"));
354 }
355 
load()356 SKGError SKGObjectBase::load()
357 {
358     SKGError err;
359     SKGTRACEINFUNCRC(20, err)
360 
361     if (Q_LIKELY(getDocument() && !getTable().isEmpty())) {
362         // Prepare where clause
363         QString wc = getWhereclauseId();
364         if (wc.isEmpty()) {
365             wc = "id=" % SKGServices::intToString(d->id);
366         }
367 
368         // Execute sql order
369         SKGStringListList result;
370         err = getDocument()->executeSelectSqliteOrder("SELECT * FROM " % d->table % " WHERE " % wc, result, false);
371         IFOK(err) {
372             int size = result.size();
373             if (Q_UNLIKELY(size == 1)) {
374                 err = SKGError(ERR_INVALIDARG, i18nc("Error message: Could not load something because it is not in the database", "Load of '%1' with '%2' failed because it was not found in the database", d->table, wc));
375             } else if (Q_UNLIKELY(size != 2)) {
376                 err = SKGError(ERR_INVALIDARG, i18np("Load of '%2' with '%3' failed because of bad size of result (found one object)",
377                                                      "Load of '%2' with '%3' failed because of bad size of result (found %1 objects)",
378                                                      size - 1, d->table, wc));
379             } else {
380                 SKGStringListList::const_iterator itrow = result.constBegin();
381                 QStringList columns = *(itrow);
382                 ++itrow;
383                 QStringList values = *(itrow);
384                 err = setAttributes(columns, values);
385             }
386         }
387     }
388     return err;
389 }
390 
getWhereclauseId() const391 QString SKGObjectBase::getWhereclauseId() const
392 {
393     int id = getID();
394     if (id != 0) {
395         return "id=" % SKGServices::intToString(id);
396     }
397     return QLatin1String("");
398 }
399 
save(bool iInsertOrUpdate,bool iReloadAfterSave)400 SKGError SKGObjectBase::save(bool iInsertOrUpdate, bool iReloadAfterSave)
401 {
402     SKGError err;
403     SKGTRACEINFUNCRC(20, err)
404 
405     if (Q_UNLIKELY(!d->document)) {
406         err = SKGError(ERR_POINTER, i18nc("Error message", "Operation impossible because the document is missing"));
407     } else {
408         // Save linking objects
409         int nb = d->objects.count();
410         for (int i = 0; !err && i < nb; ++i) {
411             SKGObjectBase ref = d->objects.at(i);
412             err = ref.save(iInsertOrUpdate, iReloadAfterSave);
413         }
414 
415         // Check if we are in a transaction
416         IFOKDO(err, d->document->checkExistingTransaction())
417         IFOK(err) {
418             // Table to use
419             QString tablename = getRealTable();
420 
421             // Build order
422             QString part1Insert;
423             QString part2Insert;
424             QString partUpdate;
425 
426             SKGQStringQStringMap::const_iterator it;
427             for (it = d->attributes.constBegin() ; it != d->attributes.constEnd(); ++it) {
428                 QString att = SKGServices::stringToSqlString(it.key());
429                 QString attlower = att.toLower();
430                 if (att.length() > 2 && att == attlower) {  // We must ignore attributes coming from views
431                     QString value = '\'' % SKGServices::stringToSqlString(it.value()) % '\'';
432 
433                     if (!part1Insert.isEmpty()) {
434                         part1Insert.append(',');
435                         part2Insert.append(',');
436                         partUpdate.append(',');
437                     }
438                     // Attribute
439                     part1Insert.append('\'' % att % '\'');
440 
441                     // Value
442                     part2Insert.append(value);
443 
444                     // Attribute=Value for update
445                     partUpdate.append(att % '=' % value);
446                 }
447             }
448 
449             // We try an Insert
450             if (d->id == 0) {
451                 // We have to try un insert
452                 err = getDocument()->executeSqliteOrder("INSERT INTO " % tablename % " (" % part1Insert % ") VALUES (" % part2Insert % ')', &(d->id));
453             } else {
454                 // We must try an update
455                 err = SKGError(ERR_ABORT, QLatin1String(""));  // Just to go in UPDATE code
456             }
457 
458             if (err && iInsertOrUpdate) {
459                 // INSERT failed, could we try an update ?
460                 QString wc = this->getWhereclauseId();
461                 if (!wc.isEmpty()) {
462                     // Yes ==> Update
463                     err = getDocument()->executeSqliteOrder("UPDATE " % tablename % " SET " % partUpdate % " WHERE " % wc);
464                 }
465             }
466         }
467     }
468 
469     // Reload object is updated
470     if (!err && iReloadAfterSave) {
471         // The object has been updated ==>load
472         err = load();
473     }
474 
475     return err;
476 }
477 
remove(bool iSendMessage,bool iForce) const478 SKGError SKGObjectBase::remove(bool iSendMessage, bool iForce) const
479 {
480     SKGError err;
481     SKGTRACEINFUNCRC(20, err)
482     if (Q_UNLIKELY(!d->document)) {
483         err = SKGError(ERR_POINTER, i18nc("Error message", "Operation impossible because the document is missing"));
484     } else {
485         // Check if we are in a transaction
486         err = d->document->checkExistingTransaction();
487 
488         // delete order
489         QString viewForDelete = QStringLiteral("v_") % getRealTable() % "_delete";
490 
491         // Check if the delete view exist
492         SKGStringListList temporaryResult;
493         d->document->executeSelectSqliteOrder("PRAGMA table_info( " % viewForDelete % " );", temporaryResult);
494         if (!iForce && temporaryResult.count() > 1) {  // At least one attribute
495             // Delete view exists, check if the delete is authorized
496             err = d->document->executeSelectSqliteOrder("SELECT t_delete_message FROM " % viewForDelete % " WHERE id=" % SKGServices::intToString(d->id), temporaryResult, false);
497             IFOK(err) {
498                 QString msg;
499                 if (temporaryResult.count() > 1) {
500                     msg = temporaryResult.at(1).at(0);
501                 }
502                 // Should the string below be translated ??? It contains no word
503                 if (!msg.isEmpty()) {
504                     err = SKGError(ERR_FORCEABLE, i18nc("Error message for an object", "'%1': %2", getDisplayName(), msg));
505                 }
506             }
507         }
508 
509         QString displayname = getDisplayName();  // Must be done before the delete order
510         IFOKDO(err, d->document->executeSqliteOrder("DELETE FROM " % getRealTable() % " WHERE id=" % SKGServices::intToString(d->id)))
511         if (iSendMessage && !err && !displayname.isEmpty()) {
512             err = d->document->sendMessage(i18nc("An information to the user that something was deleted", "'%1' has been deleted", displayname), SKGDocument::Hidden);
513         }
514     }
515 
516     return err;
517 }
518 
dump() const519 SKGError SKGObjectBase::dump() const
520 {
521     // dump
522     SKGTRACE << "=== START DUMP [" << getUniqueID() << "]===" << SKGENDL;
523     SKGQStringQStringMap::const_iterator it;
524     for (it = d->attributes.constBegin() ; it != d->attributes.constEnd(); ++it) {
525         SKGTRACE << it.key() << "=[" << it.value() << ']' << SKGENDL;
526     }
527     SKGTRACE << "=== END DUMP [" << getUniqueID() << "]===" << SKGENDL;
528     return SKGError();
529 }
530 
getProperties() const531 QStringList SKGObjectBase::getProperties() const
532 {
533     return Q_UNLIKELY(!getDocument()) ? QStringList() : getDocument()->getParameters(getUniqueID());
534 }
535 
getProperty(const QString & iName) const536 QString SKGObjectBase::getProperty(const QString& iName) const
537 {
538     return Q_UNLIKELY(!getDocument()) ? QString() : getDocument()->getParameter(iName, getUniqueID());
539 }
540 
getPropertyObject(const QString & iName) const541 SKGObjectBase SKGObjectBase::getPropertyObject(const QString& iName) const
542 {
543     SKGObjectBase property;
544     if (getDocument() != nullptr) {
545         getDocument()->getObject(QStringLiteral("parameters"), "t_name='" % SKGServices::stringToSqlString(iName) %
546                                  "' AND t_uuid_parent='" % SKGServices::stringToSqlString(getUniqueID()) % '\'', property);
547     }
548     return property;
549 }
550 
getPropertyBlob(const QString & iName) const551 QVariant SKGObjectBase::getPropertyBlob(const QString& iName) const
552 {
553     return Q_UNLIKELY(!getDocument()) ? QVariant() : getDocument()->getParameterBlob(iName, getUniqueID());
554 }
555 
setProperty(const QString & iName,const QString & iValue,const QString & iFileName,SKGPropertyObject * oObjectCreated) const556 SKGError SKGObjectBase::setProperty(const QString& iName, const QString& iValue, const QString& iFileName, SKGPropertyObject* oObjectCreated) const
557 {
558     SKGError err = Q_UNLIKELY(!getDocument()) ? SKGError() : getDocument()->setParameter(iName, iValue, iFileName, getUniqueID(), oObjectCreated);
559 
560     // Send message
561     IFOKDO(err, Q_UNLIKELY(!getDocument()) ? SKGError() :  getDocument()->sendMessage(i18nc("An information to the user", "The property '%1=%2' has been added on '%3'", iName, iValue, getDisplayName()), SKGDocument::Hidden))
562 
563     return err;
564 }
565 
setProperty(const QString & iName,const QString & iValue,const QVariant & iBlob,SKGPropertyObject * oObjectCreated) const566 SKGError SKGObjectBase::setProperty(const QString& iName, const QString& iValue, const QVariant& iBlob, SKGPropertyObject* oObjectCreated) const
567 {
568     SKGError err = Q_UNLIKELY(!getDocument()) ? SKGError() :  getDocument()->setParameter(iName, iValue, iBlob, getUniqueID(), oObjectCreated);
569 
570     // Send message
571     IFOKDO(err, Q_UNLIKELY(!getDocument()) ? SKGError() :  getDocument()->sendMessage(i18nc("An information to the user", "The property '%1=%2' has been added on '%3'", iName, iValue, getDisplayName()), SKGDocument::Hidden))
572 
573     return err;
574 }
575