1 | |
2 | #include "searchengine.h" |
3 | |
4 | #include "stdlib.h" |
5 | |
6 | #include <QTextDocument> |
7 | #include <KApplication> |
8 | #include <KConfig> |
9 | #include <KDebug> |
10 | #include <KStandardDirs> |
11 | #include <KProcess> |
12 | #include <KShell> |
13 | #include <KLocale> |
14 | #include <KMessageBox> |
15 | |
16 | #include "docmetainfo.h" |
17 | #include "formatter.h" |
18 | #include "view.h" |
19 | #include "searchhandler.h" |
20 | #include "prefs.h" |
21 | |
22 | namespace KHC |
23 | { |
24 | |
25 | SearchTraverser::SearchTraverser( SearchEngine *engine, int level ) : |
26 | mMaxLevel( 999 ), mEngine( engine), mLevel( level ) |
27 | { |
28 | #if 0 |
29 | kDebug() << "SearchTraverser(): " << mLevel |
30 | << " 0x" << QString::number( int( this ), 16 ) << endl; |
31 | #endif |
32 | } |
33 | |
34 | SearchTraverser::~SearchTraverser() |
35 | { |
36 | #if 0 |
37 | kDebug() << "~SearchTraverser(): " << mLevel |
38 | << " 0x" << QString::number( int( this ), 16 ) << endl; |
39 | #endif |
40 | |
41 | QString section; |
42 | if ( parentEntry() ) { |
43 | section = parentEntry()->name(); |
44 | } else { |
45 | section = ("Unknown Section" ); |
46 | } |
47 | |
48 | if ( !mResult.isEmpty() ) { |
49 | mEngine->view()->writeSearchResult( |
50 | mEngine->formatter()->sectionHeader( section ) ); |
51 | mEngine->view()->writeSearchResult( mResult ); |
52 | } |
53 | } |
54 | |
55 | void SearchTraverser::process( DocEntry * ) |
56 | { |
57 | kDebug() << "SearchTraverser::process()" ; |
58 | } |
59 | |
60 | void SearchTraverser::startProcess( DocEntry *entry ) |
61 | { |
62 | // kDebug() << "SearchTraverser::startProcess(): " << entry->name() << " " |
63 | // << "SEARCH: '" << entry->search() << "'" << endl; |
64 | |
65 | if ( !mEngine->canSearch( entry ) || !entry->searchEnabled() ) { |
66 | mNotifyee->endProcess( entry, this ); |
67 | return; |
68 | } |
69 | |
70 | // kDebug() << "SearchTraverser::startProcess(): " << entry->identifier() |
71 | // << endl; |
72 | |
73 | SearchHandler *handler = mEngine->handler( entry->documentType() ); |
74 | |
75 | if ( !handler ) { |
76 | QString txt; |
77 | if ( entry->documentType().isEmpty() ) { |
78 | txt = i18n("Error: No document type specified." ); |
79 | } else { |
80 | txt = i18n("Error: No search handler for document type '%1'." , |
81 | entry->documentType() ); |
82 | } |
83 | showSearchError( handler, entry, txt ); |
84 | return; |
85 | } |
86 | |
87 | connectHandler( handler ); |
88 | |
89 | handler->search( entry, mEngine->words(), mEngine->maxResults(), |
90 | mEngine->operation() ); |
91 | |
92 | // kDebug() << "SearchTraverser::startProcess() done: " << entry->name(); |
93 | } |
94 | |
95 | void SearchTraverser::connectHandler( SearchHandler *handler ) |
96 | { |
97 | QMap<SearchHandler *,int>::Iterator it; |
98 | it = mConnectCount.find( handler ); |
99 | int count = 0; |
100 | if ( it != mConnectCount.end() ) count = *it; |
101 | if ( count == 0 ) { |
102 | connect( handler, SIGNAL( searchError( SearchHandler *, DocEntry *, const QString & ) ), |
103 | SLOT( showSearchError( SearchHandler *, DocEntry *, const QString & ) ) ); |
104 | connect( handler, SIGNAL( searchFinished( SearchHandler *, DocEntry *, const QString & ) ), |
105 | SLOT( showSearchResult( SearchHandler *, DocEntry *, const QString & ) ) ); |
106 | } |
107 | mConnectCount[ handler ] = ++count; |
108 | } |
109 | |
110 | void SearchTraverser::disconnectHandler( SearchHandler *handler ) |
111 | { |
112 | QMap<SearchHandler *,int>::Iterator it; |
113 | it = mConnectCount.find( handler ); |
114 | if ( it == mConnectCount.end() ) { |
115 | kError() << "SearchTraverser::disconnectHandler() handler not connected." |
116 | << endl; |
117 | } else { |
118 | int count = *it; |
119 | --count; |
120 | if ( count == 0 ) { |
121 | disconnect( handler, SIGNAL( searchError( SearchHandler *, DocEntry *, const QString & ) ), |
122 | this, SLOT( showSearchError( SearchHandler *, DocEntry *, const QString & ) ) ); |
123 | disconnect( handler, SIGNAL( searchFinished( SearchHandler *, DocEntry *, const QString & ) ), |
124 | this, SLOT( showSearchResult( SearchHandler *, DocEntry *, const QString & ) ) ); |
125 | } |
126 | mConnectCount[ handler ] = count; |
127 | } |
128 | } |
129 | |
130 | DocEntryTraverser *SearchTraverser::createChild( DocEntry *parentEntry ) |
131 | { |
132 | // kDebug() << "SearchTraverser::createChild() level " << mLevel; |
133 | |
134 | if ( mLevel >= mMaxLevel ) { |
135 | ++mLevel; |
136 | return this; |
137 | } else { |
138 | DocEntryTraverser *t = new SearchTraverser( mEngine, mLevel + 1 ); |
139 | t->setParentEntry( parentEntry ); |
140 | return t; |
141 | } |
142 | } |
143 | |
144 | DocEntryTraverser *SearchTraverser::parentTraverser() |
145 | { |
146 | // kDebug() << "SearchTraverser::parentTraverser(): level: " << mLevel; |
147 | |
148 | if ( mLevel > mMaxLevel ) { |
149 | return this; |
150 | } else { |
151 | return mParent; |
152 | } |
153 | } |
154 | |
155 | void SearchTraverser::deleteTraverser() |
156 | { |
157 | // kDebug() << "SearchTraverser::deleteTraverser()"; |
158 | |
159 | if ( mLevel > mMaxLevel ) { |
160 | --mLevel; |
161 | } else { |
162 | delete this; |
163 | } |
164 | } |
165 | |
166 | void SearchTraverser::showSearchError( SearchHandler *handler, DocEntry *entry, const QString &error ) |
167 | { |
168 | // kDebug() << "SearchTraverser::showSearchError(): " << entry->name() |
169 | // << endl; |
170 | |
171 | mResult += mEngine->formatter()->docTitle( entry->name() ); |
172 | mResult += mEngine->formatter()->paragraph( error ); |
173 | |
174 | mEngine->logError( entry, error ); |
175 | |
176 | disconnectHandler( handler ); |
177 | |
178 | mNotifyee->endProcess( entry, this ); |
179 | } |
180 | |
181 | void SearchTraverser::showSearchResult( SearchHandler *handler, DocEntry *entry, const QString &result ) |
182 | { |
183 | // kDebug() << "SearchTraverser::showSearchResult(): " << entry->name() |
184 | // << endl; |
185 | |
186 | mResult += mEngine->formatter()->docTitle( entry->name() ); |
187 | mResult += mEngine->formatter()->processResult( result ); |
188 | |
189 | disconnectHandler( handler ); |
190 | |
191 | mNotifyee->endProcess( entry, this ); |
192 | } |
193 | |
194 | void SearchTraverser::finishTraversal() |
195 | { |
196 | // kDebug() << "SearchTraverser::finishTraversal()"; |
197 | |
198 | mEngine->view()->writeSearchResult( mEngine->formatter()->footer() ); |
199 | mEngine->view()->endSearchResult(); |
200 | |
201 | mEngine->finishSearch(); |
202 | } |
203 | |
204 | |
205 | SearchEngine::SearchEngine( View *destination ) |
206 | : QObject(), |
207 | mProc( 0 ), mSearchRunning( false ), mView( destination ), |
208 | mRootTraverser( 0 ) |
209 | { |
210 | mLang = KGlobal::locale()->language().left( 2 ); |
211 | } |
212 | |
213 | SearchEngine::~SearchEngine() |
214 | { |
215 | delete mRootTraverser; |
216 | } |
217 | |
218 | bool SearchEngine::initSearchHandlers() |
219 | { |
220 | const QStringList resources = KGlobal::dirs()->findAllResources( |
221 | "appdata" , "searchhandlers/*.desktop" ); |
222 | QStringList::ConstIterator it; |
223 | for( it = resources.constBegin(); it != resources.constEnd(); ++it ) { |
224 | QString filename = *it; |
225 | kDebug() << "SearchEngine::initSearchHandlers(): " << filename; |
226 | SearchHandler *handler = SearchHandler::initFromFile( filename ); |
227 | if ( !handler ) { |
228 | QString txt = i18n("Unable to initialize SearchHandler from file '%1'." , |
229 | filename ); |
230 | kWarning() << txt ; |
231 | // KMessageBox::sorry( mView->widget(), txt ); |
232 | } else { |
233 | QStringList documentTypes = handler->documentTypes(); |
234 | QStringList::ConstIterator it; |
235 | for( it = documentTypes.constBegin(); it != documentTypes.constEnd(); ++it ) { |
236 | mHandlers.insert( *it, handler ); |
237 | } |
238 | } |
239 | } |
240 | |
241 | if ( mHandlers.isEmpty() ) { |
242 | QString txt = i18n("No valid search handler found." ); |
243 | kWarning() << txt ; |
244 | // KMessageBox::sorry( mView->widget(), txt ); |
245 | return false; |
246 | } |
247 | |
248 | return true; |
249 | } |
250 | |
251 | void SearchEngine::searchExited(int exitCode, QProcess::ExitStatus exitStatus) |
252 | { |
253 | Q_UNUSED(exitCode); |
254 | Q_UNUSED(exitStatus); |
255 | kDebug() << "Search terminated" ; |
256 | mSearchRunning = false; |
257 | } |
258 | |
259 | bool SearchEngine::search( const QString & words, const QString & method, int matches, |
260 | const QString & scope ) |
261 | { |
262 | if ( mSearchRunning ) return false; |
263 | |
264 | // These should be removed |
265 | mWords = words; |
266 | mMethod = method; |
267 | mMatches = matches; |
268 | mScope = scope; |
269 | |
270 | // Saner variables to store search parameters: |
271 | mWordList = words.split(' '); |
272 | mMaxResults = matches; |
273 | if ( method == "or" ) mOperation = Or; |
274 | else mOperation = And; |
275 | |
276 | KConfigGroup cfg(KGlobal::config(), "Search" ); |
277 | QString commonSearchProgram = cfg.readPathEntry( "CommonProgram" , QString() ); |
278 | bool useCommon = cfg.readEntry( "UseCommonProgram" , false); |
279 | |
280 | if ( commonSearchProgram.isEmpty() || !useCommon ) { |
281 | if ( !mView ) { |
282 | return false; |
283 | } |
284 | |
285 | QString txt = i18n("Search Results for '%1':" , Qt::escape(words) ); |
286 | |
287 | mStderr = "<b>" + txt + "</b>\n" ; |
288 | |
289 | mView->beginSearchResult(); |
290 | mView->writeSearchResult( formatter()->header( i18n("Search Results" ) ) ); |
291 | mView->writeSearchResult( formatter()->title( txt ) ); |
292 | |
293 | if ( mRootTraverser ) { |
294 | kDebug() << "SearchEngine::search(): mRootTraverser not null." ; |
295 | return false; |
296 | } |
297 | mRootTraverser = new SearchTraverser( this, 0 ); |
298 | DocMetaInfo::self()->startTraverseEntries( mRootTraverser ); |
299 | |
300 | return true; |
301 | } else { |
302 | QString lang = KGlobal::locale()->language().left(2); |
303 | |
304 | if ( lang.toLower() == "c" || lang.toLower() == "posix" ) |
305 | lang = "en" ; |
306 | |
307 | // if the string contains '&' replace with a '+' and set search method to and |
308 | if (mWords.indexOf("&" ) != -1) { |
309 | mWords.replace('&', ' '); |
310 | mMethod = "and" ; |
311 | } |
312 | |
313 | // replace whitespace with a '+' |
314 | mWords = mWords.trimmed(); |
315 | mWords = mWords.simplified(); |
316 | mWords.replace(QRegExp("\\s" ), "+" ); |
317 | |
318 | commonSearchProgram = substituteSearchQuery( commonSearchProgram ); |
319 | |
320 | kDebug() << "Common Search: " << commonSearchProgram; |
321 | |
322 | mProc = new KProcess(); |
323 | *mProc << KShell::splitArgs(commonSearchProgram); |
324 | |
325 | connect( mProc, SIGNAL( finished(int, QProcess::ExitStatus) ), |
326 | this, SLOT( searchExited(int, QProcess::ExitStatus) ) ); |
327 | |
328 | mSearchRunning = true; |
329 | mSearchResult = "" ; |
330 | mStderr = "<b>" + commonSearchProgram + "</b>\n\n" ; |
331 | |
332 | mProc->start(); |
333 | if (!mProc->waitForStarted()) { |
334 | kError() << "could not start search program '" << commonSearchProgram |
335 | << "'" << endl; |
336 | delete mProc; |
337 | return false; |
338 | } |
339 | |
340 | while (mSearchRunning && mProc->state() == QProcess::Running) |
341 | kapp->processEvents(); |
342 | |
343 | // no need to use signals/slots |
344 | mStderr += mProc->readAllStandardError(); |
345 | mSearchResult += mProc->readAllStandardOutput(); |
346 | |
347 | if ( mProc->exitStatus() == KProcess::CrashExit || mProc->exitCode() != 0 ) { |
348 | kError() << "Unable to run search program '" << commonSearchProgram |
349 | << "'" << endl; |
350 | delete mProc; |
351 | |
352 | return false; |
353 | } |
354 | |
355 | delete mProc; |
356 | |
357 | // modify the search result |
358 | mSearchResult = mSearchResult.replace("http://localhost/" , "file:/" ); |
359 | mSearchResult = mSearchResult.mid( mSearchResult.indexOf( '<' ) ); |
360 | |
361 | mView->beginSearchResult(); |
362 | mView->writeSearchResult( mSearchResult ); |
363 | mView->endSearchResult(); |
364 | |
365 | emit searchFinished(); |
366 | } |
367 | |
368 | return true; |
369 | } |
370 | |
371 | QString SearchEngine::substituteSearchQuery( const QString &query ) |
372 | { |
373 | QString result = query; |
374 | result.replace( QLatin1String("%k" ), mWords ); |
375 | result.replace( QLatin1String("%n" ), QString::number( mMatches ) ); |
376 | result.replace( QLatin1String("%m" ), mMethod ); |
377 | result.replace( QLatin1String("%l" ), mLang ); |
378 | result.replace( QLatin1String("%s" ), mScope ); |
379 | |
380 | return result; |
381 | } |
382 | |
383 | QString SearchEngine::substituteSearchQuery( const QString &query, |
384 | const QString &identifier, const QStringList &words, int maxResults, |
385 | Operation operation, const QString &lang, const QString& binary ) |
386 | { |
387 | QString result = query; |
388 | result.replace( QLatin1String("%i" ), identifier ); |
389 | result.replace( QLatin1String("%w" ), words.join( "+" ) ); |
390 | result.replace( QLatin1String("%m" ), QString::number( maxResults ) ); |
391 | QString o = QLatin1String(operation == Or ? "or" : "and" ); |
392 | result.replace( QLatin1String("%o" ), o ); |
393 | result.replace( QLatin1String("%d" ), Prefs::indexDirectory() ); |
394 | result.replace( QLatin1String("%l" ), lang ); |
395 | result.replace( QLatin1String("%b" ), binary ); |
396 | |
397 | return result; |
398 | } |
399 | |
400 | Formatter *SearchEngine::formatter() const |
401 | { |
402 | return mView->formatter(); |
403 | } |
404 | |
405 | View *SearchEngine::view() const |
406 | { |
407 | return mView; |
408 | } |
409 | |
410 | void SearchEngine::finishSearch() |
411 | { |
412 | delete mRootTraverser; |
413 | mRootTraverser = 0; |
414 | |
415 | emit searchFinished(); |
416 | } |
417 | |
418 | QString SearchEngine::errorLog() const |
419 | { |
420 | return mStderr; |
421 | } |
422 | |
423 | void SearchEngine::logError( DocEntry *entry, const QString &msg ) |
424 | { |
425 | mStderr += entry->identifier() + QLatin1String(": " ) + msg; |
426 | } |
427 | |
428 | bool SearchEngine::isRunning() const |
429 | { |
430 | return mSearchRunning; |
431 | } |
432 | |
433 | SearchHandler *SearchEngine::handler( const QString &documentType ) const |
434 | { |
435 | return mHandlers.value( documentType, 0 ); |
436 | } |
437 | |
438 | QStringList SearchEngine::words() const |
439 | { |
440 | return mWordList; |
441 | } |
442 | |
443 | int SearchEngine::maxResults() const |
444 | { |
445 | return mMaxResults; |
446 | } |
447 | |
448 | SearchEngine::Operation SearchEngine::operation() const |
449 | { |
450 | return mOperation; |
451 | } |
452 | |
453 | bool SearchEngine::canSearch( DocEntry *entry ) |
454 | { |
455 | return entry->docExists() && !entry->documentType().isEmpty() && |
456 | handler( entry->documentType() ); |
457 | } |
458 | |
459 | bool SearchEngine::needsIndex( DocEntry *entry ) |
460 | { |
461 | if ( !canSearch( entry ) ) return false; |
462 | |
463 | SearchHandler *h = handler( entry->documentType() ); |
464 | if ( !h || h->indexCommand( entry->identifier() ).isEmpty() ) return false; |
465 | |
466 | return true; |
467 | } |
468 | |
469 | } |
470 | |
471 | #include "searchengine.moc" |
472 | |
473 | // vim:ts=2:sw=2:et |
474 | |