1/*
2 * Copyright 2006-2007 Aaron Seigo <aseigo@kde.org>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Library General Public License as
6 * published by the Free Software Foundation; either version 2, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20#include "runnercontext.h"
21
22#include <cmath>
23
24#include <QReadWriteLock>
25
26#include <QDir>
27#include <QFile>
28#include <QFileInfo>
29#include <QSharedData>
30
31#include <kcompletion.h>
32#include <kconfiggroup.h>
33#include <kdebug.h>
34#include <kmimetype.h>
35#include <kshell.h>
36#include <kstandarddirs.h>
37#include <kurl.h>
38#include <kprotocolinfo.h>
39
40#include "abstractrunner.h"
41#include "querymatch.h"
42
43//#define LOCK_FOR_READ(d) if (d->policy == Shared) { d->lock.lockForRead(); }
44//#define LOCK_FOR_WRITE(d) if (d->policy == Shared) { d->lock.lockForWrite(); }
45//#define UNLOCK(d) if (d->policy == Shared) { d->lock.unlock(); }
46
47#define LOCK_FOR_READ(d) d->lock.lockForRead();
48#define LOCK_FOR_WRITE(d) d->lock.lockForWrite();
49#define UNLOCK(d) d->lock.unlock();
50
51namespace Plasma
52{
53
54/*
55Corrects the case of the last component in a path (e.g. /usr/liB -> /usr/lib)
56path: The path to be processed.
57correctCasePath: The corrected-case path
58mustBeDir: Tells whether the last component is a folder or doesn't matter
59Returns true on success and false on error, in case of error, correctCasePath is not modified
60*/
61bool correctLastComponentCase(const QString &path, QString &correctCasePath, const bool mustBeDir)
62{
63 //kDebug() << "Correcting " << path;
64
65 // If the file already exists then no need to search for it.
66 if (QFile::exists(path)) {
67 correctCasePath = path;
68 //kDebug() << "Correct path is" << correctCasePath;
69 return true;
70 }
71
72 const QFileInfo pathInfo(path);
73
74 const QDir fileDir = pathInfo.dir();
75 //kDebug() << "Directory is" << fileDir;
76
77 const QString filename = pathInfo.fileName();
78 //kDebug() << "Filename is" << filename;
79
80 //kDebug() << "searching for a" << (mustBeDir ? "directory" : "directory/file");
81
82 const QStringList matchingFilenames = fileDir.entryList(QStringList(filename),
83 mustBeDir ? QDir::Dirs : QDir::NoFilter);
84
85 if (matchingFilenames.empty()) {
86 //kDebug() << "No matches found!!\n";
87 return false;
88 } else {
89 /*if (matchingFilenames.size() > 1) {
90 kDebug() << "Found multiple matches!!\n";
91 }*/
92
93 if (fileDir.path().endsWith(QDir::separator())) {
94 correctCasePath = fileDir.path() + matchingFilenames[0];
95 } else {
96 correctCasePath = fileDir.path() + QDir::separator() + matchingFilenames[0];
97 }
98
99 //kDebug() << "Correct path is" << correctCasePath;
100 return true;
101 }
102}
103
104/*
105Corrects the case of a path (e.g. /uSr/loCAL/bIN -> /usr/local/bin)
106path: The path to be processed.
107corrected: The corrected-case path
108Returns true on success and false on error, in case of error, corrected is not modified
109*/
110bool correctPathCase(const QString& path, QString &corrected)
111{
112 // early exit check
113 if (QFile::exists(path)) {
114 corrected = path;
115 return true;
116 }
117
118 // path components
119 QStringList components = QString(path).split(QDir::separator());
120
121 if (components.size() < 1) {
122 return false;
123 }
124
125 const bool mustBeDir = components.back().isEmpty();
126
127 //kDebug() << "Components are" << components;
128
129 if (mustBeDir) {
130 components.pop_back();
131 }
132
133 if (components.isEmpty()) {
134 return true;
135 }
136
137 QString correctPath;
138 const unsigned initialComponents = components.size();
139 for (unsigned i = 0; i < initialComponents - 1; i ++) {
140 const QString tmp = components[0] + QDir::separator() + components[1];
141
142 if (!correctLastComponentCase(tmp, correctPath, components.size() > 2 || mustBeDir)) {
143 //kDebug() << "search was not successful";
144 return false;
145 }
146
147 components.removeFirst();
148 components[0] = correctPath;
149 }
150
151 corrected = correctPath;
152 return true;
153}
154
155class RunnerContextPrivate : public QSharedData
156{
157 public:
158 RunnerContextPrivate(RunnerContext *context)
159 : QSharedData(),
160 type(RunnerContext::UnknownType),
161 q(context),
162 singleRunnerQueryMode(false)
163 {
164 }
165
166 RunnerContextPrivate(const RunnerContextPrivate &p)
167 : QSharedData(),
168 launchCounts(p.launchCounts),
169 type(RunnerContext::None),
170 q(p.q),
171 singleRunnerQueryMode(false)
172 {
173 //kDebug() << "¿¿¿¿¿¿¿¿¿¿¿¿¿¿¿¿¿boo yeah" << type;
174 }
175
176 ~RunnerContextPrivate()
177 {
178 }
179
180 /**
181 * Determines type of query
182 &&
183 */
184 void determineType()
185 {
186 // NOTE! this method must NEVER be called from
187 // code that may be running in multiple threads
188 // with the same data.
189 type = RunnerContext::UnknownType;
190 QString path = QDir::cleanPath(KShell::tildeExpand(term));
191
192 int space = path.indexOf(' ');
193 if (!KStandardDirs::findExe(path.left(space)).isEmpty()) {
194 // it's a shell command if there's a space because that implies
195 // that it has arguments!
196 type = (space > 0) ? RunnerContext::ShellCommand :
197 RunnerContext::Executable;
198 } else {
199 KUrl url(term);
200 // check for a normal URL first
201 //kDebug() << url << KProtocolInfo::protocolClass(url.protocol()) << url.hasHost() <<
202 // url.host() << url.isLocalFile() << path << path.indexOf('/');
203 const bool hasProtocol = !url.protocol().isEmpty();
204 const bool isLocalProtocol = KProtocolInfo::protocolClass(url.protocol()) == ":local";
205 if (hasProtocol &&
206 ((!isLocalProtocol && url.hasHost()) ||
207 (isLocalProtocol && url.protocol() != "file"))) {
208 // we either have a network protocol with a host, so we can show matches for it
209 // or we have a non-file url that may be local so a host isn't required
210 type = RunnerContext::NetworkLocation;
211 } else if (isLocalProtocol) {
212 // at this point in the game, we assume we have a path,
213 // but if a path doesn't have any slashes
214 // it's too ambiguous to be sure we're in a filesystem context
215 path = QDir::cleanPath(url.toLocalFile());
216 //kDebug( )<< "slash check" << path;
217 if (hasProtocol || ((path.indexOf('/') != -1 || path.indexOf('\\') != -1))) {
218 QString correctCasePath;
219 if (correctPathCase(path, correctCasePath)) {
220 path = correctCasePath;
221 QFileInfo info(path);
222 //kDebug( )<< "correct cas epath is" << correctCasePath << info.isSymLink() <<
223 // info.isDir() << info.isFile();
224
225 if (info.isSymLink()) {
226 path = info.canonicalFilePath();
227 info = QFileInfo(path);
228 }
229 if (info.isDir()) {
230 type = RunnerContext::Directory;
231 mimeType = "inode/folder";
232 } else if (info.isFile()) {
233 type = RunnerContext::File;
234 KMimeType::Ptr mimeTypePtr = KMimeType::findByPath(path);
235 if (mimeTypePtr) {
236 mimeType = mimeTypePtr->name();
237 }
238 }
239 }
240 }
241 }
242 }
243
244 //kDebug() << "term2type" << term << type;
245 }
246
247 void invalidate()
248 {
249 q = &s_dummyContext;
250 }
251
252 QReadWriteLock lock;
253 QList<QueryMatch> matches;
254 QMap<QString, const QueryMatch*> matchesById;
255 QHash<QString, int> launchCounts;
256 QString term;
257 QString mimeType;
258 RunnerContext::Type type;
259 RunnerContext * q;
260 static RunnerContext s_dummyContext;
261 bool singleRunnerQueryMode;
262};
263
264RunnerContext RunnerContextPrivate::s_dummyContext;
265
266RunnerContext::RunnerContext(QObject *parent)
267 : QObject(parent),
268 d(new RunnerContextPrivate(this))
269{
270}
271
272//copy ctor
273RunnerContext::RunnerContext(RunnerContext &other, QObject *parent)
274 : QObject(parent)
275{
276 LOCK_FOR_READ(other.d)
277 d = other.d;
278 UNLOCK(other.d)
279}
280
281RunnerContext::~RunnerContext()
282{
283}
284
285RunnerContext &RunnerContext::operator=(const RunnerContext &other)
286{
287 if (this->d == other.d) {
288 return *this;
289 }
290
291 QExplicitlySharedDataPointer<Plasma::RunnerContextPrivate> oldD = d;
292 LOCK_FOR_WRITE(d)
293 LOCK_FOR_READ(other.d)
294 d = other.d;
295 UNLOCK(other.d)
296 UNLOCK(oldD)
297 return *this;
298}
299
300void RunnerContext::reset()
301{
302 LOCK_FOR_WRITE(d);
303 // We will detach if we are a copy of someone. But we will reset
304 // if we are the 'main' context others copied from. Resetting
305 // one RunnerContext makes all the copies obsolete.
306
307 // We need to mark the q pointer of the detached RunnerContextPrivate
308 // as dirty on detach to avoid receiving results for old queries
309 d->invalidate();
310 UNLOCK(d);
311
312 d.detach();
313
314 // Now that we detached the d pointer we need to reset its q pointer
315
316 d->q = this;
317
318 // we still have to remove all the matches, since if the
319 // ref count was 1 (e.g. only the RunnerContext is using
320 // the dptr) then we won't get a copy made
321 if (!d->matches.isEmpty()) {
322 d->matchesById.clear();
323 d->matches.clear();
324 emit matchesChanged();
325 }
326
327 d->term.clear();
328 d->mimeType.clear();
329 d->type = UnknownType;
330 d->singleRunnerQueryMode = false;
331 //kDebug() << "match count" << d->matches.count();
332}
333
334void RunnerContext::setQuery(const QString &term)
335{
336 reset();
337
338 if (term.isEmpty()) {
339 return;
340 }
341
342 d->term = term;
343 d->determineType();
344}
345
346QString RunnerContext::query() const
347{
348 // the query term should never be set after
349 // a search starts. in fact, reset() ensures this
350 // and setQuery(QString) calls reset()
351 return d->term;
352}
353
354RunnerContext::Type RunnerContext::type() const
355{
356 return d->type;
357}
358
359QString RunnerContext::mimeType() const
360{
361 return d->mimeType;
362}
363
364bool RunnerContext::isValid() const
365{
366 // if our qptr is dirty, we aren't useful anymore
367 LOCK_FOR_READ(d)
368 const bool valid = (d->q != &(d->s_dummyContext));
369 UNLOCK(d)
370 return valid;
371}
372
373bool RunnerContext::addMatches(const QString &term, const QList<QueryMatch> &matches)
374{
375 Q_UNUSED(term)
376
377 if (matches.isEmpty() || !isValid()) {
378 //Bail out if the query is empty or the qptr is dirty
379 return false;
380 }
381
382 LOCK_FOR_WRITE(d)
383 foreach (QueryMatch match, matches) {
384 // Give previously launched matches a slight boost in relevance
385 // The boost smoothly saturates to 0.5;
386 if (int count = d->launchCounts.value(match.id())) {
387 match.setRelevance(match.relevance() + 0.5 * (1-exp(-count*0.3)));
388 }
389
390 d->matches.append(match);
391#ifndef NDEBUG
392 if (d->matchesById.contains(match.id())) {
393 kDebug() << "Duplicate match id " << match.id() << "from" << match.runner()->name();
394 }
395#endif
396 d->matchesById.insert(match.id(), &d->matches.at(d->matches.size() - 1));
397 }
398 UNLOCK(d);
399 //kDebug()<< "add matches";
400 // A copied searchContext may share the d pointer,
401 // we always want to sent the signal of the object that created
402 // the d pointer
403 emit d->q->matchesChanged();
404
405 return true;
406}
407
408bool RunnerContext::addMatch(const QString &term, const QueryMatch &match)
409{
410 Q_UNUSED(term)
411
412 if (!isValid()) {
413 // Bail out if the qptr is dirty
414 return false;
415 }
416
417 QueryMatch m(match); // match must be non-const to modify relevance
418
419 LOCK_FOR_WRITE(d)
420
421 if (int count = d->launchCounts.value(m.id())) {
422 m.setRelevance(m.relevance() + 0.05 * count);
423 }
424
425 d->matches.append(m);
426 d->matchesById.insert(m.id(), &d->matches.at(d->matches.size() - 1));
427 UNLOCK(d);
428 //kDebug()<< "added match" << match->text();
429 emit d->q->matchesChanged();
430
431 return true;
432}
433
434bool RunnerContext::removeMatches(const QStringList matchIdList)
435{
436 if (!isValid()) {
437 return false;
438 }
439
440 QStringList presentMatchIdList;
441 QList<const QueryMatch*> presentMatchList;
442
443 LOCK_FOR_READ(d)
444 foreach(const QString &matchId, matchIdList) {
445 const QueryMatch* match = d->matchesById.value(matchId, 0);
446 if (match) {
447 presentMatchList << match;
448 presentMatchIdList << matchId;
449 }
450 }
451 UNLOCK(d)
452
453 if (presentMatchIdList.isEmpty()) {
454 return false;
455 }
456
457 LOCK_FOR_WRITE(d)
458 foreach(const QueryMatch *match, presentMatchList) {
459 d->matches.removeAll(*match);
460 }
461 foreach(const QString &matchId, presentMatchIdList) {
462 d->matchesById.remove(matchId);
463 }
464 UNLOCK(d)
465
466 emit d->q->matchesChanged();
467
468 return true;
469}
470
471bool RunnerContext::removeMatch(const QString matchId)
472{
473 if (!isValid()) {
474 return false;
475 }
476 LOCK_FOR_READ(d)
477 const QueryMatch* match = d->matchesById.value(matchId, 0);
478 UNLOCK(d)
479 if (!match) {
480 return false;
481 }
482 LOCK_FOR_WRITE(d)
483 d->matches.removeAll(*match);
484 d->matchesById.remove(matchId);
485 UNLOCK(d)
486 emit d->q->matchesChanged();
487
488 return true;
489}
490
491bool RunnerContext::removeMatches(Plasma::AbstractRunner *runner)
492{
493 if (!isValid()) {
494 return false;
495 }
496
497 QList<QueryMatch> presentMatchList;
498
499 LOCK_FOR_READ(d)
500 foreach(const QueryMatch &match, d->matches) {
501 if (match.runner() == runner) {
502 presentMatchList << match;
503 }
504 }
505 UNLOCK(d)
506
507 if (presentMatchList.isEmpty()) {
508 return false;
509 }
510
511 LOCK_FOR_WRITE(d)
512 foreach (const QueryMatch &match, presentMatchList) {
513 d->matchesById.remove(match.id());
514 d->matches.removeAll(match);
515 }
516 UNLOCK(d)
517
518 emit d->q->matchesChanged();
519 return true;
520}
521
522QList<QueryMatch> RunnerContext::matches() const
523{
524 LOCK_FOR_READ(d)
525 QList<QueryMatch> matches = d->matches;
526 UNLOCK(d);
527 return matches;
528}
529
530QueryMatch RunnerContext::match(const QString &id) const
531{
532 LOCK_FOR_READ(d)
533 const QueryMatch *match = d->matchesById.value(id, 0);
534 UNLOCK(d)
535
536 if (match) {
537 return *match;
538 }
539
540 return QueryMatch(0);
541}
542
543void RunnerContext::setSingleRunnerQueryMode(bool enabled)
544{
545 d->singleRunnerQueryMode = enabled;
546}
547
548bool RunnerContext::singleRunnerQueryMode() const
549{
550 return d->singleRunnerQueryMode;
551}
552
553void RunnerContext::restore(const KConfigGroup &config)
554{
555 const QStringList cfgList = config.readEntry("LaunchCounts", QStringList());
556
557 const QRegExp r("(\\d*) (.*)");
558 foreach (const QString& entry, cfgList) {
559 r.indexIn(entry);
560 int count = r.cap(1).toInt();
561 QString id = r.cap(2);
562 d->launchCounts[id] = count;
563 }
564}
565
566void RunnerContext::save(KConfigGroup &config)
567{
568 QStringList countList;
569
570 typedef QHash<QString, int>::const_iterator Iterator;
571 Iterator end = d->launchCounts.constEnd();
572 for (Iterator i = d->launchCounts.constBegin(); i != end; ++i) {
573 countList << QString("%2 %1").arg(i.key()).arg(i.value());
574 }
575
576 config.writeEntry("LaunchCounts", countList);
577 config.sync();
578}
579
580void RunnerContext::run(const QueryMatch &match)
581{
582 ++d->launchCounts[match.id()];
583 match.run(*this);
584}
585
586} // Plasma namespace
587
588#include "runnercontext.moc"
589