/*************************************************************************** copyright : (C) 2005-2006 by Robby Stephenson email : robby@periapsis.org ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of version 2 of the GNU General Public License as * * published by the Free Software Foundation; * * * ***************************************************************************/ #include "execexternalfetcher.h" #include "messagehandler.h" #include "fetchmanager.h" #include "../collection.h" #include "../entry.h" #include "../importdialog.h" #include "../translators/tellicoimporter.h" #include "../tellico_debug.h" #include "../gui/combobox.h" #include "../gui/lineedit.h" #include "../gui/collectiontypecombo.h" #include "../tellico_utils.h" #include "../newstuff/manager.h" #include #include #include #include #include #include #include #include #include #include #include // needed for TQFile::remove using Tellico::Fetch::ExecExternalFetcher; TQStringList ExecExternalFetcher::parseArguments(const TQString& str_) { // matching escaped quotes is too hard... :( // TQRegExp quotes(TQString::fromLatin1("[^\\\\](['\"])(.*[^\\\\])\\1")); TQRegExp quotes(TQString::fromLatin1("(['\"])(.*)\\1")); quotes.setMinimal(true); TQRegExp spaces(TQString::fromLatin1("\\s+")); spaces.setMinimal(true); TQStringList args; int pos = 0; for(int nextPos = quotes.search(str_); nextPos > -1; pos = nextPos+1, nextPos = quotes.search(str_, pos)) { // a non-quotes arguments runs from pos to nextPos args += TQStringList::split(spaces, str_.mid(pos, nextPos-pos)); // move nextpos marker to end of match pos = quotes.pos(2); // skip quotation mark nextPos += quotes.matchedLength(); args += str_.mid(pos, nextPos-pos-1); } // catch the end stuff args += TQStringList::split(spaces, str_.mid(pos)); #if 0 for(TQStringList::ConstIterator it = args.begin(); it != args.end(); ++it) { myDebug() << *it << endl; } #endif return args; } ExecExternalFetcher::ExecExternalFetcher(TQObject* parent_, const char* name_/*=0*/) : Fetcher(parent_, name_), m_started(false), m_collType(-1), m_formatType(-1), m_canUpdate(false), m_process(0), m_deleteOnRemove(false) { } ExecExternalFetcher::~ExecExternalFetcher() { stop(); } TQString ExecExternalFetcher::defaultName() { return i18n("External Application"); } TQString ExecExternalFetcher::source() const { return m_name; } bool ExecExternalFetcher::canFetch(int type_) const { return m_collType == -1 ? false : m_collType == type_; } void ExecExternalFetcher::readConfigHook(const KConfigGroup& config_) { TQString s = config_.readPathEntry("ExecPath"); if(!s.isEmpty()) { m_path = s; } TQValueList il; if(config_.hasKey("ArgumentKeys")) { il = config_.readIntListEntry("ArgumentKeys"); } else { il.append(Keyword); } TQStringList sl = config_.readListEntry("Arguments"); if(il.count() != sl.count()) { kdWarning() << "ExecExternalFetcher::readConfig() - unequal number of arguments and keys" << endl; } int n = TQMIN(il.count(), sl.count()); for(int i = 0; i < n; ++i) { m_args[static_cast(il[i])] = sl[i]; } if(config_.hasKey("UpdateArgs")) { m_canUpdate = true; m_updateArgs = config_.readEntry("UpdateArgs"); } else { m_canUpdate = false; } m_collType = config_.readNumEntry("CollectionType", -1); m_formatType = config_.readNumEntry("FormatType", -1); m_deleteOnRemove = config_.readBoolEntry("DeleteOnRemove", false); m_newStuffName = config_.readEntry("NewStuffName"); } void ExecExternalFetcher::search(FetchKey key_, const TQString& value_) { m_started = true; if(!m_args.contains(key_)) { stop(); return; } // should KProcess::quote() be used? // %1 gets replaced by the search value, but since the arguments are going to be split // the search value needs to be enclosed in quotation marks // but first check to make sure the user didn't do that already // AND the "%1" wasn't used in the settings TQString value = value_; if(key_ == ISBN) { value.remove('-'); // remove hyphens from isbn values // shouldn't hurt and might keep from confusing stupid search sources } TQRegExp rx1(TQString::fromLatin1("['\"].*\\1")); if(!rx1.exactMatch(value)) { value.prepend('"').append('"'); } TQString args = m_args[key_]; TQRegExp rx2(TQString::fromLatin1("['\"]%1\\1")); args.replace(rx2, TQString::fromLatin1("%1")); startSearch(parseArguments(args.tqarg(value))); // replace %1 with search value } void ExecExternalFetcher::startSearch(const TQStringList& args_) { if(m_path.isEmpty()) { stop(); return; } #if 0 myDebug() << m_path << endl; for(TQStringList::ConstIterator it = args_.begin(); it != args_.end(); ++it) { myDebug() << " " << *it << endl; } #endif m_process = new KProcess(); connect(m_process, TQT_SIGNAL(receivedStdout(KProcess*, char*, int)), TQT_SLOT(slotData(KProcess*, char*, int))); connect(m_process, TQT_SIGNAL(receivedStderr(KProcess*, char*, int)), TQT_SLOT(slotError(KProcess*, char*, int))); connect(m_process, TQT_SIGNAL(processExited(KProcess*)), TQT_SLOT(slotProcessExited(KProcess*))); *m_process << m_path << args_; if(!m_process->start(KProcess::NotifyOnExit, KProcess::AllOutput)) { myDebug() << "ExecExternalFetcher::startSearch() - process failed to start" << endl; stop(); } } void ExecExternalFetcher::stop() { if(!m_started) { return; } if(m_process) { m_process->kill(); delete m_process; m_process = 0; } m_data.truncate(0); m_started = false; m_errors.clear(); emit signalDone(this); } void ExecExternalFetcher::slotData(KProcess*, char* buffer_, int len_) { TQDataStream stream(m_data, IO_WriteOnly | IO_Append); stream.writeRawBytes(buffer_, len_); } void ExecExternalFetcher::slotError(KProcess*, char* buffer_, int len_) { GUI::CursorSaver cs(TQt::arrowCursor); TQString msg = TQString::fromLocal8Bit(buffer_, len_); msg.prepend(source() + TQString::fromLatin1(": ")); if(msg.endsWith(TQChar('\n'))) { msg.truncate(msg.length()-1); } myDebug() << "ExecExternalFetcher::slotError() - " << msg << endl; m_errors << msg; } void ExecExternalFetcher::slotProcessExited(KProcess*) { // myDebug() << "ExecExternalFetcher::slotProcessExited()" << endl; if(!m_process->normalExit() || m_process->exitStatus()) { myDebug() << "ExecExternalFetcher::slotProcessExited() - "<< source() << ": process did not exit successfully" << endl; if(!m_errors.isEmpty()) { message(m_errors.join(TQChar('\n')), MessageHandler::Error); } stop(); return; } if(!m_errors.isEmpty()) { message(m_errors.join(TQChar('\n')), MessageHandler::Warning); } if(m_data.isEmpty()) { myDebug() << "ExecExternalFetcher::slotProcessExited() - "<< source() << ": no data" << endl; stop(); return; } Import::Format format = static_cast(m_formatType > -1 ? m_formatType : Import::TellicoXML); Import::Importer* imp = ImportDialog::importer(format, KURL::List()); if(!imp) { stop(); return; } imp->setText(TQString::fromUtf8(m_data, m_data.size())); Data::CollPtr coll = imp->collection(); if(!coll) { if(!imp->statusMessage().isEmpty()) { message(imp->statusMessage(), MessageHandler::Status); } myDebug() << "ExecExternalFetcher::slotProcessExited() - "<< source() << ": no collection pointer" << endl; delete imp; stop(); return; } delete imp; if(coll->entryCount() == 0) { // myDebug() << "ExecExternalFetcher::slotProcessExited() - no results" << endl; stop(); return; } Data::EntryVec entries = coll->entries(); for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { TQString desc; switch(coll->type()) { case Data::Collection::Book: case Data::Collection::Bibtex: desc = entry->field(TQString::fromLatin1("author")) + TQChar('/') + entry->field(TQString::fromLatin1("publisher")); if(!entry->field(TQString::fromLatin1("cr_year")).isEmpty()) { desc += TQChar('/') + entry->field(TQString::fromLatin1("cr_year")); } else if(!entry->field(TQString::fromLatin1("pub_year")).isEmpty()){ desc += TQChar('/') + entry->field(TQString::fromLatin1("pub_year")); } break; case Data::Collection::Video: desc = entry->field(TQString::fromLatin1("studio")) + TQChar('/') + entry->field(TQString::fromLatin1("director")) + TQChar('/') + entry->field(TQString::fromLatin1("year")) + TQChar('/') + entry->field(TQString::fromLatin1("medium")); break; case Data::Collection::Album: desc = entry->field(TQString::fromLatin1("artist")) + TQChar('/') + entry->field(TQString::fromLatin1("label")) + TQChar('/') + entry->field(TQString::fromLatin1("year")); break; case Data::Collection::Game: desc = entry->field(TQString::fromLatin1("platform")); break; case Data::Collection::ComicBook: desc = entry->field(TQString::fromLatin1("publisher")) + TQChar('/') + entry->field(TQString::fromLatin1("pub_year")); break; case Data::Collection::BoardGame: desc = entry->field(TQString::fromLatin1("designer")) + TQChar('/') + entry->field(TQString::fromLatin1("publisher")) + TQChar('/') + entry->field(TQString::fromLatin1("year")); break; default: break; } SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(TQString::fromLatin1("isbn"))); m_entries.insert(r->uid, entry); emit signalResultFound(r); } stop(); // be sure to call this } Tellico::Data::EntryPtr ExecExternalFetcher::fetchEntry(uint uid_) { return m_entries[uid_]; } void ExecExternalFetcher::updateEntry(Data::EntryPtr entry_) { if(!m_canUpdate) { emit signalDone(this); // must do this } m_started = true; Data::ConstEntryPtr e(entry_.data()); TQStringList args = parseArguments(m_updateArgs); for(TQStringList::Iterator it = args.begin(); it != args.end(); ++it) { *it = Data::Entry::dependentValue(e, *it, false); } startSearch(args); } Tellico::Fetch::ConfigWidget* ExecExternalFetcher::configWidget(TQWidget* parent_) const { return new ExecExternalFetcher::ConfigWidget(parent_, this); } ExecExternalFetcher::ConfigWidget::ConfigWidget(TQWidget* parent_, const ExecExternalFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_), m_deleteOnRemove(false) { TQGridLayout* l = new TQGridLayout(optionsWidget(), 5, 2); l->setSpacing(4); l->setColStretch(1, 10); int row = -1; TQLabel* label = new TQLabel(i18n("Collection &type:"), optionsWidget()); l->addWidget(label, ++row, 0); m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); connect(m_collCombo, TQT_SIGNAL(activated(int)), TQT_SLOT(slotSetModified())); l->addWidget(m_collCombo, row, 1); TQString w = i18n("Set the collection type of the data returned from the external application."); TQWhatsThis::add(label, w); TQWhatsThis::add(m_collCombo, w); label->setBuddy(m_collCombo); label = new TQLabel(i18n("&Result type: "), optionsWidget()); l->addWidget(label, ++row, 0); m_formatCombo = new GUI::ComboBox(optionsWidget()); Import::FormatMap formatMap = ImportDialog::formatMap(); for(Import::FormatMap::Iterator it = formatMap.begin(); it != formatMap.end(); ++it) { if(ImportDialog::formatImportsText(it.key())) { m_formatCombo->insertItem(it.data(), it.key()); } } connect(m_formatCombo, TQT_SIGNAL(activated(int)), TQT_SLOT(slotSetModified())); l->addWidget(m_formatCombo, row, 1); w = i18n("Set the result type of the data returned from the external application."); TQWhatsThis::add(label, w); TQWhatsThis::add(m_formatCombo, w); label->setBuddy(m_formatCombo); label = new TQLabel(i18n("Application &path: "), optionsWidget()); l->addWidget(label, ++row, 0); m_pathEdit = new KURLRequester(optionsWidget()); connect(m_pathEdit, TQT_SIGNAL(textChanged(const TQString&)), TQT_SLOT(slotSetModified())); l->addWidget(m_pathEdit, row, 1); w = i18n("Set the path of the application to run that should output a valid Tellico data file."); TQWhatsThis::add(label, w); TQWhatsThis::add(m_pathEdit, w); label->setBuddy(m_pathEdit); w = i18n("Select the search keys supported by the data source."); TQString w2 = i18n("Add any arguments that may be needed. %1 will be replaced by the search term."); TQVGroupBox* box = new TQVGroupBox(i18n("Arguments"), optionsWidget()); ++row; l->addMultiCellWidget(box, row, row, 0, 1); TQWidget* grid = new TQWidget(box); TQGridLayout* gridLayout = new TQGridLayout(grid); gridLayout->setSpacing(2); row = -1; const Fetch::KeyMap keyMap = Fetch::Manager::self()->keyMap(); for(Fetch::KeyMap::ConstIterator it = keyMap.begin(); it != keyMap.end(); ++it) { FetchKey key = it.key(); if(key == Raw) { continue; } TQCheckBox* cb = new TQCheckBox(it.data(), grid); gridLayout->addWidget(cb, ++row, 0); m_cbDict.insert(key, cb); GUI::LineEdit* le = new GUI::LineEdit(grid); le->setHint(TQString::fromLatin1("%1")); // for example le->completionObject()->addItem(TQString::fromLatin1("%1")); gridLayout->addWidget(le, row, 1); m_leDict.insert(key, le); if(fetcher_ && fetcher_->m_args.contains(key)) { cb->setChecked(true); le->setEnabled(true); le->setText(fetcher_->m_args[key]); } else { cb->setChecked(false); le->setEnabled(false); } connect(cb, TQT_SIGNAL(toggled(bool)), le, TQT_SLOT(setEnabled(bool))); TQWhatsThis::add(cb, w); TQWhatsThis::add(le, w2); } m_cbUpdate = new TQCheckBox(i18n("Update"), grid); gridLayout->addWidget(m_cbUpdate, ++row, 0); m_leUpdate = new GUI::LineEdit(grid); m_leUpdate->setHint(TQString::fromLatin1("%{title}")); // for example m_leUpdate->completionObject()->addItem(TQString::fromLatin1("%{title}")); m_leUpdate->completionObject()->addItem(TQString::fromLatin1("%{isbn}")); gridLayout->addWidget(m_leUpdate, row, 1); /* TRANSLATORS: Do not translate %{author}. */ w2 = i18n("

