~zeal-developers/zeal/master

« back to all changes in this revision

Viewing changes to src/zealdocsetsregistry.cpp

  • Committer: Oleg Shparber
  • Date: 2015-01-12 03:51:13 UTC
  • Revision ID: git-v1:792a35241c995b7f5913ee426f4bf5aa706b43d9
Move .pro file to the top, rename zeal dir into src

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#include "zealdocsetsregistry.h"
 
2
 
 
3
#include "zealsearchquery.h"
 
4
#include "zealsearchresult.h"
 
5
 
 
6
#include <QCoreApplication>
 
7
#include <QSettings>
 
8
#include <QSqlQuery>
 
9
#include <QStandardPaths>
 
10
#include <QThread>
 
11
#include <QUrl>
 
12
 
 
13
using namespace Zeal;
 
14
 
 
15
DocsetsRegistry *DocsetsRegistry::m_instance;
 
16
 
 
17
DocsetsRegistry *DocsetsRegistry::instance()
 
18
{
 
19
    static QMutex mutex;
 
20
    if (!m_instance) {
 
21
        mutex.lock();
 
22
 
 
23
        if (!m_instance)
 
24
            m_instance = new DocsetsRegistry();
 
25
 
 
26
        mutex.unlock();
 
27
    }
 
28
 
 
29
    return m_instance;
 
30
}
 
31
 
 
32
int DocsetsRegistry::count() const
 
33
{
 
34
    return m_docs.count();
 
35
}
 
36
 
 
37
QStringList DocsetsRegistry::names() const
 
38
{
 
39
    return m_docs.keys();
 
40
}
 
41
 
 
42
void DocsetsRegistry::remove(const QString &name)
 
43
{
 
44
    m_docs[name].db.close();
 
45
    m_docs.remove(name);
 
46
}
 
47
 
 
48
void DocsetsRegistry::clear()
 
49
{
 
50
    for (const QString &key : m_docs.keys())
 
51
        remove(key);
 
52
}
 
53
 
 
54
QSqlDatabase &DocsetsRegistry::db(const QString &name)
 
55
{
 
56
    Q_ASSERT(m_docs.contains(name));
 
57
    return m_docs[name].db;
 
58
}
 
59
 
 
60
const QDir &DocsetsRegistry::dir(const QString &name)
 
61
{
 
62
    Q_ASSERT(m_docs.contains(name));
 
63
    return m_docs[name].dir;
 
64
}
 
65
 
 
66
const DocsetMetadata &DocsetsRegistry::meta(const QString &name)
 
67
{
 
68
    Q_ASSERT(m_docs.contains(name));
 
69
    return m_docs[name].metadata;
 
70
}
 
71
 
 
72
QIcon DocsetsRegistry::icon(const QString &docsetName) const
 
73
{
 
74
    const DocsetEntry &entry = m_docs[docsetName];
 
75
    QString bundleName = entry.info.bundleName;
 
76
    bundleName.replace(" ", "_");
 
77
    QString identifier = entry.info.bundleIdentifier;
 
78
    QIcon icon(entry.dir.absoluteFilePath("favicon.ico"));
 
79
    if (icon.availableSizes().isEmpty())
 
80
        icon = QIcon(entry.dir.absoluteFilePath("icon.png"));
 
81
 
 
82
    if (icon.availableSizes().isEmpty()) {
 
83
        icon = QIcon(QString("icons:%1.png").arg(bundleName));
 
84
 
 
85
        // Fallback to identifier and docset file name.
 
86
        if (icon.availableSizes().isEmpty())
 
87
            icon = QIcon(QString("icons:%1.png").arg(identifier));
 
88
        if (icon.availableSizes().isEmpty())
 
89
            icon = QIcon(QString("icons:%1.png").arg(docsetName));
 
90
    }
 
91
    return icon;
 
92
}
 
93
 
 
94
DocsetType DocsetsRegistry::type(const QString &name) const
 
95
{
 
96
    Q_ASSERT(m_docs.contains(name));
 
97
    return m_docs[name].type;
 
98
}
 
99
 
 
100
DocsetsRegistry::DocsetsRegistry()
 
101
{
 
102
    /// FIXME: Only search should be performed in a separate thread
 
103
    auto thread = new QThread(this);
 
104
    moveToThread(thread);
 
105
    thread->start();
 
106
}
 
107
 
 
108
QList<DocsetsRegistry::DocsetEntry> DocsetsRegistry::docsets()
 
109
{
 
110
    return m_docs.values();
 
111
}
 
