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