Enter the arguments which should be used to search for available updates to an entry.

" "The format is the same as for Dependent fields, where field values " "are contained inside braces, such as %{author}. See the documentation for details.

"); TQWhatsThis::add(m_cbUpdate, w); TQWhatsThis::add(m_leUpdate, w2); if(fetcher_ && fetcher_->m_canUpdate) { m_cbUpdate->setChecked(true); m_leUpdate->setEnabled(true); m_leUpdate->setText(fetcher_->m_updateArgs); } else { m_cbUpdate->setChecked(false); m_leUpdate->setEnabled(false); } connect(m_cbUpdate, TQT_SIGNAL(toggled(bool)), m_leUpdate, TQT_SLOT(setEnabled(bool))); l->setRowStretch(++row, 1); if(fetcher_) { m_pathEdit->setURL(fetcher_->m_path); m_newStuffName = fetcher_->m_newStuffName; } if(fetcher_ && fetcher_->m_collType > -1) { m_collCombo->setCurrentType(fetcher_->m_collType); } else { m_collCombo->setCurrentType(Data::Collection::Book); } if(fetcher_ && fetcher_->m_formatType > -1) { m_formatCombo->setCurrentItem(formatMap[static_cast(fetcher_->m_formatType)]); } else { m_formatCombo->setCurrentItem(formatMap[Import::TellicoXML]); } m_deleteOnRemove = fetcher_ && fetcher_->m_deleteOnRemove; KAcceleratorManager::manage(optionsWidget()); } ExecExternalFetcher::ConfigWidget::~ConfigWidget() { } void ExecExternalFetcher::ConfigWidget::readConfig(KConfig* config_) { m_pathEdit->setURL(config_->readPathEntry("ExecPath")); TQValueList argKeys = config_->readIntListEntry("ArgumentKeys"); TQStringList argValues = config_->readListEntry("Arguments"); if(argKeys.count() != argValues.count()) { kdWarning() << "ExecExternalFetcher::ConfigWidget::readConfig() - unequal number of arguments and keys" << endl; } int n = TQMIN(argKeys.count(), argValues.count()); TQMap args; for(int i = 0; i < n; ++i) { args[static_cast(argKeys[i])] = argValues[i]; } for(TQValueList::Iterator it = argKeys.begin(); it != argKeys.end(); ++it) { if(*it == Raw) { continue; } FetchKey key = static_cast(*it); TQCheckBox* cb = m_cbDict[key]; KLineEdit* le = m_leDict[key]; if(cb && le) { if(args.contains(key)) { cb->setChecked(true); le->setEnabled(true); le->setText(args[key]); } else { cb->setChecked(false); le->setEnabled(false); le->clear(); } } } if(config_->hasKey("UpdateArgs")) { m_cbUpdate->setChecked(true); m_leUpdate->setEnabled(true); m_leUpdate->setText(config_->readEntry("UpdateArgs")); } else { m_cbUpdate->setChecked(false); m_leUpdate->setEnabled(false); m_leUpdate->clear(); } int collType = config_->readNumEntry("CollectionType"); m_collCombo->setCurrentType(collType); Import::FormatMap formatMap = ImportDialog::formatMap(); int formatType = config_->readNumEntry("FormatType"); m_formatCombo->setCurrentItem(formatMap[static_cast(formatType)]); m_deleteOnRemove = config_->readBoolEntry("DeleteOnRemove", false); m_name = config_->readEntry("Name"); m_newStuffName = config_->readEntry("NewStuffName"); } void ExecExternalFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { TQString s = m_pathEdit->url(); if(!s.isEmpty()) { config_.writePathEntry("ExecPath", s); } TQValueList keys; TQStringList args; for(TQIntDictIterator it(m_cbDict); it.current(); ++it) { if(it.current()->isChecked()) { keys << it.currentKey(); args << m_leDict[it.currentKey()]->text(); } } config_.writeEntry("ArgumentKeys", keys); config_.writeEntry("Arguments", args); if(m_cbUpdate->isChecked()) { config_.writeEntry("UpdateArgs", m_leUpdate->text()); } else { config_.deleteEntry("UpdateArgs"); } config_.writeEntry("CollectionType", m_collCombo->currentType()); config_.writeEntry("FormatType", m_formatCombo->currentData().toInt()); config_.writeEntry("DeleteOnRemove", m_deleteOnRemove); if(!m_newStuffName.isEmpty()) { config_.writeEntry("NewStuffName", m_newStuffName); } slotSetModified(false); } void ExecExternalFetcher::ConfigWidget::removed() { if(!m_deleteOnRemove) { return; } if(!m_newStuffName.isEmpty()) { NewStuff::Manager man(TQT_TQOBJECT(this)); man.removeScript(m_newStuffName); } } TQString ExecExternalFetcher::ConfigWidget::preferredName() const { return m_name.isEmpty() ? ExecExternalFetcher::defaultName() : m_name; } #include "execexternalfetcher.moc"