Move search code into separate files in xpcom/data

This commit is contained in:
Dan Stillman 2016-10-04 23:44:07 -04:00
parent 0769a84a00
commit 8f38b01712
4 changed files with 865 additions and 817 deletions

View file

@ -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];
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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];
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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))();

View file

@ -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',