You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tellico/src/fetch/execexternalfetcher.cpp

562 lines
19 KiB

/***************************************************************************
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 <tdelocale.h>
#include <tdeconfig.h>
#include <kprocess.h>
#include <kurlrequester.h>
#include <tdeaccelmanager.h>
#include <tqlayout.h>
#include <tqlabel.h>
#include <tqwhatsthis.h>
#include <tqregexp.h>
#include <tqvgroupbox.h>
#include <tqfile.h> // 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 TDEConfigGroup& config_) {
TQString s = config_.readPathEntry("ExecPath");
if(!s.isEmpty()) {
m_path = s;
}
TQValueList<int> 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<FetchKey>(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 TDEProcess::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.arg(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 TDEProcess();
connect(m_process, TQ_SIGNAL(receivedStdout(TDEProcess*, char*, int)), TQ_SLOT(slotData(TDEProcess*, char*, int)));
connect(m_process, TQ_SIGNAL(receivedStderr(TDEProcess*, char*, int)), TQ_SLOT(slotError(TDEProcess*, char*, int)));
connect(m_process, TQ_SIGNAL(processExited(TDEProcess*)), TQ_SLOT(slotProcessExited(TDEProcess*)));
*m_process << m_path << args_;
if(!m_process->start(TDEProcess::NotifyOnExit, TDEProcess::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(TDEProcess*, char* buffer_, int len_) {
TQDataStream stream(m_data, IO_WriteOnly | IO_Append);
stream.writeRawBytes(buffer_, len_);
}
void ExecExternalFetcher::slotError(TDEProcess*, 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(TDEProcess*) {
// 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<Import::Format>(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, TQ_SIGNAL(activated(int)), TQ_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, TQ_SIGNAL(activated(int)), TQ_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, TQ_SIGNAL(textChanged(const TQString&)), TQ_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. <b>%1</b> 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, TQ_SIGNAL(toggled(bool)), le, TQ_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("<p>Enter the arguments which should be used to search for available updates to an entry.</p><p>"
"The format is the same as for <i>Dependent</i> fields, where field values "
"are contained inside braces, such as <i>%{author}</i>. See the documentation for details.</p>");
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, TQ_SIGNAL(toggled(bool)), m_leUpdate, TQ_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<Import::Format>(fetcher_->m_formatType)]);
} else {
m_formatCombo->setCurrentItem(formatMap[Import::TellicoXML]);
}
m_deleteOnRemove = fetcher_ && fetcher_->m_deleteOnRemove;
TDEAcceleratorManager::manage(optionsWidget());
}
ExecExternalFetcher::ConfigWidget::~ConfigWidget() {
}
void ExecExternalFetcher::ConfigWidget::readConfig(TDEConfig* config_) {
m_pathEdit->setURL(config_->readPathEntry("ExecPath"));
TQValueList<int> 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<FetchKey, TQString> args;
for(int i = 0; i < n; ++i) {
args[static_cast<FetchKey>(argKeys[i])] = argValues[i];
}
for(TQValueList<int>::Iterator it = argKeys.begin(); it != argKeys.end(); ++it) {
if(*it == Raw) {
continue;
}
FetchKey key = static_cast<FetchKey>(*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<Import::Format>(formatType)]);
m_deleteOnRemove = config_->readBoolEntry("DeleteOnRemove", false);
m_name = config_->readEntry("Name");
m_newStuffName = config_->readEntry("NewStuffName");
}
void ExecExternalFetcher::ConfigWidget::saveConfig(TDEConfigGroup& config_) {
TQString s = m_pathEdit->url();
if(!s.isEmpty()) {
config_.writePathEntry("ExecPath", s);
}
TQValueList<int> keys;
TQStringList args;
for(TQIntDictIterator<TQCheckBox> 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(this);
man.removeScript(m_newStuffName);
}
}
TQString ExecExternalFetcher::ConfigWidget::preferredName() const {
return m_name.isEmpty() ? ExecExternalFetcher::defaultName() : m_name;
}
#include "execexternalfetcher.moc"