112
 
 
113
void DocsetsRegistry::addDocset(const QString &path)
 
114
{
 
115
    QDir dir(path);
 
116
    auto name = dir.dirName().replace(QStringLiteral(".docset"), QString());
 
117
    QSqlDatabase db;
 
118
    DocsetEntry entry;
 
119
 
 
120
    if (QFile::exists(dir.filePath("index.sqlite"))) {
 
121
        db = QSqlDatabase::addDatabase("QSQLITE", name);
 
122
        db.setDatabaseName(dir.filePath("index.sqlite"));
 
123
        db.open();
 
124
        entry.name = name;
 
125
        entry.prefix = name;
 
126
        entry.type = ZEAL;
 
127
    } else {
 
128
        QDir contentsDir(dir.filePath("Contents"));
 
129
        entry.info.readDocset(contentsDir.absoluteFilePath("Info.plist"));
 
130
 
 
131
        if (entry.info.family == "cheatsheet")
 
132
            name = QString("%1_cheats").arg(name);
 
133
        entry.name = name;
 
134
 
 
135
        auto dashFile = QDir(contentsDir.filePath("Resources")).filePath("docSet.dsidx");
 
136
        db = QSqlDatabase::addDatabase("QSQLITE", name);
 
137
        db.setDatabaseName(dashFile);
 
138
        db.open();
 
139
        auto q = db.exec("select name from sqlite_master where type='table'");
 
140
        QStringList tables;
 
141
        while (q.next())
 
142
            tables.append(q.value(0).toString());
 
143
 
 
144
        if (tables.contains("searchIndex"))
 
145
            entry.type = DASH;
 
146
        else
 
147
            entry.type = ZDASH;
 
148
 
 
149
        dir.cd("Contents");
 
150
        dir.cd("Resources");
 
151
        dir.cd("Documents");
 
152
    }
 
153
 
 
154
    if (m_docs.contains(name))
 
155
        remove(name);
 
156
 
 
157
    entry.prefix = entry.info.bundleName.isEmpty()
 
158
                   ? name
 
159
                   : entry.info.bundleName;
 
160
    entry.db = db;
 
161
    entry.dir = dir;
 
162
 
 
163
    // Read metadata
 
164
    DocsetMetadata meta;
 
165
    meta.read(path+"/meta.json");
 
166
    entry.metadata = meta;
 
167
    m_docs[name] = entry;
 
168
}
 
169
 
 
170
DocsetsRegistry::DocsetEntry *DocsetsRegistry::entry(const QString &name)
 
171
{
 
172
    return &m_docs[name];
 
173
}
 
174
 
 
175
void DocsetsRegistry::runQuery(const QString &query)
 
176
{
 
177
    m_lastQuery += 1;
 
178
    QMetaObject::invokeMethod(this, "_runQuery", Qt::QueuedConnection, Q_ARG(QString, query),
 
179
                              Q_ARG(int, m_lastQuery));
 
180
}
 
181
 
 
182
void DocsetsRegistry::invalidateQueries()
 
183
{
 
184
    m_lastQuery += 1;
 
185
}
 
186
 
 
187
void DocsetsRegistry::_runQuery(const QString &rawQuery, int queryNum)
 
