File indexing completed on 2024-05-12 16:46:03

0001 /***************************************************************************
0002     Copyright (C) 2003-2020 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  *   In addition, as a special exception, the author gives permission to   *
0024  *   link the code of this program with the OpenSSL library released by    *
0025  *   the OpenSSL Project (or with modified versions of OpenSSL that use    *
0026  *   the same license as OpenSSL), and distribute linked combinations      *
0027  *   including the two.  You must obey the GNU General Public License in   *
0028  *   all respects for all of the code used other than OpenSSL.  If you     *
0029  *   modify this file, you may extend this exception to your version of    *
0030  *   the file, but you are not obligated to do so.  If you do not wish to  *
0031  *   do so, delete this exception statement from your version.             *
0032  *                                                                         *
0033  ***************************************************************************/
0034 
0035 #include <config.h>
0036 
0037 #include "z3950fetcher.h"
0038 #include "z3950connection.h"
0039 #include "../collection.h"
0040 #include "../fieldformat.h"
0041 #include "../translators/xslthandler.h"
0042 #include "../translators/tellicoimporter.h"
0043 #include "../translators/grs1importer.h"
0044 #include "../translators/adsimporter.h"
0045 #include "../gui/lineedit.h"
0046 #include "../gui/combobox.h"
0047 #include "../utils/isbnvalidator.h"
0048 #include "../utils/lccnvalidator.h"
0049 #include "../utils/datafileregistry.h"
0050 #include "../tellico_debug.h"
0051 
0052 #include <KLocalizedString>
0053 #include <KConfigGroup>
0054 #include <KComboBox>
0055 #include <KAcceleratorManager>
0056 #include <KSeparator>
0057 #include <KConfig>
0058 
0059 #include <QSpinBox>
0060 #include <QFile>
0061 #include <QLabel>
0062 #include <QDomDocument>
0063 #include <QTextStream>
0064 #include <QTextCodec>
0065 #include <QGridLayout>
0066 #include <QIcon>
0067 
0068 namespace {
0069   static const int Z3950_DEFAULT_PORT = 210;
0070   static const char* Z3950_DEFAULT_ESN = "F";
0071 }
0072 
0073 using namespace Tellico;
0074 using Tellico::Fetch::Z3950Fetcher;
0075 
0076 Z3950Fetcher::Z3950Fetcher(QObject* parent_)
0077     : Fetcher(parent_), m_conn(nullptr), m_port(Z3950_DEFAULT_PORT), m_esn(QLatin1String(Z3950_DEFAULT_ESN)),
0078       m_started(false), m_done(true), m_MARC21XMLHandler(nullptr),
0079       m_UNIMARCXMLHandler(nullptr), m_MODSHandler(nullptr) {
0080 }
0081 
0082 Z3950Fetcher::Z3950Fetcher(QObject* parent_, const QString& preset_)
0083     : Fetcher(parent_), m_conn(nullptr), m_port(Z3950_DEFAULT_PORT), m_started(false), m_done(true), m_preset(preset_),
0084       m_MARC21XMLHandler(nullptr), m_UNIMARCXMLHandler(nullptr), m_MODSHandler(nullptr) {
0085   QString serverFile = DataFileRegistry::self()->locate(QStringLiteral("z3950-servers.cfg"));
0086   if(!serverFile.isEmpty()) {
0087     KConfig serverConfig(serverFile, KConfig::SimpleConfig);
0088     const QStringList servers = serverConfig.groupList();
0089     for(QStringList::ConstIterator server = servers.begin(); server != servers.end(); ++server) {
0090       if(*server == m_preset) {
0091         KConfigGroup cfg(&serverConfig, *server);
0092         m_host = cfg.readEntry("Host");
0093         m_port = cfg.readEntry("Port", Z3950_DEFAULT_PORT);
0094         m_dbname = cfg.readEntry("Database");
0095         m_queryCharSet = cfg.readEntry("Charset");
0096         m_responseCharSet = cfg.readEntry("CharsetResponse");
0097         m_syntax = cfg.readEntry("Syntax");
0098         m_user = cfg.readEntry("User");
0099         m_password = cfg.readEntry("Password");
0100       }
0101     }
0102   } else {
0103     myDebug() << "z3950-servers.cfg not found";
0104   }
0105 }
0106 
0107 Z3950Fetcher::Z3950Fetcher(QObject* parent_, const QString& host_, int port_,
0108                            const QString& dbName_, const QString& syntax_)
0109     : Fetcher(parent_), m_conn(nullptr), m_host(host_), m_port(port_), m_dbname(dbName_)
0110     , m_syntax(syntax_), m_esn(QLatin1String(Z3950_DEFAULT_ESN))
0111     , m_started(false), m_done(true), m_MARC21XMLHandler(nullptr)
0112     , m_UNIMARCXMLHandler(nullptr), m_MODSHandler(nullptr) {
0113 }
0114 
0115 Z3950Fetcher::~Z3950Fetcher() {
0116   delete m_MARC21XMLHandler;
0117   m_MARC21XMLHandler = nullptr;
0118   delete m_UNIMARCXMLHandler;
0119   m_UNIMARCXMLHandler = nullptr;
0120   delete m_MODSHandler;
0121   m_MODSHandler = nullptr;
0122 
0123   if(m_conn) {
0124     m_conn->wait();
0125     m_conn->deleteLater();
0126     m_conn = nullptr;
0127   }
0128 }
0129 
0130 QString Z3950Fetcher::source() const {
0131   return m_name.isEmpty() ? defaultName() : m_name;
0132 }
0133 
0134 // No UPC or Raw for now.
0135 bool Z3950Fetcher::canSearch(Fetch::FetchKey k) const {
0136   return k == Title || k == Person || k == ISBN || k == Keyword || k == LCCN;
0137 }
0138 
0139 bool Z3950Fetcher::canFetch(int type) const {
0140   return type == Data::Collection::Book || type == Data::Collection::Bibtex;
0141 }
0142 
0143 void Z3950Fetcher::readConfigHook(const KConfigGroup& config_) {
0144   QString preset = config_.readEntry("Preset");
0145   if(preset.isEmpty()) {
0146     m_host = config_.readEntry("Host");
0147     int p = config_.readEntry("Port", Z3950_DEFAULT_PORT);
0148     if(p > 0) {
0149       m_port = p;
0150     }
0151     m_dbname = config_.readEntry("Database");
0152     m_queryCharSet = config_.readEntry("Charset");
0153     m_responseCharSet = config_.readEntry("CharsetResponse");
0154     if(m_responseCharSet.isEmpty()) {
0155       m_responseCharSet = m_queryCharSet;
0156     }
0157     m_syntax = config_.readEntry("Syntax");
0158     m_user = config_.readEntry("User");
0159     m_password = config_.readEntry("Password");
0160   } else {
0161     m_preset = preset;
0162     QString serverFile = DataFileRegistry::self()->locate(QStringLiteral("z3950-servers.cfg"));
0163     if(!serverFile.isEmpty()) {
0164       KConfig serverConfig(serverFile, KConfig::SimpleConfig);
0165       const QStringList servers = serverConfig.groupList();
0166       for(QStringList::ConstIterator server = servers.begin(); server != servers.end(); ++server) {
0167         if(*server == preset) {
0168           KConfigGroup cfg(&serverConfig, *server);
0169 //          const QString name = cfg.readEntry("Name");
0170           m_host = cfg.readEntry("Host");
0171           m_port = cfg.readEntry("Port", Z3950_DEFAULT_PORT);
0172           m_dbname = cfg.readEntry("Database");
0173           m_queryCharSet = cfg.readEntry("Charset");
0174           m_syntax = cfg.readEntry("Syntax");
0175           m_user = cfg.readEntry("User");
0176           m_password = cfg.readEntry("Password");
0177         }
0178       }
0179     } else {
0180       myDebug() << "z3950-servers.cfg not found";
0181     }
0182   }
0183 }
0184 
0185 void Z3950Fetcher::saveConfigHook(KConfigGroup& config_) {
0186   config_.writeEntry("Syntax", m_syntax);
0187 }
0188 
0189 void Z3950Fetcher::search() {
0190 #ifdef HAVE_YAZ
0191   m_started = true;
0192   m_done = false;
0193   if(m_host.isEmpty() || m_dbname.isEmpty()) {
0194     myDebug() << "settings are not set!";
0195     stop();
0196     return;
0197   }
0198   m_started = true;
0199 
0200   QString svalue = request().value();
0201   QRegExp rx1(QLatin1String("^['\"].*\\1$"));
0202   if(!rx1.exactMatch(svalue)) {
0203     svalue = QLatin1Char('"') + svalue + QLatin1Char('"');
0204   }
0205 
0206   switch(request().key()) {
0207     case Title:
0208       m_pqn = QLatin1String("@attr 1=4 ") + svalue;
0209       break;
0210     case Person:
0211 //      m_pqn = QLatin1String("@or ");
0212 //      m_pqn += QLatin1String("@attr 1=1 \"") + request().value() + QLatin1Char('"');
0213       m_pqn = QLatin1String(" @attr 1=1003 ") + svalue;
0214       break;
0215     case ISBN:
0216       {
0217         m_pqn.clear();
0218         QString s = request().value();
0219         s.remove(QLatin1Char('-'));
0220         QStringList isbnList = FieldFormat::splitValue(s);
0221         // also search for isbn10 values
0222         for(QStringList::Iterator it = isbnList.begin(); it != isbnList.end(); ++it) {
0223           if((*it).startsWith(QLatin1String("978"))) {
0224             QString isbn10 = ISBNValidator::isbn10(*it);
0225             isbn10.remove(QLatin1Char('-'));
0226             it = isbnList.insert(it, isbn10);
0227             ++it;
0228           }
0229         }
0230         const int count = isbnList.count();
0231         if(count > 1) {
0232           m_pqn = QStringLiteral("@or ");
0233         }
0234         for(int i = 0; i < count; ++i) {
0235           m_pqn += QLatin1String(" @attr 1=7 ") + isbnList.at(i);
0236           if(count > 1 && i < count-2) {
0237             m_pqn += QLatin1String(" @or");
0238           }
0239         }
0240       }
0241       break;
0242     case LCCN:
0243       {
0244         m_pqn.clear();
0245         QString s = request().value();
0246         s.remove(QLatin1Char('-'));
0247         QStringList lccnList = FieldFormat::splitValue(s);
0248         while(!lccnList.isEmpty()) {
0249           m_pqn += QLatin1String(" @or @attr 1=9 ") + lccnList.front();
0250           if(lccnList.count() > 1) {
0251             m_pqn += QLatin1String(" @or");
0252           }
0253           m_pqn += QLatin1String(" @attr 1=9 ") + LCCNValidator::formalize(lccnList.front());
0254           lccnList.pop_front();
0255         }
0256       }
0257       break;
0258     case Keyword:
0259       m_pqn = QLatin1String("@attr 1=1016 ") + svalue;
0260       break;
0261     case Raw:
0262       m_pqn = request().value();
0263       break;
0264     default:
0265       myWarning() << "key not recognized: " << request().key();
0266       stop();
0267       return;
0268   }
0269 //  m_pqn = QLatin1String("@attr 1=7 0253333490");
0270 //  myLog() << "PQN query = " << m_pqn;
0271 
0272   if(m_conn) {
0273     m_conn->reset(); // reset counts
0274   }
0275 
0276   process();
0277 #else // HAVE_YAZ
0278   stop();
0279   return;
0280 #endif
0281 }
0282 
0283 void Z3950Fetcher::setCharacterSet(const QString& qcs_, const QString& rcs_) {
0284   m_queryCharSet = qcs_;
0285   m_responseCharSet = rcs_.isEmpty() ? qcs_ : rcs_;
0286 }
0287 
0288 void Z3950Fetcher::continueSearch() {
0289 #ifdef HAVE_YAZ
0290   m_started = true;
0291   process();
0292 #endif
0293 }
0294 
0295 void Z3950Fetcher::stop() {
0296   if(!m_started) {
0297     return;
0298   }
0299   m_started = false;
0300   if(m_conn) {
0301    // give it a second to cleanup
0302     m_conn->abort();
0303     m_conn->wait(1000);
0304   }
0305   emit signalDone(this);
0306 }
0307 
0308 bool Z3950Fetcher::initMARC21Handler() {
0309   if(m_MARC21XMLHandler) {
0310     return true;
0311   }
0312 
0313   QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("MARC21slim2MODS3.xsl"));
0314   if(xsltfile.isEmpty()) {
0315     myWarning() << "can not locate MARC21slim2MODS3.xsl.";
0316     return false;
0317   }
0318 
0319   QUrl u = QUrl::fromLocalFile(xsltfile);
0320 
0321   m_MARC21XMLHandler = new XSLTHandler(u);
0322   if(!m_MARC21XMLHandler->isValid()) {
0323     myWarning() << "error in MARC21slim2MODS3.xsl.";
0324     delete m_MARC21XMLHandler;
0325     m_MARC21XMLHandler = nullptr;
0326     return false;
0327   }
0328   return true;
0329 }
0330 
0331 bool Z3950Fetcher::initUNIMARCHandler() {
0332   if(m_UNIMARCXMLHandler) {
0333     return true;
0334   }
0335 
0336   QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("UNIMARC2MODS3.xsl"));
0337   if(xsltfile.isEmpty()) {
0338     myWarning() << "can not locate UNIMARC2MODS3.xsl.";
0339     return false;
0340   }
0341 
0342   QUrl u = QUrl::fromLocalFile(xsltfile);
0343 
0344   m_UNIMARCXMLHandler = new XSLTHandler(u);
0345   if(!m_UNIMARCXMLHandler->isValid()) {
0346     myWarning() << "error in UNIMARC2MODS3.xsl.";
0347     delete m_UNIMARCXMLHandler;
0348     m_UNIMARCXMLHandler = nullptr;
0349     return false;
0350   }
0351   return true;
0352 }
0353 
0354 bool Z3950Fetcher::initMODSHandler() {
0355   if(m_MODSHandler) {
0356     return true;
0357   }
0358 
0359   QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("mods2tellico.xsl"));
0360   if(xsltfile.isEmpty()) {
0361     myWarning() << "can not locate mods2tellico.xsl.";
0362     return false;
0363   }
0364 
0365   QUrl u = QUrl::fromLocalFile(xsltfile);
0366 
0367   m_MODSHandler = new XSLTHandler(u);
0368   if(!m_MODSHandler->isValid()) {
0369     myWarning() << "error in mods2tellico.xsl.";
0370     delete m_MODSHandler;
0371     m_MODSHandler = nullptr;
0372     // no use in keeping the MARC handlers now
0373     delete m_MARC21XMLHandler;
0374     m_MARC21XMLHandler = nullptr;
0375     delete m_UNIMARCXMLHandler;
0376     m_UNIMARCXMLHandler = nullptr;
0377     return false;
0378   }
0379   return true;
0380 }
0381 
0382 void Z3950Fetcher::process() {
0383   if(m_conn) {
0384     m_conn->wait();
0385   } else {
0386     m_conn = new Z3950Connection(this, m_host, m_port, m_dbname, m_syntax, m_esn);
0387     m_conn->setCharacterSet(m_queryCharSet, m_responseCharSet);
0388     if(!m_user.isEmpty()) {
0389       m_conn->setUserPassword(m_user, m_password);
0390     }
0391   }
0392 
0393   m_conn->setQuery(m_pqn);
0394   m_conn->start();
0395 }
0396 
0397 void Z3950Fetcher::handleResult(const QString& result_) {
0398   if(result_.isEmpty()) {
0399     myDebug() << "empty record found, maybe the character encoding or record format is wrong?";
0400     return;
0401   }
0402   // possible to get a race condition where the fetcher has been stopped
0403   // even as new results come in
0404   if(!m_started) {
0405     return;
0406   }
0407 
0408 #if 0
0409   myWarning() << "Remove debug from z3950fetcher.cpp";
0410   {
0411     QFile f1(QLatin1String("/tmp/z3950.txt"));
0412     if(f1.open(QIODevice::WriteOnly)) {
0413 //      if(f1.open(QIODevice::WriteOnly | QIODevice::Append)) {
0414       QTextStream t(&f1);
0415       t.setCodec("UTF-8");
0416       t << result_;
0417     }
0418     f1.close();
0419   }
0420 #endif
0421   // assume always utf-8
0422   QString str, msg;
0423   Data::CollPtr coll;
0424   // not marc, has to be grs-1
0425   if(m_syntax == QLatin1String("grs-1")) {
0426     Import::GRS1Importer imp(result_);
0427     coll = imp.collection();
0428     msg = imp.statusMessage();
0429   } else if(m_syntax == QLatin1String("ads")) {
0430     Import::ADSImporter imp(result_);
0431     coll = imp.collection();
0432     msg = imp.statusMessage();
0433   } else { // now the MODS stuff
0434     if(m_syntax == QLatin1String("mods")) {
0435       str = result_;
0436     } else if(m_syntax == QLatin1String("unimarc") && initUNIMARCHandler()) {
0437       str = m_UNIMARCXMLHandler->applyStylesheet(result_);
0438     } else if(initMARC21Handler()) { // got to be usmarc/marc21
0439       str = m_MARC21XMLHandler->applyStylesheet(result_);
0440     }
0441     if(str.isEmpty() || !initMODSHandler()) {
0442       myDebug() << "empty string or can't init";
0443       stop();
0444       return;
0445     }
0446 #if 0
0447     myWarning() << "Remove debug from z3950fetcher.cpp";
0448     {
0449       QFile f2(QLatin1String("/tmp/mods.xml"));
0450 //      if(f2.open(QIODevice::WriteOnly)) {
0451       if(f2.open(QIODevice::WriteOnly | QIODevice::Append)) {
0452         QTextStream t(&f2);
0453         t.setCodec("UTF-8");
0454         t << str;
0455       }
0456       f2.close();
0457     }
0458 #endif
0459     Import::TellicoImporter imp(m_MODSHandler->applyStylesheet(str));
0460     imp.setOptions(imp.options() ^ Import::ImportProgress); // no progress needed
0461     coll = imp.collection();
0462     msg = imp.statusMessage();
0463   }
0464 
0465   if(!coll) {
0466     if(!msg.isEmpty()) {
0467       message(msg, MessageHandler::Warning);
0468     }
0469     myDebug() << "no collection pointer:" << msg;
0470     return;
0471   }
0472 
0473   if(coll->entryCount() == 0) {
0474 //    myDebug() << "no Tellico entry in result";
0475     return;
0476   }
0477 
0478   // since the Dewey and LoC field titles have a context in their i18n call here
0479   // but not in the mods2tellico.xsl stylesheet where the field is actually created
0480   // update the field titles here
0481   QHashIterator<QString, QString> i(allOptionalFields());
0482   while(i.hasNext()) {
0483     i.next();
0484     Data::FieldPtr field = coll->fieldByName(i.key());
0485     if(field) {
0486       field->setTitle(i.value());
0487       coll->modifyField(field);
0488     }
0489   }
0490 
0491   Data::EntryList entries = coll->entries();
0492   foreach(Data::EntryPtr entry, entries) {
0493     FetchResult* r = new FetchResult(this, entry);
0494     m_entries.insert(r->uid, entry);
0495     emit signalResultFound(r);
0496   }
0497 }
0498 
0499 void Z3950Fetcher::done() {
0500   m_done = true;
0501   stop();
0502 }
0503 
0504 Tellico::Data::EntryPtr Z3950Fetcher::fetchEntryHook(uint uid_) {
0505   return m_entries[uid_];
0506 }
0507 
0508 void Z3950Fetcher::customEvent(QEvent* event_) {
0509   if(!m_conn) {
0510     return;
0511   }
0512 
0513   if(event_->type() == Z3950ResultFound::uid()) {
0514     if(m_done) {
0515       myWarning() << "result returned after done signal!";
0516     }
0517     Z3950ResultFound* e = static_cast<Z3950ResultFound*>(event_);
0518     handleResult(e->result());
0519   } else if(event_->type() == Z3950ConnectionDone::uid()) {
0520     Z3950ConnectionDone* e = static_cast<Z3950ConnectionDone*>(event_);
0521     if(e->messageType() > -1) {
0522       message(e->message(), e->messageType());
0523     }
0524     m_hasMoreResults = e->hasMoreResults();
0525     m_conn->wait();
0526     done();
0527   } else if(event_->type() == Z3950SyntaxChange::uid()) {
0528     if(m_done) {
0529       myWarning() << "syntax changed after done signal!";
0530     }
0531     Z3950SyntaxChange* e = static_cast<Z3950SyntaxChange*>(event_);
0532     if(m_syntax != e->syntax()) {
0533       m_syntax = e->syntax();
0534       // it gets saved when saveConfigHook() get's called from the Fetcher() d'tor
0535     }
0536   } else {
0537     myWarning() << "weird type: " << event_->type();
0538   }
0539 }
0540 
0541 Tellico::Fetch::FetchRequest Z3950Fetcher::updateRequest(Data::EntryPtr entry_) {
0542 //  myDebug() << source() << ": " << entry_->title();
0543   QString isbn = entry_->field(QStringLiteral("isbn"));
0544   if(!isbn.isEmpty()) {
0545     return FetchRequest(Fetch::ISBN, isbn);
0546   }
0547 
0548   QString lccn = entry_->field(QStringLiteral("lccn"));
0549   if(!lccn.isEmpty()) {
0550     return FetchRequest(Fetch::LCCN, lccn);
0551   }
0552 
0553   // optimistically try searching for title and rely on Collection::sameEntry() to figure things out
0554   QString t = entry_->field(QStringLiteral("title"));
0555   if(!t.isEmpty()) {
0556     return FetchRequest(Fetch::Title, t);
0557   }
0558   return FetchRequest();
0559 }
0560 
0561 Tellico::Fetch::ConfigWidget* Z3950Fetcher::configWidget(QWidget* parent_) const {
0562   return new Z3950Fetcher::ConfigWidget(parent_, this);
0563 }
0564 
0565 QString Z3950Fetcher::defaultName() {
0566   return i18n("z39.50 Server");
0567 }
0568 
0569 QString Z3950Fetcher::defaultIcon() {
0570 //  return QLatin1String("network-server"); // rather arbitrary
0571   return QStringLiteral("network-server-database"); // rather arbitrary
0572 }
0573 
0574 // static
0575 Tellico::StringHash Z3950Fetcher::allOptionalFields() {
0576   StringHash hash;
0577   hash[QStringLiteral("address")]  = i18n("Address");
0578   hash[QStringLiteral("abstract")] = i18n("Abstract");
0579   hash[QStringLiteral("illustrator")] = i18n("Illustrator");
0580   hash[QStringLiteral("dewey")] = i18nc("Dewey Decimal classification system", "Dewey Decimal");
0581   hash[QStringLiteral("lcc")] = i18nc("Library of Congress classification system", "LoC Classification");
0582   return hash;
0583 }
0584 
0585 Z3950Fetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const Z3950Fetcher* fetcher_/*=0*/)
0586     : Fetch::ConfigWidget(parent_) {
0587   QGridLayout* l = new QGridLayout(optionsWidget());
0588   l->setSpacing(4);
0589   l->setColumnStretch(1, 10);
0590 
0591   int row = -1;
0592 
0593   m_usePreset = new QCheckBox(i18n("Use preset &server:"), optionsWidget());
0594   l->addWidget(m_usePreset, ++row, 0);
0595   connect(m_usePreset, &QAbstractButton::toggled, this, &ConfigWidget::slotTogglePreset);
0596   m_serverCombo = new GUI::ComboBox(optionsWidget());
0597   void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated;
0598   connect(m_serverCombo, activatedInt, this, &ConfigWidget::slotPresetChanged);
0599   l->addWidget(m_serverCombo, row, 1);
0600   ++row;
0601   l->addWidget(new KSeparator(optionsWidget()), row, 0, 1, 2);
0602   l->setRowMinimumHeight(row, 10);
0603 
0604   QLabel* label = new QLabel(i18n("Hos&t: "), optionsWidget());
0605   l->addWidget(label, ++row, 0);
0606   m_hostEdit = new GUI::LineEdit(optionsWidget());
0607   connect(m_hostEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0608   connect(m_hostEdit, &QLineEdit::textChanged, this, &ConfigWidget::signalName);
0609   l->addWidget(m_hostEdit, row, 1);
0610   QString w = i18n("Enter the host name of the server.");
0611   label->setWhatsThis(w);
0612   m_hostEdit->setWhatsThis(w);
0613   label->setBuddy(m_hostEdit);
0614 
0615   label = new QLabel(i18n("&Port: "), optionsWidget());
0616   l->addWidget(label, ++row, 0);
0617   m_portSpinBox = new QSpinBox(optionsWidget());
0618   m_portSpinBox->setMaximum(999999);
0619   m_portSpinBox->setMinimum(0);
0620   m_portSpinBox->setValue(Z3950_DEFAULT_PORT);
0621   void (QSpinBox::* valueChanged)(int) = &QSpinBox::valueChanged;
0622   connect(m_portSpinBox, valueChanged, this, &ConfigWidget::slotSetModified);
0623   l->addWidget(m_portSpinBox, row, 1);
0624   w = i18n("Enter the port number of the server. The default is %1.", Z3950_DEFAULT_PORT);
0625   label->setWhatsThis(w);
0626   m_portSpinBox->setWhatsThis(w);
0627   label->setBuddy(m_portSpinBox);
0628 
0629   label = new QLabel(i18n("&Database: "), optionsWidget());
0630   l->addWidget(label, ++row, 0);
0631   m_databaseEdit = new GUI::LineEdit(optionsWidget());
0632   connect(m_databaseEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0633   l->addWidget(m_databaseEdit, row, 1);
0634   w = i18n("Enter the database name used by the server.");
0635   label->setWhatsThis(w);
0636   m_databaseEdit->setWhatsThis(w);
0637   label->setBuddy(m_databaseEdit);
0638 
0639   label = new QLabel(i18n("Query character set: "), optionsWidget());
0640   l->addWidget(label, ++row, 0);
0641   m_charSetCombo1 = new KComboBox(true, optionsWidget());
0642   m_charSetCombo1->addItem(QString());
0643   m_charSetCombo1->addItem(QStringLiteral("marc8"));
0644   m_charSetCombo1->addItem(QStringLiteral("iso-8859-1"));
0645   m_charSetCombo1->addItem(QStringLiteral("utf-8"));
0646   connect(m_charSetCombo1, &QComboBox::currentTextChanged, this, &ConfigWidget::slotSetModified);
0647   l->addWidget(m_charSetCombo1, row, 1);
0648   w = i18n("Enter the character set encoding used for queries by the z39.50 server. The most likely choice "
0649            "is MARC-8, although ISO-8859-1 is common as well.");
0650   label->setWhatsThis(w);
0651   m_charSetCombo1->setWhatsThis(w);
0652   label->setBuddy(m_charSetCombo1);
0653 
0654   label = new QLabel(i18n("Results character set: "), optionsWidget());
0655   l->addWidget(label, ++row, 0);
0656   m_charSetCombo2 = new KComboBox(true, optionsWidget());
0657   m_charSetCombo2->addItem(QString());
0658   m_charSetCombo2->addItem(QStringLiteral("marc8"));
0659   m_charSetCombo2->addItem(QStringLiteral("iso-8859-1"));
0660   m_charSetCombo2->addItem(QStringLiteral("utf-8"));
0661   connect(m_charSetCombo2, &QComboBox::currentTextChanged, this, &ConfigWidget::slotSetModified);
0662   l->addWidget(m_charSetCombo2, row, 1);
0663   w = i18n("Enter the character set encoding used for responses by the z39.50 server. The most likely choice "
0664            "is MARC-8, although ISO-8859-1 is common as well.");
0665   label->setWhatsThis(w);
0666   m_charSetCombo2->setWhatsThis(w);
0667   label->setBuddy(m_charSetCombo2);
0668 
0669   label = new QLabel(i18n("&Format: "), optionsWidget());
0670   l->addWidget(label, ++row, 0);
0671   m_syntaxCombo = new GUI::ComboBox(optionsWidget());
0672   m_syntaxCombo->addItem(i18n("Auto-detect"), QString());
0673   m_syntaxCombo->addItem(QStringLiteral("MODS"), QLatin1String("mods"));
0674   m_syntaxCombo->addItem(QStringLiteral("MARC21"), QLatin1String("marc21"));
0675   m_syntaxCombo->addItem(QStringLiteral("UNIMARC"), QLatin1String("unimarc"));
0676   m_syntaxCombo->addItem(QStringLiteral("USMARC"), QLatin1String("usmarc"));
0677   m_syntaxCombo->addItem(QStringLiteral("ADS"), QLatin1String("ads"));
0678   m_syntaxCombo->addItem(QStringLiteral("GRS-1"), QLatin1String("grs-1"));
0679   connect(m_syntaxCombo, &QComboBox::currentTextChanged, this, &ConfigWidget::slotSetModified);
0680   l->addWidget(m_syntaxCombo, row, 1);
0681   w = i18n("Enter the data format used by the z39.50 server. Tellico will attempt to "
0682            "automatically detect the best setting if <i>auto-detect</i> is selected.");
0683   label->setWhatsThis(w);
0684   m_syntaxCombo->setWhatsThis(w);
0685   label->setBuddy(m_syntaxCombo);
0686 
0687   label = new QLabel(i18n("&User: "), optionsWidget());
0688   l->addWidget(label, ++row, 0);
0689   m_userEdit = new GUI::LineEdit(optionsWidget());
0690   m_userEdit->setPlaceholderText(i18n("Optional"));
0691   connect(m_userEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0692   l->addWidget(m_userEdit, row, 1);
0693   w = i18n("Enter the authentication user name used by the z39.50 database. Most servers "
0694            "do not need one.");
0695   label->setWhatsThis(w);
0696   m_userEdit->setWhatsThis(w);
0697   label->setBuddy(m_userEdit);
0698 
0699   label = new QLabel(i18n("Pass&word: "), optionsWidget());
0700   l->addWidget(label, ++row, 0);
0701   m_passwordEdit = new GUI::LineEdit(optionsWidget());
0702   m_passwordEdit->setPlaceholderText(i18n("Optional"));
0703   m_passwordEdit->setEchoMode(QLineEdit::Password);
0704   connect(m_passwordEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0705   l->addWidget(m_passwordEdit, row, 1);
0706   w = i18n("Enter the authentication password used by the z39.50 database. Most servers "
0707            "do not need one. The password will be saved in plain text in the Tellico "
0708            "configuration file.");
0709   label->setWhatsThis(w);
0710   m_passwordEdit->setWhatsThis(w);
0711   label->setBuddy(m_passwordEdit);
0712 
0713   l->setRowStretch(++row, 1);
0714 
0715   // now add additional fields widget
0716   addFieldsWidget(Z3950Fetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0717 
0718   loadPresets(fetcher_ ? fetcher_->m_preset : QString());
0719   if(fetcher_) {
0720     m_hostEdit->setText(fetcher_->m_host);
0721     m_portSpinBox->setValue(fetcher_->m_port);
0722     m_databaseEdit->setText(fetcher_->m_dbname);
0723     m_userEdit->setText(fetcher_->m_user);
0724     m_passwordEdit->setText(fetcher_->m_password);
0725     m_charSetCombo1->setEditText(fetcher_->m_queryCharSet);
0726     m_charSetCombo2->setEditText(fetcher_->m_responseCharSet);
0727     // the syntax is detected automatically by the fetcher
0728     // since the config group gets deleted in the config file,
0729     // the value needs to be retained here
0730     m_syntax = fetcher_->m_syntax;
0731     m_syntaxCombo->setCurrentData(m_syntax);
0732   }
0733   KAcceleratorManager::manage(optionsWidget());
0734 
0735   // start with presets turned off
0736   m_usePreset->setChecked(fetcher_ && !fetcher_->m_preset.isEmpty());
0737 
0738   slotTogglePreset(m_usePreset->isChecked());
0739 }
0740 
0741 Z3950Fetcher::ConfigWidget::~ConfigWidget() {
0742 }
0743 
0744 void Z3950Fetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
0745   if(m_usePreset->isChecked()) {
0746     QString presetID = m_serverCombo->currentData().toString();
0747     config_.writeEntry("Preset", presetID);
0748     return;
0749   }
0750   config_.deleteEntry("Preset");
0751 
0752   QString s = m_hostEdit->text().trimmed();
0753   if(!s.isEmpty()) {
0754     config_.writeEntry("Host", s);
0755   }
0756   int port = m_portSpinBox->value();
0757   if(port > 0) {
0758     config_.writeEntry("Port", port);
0759   }
0760   s = m_databaseEdit->text().trimmed();
0761   if(!s.isEmpty()) {
0762     config_.writeEntry("Database", s);
0763   }
0764   s = m_charSetCombo1->currentText().trimmed();
0765   if(!s.isEmpty()) {
0766     config_.writeEntry("Charset", s);
0767   }
0768   s = m_charSetCombo2->currentText().trimmed();
0769   if(!s.isEmpty()) {
0770     config_.writeEntry("CharsetResponse", s);
0771   }
0772   s = m_userEdit->text();
0773   if(!s.isEmpty()) {
0774     config_.writeEntry("User", s);
0775   }
0776   s = m_passwordEdit->text();
0777   if(!s.isEmpty()) {
0778     config_.writeEntry("Password", s);
0779   }
0780   s = m_syntaxCombo->currentData().toString();
0781   if(!s.isEmpty()) {
0782     m_syntax = s;
0783   }
0784   config_.writeEntry("Syntax", m_syntax);
0785 }
0786 
0787 void Z3950Fetcher::ConfigWidget::slotTogglePreset(bool on) {
0788   m_serverCombo->setEnabled(on);
0789   if(on) {
0790     emit signalName(m_serverCombo->currentText());
0791   }
0792   m_hostEdit->setEnabled(!on);
0793   if(!on && !m_hostEdit->text().isEmpty()) {
0794     emit signalName(m_hostEdit->text());
0795   }
0796   m_portSpinBox->setEnabled(!on);
0797   m_databaseEdit->setEnabled(!on);
0798   m_userEdit->setEnabled(!on);
0799   m_passwordEdit->setEnabled(!on);
0800   m_charSetCombo1->setEnabled(!on);
0801   m_charSetCombo2->setEnabled(!on);
0802   m_syntaxCombo->setEnabled(!on);
0803   if(on) {
0804     emit signalName(m_serverCombo->currentText());
0805   }
0806 }
0807 
0808 void Z3950Fetcher::ConfigWidget::slotPresetChanged() {
0809   emit signalName(m_serverCombo->currentText());
0810 }
0811 
0812 void Z3950Fetcher::ConfigWidget::loadPresets(const QString& current_) {
0813   const QString lang = QLocale().uiLanguages().constFirst();
0814   const QString lang2A = lang.contains(QLatin1Char('-')) ? lang.section(QLatin1Char('-'), 0, 0) : lang;
0815 
0816   QString serverFile = DataFileRegistry::self()->locate(QStringLiteral("z3950-servers.cfg"));
0817   if(serverFile.isEmpty()) {
0818     myWarning() << "no z3950 servers file found";
0819     return;
0820   }
0821 
0822   int idx = -1;
0823 
0824   KConfig serverConfig(serverFile, KConfig::SimpleConfig);
0825   const QStringList servers = serverConfig.groupList();
0826   // I want the list of servers sorted by name so use QMap instead of QHash
0827   QMap<QString, QString> serverNameMap;
0828   QHash<QString, QIcon> flags;
0829   for(QStringList::ConstIterator server = servers.constBegin(); server != servers.constEnd(); ++server) {
0830     if((*server).isEmpty()) {
0831       myDebug() << "empty id";
0832       continue;
0833     }
0834     KConfigGroup cfg(&serverConfig, *server);
0835     const QString name = cfg.readEntry("Name");
0836     if(!name.isEmpty()) {
0837       serverNameMap.insert(name, *server);
0838     }
0839   }
0840   for(QMap<QString, QString>::ConstIterator it = serverNameMap.constBegin(); it != serverNameMap.constEnd(); ++it) {
0841     const QString name = it.key();
0842     const QString group = it.value();
0843     KConfigGroup cfg(&serverConfig, group);
0844     const QString country = cfg.readEntry("Country", QString());
0845 
0846     QIcon icon;
0847     if(!country.isEmpty()) {
0848       QHash<QString, QIcon>::ConstIterator it = flags.constFind(country);
0849       if(it != flags.constEnd()) {
0850         icon = it.value();
0851       } else {
0852         const QString flag = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0853                                                     QStringLiteral("kf5/locale/countries/%1/flag.png").arg(country));
0854         if (!flag.isEmpty()) {
0855           icon = QIcon(flag);
0856           flags.insert(country, icon);
0857         }
0858       }
0859     }
0860     m_serverCombo->addItem(icon, name, group);
0861 
0862     if(current_.isEmpty() && idx == -1) {
0863       // set the initial selection to something depending on the language
0864       const QStringList locales = cfg.readEntry("Locale", QStringList());
0865       if(locales.contains(lang) || locales.contains(lang2A)) {
0866         idx = m_serverCombo->count() - 1;
0867       }
0868     } else if(group == current_) {
0869       idx = m_serverCombo->count() - 1;
0870     }
0871   }
0872   if(idx > -1) {
0873     m_serverCombo->setCurrentIndex(idx);
0874   }
0875 }
0876 
0877 QString Z3950Fetcher::ConfigWidget::preferredName() const {
0878   if(m_usePreset->isChecked()) {
0879     return m_serverCombo->currentText();
0880   }
0881   QString s = m_hostEdit->text();
0882   return s.isEmpty() ? i18n("z39.50 Server") : s;
0883 }