1 /***************************************************************************
2  *   Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "file.h"
19 
20 #include <QFile>
21 #include <QTextStream>
22 #include <QIODevice>
23 #include <QStringList>
24 
25 #ifdef HAVE_KF5
26 #include <KSharedConfig>
27 #include <KConfigGroup>
28 #endif // HAVE_KF5
29 
30 #include "preferences.h"
31 #include "entry.h"
32 #include "element.h"
33 #include "macro.h"
34 #include "comment.h"
35 #include "preamble.h"
36 #include "logging_data.h"
37 
38 const QString File::Url = QStringLiteral("Url");
39 const QString File::Encoding = QStringLiteral("Encoding");
40 const QString File::StringDelimiter = QStringLiteral("StringDelimiter");
41 const QString File::QuoteComment = QStringLiteral("QuoteComment");
42 const QString File::KeywordCasing = QStringLiteral("KeywordCasing");
43 const QString File::ProtectCasing = QStringLiteral("ProtectCasing");
44 const QString File::NameFormatting = QStringLiteral("NameFormatting");
45 const QString File::ListSeparator = QStringLiteral("ListSeparator");
46 
47 const quint64 valid = Q_UINT64_C(0x08090a0b0c0d0e0f);
48 const quint64 invalid = Q_UINT64_C(0x0102030405060708);
49 
50 class File::FilePrivate
51 {
52 private:
53     quint64 validInvalidField;
54     static const quint64 initialInternalIdCounter;
55     static quint64 internalIdCounter;
56 
57 #ifdef HAVE_KF5
58     KSharedConfigPtr config;
59     const QString configGroupName;
60 #endif // HAVE_KF5
61 
62 public:
63     const quint64 internalId;
64     QHash<QString, QVariant> properties;
65 
66     explicit FilePrivate(File *parent)
67             : validInvalidField(valid),
68 #ifdef HAVE_KF5
69         config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("FileExporterBibTeX")),
70 #endif // HAVE_KF5
71         internalId(++internalIdCounter) {
72         Q_UNUSED(parent)
73         const bool isValid = checkValidity();
74         if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Creating File instance" << internalId << "  Valid?" << isValid;
75 #ifdef HAVE_KF5
76         loadConfiguration();
77 #endif // HAVE_KF5
78     }
79 
80     ~FilePrivate() {
81         const bool isValid = checkValidity();
82         if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Deleting File instance" << internalId << "  Valid?" << isValid;
83         validInvalidField = invalid;
84     }
85 
86     /// Copy-assignment operator
87     FilePrivate &operator= (const FilePrivate &other) {
88         if (this != &other) {
89             validInvalidField = other.validInvalidField;
90             properties = other.properties;
91             const bool isValid = checkValidity();
92             if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Assigning File instance" << other.internalId << "to" << internalId << "  Is other valid?" << other.checkValidity() << "  Self valid?" << isValid;
93         }
94         return *this;
95     }
96 
97     /// Move-assignment operator
98     FilePrivate &operator= (FilePrivate &&other) {
99         if (this != &other) {
100             validInvalidField = std::move(other.validInvalidField);
101             properties = std::move(other.properties);
102             const bool isValid = checkValidity();
103             if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Assigning File instance" << other.internalId << "to" << internalId << "  Is other valid?" << other.checkValidity() << "  Self valid?" << isValid;
104         }
105         return *this;
106     }
107 
108 #ifdef HAVE_KF5
109     void loadConfiguration() {
110         /// Load and set configuration as stored in settings
111         KConfigGroup configGroup(config, configGroupName);
112         properties.insert(File::Encoding, configGroup.readEntry(Preferences::keyEncoding, Preferences::defaultEncoding));
113         properties.insert(File::StringDelimiter, configGroup.readEntry(Preferences::keyStringDelimiter, Preferences::defaultStringDelimiter));
114         properties.insert(File::QuoteComment, static_cast<Preferences::QuoteComment>(configGroup.readEntry(Preferences::keyQuoteComment, static_cast<int>(Preferences::defaultQuoteComment))));
115         properties.insert(File::KeywordCasing, static_cast<KBibTeX::Casing>(configGroup.readEntry(Preferences::keyKeywordCasing, static_cast<int>(Preferences::defaultKeywordCasing))));
116         properties.insert(File::NameFormatting, configGroup.readEntry(Preferences::keyPersonNameFormatting, QString()));
117         properties.insert(File::ProtectCasing, configGroup.readEntry(Preferences::keyProtectCasing, static_cast<int>(Preferences::defaultProtectCasing)));
118         properties.insert(File::ListSeparator, configGroup.readEntry(Preferences::keyListSeparator, Preferences::defaultListSeparator));
119     }
120 #endif // HAVE_KF5
121 
122     bool checkValidity() const {
123         if (validInvalidField != valid) {
124             /// 'validInvalidField' must equal to the know 'valid' value
125             qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << validInvalidField << "!=" << valid;
126             return false;
127         } else if (internalId <= initialInternalIdCounter) {
128             /// Internal id counter starts at initialInternalIdCounter+1
129             qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << internalId << "< " << (initialInternalIdCounter + 1);
130             return false;
131         } else if (internalId > 600000) {
132             /// Reasonable assumption: not more that 500000 ids used
133             qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << internalId << "> 600000";
134             return false;
135         }
136         return true;
137     }
138 };
139 
140 const quint64 File::FilePrivate::initialInternalIdCounter = 99999;
141 quint64 File::FilePrivate::internalIdCounter = File::FilePrivate::initialInternalIdCounter;
142 
143 File::File()
144         : QList<QSharedPointer<Element> >(), d(new FilePrivate(this))
145 {
146     /// nothing
147 }
148 
149 File::File(const File &other)
150         : QList<QSharedPointer<Element> >(other), d(new FilePrivate(this))
151 {
152     d->operator =(*other.d);
153 }
154 
155 File::File(File &&other)
156         : QList<QSharedPointer<Element> >(std::move(other)), d(new FilePrivate(this))
157 {
158     d->operator =(std::move(*other.d));
159 }
160 
161 
162 File::~File()
163 {
164     Q_ASSERT_X(d->checkValidity(), "File::~File()", "This File object is not valid");
165     delete d;
166 }
167 
168 File &File::operator= (const File &other) {
169     if (this != &other)
170         d->operator =(*other.d);
171     return *this;
172 }
173 
174 File &File::operator= (File &&other) {
175     if (this != &other)
176         d->operator =(std::move(*other.d));
177     return *this;
178 }
179 
180 bool File::operator==(const File &other) const {
181     if (size() != other.size()) return false;
182 
183     for (File::ConstIterator myIt = constBegin(), otherIt = other.constBegin(); myIt != constEnd() && otherIt != constEnd(); ++myIt, ++otherIt) {
184         QSharedPointer<const Entry> myEntry = myIt->dynamicCast<const Entry>();
185         QSharedPointer<const Entry> otherEntry = otherIt->dynamicCast<const Entry>();
186         if ((myEntry.isNull() && !otherEntry.isNull()) || (!myEntry.isNull() && otherEntry.isNull())) return false;
187         if (!myEntry.isNull() && !otherEntry.isNull()) {
188             if (myEntry->operator !=(*otherEntry.data()))
189                 return false;
190         } else {
191             QSharedPointer<const Macro> myMacro = myIt->dynamicCast<const Macro>();
192             QSharedPointer<const Macro> otherMacro = otherIt->dynamicCast<const Macro>();
193             if ((myMacro.isNull() && !otherMacro.isNull()) || (!myMacro.isNull() && otherMacro.isNull())) return false;
194             if (!myMacro.isNull() && !otherMacro.isNull()) {
195                 if (myMacro->operator !=(*otherMacro.data()))
196                     return false;
197             } else {
198                 QSharedPointer<const Preamble> myPreamble = myIt->dynamicCast<const Preamble>();
199                 QSharedPointer<const Preamble> otherPreamble = otherIt->dynamicCast<const Preamble>();
200                 if ((myPreamble.isNull() && !otherPreamble.isNull()) || (!myPreamble.isNull() && otherPreamble.isNull())) return false;
201                 if (!myPreamble.isNull() && !otherPreamble.isNull()) {
202                     if (myPreamble->operator !=(*otherPreamble.data()))
203                         return false;
204                 } else {
205                     QSharedPointer<const Comment> myComment = myIt->dynamicCast<const Comment>();
206                     QSharedPointer<const Comment> otherComment = otherIt->dynamicCast<const Comment>();
207                     if ((myComment.isNull() && !otherComment.isNull()) || (!myComment.isNull() && otherComment.isNull())) return false;
208                     if (!myComment.isNull() && !otherComment.isNull()) {
209                         // TODO right now, don't care if comments are equal
210                         qCDebug(LOG_KBIBTEX_DATA) << "File objects being compared contain comments, ignoring those";
211                     } else {
212                         /// This case should never be reached
213                         qCWarning(LOG_KBIBTEX_DATA) << "Met unhandled case while comparing two File objects";
214                         return false;
215                     }
216                 }
217             }
218         }
219     }
220 
221     return true;
222 }
223 
224 bool File::operator!=(const File &other) const {
225     return !operator ==(other);
226 }
227 
228 const QSharedPointer<Element> File::containsKey(const QString &key, ElementTypes elementTypes) const
229 {
230     if (!d->checkValidity())
231         qCCritical(LOG_KBIBTEX_DATA) << "const QSharedPointer<Element> File::containsKey(const QString &key, ElementTypes elementTypes) const" << "This File object is not valid";
232     for (const auto &element : const_cast<const File &>(*this)) {
233         const QSharedPointer<Entry> entry = elementTypes.testFlag(etEntry) ? element.dynamicCast<Entry>() : QSharedPointer<Entry>();
234         if (!entry.isNull()) {
235             if (entry->id() == key)
236                 return entry;
237         } else {
238             const QSharedPointer<Macro> macro = elementTypes.testFlag(etMacro) ? element.dynamicCast<Macro>() : QSharedPointer<Macro>();
239             if (!macro.isNull()) {
240                 if (macro->key() == key)
241                     return macro;
242             }
243         }
244     }
245 
246     return QSharedPointer<Element>();
247 }
248 
249 QStringList File::allKeys(ElementTypes elementTypes) const
250 {
251     if (!d->checkValidity())
252         qCCritical(LOG_KBIBTEX_DATA) << "QStringList File::allKeys(ElementTypes elementTypes) const" << "This File object is not valid";
253     QStringList result;
254     result.reserve(size());
255     for (const auto &element : const_cast<const File &>(*this)) {
256         const QSharedPointer<Entry> entry = elementTypes.testFlag(etEntry) ? element.dynamicCast<Entry>() : QSharedPointer<Entry>();
257         if (!entry.isNull())
258             result.append(entry->id());
259         else {
260             const QSharedPointer<Macro> macro = elementTypes.testFlag(etMacro) ? element.dynamicCast<Macro>() : QSharedPointer<Macro>();
261             if (!macro.isNull())
262                 result.append(macro->key());
263         }
264     }
265 
266     return result;
267 }
268 
269 QSet<QString> File::uniqueEntryValuesSet(const QString &fieldName) const
270 {
271     if (!d->checkValidity())
272         qCCritical(LOG_KBIBTEX_DATA) << "QSet<QString> File::uniqueEntryValuesSet(const QString &fieldName) const" << "This File object is not valid";
273     QSet<QString> valueSet;
274     const QString lcFieldName = fieldName.toLower();
275 
276     for (const auto &element : const_cast<const File &>(*this)) {
277         const QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
278         if (!entry.isNull())
279             for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it)
280                 if (it.key().toLower() == lcFieldName) {
281                     const auto itValue = it.value();
282                     for (const QSharedPointer<ValueItem> &valueItem : itValue) {
283                         /// Check if ValueItem to process points to a person
284                         const QSharedPointer<Person> person = valueItem.dynamicCast<Person>();
285                         if (!person.isNull()) {
286                             /// Assemble a list of formatting templates for a person's name
287                             static QStringList personNameFormattingList; ///< use static to do pattern assembly only once
288                             if (personNameFormattingList.isEmpty()) {
289                                 /// Use the two default patterns last-name-first and first-name-first
290 #ifdef HAVE_KF5
291                                 personNameFormattingList << Preferences::personNameFormatLastFirst << Preferences::personNameFormatFirstLast;
292                                 /// Check configuration if user-specified formatting template is different
293                                 KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc")));
294                                 KConfigGroup configGroup(config, "General");
295                                 QString personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting);
296                                 /// Add user's template if it differs from the two specified above
297                                 if (!personNameFormattingList.contains(personNameFormatting))
298                                     personNameFormattingList << personNameFormatting;
299 #else // HAVE_KF5
300                                 personNameFormattingList << QStringLiteral("<%l><, %s><, %f>") << QStringLiteral("<%f ><%l>< %s>");
301 #endif // HAVE_KF5
302                             }
303                             /// Add person's name formatted using each of the templates assembled above
304                             for (const QString &personNameFormatting : const_cast<const QStringList &>(personNameFormattingList)) {
305                                 valueSet.insert(Person::transcribePersonName(person.data(), personNameFormatting));
306                             }
307                         } else {
308                             /// Default case: use PlainTextValue::text to translate ValueItem
309                             /// to a human-readable text
310                             valueSet.insert(PlainTextValue::text(*valueItem));
311                         }
312                     }
313                 }
314     }
315 
316     return valueSet;
317 }
318 
319 QStringList File::uniqueEntryValuesList(const QString &fieldName) const
320 {
321     if (!d->checkValidity())
322         qCCritical(LOG_KBIBTEX_DATA) << "QStringList File::uniqueEntryValuesList(const QString &fieldName) const" << "This File object is not valid";
323     QSet<QString> valueSet = uniqueEntryValuesSet(fieldName);
324     QStringList list = valueSet.toList();
325     list.sort();
326     return list;
327 }
328 
329 void File::setProperty(const QString &key, const QVariant &value)
330 {
331     if (!d->checkValidity())
332         qCCritical(LOG_KBIBTEX_DATA) << "void File::setProperty(const QString &key, const QVariant &value)" << "This File object is not valid";
333     d->properties.insert(key, value);
334 }
335 
336 QVariant File::property(const QString &key) const
337 {
338     if (!d->checkValidity())
339         qCCritical(LOG_KBIBTEX_DATA) << "QVariant File::property(const QString &key) const" << "This File object is not valid";
340     return d->properties.contains(key) ? d->properties.value(key) : QVariant();
341 }
342 
343 QVariant File::property(const QString &key, const QVariant &defaultValue) const
344 {
345     if (!d->checkValidity())
346         qCCritical(LOG_KBIBTEX_DATA) << "QVariant File::property(const QString &key, const QVariant &defaultValue) const" << "This File object is not valid";
347     return d->properties.value(key, defaultValue);
348 }
349 
350 bool File::hasProperty(const QString &key) const
351 {
352     if (!d->checkValidity())
353         qCCritical(LOG_KBIBTEX_DATA) << "bool File::hasProperty(const QString &key) const" << "This File object is not valid";
354     return d->properties.contains(key);
355 }
356 
357 #ifdef HAVE_KF5
358 void File::setPropertiesToDefault()
359 {
360     if (!d->checkValidity())
361         qCCritical(LOG_KBIBTEX_DATA) << "void File::setPropertiesToDefault()" << "This File object is not valid";
362     d->loadConfiguration();
363 }
364 #endif // HAVE_KF5
365 
366 bool File::checkValidity() const
367 {
368     return d->checkValidity();
369 }
370 
371 QDebug operator<<(QDebug dbg, const File &file) {
372     dbg.nospace() << "File is " << (file.checkValidity() ? "" : "NOT ") << "valid and has " << file.count() << " members";
373     return dbg;
374 }
375