188
{
 
189
    // If some other queries pending, ignore this one.
 
190
    if (queryNum != m_lastQuery)
 
191
        return;
 
192
 
 
193
    QList<SearchResult> results;
 
194
    SearchQuery query(rawQuery);
 
195
 
 
196
    QString preparedQuery = query.sanitizedQuery();
 
197
    bool hasDocsetFilter = query.hasDocsetFilter();
 
198
 
 
199
    for (const DocsetsRegistry::DocsetEntry &docset : docsets()) {
 
200
        // Filter out this docset as the names don't match the docset prefix
 
201
        if (hasDocsetFilter && !query.docsetPrefixMatch(docset.prefix))
 
202
            continue;
 
203
 
 
204
        QString qstr;
 
205
        QSqlQuery q;
 
206
        QList<QList<QVariant>> found;
 
207
        bool withSubStrings = false;
 
208
        // %.%1% for long Django docset values like django.utils.http
 
209
        // %::%1% for long C++ docset values like std::set
 
210
        // %/%1% for long Go docset values like archive/tar
 
211
        QString subNames = QStringLiteral(" or %1 like '%.%2%' escape '\\'");
 
212
        subNames += QStringLiteral(" or %1 like '%::%2%' escape '\\'");
 
213
        subNames += QStringLiteral(" or %1 like '%/%2%' escape '\\'");
 
214
        while (found.size() < 100) {
 
215
            auto curQuery = preparedQuery;
 
216
            QString notQuery; // don't return the same result twice
 
217
            QString parentQuery;
 
218
            if (withSubStrings) {
 
219
                // if less than 100 found starting with query, search all substrings
 
220
                curQuery = "%" + preparedQuery;
 
221
                // don't return 'starting with' results twice
 
222
                if (docset.type == ZDASH) {
 
223
                    notQuery = QString(" and not (ztokenname like '%1%' escape '\\' %2) ").arg(preparedQuery, subNames.arg("ztokenname", preparedQuery));
 
224
                } else {
 
225
                    if (docset.type == ZEAL) {
 
226
                        notQuery = QString(" and not (t.name like '%1%' escape '\\') ").arg(preparedQuery);
 
227
                        parentQuery = QString(" or t2.name like '%1%' escape '\\' ").arg(preparedQuery);
 
228
                    } else { // DASH
 
229
                        notQuery = QString(" and not (t.name like '%1%' escape '\\' %2) ").arg(preparedQuery, subNames.arg("t.name", preparedQuery));
 
230
                    }
 
231
                }
 
232
            }
 
233
            int cols = 3;
 
234
            if (docset.type == ZEAL) {
 
235
                qstr = QString("select t.name, t2.name, t.path from things t left join things t2 on t2.id=t.parent where "
 
236
                               "(t.name like '%1%' escape '\\'  %3) %2 order by length(t.name), lower(t.name) asc, t.path asc limit 100").arg(curQuery, notQuery, parentQuery);
 
237
 
 
238
            } else if (docset.type == DASH) {
 
239
                qstr = QString("select t.name, null, t.path from searchIndex t where (t.name "
 
240
                               "like '%1%' escape '\\' %3)  %2 order by length(t.name), lower(t.name) asc, t.path asc limit 100").arg(curQuery, notQuery, subNames.arg("t.name", curQuery));
 
241
            } else if (docset.type == ZDASH) {
 
242
                cols = 4;
 
243
                qstr = QString("select ztokenname, null, zpath, zanchor from ztoken "
 
244
                               "join ztokenmetainformation on ztoken.zmetainformation = ztokenmetainformation.z_pk "
 
245
                               "join zfilepath on ztokenmetainformation.zfile = zfilepath.z_pk where (ztokenname "
 
246
                               "like '%1%' escape '\\' %3) %2 order by length(ztokenname), lower(ztokenname) asc, zpath asc, "
 
247
                               "zanchor asc limit 100").arg(curQuery, notQuery,
 
248
                                                            subNames.arg("ztokenname", curQuery));
 
249
            }
 
250
            q = db(docset.name).exec(qstr);
 
251
            while (q.next()) {
 
252
                QList<QVariant> values;
 
253
                for (int i = 0; i < cols; ++i)
 
254
                    values.append(q.value(i));
 
255
                found.append(values);
 
256
            }
 
257
 
 
258
            if (withSubStrings)
 
259
                break;
 
260
            withSubStrings = true;  // try again searching for substrings
 
261
        }
 
262
 
 
263
        for (const auto &row : found) {
 
264
            QString parentName;
 
265
            if (!row[1].isNull())
 
266
                parentName = row[1].toString();
 
267
 
 
268
            auto path = row[2].toString();
 
269
            // FIXME: refactoring to use common code in ZealListModel and DocsetsRegistry
 
270
            if (docset.type == ZDASH)
 
271
                path += "#" + row[3].toString();
 
272
 
 
273
            auto itemName = row[0].toString();
 
274
            normalizeName(itemName, parentName, row[1].toString());
 
275
            results.append(SearchResult(itemName, parentName, path, docset.name,
 
276
                                            preparedQuery));
 
277
        }
 
278
    }
 
279
    qSort(results);
 
280
    if (queryNum != m_lastQuery)
 
281
        return; // some other queries pending - ignore this one
 
282
 
 
283
    m_queryResults = results;
 
284
    emit queryCompleted();
 
285
}
 
286
 
 
287
void DocsetsRegistry::normalizeName(QString &itemName, QString &parentName,
 
288
                                        const QString &initialParent)
 
