From 8f38b01712d8c3c04e2889ca6a3f4f77d07c81d6 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Tue, 4 Oct 2016 23:44:07 -0400 Subject: [PATCH] Move search code into separate files in xpcom/data --- .../content/zotero/xpcom/{ => data}/search.js | 816 ------------------ .../zotero/xpcom/data/searchConditions.js | 690 +++++++++++++++ chrome/content/zotero/xpcom/data/searches.js | 172 ++++ components/zotero-service.js | 4 +- 4 files changed, 865 insertions(+), 817 deletions(-) rename chrome/content/zotero/xpcom/{ => data}/search.js (73%) create mode 100644 chrome/content/zotero/xpcom/data/searchConditions.js create mode 100644 chrome/content/zotero/xpcom/data/searches.js diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/data/search.js similarity index 73% rename from chrome/content/zotero/xpcom/search.js rename to chrome/content/zotero/xpcom/data/search.js index 261ed8561b..56162b2257 100644 --- a/chrome/content/zotero/xpcom/search.js +++ b/chrome/content/zotero/xpcom/data/search.js @@ -1592,819 +1592,3 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () { this._sql = sql; this._sqlParams = sqlParams.length ? sqlParams : false; }); - -Zotero.Searches = function() { - this.constructor = null; - - this._ZDO_object = 'search'; - this._ZDO_id = 'savedSearchID'; - this._ZDO_table = 'savedSearches'; - - this._primaryDataSQLParts = { - savedSearchID: "O.savedSearchID", - name: "O.savedSearchName AS name", - libraryID: "O.libraryID", - key: "O.key", - version: "O.version", - synced: "O.synced" - } - - this._primaryDataSQLFrom = "FROM savedSearches O"; - - this.init = Zotero.Promise.coroutine(function* () { - yield Zotero.DataObjects.prototype.init.apply(this); - yield Zotero.SearchConditions.init(); - }); - - - /** - * Returns an array of Zotero.Search objects, ordered by name - * - * @param {Integer} [libraryID] - */ - this.getAll = Zotero.Promise.coroutine(function* (libraryID) { - var sql = "SELECT savedSearchID FROM savedSearches WHERE libraryID=?"; - var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID); - if (!ids.length) { - return [] - } - - var searches = this.get(ids); - // Do proper collation sort - var collation = Zotero.getLocaleCollation(); - searches.sort(function (a, b) { - return collation.compareString(1, a.name, b.name); - }); - return searches; - }); - - - this.getPrimaryDataSQL = function () { - // This should be the same as the query in Zotero.Search.loadPrimaryData(), - // just without a specific savedSearchID - return "SELECT " - + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " " - + "FROM savedSearches O WHERE 1"; - } - - - this._loadConditions = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { - var sql = "SELECT savedSearchID, searchConditionID, condition, operator, value, required " - + "FROM savedSearches LEFT JOIN savedSearchConditions USING (savedSearchID) " - + "WHERE libraryID=?" + idSQL - + "ORDER BY savedSearchID, searchConditionID"; - var params = [libraryID]; - var lastID = null; - var rows = []; - var setRows = function (searchID, rows) { - var search = this._objectCache[searchID]; - if (!search) { - throw new Error("Search " + searchID + " not found"); - } - - search._conditions = {}; - - if (rows.length) { - search._maxSearchConditionID = rows[rows.length - 1].searchConditionID; - } - - // Reindex conditions, in case they're not contiguous in the DB - for (let i = 0; i < rows.length; i++) { - let condition = rows[i]; - - // Parse "condition[/mode]" - let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition); - - let cond = Zotero.SearchConditions.get(conditionName); - if (!cond || cond.noLoad) { - Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2); - continue; - } - - // Convert itemTypeID to itemType - // - // TEMP: This can be removed at some point - if (conditionName == 'itemTypeID') { - conditionName = 'itemType'; - condition.value = Zotero.ItemTypes.getName(condition.value); - } - - search._conditions[i] = { - id: i, - condition: conditionName, - mode: mode, - operator: condition.operator, - value: condition.value, - required: !!condition.required - }; - } - search._loaded.conditions = true; - search._clearChanged('conditions'); - }.bind(this); - - yield Zotero.DB.queryAsync( - sql, - params, - { - noCache: ids.length != 1, - onRow: function (row) { - let searchID = row.getResultByIndex(0); - - if (lastID && searchID != lastID) { - setRows(lastID, rows); - rows = []; - } - - lastID = searchID; - let searchConditionID = row.getResultByIndex(1); - // No conditions - if (searchConditionID === null) { - return; - } - rows.push({ - searchConditionID, - condition: row.getResultByIndex(2), - operator: row.getResultByIndex(3), - value: row.getResultByIndex(4), - required: row.getResultByIndex(5) - }); - }.bind(this) - } - ); - if (lastID) { - setRows(lastID, rows); - } - }); - - Zotero.DataObjects.call(this); - - return this; -}.bind(Object.create(Zotero.DataObjects.prototype))(); - - - -Zotero.SearchConditions = new function(){ - this.get = get; - this.getStandardConditions = getStandardConditions; - this.hasOperator = hasOperator; - this.getLocalizedName = getLocalizedName; - this.parseSearchString = parseSearchString; - this.parseCondition = parseCondition; - - var _initialized = false; - var _conditions; - var _standardConditions; - - var self = this; - - /* - * Define the advanced search operators - */ - var _operators = { - // Standard -- these need to match those in zoterosearch.xml - is: true, - isNot: true, - beginsWith: true, - contains: true, - doesNotContain: true, - isLessThan: true, - isGreaterThan: true, - isBefore: true, - isAfter: true, - isInTheLast: true, - - // Special - any: true, - all: true, - true: true, - false: true - }; - - - /* - * Define and set up the available advanced search conditions - * - * Flags: - * - special (don't show in search window menu) - * - template (special handling) - * - noLoad (can't load from saved search) - */ - this.init = Zotero.Promise.coroutine(function* () { - var conditions = [ - // - // Special conditions - // - { - name: 'deleted', - operators: { - true: true, - false: true - } - }, - - // Don't include child items - { - name: 'noChildren', - operators: { - true: true, - false: true - } - }, - - { - name: 'unfiled', - operators: { - true: true, - false: true - } - }, - - { - name: 'includeParentsAndChildren', - operators: { - true: true, - false: true - } - }, - - { - name: 'includeParents', - operators: { - true: true, - false: true - } - }, - - { - name: 'includeChildren', - operators: { - true: true, - false: true - } - }, - - // Search recursively within collections - { - name: 'recursive', - operators: { - true: true, - false: true - } - }, - - // Join mode - { - name: 'joinMode', - operators: { - any: true, - all: true - } - }, - - { - name: 'quicksearch-titleCreatorYear', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - { - name: 'quicksearch-fields', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - { - name: 'quicksearch-everything', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - // Deprecated - { - name: 'quicksearch', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - // Quicksearch block markers - { - name: 'blockStart', - noLoad: true - }, - - { - name: 'blockEnd', - noLoad: true - }, - - // Shortcuts for adding collections and searches by id - { - name: 'collectionID', - operators: { - is: true, - isNot: true - }, - noLoad: true - }, - - { - name: 'savedSearchID', - operators: { - is: true, - isNot: true - }, - noLoad: true - }, - - - // - // Standard conditions - // - - // Collection id to search within - { - name: 'collection', - operators: { - is: true, - isNot: true - }, - table: 'collectionItems', - field: 'collectionID' - }, - - // Saved search to search within - { - name: 'savedSearch', - operators: { - is: true, - isNot: true - }, - special: false - }, - - { - name: 'dateAdded', - operators: { - is: true, - isNot: true, - isBefore: true, - isAfter: true, - isInTheLast: true - }, - table: 'items', - field: 'dateAdded' - }, - - { - name: 'dateModified', - operators: { - is: true, - isNot: true, - isBefore: true, - isAfter: true, - isInTheLast: true - }, - table: 'items', - field: 'dateModified' - }, - - // Deprecated - { - name: 'itemTypeID', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'itemTypeID', - special: true - }, - - { - name: 'itemType', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'typeName' - }, - - { - name: 'fileTypeID', - operators: { - is: true, - isNot: true - }, - table: 'itemAttachments', - field: 'fileTypeID' - }, - - { - name: 'tagID', - operators: { - is: true, - isNot: true - }, - table: 'itemTags', - field: 'tagID', - special: true - }, - - { - name: 'tag', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemTags', - field: 'name' - }, - - { - name: 'note', - operators: { - contains: true, - doesNotContain: true - }, - table: 'itemNotes', - field: 'note' - }, - - { - name: 'childNote', - operators: { - contains: true, - doesNotContain: true - }, - table: 'items', - field: 'note' - }, - - { - name: 'creator', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemCreators', - field: "TRIM(firstName || ' ' || lastName)" - }, - - { - name: 'lastName', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemCreators', - field: 'lastName', - special: true - }, - - { - name: 'field', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemData', - field: 'value', - aliases: yield Zotero.DB.columnQueryAsync("SELECT fieldName FROM fieldsCombined " - + "WHERE fieldName NOT IN ('accessDate', 'date', 'pages', " - + "'section','seriesNumber','issue')"), - template: true // mark for special handling - }, - - { - name: 'datefield', - operators: { - is: true, - isNot: true, - isBefore: true, - isAfter: true, - isInTheLast: true - }, - table: 'itemData', - field: 'value', - aliases: ['accessDate', 'date', 'dateDue', 'accepted'], // TEMP - NSF - template: true // mark for special handling - }, - - { - name: 'year', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemData', - field: 'SUBSTR(value, 1, 4)', - special: true - }, - - { - name: 'numberfield', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true, - isLessThan: true, - isGreaterThan: true - }, - table: 'itemData', - field: 'value', - aliases: ['pages', 'numPages', 'numberOfVolumes', 'section', 'seriesNumber','issue'], - template: true // mark for special handling - }, - - { - name: 'libraryID', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'libraryID', - special: true, - noLoad: true - }, - - { - name: 'key', - operators: { - is: true, - isNot: true, - beginsWith: true - }, - table: 'items', - field: 'key', - special: true, - noLoad: true - }, - - { - name: 'itemID', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'itemID', - special: true, - noLoad: true - }, - - { - name: 'annotation', - operators: { - contains: true, - doesNotContain: true - }, - table: 'annotations', - field: 'text' - }, - - { - name: 'fulltextWord', - operators: { - contains: true, - doesNotContain: true - }, - table: 'fulltextItemWords', - field: 'word', - flags: { - leftbound: true - }, - special: true - }, - - { - name: 'fulltextContent', - operators: { - contains: true, - doesNotContain: true - }, - special: false - }, - - { - name: 'tempTable', - operators: { - is: true - } - } - ]; - - // Index conditions by name and aliases - _conditions = {}; - for (var i in conditions) { - _conditions[conditions[i]['name']] = conditions[i]; - if (conditions[i]['aliases']) { - for (var j in conditions[i]['aliases']) { - // TEMP - NSF - switch (conditions[i]['aliases'][j]) { - case 'dateDue': - case 'accepted': - if (!Zotero.ItemTypes.getID('nsfReviewer')) { - continue; - } - } - _conditions[conditions[i]['aliases'][j]] = conditions[i]; - } - } - _conditions[conditions[i]['name']] = conditions[i]; - } - - _standardConditions = []; - - var baseMappedFields = Zotero.ItemFields.getBaseMappedFields(); - var locale = Zotero.locale; - - // Separate standard conditions for menu display - for (var i in _conditions){ - var fieldID = false; - if (['field', 'datefield', 'numberfield'].indexOf(_conditions[i]['name']) != -1) { - fieldID = Zotero.ItemFields.getID(i); - } - - // If explicitly special... - if (_conditions[i]['special'] || - // or a template master (e.g. 'field')... - (_conditions[i]['template'] && i==_conditions[i]['name']) || - // or no table and not explicitly unspecial... - (!_conditions[i]['table'] && - typeof _conditions[i]['special'] == 'undefined') || - // or field is a type-specific version of a base field... - (fieldID && baseMappedFields.indexOf(fieldID) != -1)) { - // ...then skip - continue; - } - - let localized = self.getLocalizedName(i); - // Hack to use a different name for "issue" in French locale, - // where 'number' and 'issue' are translated the same - // https://forums.zotero.org/discussion/14942/ - if (fieldID == 5 && locale.substr(0, 2).toLowerCase() == 'fr') { - localized = "Num\u00E9ro (p\u00E9riodique)"; - } - - _standardConditions.push({ - name: i, - localized: localized, - operators: _conditions[i]['operators'], - flags: _conditions[i]['flags'] - }); - } - - var collation = Zotero.getLocaleCollation(); - _standardConditions.sort(function(a, b) { - return collation.compareString(1, a.localized, b.localized); - }); - }); - - - /* - * Get condition data - */ - function get(condition){ - return _conditions[condition]; - } - - - /* - * Returns array of possible conditions - * - * Does not include special conditions, only ones that would show in a drop-down list - */ - function getStandardConditions(){ - // TODO: return copy instead - return _standardConditions; - } - - - /* - * Check if an operator is valid for a given condition - */ - function hasOperator(condition, operator){ - var [condition, mode] = this.parseCondition(condition); - - if (!_conditions) { - throw new Zotero.Exception.UnloadedDataException("Search conditions not yet loaded"); - } - - if (!_conditions[condition]){ - let e = new Error("Invalid condition '" + condition + "' in hasOperator()"); - e.name = "ZoteroUnknownFieldError"; - throw e; - } - - if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){ - return true; - } - - return !!_conditions[condition]['operators'][operator]; - } - - - function getLocalizedName(str) { - // TEMP - if (str == 'itemType') { - str = 'itemTypeID'; - } - - try { - return Zotero.getString('searchConditions.' + str) - } - catch (e) { - return Zotero.ItemFields.getLocalizedString(null, str); - } - } - - - /** - * Compare two API JSON condition objects - */ - this.equals = function (data1, data2) { - return data1.condition === data2.condition - && data1.operator === data2.operator - && data1.value === data2.value; - } - - - /* - * Parses a search into words and "double-quoted phrases" - * - * Also strips unpaired quotes at the beginning and end of words - * - * Returns array of objects containing 'text' and 'inQuotes' - */ - function parseSearchString(str) { - var parts = str.split(/\s*("[^"]*")\s*|"\s|\s"|^"|"$|'\s|\s'|^'|'$|\s/m); - var parsed = []; - - for (var i in parts) { - var part = parts[i]; - if (!part || !part.length) { - continue; - } - - if (part.charAt(0)=='"' && part.charAt(part.length-1)=='"') { - parsed.push({ - text: part.substring(1, part.length-1), - inQuotes: true - }); - } - else { - parsed.push({ - text: part, - inQuotes: false - }); - } - } - - return parsed; - } - - - function parseCondition(condition){ - var mode = false; - var pos = condition.indexOf('/'); - if (pos != -1){ - mode = condition.substr(pos+1); - condition = condition.substr(0, pos); - } - - return [condition, mode]; - } -} diff --git a/chrome/content/zotero/xpcom/data/searchConditions.js b/chrome/content/zotero/xpcom/data/searchConditions.js new file mode 100644 index 0000000000..bdf20a1a59 --- /dev/null +++ b/chrome/content/zotero/xpcom/data/searchConditions.js @@ -0,0 +1,690 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2006-2016 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + https://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.SearchConditions = new function(){ + this.get = get; + this.getStandardConditions = getStandardConditions; + this.hasOperator = hasOperator; + this.getLocalizedName = getLocalizedName; + this.parseSearchString = parseSearchString; + this.parseCondition = parseCondition; + + var _initialized = false; + var _conditions; + var _standardConditions; + + var self = this; + + /* + * Define the advanced search operators + */ + var _operators = { + // Standard -- these need to match those in zoterosearch.xml + is: true, + isNot: true, + beginsWith: true, + contains: true, + doesNotContain: true, + isLessThan: true, + isGreaterThan: true, + isBefore: true, + isAfter: true, + isInTheLast: true, + + // Special + any: true, + all: true, + true: true, + false: true + }; + + + /* + * Define and set up the available advanced search conditions + * + * Flags: + * - special (don't show in search window menu) + * - template (special handling) + * - noLoad (can't load from saved search) + */ + this.init = Zotero.Promise.coroutine(function* () { + var conditions = [ + // + // Special conditions + // + { + name: 'deleted', + operators: { + true: true, + false: true + } + }, + + // Don't include child items + { + name: 'noChildren', + operators: { + true: true, + false: true + } + }, + + { + name: 'unfiled', + operators: { + true: true, + false: true + } + }, + + { + name: 'includeParentsAndChildren', + operators: { + true: true, + false: true + } + }, + + { + name: 'includeParents', + operators: { + true: true, + false: true + } + }, + + { + name: 'includeChildren', + operators: { + true: true, + false: true + } + }, + + // Search recursively within collections + { + name: 'recursive', + operators: { + true: true, + false: true + } + }, + + // Join mode + { + name: 'joinMode', + operators: { + any: true, + all: true + } + }, + + { + name: 'quicksearch-titleCreatorYear', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + { + name: 'quicksearch-fields', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + { + name: 'quicksearch-everything', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + // Deprecated + { + name: 'quicksearch', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + // Quicksearch block markers + { + name: 'blockStart', + noLoad: true + }, + + { + name: 'blockEnd', + noLoad: true + }, + + // Shortcuts for adding collections and searches by id + { + name: 'collectionID', + operators: { + is: true, + isNot: true + }, + noLoad: true + }, + + { + name: 'savedSearchID', + operators: { + is: true, + isNot: true + }, + noLoad: true + }, + + + // + // Standard conditions + // + + // Collection id to search within + { + name: 'collection', + operators: { + is: true, + isNot: true + }, + table: 'collectionItems', + field: 'collectionID' + }, + + // Saved search to search within + { + name: 'savedSearch', + operators: { + is: true, + isNot: true + }, + special: false + }, + + { + name: 'dateAdded', + operators: { + is: true, + isNot: true, + isBefore: true, + isAfter: true, + isInTheLast: true + }, + table: 'items', + field: 'dateAdded' + }, + + { + name: 'dateModified', + operators: { + is: true, + isNot: true, + isBefore: true, + isAfter: true, + isInTheLast: true + }, + table: 'items', + field: 'dateModified' + }, + + // Deprecated + { + name: 'itemTypeID', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'itemTypeID', + special: true + }, + + { + name: 'itemType', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'typeName' + }, + + { + name: 'fileTypeID', + operators: { + is: true, + isNot: true + }, + table: 'itemAttachments', + field: 'fileTypeID' + }, + + { + name: 'tagID', + operators: { + is: true, + isNot: true + }, + table: 'itemTags', + field: 'tagID', + special: true + }, + + { + name: 'tag', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemTags', + field: 'name' + }, + + { + name: 'note', + operators: { + contains: true, + doesNotContain: true + }, + table: 'itemNotes', + field: 'note' + }, + + { + name: 'childNote', + operators: { + contains: true, + doesNotContain: true + }, + table: 'items', + field: 'note' + }, + + { + name: 'creator', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemCreators', + field: "TRIM(firstName || ' ' || lastName)" + }, + + { + name: 'lastName', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemCreators', + field: 'lastName', + special: true + }, + + { + name: 'field', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemData', + field: 'value', + aliases: yield Zotero.DB.columnQueryAsync("SELECT fieldName FROM fieldsCombined " + + "WHERE fieldName NOT IN ('accessDate', 'date', 'pages', " + + "'section','seriesNumber','issue')"), + template: true // mark for special handling + }, + + { + name: 'datefield', + operators: { + is: true, + isNot: true, + isBefore: true, + isAfter: true, + isInTheLast: true + }, + table: 'itemData', + field: 'value', + aliases: ['accessDate', 'date', 'dateDue', 'accepted'], // TEMP - NSF + template: true // mark for special handling + }, + + { + name: 'year', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemData', + field: 'SUBSTR(value, 1, 4)', + special: true + }, + + { + name: 'numberfield', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true, + isLessThan: true, + isGreaterThan: true + }, + table: 'itemData', + field: 'value', + aliases: ['pages', 'numPages', 'numberOfVolumes', 'section', 'seriesNumber','issue'], + template: true // mark for special handling + }, + + { + name: 'libraryID', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'libraryID', + special: true, + noLoad: true + }, + + { + name: 'key', + operators: { + is: true, + isNot: true, + beginsWith: true + }, + table: 'items', + field: 'key', + special: true, + noLoad: true + }, + + { + name: 'itemID', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'itemID', + special: true, + noLoad: true + }, + + { + name: 'annotation', + operators: { + contains: true, + doesNotContain: true + }, + table: 'annotations', + field: 'text' + }, + + { + name: 'fulltextWord', + operators: { + contains: true, + doesNotContain: true + }, + table: 'fulltextItemWords', + field: 'word', + flags: { + leftbound: true + }, + special: true + }, + + { + name: 'fulltextContent', + operators: { + contains: true, + doesNotContain: true + }, + special: false + }, + + { + name: 'tempTable', + operators: { + is: true + } + } + ]; + + // Index conditions by name and aliases + _conditions = {}; + for (var i in conditions) { + _conditions[conditions[i]['name']] = conditions[i]; + if (conditions[i]['aliases']) { + for (var j in conditions[i]['aliases']) { + // TEMP - NSF + switch (conditions[i]['aliases'][j]) { + case 'dateDue': + case 'accepted': + if (!Zotero.ItemTypes.getID('nsfReviewer')) { + continue; + } + } + _conditions[conditions[i]['aliases'][j]] = conditions[i]; + } + } + _conditions[conditions[i]['name']] = conditions[i]; + } + + _standardConditions = []; + + var baseMappedFields = Zotero.ItemFields.getBaseMappedFields(); + var locale = Zotero.locale; + + // Separate standard conditions for menu display + for (var i in _conditions){ + var fieldID = false; + if (['field', 'datefield', 'numberfield'].indexOf(_conditions[i]['name']) != -1) { + fieldID = Zotero.ItemFields.getID(i); + } + + // If explicitly special... + if (_conditions[i]['special'] || + // or a template master (e.g. 'field')... + (_conditions[i]['template'] && i==_conditions[i]['name']) || + // or no table and not explicitly unspecial... + (!_conditions[i]['table'] && + typeof _conditions[i]['special'] == 'undefined') || + // or field is a type-specific version of a base field... + (fieldID && baseMappedFields.indexOf(fieldID) != -1)) { + // ...then skip + continue; + } + + let localized = self.getLocalizedName(i); + // Hack to use a different name for "issue" in French locale, + // where 'number' and 'issue' are translated the same + // https://forums.zotero.org/discussion/14942/ + if (fieldID == 5 && locale.substr(0, 2).toLowerCase() == 'fr') { + localized = "Num\u00E9ro (p\u00E9riodique)"; + } + + _standardConditions.push({ + name: i, + localized: localized, + operators: _conditions[i]['operators'], + flags: _conditions[i]['flags'] + }); + } + + var collation = Zotero.getLocaleCollation(); + _standardConditions.sort(function(a, b) { + return collation.compareString(1, a.localized, b.localized); + }); + }); + + + /* + * Get condition data + */ + function get(condition){ + return _conditions[condition]; + } + + + /* + * Returns array of possible conditions + * + * Does not include special conditions, only ones that would show in a drop-down list + */ + function getStandardConditions(){ + // TODO: return copy instead + return _standardConditions; + } + + + /* + * Check if an operator is valid for a given condition + */ + function hasOperator(condition, operator){ + var [condition, mode] = this.parseCondition(condition); + + if (!_conditions) { + throw new Zotero.Exception.UnloadedDataException("Search conditions not yet loaded"); + } + + if (!_conditions[condition]){ + let e = new Error("Invalid condition '" + condition + "' in hasOperator()"); + e.name = "ZoteroUnknownFieldError"; + throw e; + } + + if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){ + return true; + } + + return !!_conditions[condition]['operators'][operator]; + } + + + function getLocalizedName(str) { + // TEMP + if (str == 'itemType') { + str = 'itemTypeID'; + } + + try { + return Zotero.getString('searchConditions.' + str) + } + catch (e) { + return Zotero.ItemFields.getLocalizedString(null, str); + } + } + + + /** + * Compare two API JSON condition objects + */ + this.equals = function (data1, data2) { + return data1.condition === data2.condition + && data1.operator === data2.operator + && data1.value === data2.value; + } + + + /* + * Parses a search into words and "double-quoted phrases" + * + * Also strips unpaired quotes at the beginning and end of words + * + * Returns array of objects containing 'text' and 'inQuotes' + */ + function parseSearchString(str) { + var parts = str.split(/\s*("[^"]*")\s*|"\s|\s"|^"|"$|'\s|\s'|^'|'$|\s/m); + var parsed = []; + + for (var i in parts) { + var part = parts[i]; + if (!part || !part.length) { + continue; + } + + if (part.charAt(0)=='"' && part.charAt(part.length-1)=='"') { + parsed.push({ + text: part.substring(1, part.length-1), + inQuotes: true + }); + } + else { + parsed.push({ + text: part, + inQuotes: false + }); + } + } + + return parsed; + } + + + function parseCondition(condition){ + var mode = false; + var pos = condition.indexOf('/'); + if (pos != -1){ + mode = condition.substr(pos+1); + condition = condition.substr(0, pos); + } + + return [condition, mode]; + } +} diff --git a/chrome/content/zotero/xpcom/data/searches.js b/chrome/content/zotero/xpcom/data/searches.js new file mode 100644 index 0000000000..c996aab30d --- /dev/null +++ b/chrome/content/zotero/xpcom/data/searches.js @@ -0,0 +1,172 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2006-2016 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + https://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Searches = function() { + this.constructor = null; + + this._ZDO_object = 'search'; + this._ZDO_id = 'savedSearchID'; + this._ZDO_table = 'savedSearches'; + + this._primaryDataSQLParts = { + savedSearchID: "O.savedSearchID", + name: "O.savedSearchName AS name", + libraryID: "O.libraryID", + key: "O.key", + version: "O.version", + synced: "O.synced" + } + + this._primaryDataSQLFrom = "FROM savedSearches O"; + + this.init = Zotero.Promise.coroutine(function* () { + yield Zotero.DataObjects.prototype.init.apply(this); + yield Zotero.SearchConditions.init(); + }); + + + /** + * Returns an array of Zotero.Search objects, ordered by name + * + * @param {Integer} [libraryID] + */ + this.getAll = Zotero.Promise.coroutine(function* (libraryID) { + var sql = "SELECT savedSearchID FROM savedSearches WHERE libraryID=?"; + var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID); + if (!ids.length) { + return [] + } + + var searches = this.get(ids); + // Do proper collation sort + var collation = Zotero.getLocaleCollation(); + searches.sort(function (a, b) { + return collation.compareString(1, a.name, b.name); + }); + return searches; + }); + + + this.getPrimaryDataSQL = function () { + // This should be the same as the query in Zotero.Search.loadPrimaryData(), + // just without a specific savedSearchID + return "SELECT " + + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " " + + "FROM savedSearches O WHERE 1"; + } + + + this._loadConditions = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = "SELECT savedSearchID, searchConditionID, condition, operator, value, required " + + "FROM savedSearches LEFT JOIN savedSearchConditions USING (savedSearchID) " + + "WHERE libraryID=?" + idSQL + + "ORDER BY savedSearchID, searchConditionID"; + var params = [libraryID]; + var lastID = null; + var rows = []; + var setRows = function (searchID, rows) { + var search = this._objectCache[searchID]; + if (!search) { + throw new Error("Search " + searchID + " not found"); + } + + search._conditions = {}; + + if (rows.length) { + search._maxSearchConditionID = rows[rows.length - 1].searchConditionID; + } + + // Reindex conditions, in case they're not contiguous in the DB + for (let i = 0; i < rows.length; i++) { + let condition = rows[i]; + + // Parse "condition[/mode]" + let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition); + + let cond = Zotero.SearchConditions.get(conditionName); + if (!cond || cond.noLoad) { + Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2); + continue; + } + + // Convert itemTypeID to itemType + // + // TEMP: This can be removed at some point + if (conditionName == 'itemTypeID') { + conditionName = 'itemType'; + condition.value = Zotero.ItemTypes.getName(condition.value); + } + + search._conditions[i] = { + id: i, + condition: conditionName, + mode: mode, + operator: condition.operator, + value: condition.value, + required: !!condition.required + }; + } + search._loaded.conditions = true; + search._clearChanged('conditions'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let searchID = row.getResultByIndex(0); + + if (lastID && searchID != lastID) { + setRows(lastID, rows); + rows = []; + } + + lastID = searchID; + let searchConditionID = row.getResultByIndex(1); + // No conditions + if (searchConditionID === null) { + return; + } + rows.push({ + searchConditionID, + condition: row.getResultByIndex(2), + operator: row.getResultByIndex(3), + value: row.getResultByIndex(4), + required: row.getResultByIndex(5) + }); + }.bind(this) + } + ); + if (lastID) { + setRows(lastID, rows); + } + }); + + Zotero.DataObjects.call(this); + + return this; +}.bind(Object.create(Zotero.DataObjects.prototype))(); diff --git a/components/zotero-service.js b/components/zotero-service.js index ca66420860..535ff705ab 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -82,6 +82,9 @@ const xpcomFilesLocal = [ 'data/groups', 'data/itemFields', 'data/relations', + 'data/search', + 'data/searchConditions', + 'data/searches', 'data/tags', 'db', 'duplicates', @@ -98,7 +101,6 @@ const xpcomFilesLocal = [ 'report', 'router', 'schema', - 'search', 'server', 'style', 'sync',