Commit c49a922e authored by Jean-Francois Dockes's avatar Jean-Francois Dockes
Browse files

Implemented saving/loading simple and advanced searches to/from named XML files. Close issue #220

parent b53686a0
......@@ -20,180 +20,11 @@
#include "advshist.h"
#include "guiutils.h"
#include "debuglog.h"
#include <QtXml/QXmlDefaultHandler>
#include "xmltosd.h"
using namespace std;
using namespace Rcl;
class SDHXMLHandler : public QXmlDefaultHandler {
public:
SDHXMLHandler()
{
resetTemps();
}
bool startElement(const QString & /* namespaceURI */,
const QString & /* localName */,
const QString &qName,
const QXmlAttributes &attributes);
bool endElement(const QString & /* namespaceURI */,
const QString & /* localName */,
const QString &qName);
bool characters(const QString &str)
{
currentText += str;
return true;
}
// The object we set up
RefCntr<SearchData> sd;
private:
void resetTemps()
{
currentText = whatclause = "";
text.clear();
field.clear();
slack = 0;
d = m = y = di.d1 = di.m1 = di.y1 = di.d2 = di.m2 = di.y2 = 0;
hasdates = false;
exclude = false;
}
// Temporary data while parsing.
QString currentText;
QString whatclause;
string field, text;
int slack;
int d, m, y;
DateInterval di;
bool hasdates;
bool exclude;
};
bool SDHXMLHandler::startElement(const QString & /* namespaceURI */,
const QString & /* localName */,
const QString &qName,
const QXmlAttributes &)
{
LOGDEB2(("SDHXMLHandler::startElement: name [%s]\n",
(const char *)qName.toUtf8()));
if (qName == "SD") {
resetTemps();
// A new search descriptor. Allocate data structure
sd = RefCntr<SearchData>(new SearchData);
if (sd.isNull()) {
LOGERR(("SDHXMLHandler::startElement: out of memory\n"));
return false;
}
}
return true;
}
bool SDHXMLHandler::endElement(const QString & /* namespaceURI */,
const QString & /* localName */,
const QString &qName)
{
LOGDEB2(("SDHXMLHandler::endElement: name [%s]\n",
(const char *)qName.toUtf8()));
if (qName == "CLT") {
if (currentText == "OR") {
sd->setTp(SCLT_OR);
}
} else if (qName == "CT") {
whatclause = currentText.trimmed();
} else if (qName == "NEG") {
exclude = true;
} else if (qName == "F") {
field = base64_decode(qs2utf8s(currentText.trimmed()));
} else if (qName == "T") {
text = base64_decode(qs2utf8s(currentText.trimmed()));
} else if (qName == "S") {
slack = atoi((const char *)currentText.toUtf8());
} else if (qName == "C") {
SearchDataClause *c;
if (whatclause == "AND" || whatclause.isEmpty()) {
c = new SearchDataClauseSimple(SCLT_AND, text, field);
c->setexclude(exclude);
} else if (whatclause == "OR") {
c = new SearchDataClauseSimple(SCLT_OR, text, field);
c->setexclude(exclude);
} else if (whatclause == "EX") {
// Compat with old hist. We don't generete EX (SCLT_EXCL) anymore
// it's replaced with OR + exclude flag
c = new SearchDataClauseSimple(SCLT_OR, text, field);
c->setexclude(true);
} else if (whatclause == "FN") {
c = new SearchDataClauseFilename(text);
c->setexclude(exclude);
} else if (whatclause == "PH") {
c = new SearchDataClauseDist(SCLT_PHRASE, text, slack, field);
c->setexclude(exclude);
} else if (whatclause == "NE") {
c = new SearchDataClauseDist(SCLT_NEAR, text, slack, field);
c->setexclude(exclude);
} else {
LOGERR(("Bad clause type [%s]\n", qs2utf8s(whatclause).c_str()));
return false;
}
sd->addClause(c);
whatclause = "";
text.clear();
field.clear();
slack = 0;
exclude = false;
} else if (qName == "D") {
d = atoi((const char *)currentText.toUtf8());
} else if (qName == "M") {
m = atoi((const char *)currentText.toUtf8());
} else if (qName == "Y") {
y = atoi((const char *)currentText.toUtf8());
} else if (qName == "DMI") {
di.d1 = d;
di.m1 = m;
di.y1 = y;
hasdates = true;
} else if (qName == "DMA") {
di.d2 = d;
di.m2 = m;
di.y2 = y;
hasdates = true;
} else if (qName == "MIS") {
sd->setMinSize(atoll((const char *)currentText.toUtf8()));
} else if (qName == "MAS") {
sd->setMaxSize(atoll((const char *)currentText.toUtf8()));
} else if (qName == "ST") {
string types = (const char *)currentText.toUtf8();
vector<string> vt;
stringToTokens(types, vt);
for (unsigned int i = 0; i < vt.size(); i++)
sd->addFiletype(vt[i]);
} else if (qName == "IT") {
string types(qs2utf8s(currentText));
vector<string> vt;
stringToTokens(types, vt);
for (unsigned int i = 0; i < vt.size(); i++)
sd->remFiletype(vt[i]);
} else if (qName == "YD") {
string d;
base64_decode(qs2utf8s(currentText.trimmed()), d);
sd->addClause(new SearchDataClausePath(d));
} else if (qName == "ND") {
string d;
base64_decode(qs2utf8s(currentText.trimmed()), d);
sd->addClause(new SearchDataClausePath(d, true));
} else if (qName == "SD") {
// Closing current search descriptor. Finishing touches...
if (hasdates)
sd->setDateSpan(&di);
resetTemps();
}
currentText.clear();
return true;
}
AdvSearchHist::AdvSearchHist()
{
read();
......@@ -208,6 +39,14 @@ AdvSearchHist::~AdvSearchHist()
}
}
RefCntr<Rcl::SearchData> AdvSearchHist::getnewest()
{
if (m_entries.empty())
return RefCntr<Rcl::SearchData>();
return m_entries[0];
}
RefCntr<Rcl::SearchData> AdvSearchHist::getolder()
{
m_current++;
......@@ -244,18 +83,9 @@ bool AdvSearchHist::read()
for (list<string>::const_iterator it = lxml.begin(); it != lxml.end();
it++) {
SDHXMLHandler handler;
QXmlSimpleReader reader;
reader.setContentHandler(&handler);
reader.setErrorHandler(&handler);
QXmlInputSource xmlInputSource;
xmlInputSource.setData(QString::fromUtf8(it->c_str()));
if (!reader.parse(xmlInputSource)) {
LOGERR(("AdvSearchHist::read: parse failed for [%s]\n",
it->c_str()));
return false;
}
m_entries.push_back(handler.sd);
RefCntr<SearchData> sd = xmlToSearchData(*it);
if (sd)
m_entries.push_back(sd);
}
return true;
}
......
......@@ -33,14 +33,25 @@
* turn the XML back into a SearchData object, which is then passed to
* the advanced search object fromSearch() method to rebuild the
* window state.
*/
*
* XML generation is performed by ../rcldb/searchdataxml.cpp.
* See xmltosd.h for a schema description
*/
class AdvSearchHist {
public:
AdvSearchHist();
~AdvSearchHist();
// Add entry
bool push(RefCntr<Rcl::SearchData>);
// Get latest. does not change state
RefCntr<Rcl::SearchData> getnewest();
// Cursor
RefCntr<Rcl::SearchData> getolder();
RefCntr<Rcl::SearchData> getnewer();
bool push(RefCntr<Rcl::SearchData>);
void clear();
private:
......
/* Copyright (C) 2005 J.F.Dockes
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the
* Free Software Foundation, Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "autoconfig.h"
/** Saving and restoring named queries */
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <QSettings>
#include <QMessageBox>
#include <QFileDialog>
#include "rclmain_w.h"
#include "debuglog.h"
#include "readfile.h"
#include "xmltosd.h"
#include "searchdata.h"
using namespace std;
using namespace Rcl;
static QString prevDir()
{
QSettings settings;
QString prevdir =
settings.value("/Recoll/prefs/lastQuerySaveDir").toString();
string defpath = path_cat(theconfig->getConfDir(), "saved_queries");
if (prevdir.isEmpty()) {
if (access(defpath.c_str(), 0) != 0) {
mkdir(defpath.c_str(), 0700);
}
return QString::fromLocal8Bit(defpath.c_str());
} else {
return prevdir;
}
}
void RclMain::saveLastQuery()
{
string xml;
if (lastSearchSimple()) {
xml = sSearch->asXML();
} else {
if (g_advshistory) {
RefCntr<Rcl::SearchData> sd;
sd = g_advshistory->getnewest();
if (sd) {
xml = sd->asXML();
}
}
}
if (xml.empty()) {
QMessageBox::information(this, tr("No search"),
tr("No preserved previous search"));
return;
}
xml = string("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") +
"<recollquery version='1.0'>\n" + xml + "\n</recollquery>\n";
QFileDialog fileDialog(this, tr("Choose file to save"));
fileDialog.setNameFilter(tr("Saved Queries (*.rclq)"));
fileDialog.setDefaultSuffix("rclq");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
fileDialog.setDirectory(prevDir());
if (!fileDialog.exec())
return;
QString s = fileDialog.selectedFiles().first();
if (s.isEmpty()) {
return;
}
string tofile((const char *)s.toLocal8Bit());
LOGDEB(("RclMain::saveLastQuery: XML: [%s]\n", xml.c_str()));
int fd = ::open(tofile.c_str(), O_WRONLY|O_CREAT|O_TRUNC, 0600);
if (fd < 0) {
QMessageBox::warning(this, tr("Open failed"),
tr("Could not open/create file"));
return;
}
if (::write(fd, xml.c_str(), xml.size()) != int(xml.size())) {
::close(fd);
QMessageBox::warning(this, tr("Write failed"),
tr("Could not write to file"));
return;
}
if (::close(fd) != 0) {
QMessageBox::warning(this, tr("Close failed"), tr("File close error"));
return;
}
}
void RclMain::loadSavedQuery()
{
QString s =
QFileDialog::getOpenFileName(this, "Open saved query", prevDir(),
tr("Saved Queries (*.rclq)"));
if (s.isEmpty())
return;
string fromfile((const char *)s.toLocal8Bit());
string xml, reason;
if (!file_to_string(fromfile, xml, &reason)) {
QMessageBox::warning(this, tr("Read failed"),
tr("Could not open file: ") +
QString::fromUtf8(reason.c_str()));
return;
}
// Try to parse as SearchData
RefCntr<SearchData> sd = xmlToSearchData(xml);
if (sd) {
showAdvSearchDialog();
asearchform->fromSearch(sd);
return;
}
// Try to parse as Simple Search
SSearchDef sdef;
if (xmlToSSearch(xml, sdef)) {
if (sSearch->fromXML(sdef))
return;
}
QMessageBox::warning(this, tr("Load error"),
tr("Could not load saved query"));
}
......@@ -76,6 +76,9 @@
<addaction name="fileRetryFailedAction"/>
<addaction name="fileRebuildIndexAction"/>
<addaction name="separator"/>
<addaction name="actionSave_last_query"/>
<addaction name="actionLoad_saved_query"/>
<addaction name="separator"/>
<addaction name="fileEraseSearchHistoryAction"/>
<addaction name="fileEraseDocHistoryAction"/>
<addaction name="separator"/>
......@@ -484,6 +487,16 @@
<cstring>fileToggleIndexingAction</cstring>
</property>
</action>
<action name="actionSave_last_query">
<property name="text">
<string>Save last query</string>
</property>
</action>
<action name="actionLoad_saved_query">
<property name="text">
<string>Load saved query</string>
</property>
</action>
</widget>
<layoutdefault spacing="2" margin="2"/>
<customwidgets>
......
......@@ -324,6 +324,11 @@ void RclMain::init()
this, SLOT(eraseDocHistory()));
connect(fileEraseSearchHistoryAction, SIGNAL(triggered()),
this, SLOT(eraseSearchHistory()));
connect(actionSave_last_query, SIGNAL(triggered()),
this, SLOT(saveLastQuery()));
connect(actionLoad_saved_query, SIGNAL(triggered()),
this, SLOT(loadSavedQuery()));
connect(helpAbout_RecollAction, SIGNAL(triggered()),
this, SLOT(showAboutDialog()));
connect(showMissingHelpers_Action, SIGNAL(triggered()),
......@@ -1077,6 +1082,9 @@ void RclMain::showAdvSearchDialog()
{
if (asearchform == 0) {
asearchform = new AdvSearch(0);
if (asearchform == 0) {
return;
}
connect(new QShortcut(quitKeySeq, asearchform), SIGNAL (activated()),
this, SLOT (fileExit()));
......
......@@ -153,6 +153,8 @@ public slots:
virtual void resetSearch();
virtual void eraseDocHistory();
virtual void eraseSearchHistory();
virtual void saveLastQuery();
virtual void loadSavedQuery();
virtual void setStemLang(QAction *id);
virtual void adjustPrefsMenu();
virtual void catgFilter(int);
......
......@@ -52,6 +52,7 @@ SOURCES += \
ptrans_w.cpp \
rclhelp.cpp \
rclmain_w.cpp \
rclm_saveload.cpp \
rclzg.cpp \
respopup.cpp \
reslist.cpp \
......@@ -64,7 +65,7 @@ SOURCES += \
systray.cpp \
uiprefs_w.cpp \
viewaction_w.cpp \
xmltosd.cpp
FORMS = \
advsearch.ui \
......
......@@ -14,6 +14,9 @@
* Free Software Foundation, Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include <sstream>
#include <set>
#include <qapplication.h>
#include <qinputdialog.h>
#include <qvariant.h>
......@@ -35,6 +38,10 @@
#include "textsplit.h"
#include "wasatorcl.h"
#include "rclhelp.h"
#include "xmltosd.h"
#include "smallut.h"
using namespace std;
// Typing interval after which we consider starting autosearch: no sense to do
// this is user is typing fast and continuously
......@@ -226,21 +233,36 @@ void SSearch::setPrefs()
}
}
string SSearch::asXML()
{
return m_xml;
}
bool SSearch::startSimpleSearch(const string& u8, int maxexp)
{
LOGDEB(("SSearch::startSimpleSearch(%s)\n", u8.c_str()));
string stemlang = prefs.stemlang();
ostringstream xml;
xml << "<SD type='ssearch'>\n";
xml << " <SL>" << stemlang << "</SL>\n";
xml << " <T>" << base64_encode(u8) << "</T>\n";
SSearchType tp = (SSearchType)searchTypCMB->currentIndex();
Rcl::SearchData *sdata = 0;
if (tp == SST_LANG) {
xml << " <SM>QL</SM>\n";
string reason;
if (prefs.autoSuffsEnable)
if (prefs.autoSuffsEnable) {
sdata = wasaStringToRcl(theconfig, stemlang, u8, reason,
(const char *)prefs.autoSuffs.toUtf8());
else
if (!prefs.autoSuffs.isEmpty()) {
xml << " <AS>" << qs2utf8s(prefs.autoSuffs) << "</AS>\n";
}
} else {
sdata = wasaStringToRcl(theconfig, stemlang, u8, reason);
}
if (sdata == 0) {
QMessageBox::warning(0, "Recoll", tr("Bad query string") + ": " +
QString::fromUtf8(reason.c_str()));
......@@ -254,12 +276,15 @@ bool SSearch::startSimpleSearch(const string& u8, int maxexp)
}
Rcl::SearchDataClause *clp = 0;
if (tp == SST_FNM) {
xml << " <SM>FN</SM>\n";
clp = new Rcl::SearchDataClauseFilename(u8);
} else {
// ANY or ALL, several words.
if (tp == SST_ANY) {
xml << " <SM>OR</SM>\n";
clp = new Rcl::SearchDataClauseSimple(Rcl::SCLT_OR, u8);
} else {
xml << " <SM>AND</SM>\n";
clp = new Rcl::SearchDataClauseSimple(Rcl::SCLT_AND, u8);
}
}
......@@ -267,17 +292,82 @@ bool SSearch::startSimpleSearch(const string& u8, int maxexp)
}
if (prefs.ssearchAutoPhrase && rcldb) {
xml << " <AP/>\n";
sdata->maybeAddAutoPhrase(*rcldb,
prefs.ssearchAutoPhraseThreshPC / 100.0);
}
if (maxexp != -1) {
sdata->setMaxExpand(maxexp);
}
for (list<string>::const_iterator it = prefs.activeExtraDbs.begin();
it != prefs.activeExtraDbs.end(); it++) {
xml << " <EX>" << base64_encode(*it) << "</EX>";
}
xml << "</SD>\n";
m_xml = xml.str();
LOGDEB(("SSearch::startSimpleSearch:xml:[%s]\n", m_xml.c_str()));
RefCntr<Rcl::SearchData> rsdata(sdata);
emit startSearch(rsdata, true);
return true;
}
bool SSearch::fromXML(const SSearchDef& fxml)
{
string asString;
set<string> cur;
set<string> stored;
// Retrieve current list of stemlangs. prefs returns a
// space-separated list Warn if stored differs from current,
// but don't change the latter.
stringToStrings(prefs.stemlang(), cur);
stored = set<string>(fxml.stemlangs.begin(), fxml.stemlangs.end());
stringsToString(fxml.stemlangs, asString);
if (cur != stored) {
QMessageBox::warning(
0, "Recoll", tr("Stemming languages for stored query: ") +
QString::fromUtf8(asString.c_str()) +
tr(" differ from current preferences (kept)"));
}
// Same for autosuffs
stringToStrings(qs2utf8s(prefs.autoSuffs), cur);
stored = set<string>(fxml.autosuffs.begin(), fxml.autosuffs.end());
stringsToString(fxml.stemlangs, asString);
if (cur != stored) {
QMessageBox::warning(
0, "Recoll", tr("Auto suffixes for stored query: ") +
QString::fromUtf8(asString.c_str()) +
tr(" differ from current preferences (kept)"));
}
cur = set<string>(prefs.activeExtraDbs.begin(), prefs.activeExtraDbs.end());
stored = set<string>(fxml.extindexes.begin(), fxml.extindexes.end());
stringsToString(fxml.extindexes, asString);
if (cur != stored) {
QMessageBox::warning(
0, "Recoll", tr("External indexes for stored query: ") +
QString::fromUtf8(asString.c_str()) +
tr(" differ from current preferences (kept)"));
}
if (prefs.ssearchAutoPhrase && !fxml.autophrase) {
QMessageBox::warning(
0, "Recoll",
tr("Autophrase is set but it was unset for stored query"));
} else if (!prefs.ssearchAutoPhrase && fxml.autophrase) {
QMessageBox::warning(