289
{
 
290
    QRegExp matchMethodName("^([^\\(]+)(?:\\(.*\\))?$");
 
291
    if (matchMethodName.indexIn(itemName) != -1)
 
292
        itemName = matchMethodName.cap(1);
 
293
 
 
294
    QString separators[] = {".", "::", "/"};
 
295
    for (unsigned i = 0; i < sizeof separators / sizeof *separators; ++i) {
 
296
        QString sep = separators[i];
 
297
        if (itemName.indexOf(sep) != -1 && itemName.indexOf(sep) != 0 && initialParent.isNull()) {
 
298
            auto splitted = itemName.split(sep);
 
299
            itemName = splitted.at(splitted.size()-1);
 
300
            parentName = splitted.at(splitted.size()-2);
 
301
        }
 
302
    }
 
303
}
 
304
 
 
305
const QList<SearchResult> &DocsetsRegistry::queryResults()
 
306
{
 
307
    return m_queryResults;
 
308
}
 
309
 
 
310
QList<SearchResult> DocsetsRegistry::relatedLinks(const QString &name, const QString &path)
 
311
{
 
312
    QList<SearchResult> results;
 
313
    // Get the url without the #anchor.
 
314
    QUrl mainUrl(path);
 
315
    mainUrl.setFragment(NULL);
 
316
    QString pageUrl(mainUrl.toString());
 
317
    DocsetEntry entry = m_docs[name];
 
318
 
 
319
    // Prepare the query to look up all pages with the same url.
 
320
    QString query;
 
321
    if (entry.type == DASH) {
 
322
        query = QString("SELECT name, type, path FROM searchIndex WHERE path LIKE \"%1%%\"").arg(pageUrl);
 
323
    } else if (entry.type == ZDASH) {
 
324
        query = QString("SELECT ztoken.ztokenname, ztokentype.ztypename, zfilepath.zpath, ztokenmetainformation.zanchor "
 
325
                        "FROM ztoken "
 
326
                        "JOIN ztokenmetainformation ON ztoken.zmetainformation = ztokenmetainformation.z_pk "
 
327
                        "JOIN zfilepath ON ztokenmetainformation.zfile = zfilepath.z_pk "
 
328
                        "JOIN ztokentype ON ztoken.ztokentype = ztokentype.z_pk "
 
329
                        "WHERE zfilepath.zpath = \"%1\"").arg(pageUrl);
 
330
    } else if (entry.type == ZEAL) {
 
331
        query = QString("SELECT name type, path FROM things WHERE path LIKE \"%1%%\"").arg(pageUrl);
 
332
    }
 
333
 
 
334
    QSqlQuery result = entry.db.exec(query);
 
335
    while (result.next()) {
 
336
        QString sectionName = result.value(0).toString();
 
337
        QString sectionPath = result.value(2).toString();
 
338
        QString parentName;
 
339
        if (entry.type == ZDASH) {
 
340
            sectionPath.append("#");
 
341
            sectionPath.append(result.value(3).toString());
 
342
        }
 
343
 
 
344
        normalizeName(sectionName, parentName);
 
345
 
 
346
        results.append(SearchResult(sectionName, QString(), sectionPath, name, QString()));
 
347
    }
 
348
 
 
349
    return results;
 
350
}
 
351
 
 
352
QString DocsetsRegistry::docsetsDir() const
 
353
{
 
354
    const QScopedPointer<const QSettings> settings(new QSettings());
 
355
    if (settings->contains("docsetsDir"))
 
356
        return settings->value("docsetsDir").toString();
 
357
 
 
358
    QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation));
 
359
    if (!dataDir.cd("docsets"))
 
360
        dataDir.mkpath("docsets");
 
361
    dataDir.cd("docsets");
 
362
    return dataDir.absolutePath();
 
363
}
 
364
 
 
365
// Recursively finds and adds all docsets in a given directory.
 
366
void DocsetsRegistry::addDocsetsFromFolder(const QDir &folder)
 
367
{
 
368
    for (const QFileInfo &subdir : folder.entryInfoList(QDir::NoDotAndDotDot | QDir::AllDirs)) {
 
369
        if (subdir.suffix() == "docset") {
 
370
            QMetaObject::invokeMethod(this, "addDocset", Qt::BlockingQueuedConnection,
 
371
                                      Q_ARG(QString, subdir.absoluteFilePath()));
 
372
        } else {
 
373
            addDocsetsFromFolder(QDir(subdir.absoluteFilePath()));
 
374
        }
 
375
    }
 
376
}
 
377
 
 
378
void DocsetsRegistry::initialiseDocsets()
 
379
{
 
380
    clear();
 
381
    addDocsetsFromFolder(QDir(docsetsDir()));
 
382
    QDir appDir(QCoreApplication::applicationDirPath());
 
383
    if (appDir.cd("docsets"))
 
384
        addDocsetsFromFolder(appDir);
 
385
}