zotero/chrome/content/zotero/xpcom/search.js
2013-08-31 04:47:39 -04:00

2438 lines
59 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://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.Search = function() {
if (arguments[0]) {
throw ("Zotero.Search constructor doesn't take any parameters");
}
this._loaded = false;
this._init();
}
Zotero.Search.prototype._init = function () {
// Public members for access by public methods -- do not access directly
this._id = null;
this._libraryID = null;
this._key = null;
this._name = null;
this._dateAdded = null;
this._dateModified = null;
this._changed = false;
this._previousData = false;
this._scope = null;
this._scopeIncludeChildren = null;
this._sql = null;
this._sqlParams = null;
this._maxSearchConditionID = 0;
this._conditions = [];
this._hasPrimaryConditions = false;
}
Zotero.Search.prototype.getID = function(){
Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.id');
return this._id;
}
Zotero.Search.prototype.getName = function() {
Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.name');
return this.name;
}
Zotero.Search.prototype.setName = function(val) {
Zotero.debug('Zotero.Search.setName() is deprecated -- use Search.name');
this.name = val;
}
Zotero.Search.prototype.__defineGetter__('objectType', function () { return 'search'; });
Zotero.Search.prototype.__defineGetter__('id', function () { return this._get('id'); });
Zotero.Search.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
Zotero.Search.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); });
Zotero.Search.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); });
Zotero.Search.prototype.__defineGetter__('key', function () { return this._get('key'); });
Zotero.Search.prototype.__defineSetter__('key', function (val) { this._set('key', val) });
Zotero.Search.prototype.__defineGetter__('name', function () { return this._get('name'); });
Zotero.Search.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
Zotero.Search.prototype.__defineGetter__('dateAdded', function () { return this._get('dateAdded'); });
Zotero.Search.prototype.__defineSetter__('dateAdded', function (val) { this._set('dateAdded', val); });
Zotero.Search.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
Zotero.Search.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
Zotero.Search.prototype.__defineGetter__('conditions', function (arr) { this.getSearchConditions(); });
Zotero.Search.prototype._get = function (field) {
if ((this._id || this._key) && !this._loaded) {
this.load();
}
return this['_' + field];
}
Zotero.Search.prototype._set = function (field, val) {
switch (field) {
case 'id':
case 'libraryID':
case 'key':
if (val == this['_' + field]) {
return;
}
if (this._loaded) {
throw ("Cannot set " + field + " after object is already loaded in Zotero.Search._set()");
}
//this._checkValue(field, val);
this['_' + field] = val;
return;
case 'name':
val = Zotero.Utilities.trim(val);
break;
}
if (this.id || this.key) {
if (!this._loaded) {
this.load();
}
}
else {
this._loaded = true;
}
if (this['_' + field] != val) {
this._prepFieldChange(field);
switch (field) {
default:
this['_' + field] = val;
}
}
}
/**
* Check if saved search exists in the database
*
* @return bool TRUE if the search exists, FALSE if not
*/
Zotero.Search.prototype.exists = function() {
if (!this.id) {
throw ('searchID not set in Zotero.Search.exists()');
}
var sql = "SELECT COUNT(*) FROM savedSearches WHERE savedSearchID=?";
return !!Zotero.DB.valueQuery(sql, this.id);
}
/*
* Load a saved search from the DB
*/
Zotero.Search.prototype.load = function() {
// Changed in 1.5
if (arguments[0]) {
throw ('Parameter no longer allowed in Zotero.Search.load()');
}
var id = this._id;
var key = this._key;
var libraryID = this._libraryID;
var desc = id ? id : libraryID + "/" + key;
var sql = "SELECT S.*, "
+ "MAX(searchConditionID) AS maxID "
+ "FROM savedSearches S LEFT JOIN savedSearchConditions "
+ "USING (savedSearchID) WHERE ";
if (id) {
sql += "savedSearchID=?";
var params = id;
}
else {
sql += "key=?";
var params = [key];
if (libraryID) {
sql += " AND libraryID=?";
params.push(libraryID);
}
else {
sql += " AND libraryID IS NULL";
}
}
sql += " GROUP BY savedSearchID";
var data = Zotero.DB.rowQuery(sql, params);
this._loaded = true;
if (!data) {
return;
}
this._init();
this._id = data.savedSearchID;
this._libraryID = data.libraryID;
this._key = data.key;
this._name = data.savedSearchName;
this._dateAdded = data.dateAdded;
this._dateModified = data.dateModified;
this._maxSearchConditionID = data.maxID;
var sql = "SELECT * FROM savedSearchConditions "
+ "WHERE savedSearchID=? ORDER BY searchConditionID";
var conditions = Zotero.DB.query(sql, this._id);
// Reindex conditions, in case they're not contiguous in the DB
var conditionID = 1;
for (var i in conditions) {
// Parse "condition[/mode]"
var [condition, mode] =
Zotero.SearchConditions.parseCondition(conditions[i]['condition']);
var cond = Zotero.SearchConditions.get(condition);
if (!cond || cond.noLoad) {
Zotero.debug("Invalid saved search condition '" + condition + "' -- skipping", 2);
continue;
}
// Convert itemTypeID to itemType
//
// TEMP: This can be removed at some point
if (condition == 'itemTypeID') {
condition = 'itemType';
conditions[i].value = Zotero.ItemTypes.getName(conditions[i].value);
}
this._conditions[conditionID] = {
id: conditionID,
condition: condition,
mode: mode,
operator: conditions[i]['operator'],
value: conditions[i]['value'],
required: conditions[i]['required']
};
conditionID++;
}
}
/*
* Save the search to the DB and return a savedSearchID
*
* If there are gaps in the searchConditionIDs, |fixGaps| must be true
* and the caller must dispose of the search or reload the condition ids,
* which may change after the save.
*
* For new searches, name must be set called before saving
*/
Zotero.Search.prototype.save = function(fixGaps) {
Zotero.Searches.editCheck(this);
if (!this.name) {
throw('Name not provided for saved search');
}
Zotero.DB.beginTransaction();
var isNew = !this.id || !this.exists();
try {
var searchID = this.id ? this.id : Zotero.ID.get('savedSearches');
Zotero.debug("Saving " + (isNew ? 'new ' : '') + "search " + this.id);
var key = this.key ? this.key : this._generateKey();
var columns = [
'savedSearchID',
'savedSearchName',
'dateAdded',
'dateModified',
'clientDateModified',
'libraryID',
'key'
];
var placeholders = ['?', '?', '?', '?', '?', '?', '?'];
var sqlValues = [
searchID ? { int: searchID } : null,
{ string: this.name },
// If date added isn't set, use current timestamp
this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime,
// If date modified hasn't changed, use current timestamp
this._changed.dateModified ?
this.dateModified : Zotero.DB.transactionDateTime,
Zotero.DB.transactionDateTime,
this.libraryID ? this.libraryID : this.libraryID,
key
];
var sql = "REPLACE INTO savedSearches (" + columns.join(', ') + ") VALUES ("
+ placeholders.join(', ') + ")";
var insertID = Zotero.DB.query(sql, sqlValues);
if (!searchID) {
searchID = insertID;
}
if (!isNew) {
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
Zotero.DB.query(sql, this.id);
}
// Close gaps in savedSearchIDs
var saveConditions = {};
var i = 1;
for (var id in this._conditions) {
if (!fixGaps && id != i) {
Zotero.DB.rollbackTransaction();
throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._id);
}
saveConditions[i] = this._conditions[id];
i++;
}
this._conditions = saveConditions;
// TODO: use proper bound parameters once DB class is updated
for (var i in this._conditions){
var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
+ "searchConditionID, condition, operator, value, required) "
+ "VALUES (?,?,?,?,?,?)";
// Convert condition and mode to "condition[/mode]"
var condition = this._conditions[i].mode ?
this._conditions[i].condition + '/' + this._conditions[i].mode :
this._conditions[i].condition
var sqlParams = [
searchID, i, condition,
this._conditions[i].operator
? this._conditions[i].operator : null,
this._conditions[i].value
? this._conditions[i].value : null,
this._conditions[i].required
? 1 : null
];
Zotero.DB.query(sql, sqlParams);
}
if (isNew && this.libraryID) {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID);
var group = Zotero.Groups.get(groupID);
group.clearSearchCache();
}
Zotero.DB.commitTransaction();
}
catch (e) {
Zotero.DB.rollbackTransaction();
throw (e);
}
// If successful, set values in object
if (!this.id) {
this._id = searchID;
}
if (!this.key) {
this._key = key;
}
if (isNew) {
Zotero.Notifier.trigger('add', 'search', this.id);
}
else {
Zotero.Notifier.trigger('modify', 'search', this.id, this._previousData);
}
return this._id;
}
Zotero.Search.prototype.clone = function() {
var s = new Zotero.Search();
var conditions = this.getSearchConditions();
for each(var condition in conditions) {
var name = condition.mode ?
condition.condition + '/' + condition.mode :
condition.condition
s.addCondition(name, condition.operator, condition.value,
condition.required);
}
return s;
}
Zotero.Search.prototype.addCondition = function(condition, operator, value, required) {
if ((this.id || this.key) && !this._loaded) {
this.load();
}
if (!Zotero.SearchConditions.hasOperator(condition, operator)){
throw ("Invalid operator '" + operator + "' for condition " + condition);
}
// Shortcut to add a condition on every table -- does not return an id
if (condition.match(/^quicksearch/)) {
var parts = Zotero.SearchConditions.parseSearchString(value);
for each(var part in parts) {
this.addCondition('blockStart');
// If search string is 8 characters, see if this is a item key
if (operator == 'contains' && part.text.length == 8) {
this.addCondition('key', 'is', part.text, false);
}
if (condition == 'quicksearch-titleCreatorYear') {
this.addCondition('title', operator, part.text, false);
this.addCondition('publicationTitle', operator, part.text, false);
this.addCondition('year', operator, part.text, false);
}
else {
this.addCondition('field', operator, part.text, false);
this.addCondition('tag', operator, part.text, false);
this.addCondition('note', operator, part.text, false);
}
this.addCondition('creator', operator, part.text, false);
if (condition == 'quicksearch-everything') {
this.addCondition('annotation', operator, part.text, false);
if (part.inQuotes) {
this.addCondition('fulltextContent', operator, part.text, false);
}
else {
var splits = Zotero.Fulltext.semanticSplitter(part.text);
for each(var split in splits) {
this.addCondition('fulltextWord', operator, split, false);
}
}
}
this.addCondition('blockEnd');
}
if (condition == 'quicksearch-titleCreatorYear') {
this.addCondition('noChildren', 'true');
}
return false;
}
// Shortcut to add a collection
else if (condition == 'collectionID') {
var c = Zotero.Collections.get(value);
if (!c) {
var msg = "Collection " + value + " not found";
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
return;
}
var lkh = Zotero.Collections.getLibraryKeyHash(c);
return this.addCondition('collection', operator, lkh, required);
}
// Shortcut to add a saved search
else if (condition == 'savedSearchID') {
var s = Zotero.Searches.get(value);
if (!s) {
var msg = "Saved search " + value + " not found";
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
return;
}
var lkh = Zotero.Searches.getLibraryKeyHash(s);
return this.addCondition('savedSearch', operator, lkh, required);
}
var searchConditionID = ++this._maxSearchConditionID;
var [condition, mode] = Zotero.SearchConditions.parseCondition(condition);
this._conditions[searchConditionID] = {
id: searchConditionID,
condition: condition,
mode: mode,
operator: operator,
value: value,
required: required
};
this._sql = null;
this._sqlParams = null;
return searchConditionID;
}
/*
* Sets scope of search to the results of the passed Search object
*/
Zotero.Search.prototype.setScope = function (searchObj, includeChildren) {
this._scope = searchObj;
this._scopeIncludeChildren = includeChildren;
}
Zotero.Search.prototype.updateCondition = function(searchConditionID, condition, operator, value, required){
if ((this.id || this.key) && !this._loaded) {
this.load();
}
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in updateCondition()');
}
if (!Zotero.SearchConditions.hasOperator(condition, operator)){
throw ("Invalid operator '" + operator + "' for condition " + condition);
}
var [condition, mode] = Zotero.SearchConditions.parseCondition(condition);
this._conditions[searchConditionID] = {
id: parseInt(searchConditionID),
condition: condition,
mode: mode,
operator: operator,
value: value,
required: required
};
this._sql = null;
this._sqlParams = null;
}
Zotero.Search.prototype.removeCondition = function(searchConditionID){
if ((this.id || this.key) && !this._loaded) {
this.load();
}
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()');
}
delete this._conditions[searchConditionID];
}
/*
* Returns an array with 'condition', 'operator', 'value', 'required'
* for the given searchConditionID
*/
Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
if ((this.id || this.key) && !this._loaded) {
this.load();
}
return this._conditions[searchConditionID];
}
/*
* Returns a multidimensional array of conditions/operator/value sets
* used in the search, indexed by searchConditionID
*/
Zotero.Search.prototype.getSearchConditions = function(){
if ((this.id || this.key) && !this._loaded) {
this.load();
}
var conditions = [];
for (var id in this._conditions) {
var condition = this._conditions[id];
conditions[id] = {
id: id,
condition: condition.condition,
mode: condition.mode,
operator: condition.operator,
value: condition.value,
required: condition.required
};
}
return conditions;
}
Zotero.Search.prototype.hasPostSearchFilter = function() {
if ((this.id || this.key) && !this._loaded) {
this.load();
}
for each(var i in this._conditions){
if (i.condition == 'fulltextContent'){
return true;
}
}
return false;
}
/*
* Run the search and return an array of item ids for results
*/
Zotero.Search.prototype.search = function(asTempTable){
if ((this.id || this.key) && !this._loaded) {
this.load();
}
if (!this._sql){
this._buildQuery();
}
// Default to 'all' mode
var joinMode = 'all';
// Set some variables for conditions to avoid further lookups
for each(var condition in this._conditions) {
switch (condition.condition) {
case 'joinMode':
if (condition.operator == 'any') {
joinMode = 'any';
}
break;
case 'fulltextContent':
var fulltextContent = true;
break;
case 'includeParentsAndChildren':
if (condition.operator == 'true') {
var includeParentsAndChildren = true;
}
break;
case 'includeParents':
if (condition.operator == 'true') {
var includeParents = true;
}
break;
case 'includeChildren':
if (condition.operator == 'true') {
var includeChildren = true;
}
break;
case 'blockStart':
var hasQuicksearch = true;
break;
}
}
// Run a subsearch to define the superset of possible results
if (this._scope) {
Zotero.DB.beginTransaction();
// If subsearch has post-search filter, run and insert ids into temp table
if (this._scope.hasPostSearchFilter()) {
var ids = this._scope.search();
if (!ids) {
Zotero.DB.commitTransaction();
return false;
}
var tmpTable = Zotero.Search.idsToTempTable(ids);
}
// Otherwise, just copy to temp table directly
else {
var tmpTable = "tmpSearchResults_" + Zotero.randomString(8);
var sql = "CREATE TEMPORARY TABLE " + tmpTable + " AS "
+ this._scope.getSQL();
Zotero.DB.query(sql, this._scope.getSQLParams());
var sql = "CREATE INDEX " + tmpTable + "_itemID ON " + tmpTable + "(itemID)";
Zotero.DB.query(sql);
}
// Search ids in temp table
var sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE itemID IN (" + this._sql + ") "
+ "AND ("
+ "itemID IN (SELECT itemID FROM " + tmpTable + ")";
if (this._scopeIncludeChildren) {
sql += " OR itemID IN (SELECT itemID FROM itemAttachments"
+ " WHERE sourceItemID IN (SELECT itemID FROM " + tmpTable + ")) OR "
+ "itemID IN (SELECT itemID FROM itemNotes"
+ " WHERE sourceItemID IN (SELECT itemID FROM " + tmpTable + "))";
}
sql += ")";
var res = Zotero.DB.valueQuery(sql, this._sqlParams),
ids = res ? res.split(",") : [];
/*
// DEBUG: Should this be here?
//
if (!ids) {
Zotero.DB.query("DROP TABLE " + tmpTable);
Zotero.DB.commitTransaction();
return false;
}
*/
}
// Or just run main search
else {
var ids = Zotero.DB.columnQuery(this._sql, this._sqlParams);
}
//Zotero.debug('IDs from main search or subsearch: ');
//Zotero.debug(ids);
//Zotero.debug('Join mode: ' + joinMode);
// Filter results with fulltext search
//
// If join mode ALL, return the (intersection of main and fulltext word search)
// filtered by fulltext content
//
// If join mode ANY or there's a quicksearch (which we assume
// fulltextContent is part of), return the union of the main search and
// (a separate fulltext word search filtered by fulltext content)
for each(var condition in this._conditions){
if (condition['condition']=='fulltextContent'){
var filter = function(val, index, array) {
return hash[val] ?
(condition.operator == 'contains') :
(condition.operator == 'doesNotContain');
};
// Regexp mode -- don't use fulltext word index
if (condition.mode && condition.mode.indexOf('regexp') == 0) {
// In an ANY search, only bother scanning items that
// haven't already been found by the main search
if (joinMode == 'any') {
if (!tmpTable) {
Zotero.DB.beginTransaction();
var tmpTable = Zotero.Search.idsToTempTable(ids);
}
var sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE "
+ "itemID NOT IN (SELECT itemID FROM " + tmpTable + ")";
var res = Zotero.DB.valueQuery(sql);
var scopeIDs = res ? res.split(",") : [];
}
// If an ALL search, scan only items from the main search
else {
var scopeIDs = ids;
}
}
// If not regexp mode, run a new search against the fulltext word
// index for words in this phrase
else {
Zotero.debug('Running subsearch against fulltext word index');
var s = new Zotero.Search();
// Add any necessary conditions to the fulltext word search --
// those that are required in an ANY search and any outside the
// quicksearch in an ALL search
for each(var c in this._conditions) {
if (c.condition == 'blockStart') {
var inQS = true;
continue;
}
else if (c.condition == 'blockEnd') {
inQS = false;
continue;
}
else if (c.condition == 'fulltextContent' ||
c.condition == 'fulltextContent' ||
inQS) {
continue;
}
else if (joinMode == 'any' && !c.required) {
continue;
}
s.addCondition(c.condition, c.operator, c.value);
}
var splits = Zotero.Fulltext.semanticSplitter(condition.value);
for each(var split in splits){
s.addCondition('fulltextWord', condition.operator, split);
}
var fulltextWordIDs = s.search();
//Zotero.debug("Fulltext word IDs");
//Zotero.debug(fulltextWordIDs);
// If ALL mode, set intersection of main search and fulltext word index
// as the scope for the fulltext content search
if (joinMode == 'all' && !hasQuicksearch) {
var hash = {};
for each(var id in fulltextWordIDs){
hash[id] = true;
}
if (ids) {
var scopeIDs = ids.filter(filter);
}
else {
var scopeIDs = [];
}
}
// If ANY mode, just use fulltext word index hits for content search,
// since the main results will be added in below
else {
var scopeIDs = fulltextWordIDs;
}
}
if (scopeIDs && scopeIDs.length) {
var fulltextIDs = Zotero.Fulltext.findTextInItems(scopeIDs,
condition['value'], condition['mode']);
var hash = {};
for each(var val in fulltextIDs){
hash[val.id] = true;
}
filteredIDs = scopeIDs.filter(filter);
}
else {
var filteredIDs = [];
}
//Zotero.debug("Filtered IDs:")
//Zotero.debug(filteredIDs);
// If join mode ANY, add any new items from the fulltext content
// search to the main search results
//
// We only do this if there are primary conditions that alter the
// main search, since otherwise all items will match
if (this._hasPrimaryConditions &&
(joinMode == 'any' || hasQuicksearch) && ids) {
//Zotero.debug("Adding filtered IDs to main set");
for each(var id in filteredIDs) {
if (ids.indexOf(id) == -1) {
ids.push(id);
}
}
}
else {
//Zotero.debug("Replacing main set with filtered IDs");
ids = filteredIDs;
}
}
}
if (tmpTable) {
Zotero.DB.query("DROP TABLE " + tmpTable);
Zotero.DB.commitTransaction();
}
if (this.hasPostSearchFilter() &&
(includeParentsAndChildren || includeParents || includeChildren)) {
Zotero.DB.beginTransaction();
var tmpTable = Zotero.Search.idsToTempTable(ids);
if (includeParentsAndChildren || includeParents) {
//Zotero.debug("Adding parent items to result set");
var sql = "SELECT sourceItemID FROM itemAttachments "
+ "WHERE itemID IN (SELECT itemID FROM " + tmpTable + ") "
+ " AND sourceItemID IS NOT NULL "
+ "UNION SELECT sourceItemID FROM itemNotes "
+ "WHERE itemID IN (SELECT itemID FROM " + tmpTable + ")"
+ " AND sourceItemID IS NOT NULL";
}
if (includeParentsAndChildren || includeChildren) {
//Zotero.debug("Adding child items to result set");
var childrenSQL = "SELECT itemID FROM itemAttachments WHERE "
+ "sourceItemID IN (SELECT itemID FROM " + tmpTable + ") UNION "
+ "SELECT itemID FROM itemNotes WHERE sourceItemID IN "
+ "(SELECT itemID FROM " + tmpTable + ")";
if (includeParentsAndChildren || includeParents) {
sql += " UNION " + childrenSQL;
}
else {
sql = childrenSQL;
}
}
sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE itemID IN (" + sql + ")";
var res = Zotero.DB.valueQuery(sql);
var parentChildIDs = res ? res.split(",") : [];
Zotero.DB.query("DROP TABLE " + tmpTable);
Zotero.DB.commitTransaction();
// Add parents and children to main ids
if (parentChildIDs) {
for (var i=0; i<parentChildIDs.length; i++) {
var id = parentChildIDs[i];
if (ids.indexOf(id) == -1) {
ids.push(id);
}
}
}
}
//Zotero.debug('Final result set');
//Zotero.debug(ids);
if (!ids || !ids.length) {
return false;
}
if (asTempTable) {
var table = Zotero.Search.idsToTempTable(ids);
return table;
}
return ids;
}
Zotero.Search.prototype.serialize = function() {
var obj = {
primary: {
id: this.id,
libraryID: this.libraryID,
key: this.key,
dateAdded: this.dateAdded,
dateModified: this.dateModified
},
fields: {
name: this.name,
},
conditions: this.getSearchConditions()
};
return obj;
}
/*
* Get the SQL string for the search
*/
Zotero.Search.prototype.getSQL = function(){
if (!this._sql){
this._buildQuery();
}
return this._sql;
}
Zotero.Search.prototype.getSQLParams = function(){
if (!this._sql){
this._buildQuery();
}
return this._sqlParams;
}
Zotero.Search.prototype._prepFieldChange = function (field) {
if (!this._changed) {
this._changed = {};
}
this._changed[field] = true;
// Save a copy of the data before changing
// TODO: only save previous data if search exists
if (this.id && this.exists() && !this._previousData) {
this._previousData = this.serialize();
}
}
/*
* Batch insert
*/
Zotero.Search.idsToTempTable = function (ids) {
const N_COMBINED_INSERTS = 128;
var tmpTable = "tmpSearchResults_" + Zotero.randomString(8);
Zotero.DB.beginTransaction();
var sql = "CREATE TEMPORARY TABLE " + tmpTable + " (itemID INT)";
Zotero.DB.query(sql);
var sql = "INSERT INTO " + tmpTable + " SELECT ? ";
for (var i=0; i<(N_COMBINED_INSERTS-1); i++) {
sql += "UNION SELECT ? ";
}
var insertStatement = Zotero.DB.getStatement(sql),
n = ids.length;
for (var i=0; i<n; i+=N_COMBINED_INSERTS) {
for (var j=0; j<N_COMBINED_INSERTS; j++) {
insertStatement.bindInt32Parameter(j, ids[i+j]);
}
try {
insertStatement.execute();
}
catch (e) {
throw (Zotero.DB.getLastErrorString());
}
}
insertStatement.finalize();
var nRemainingInserts = (n % N_COMBINED_INSERTS);
var remainingInsertStart = n-nRemainingInserts-1;
var sql = "INSERT INTO " + tmpTable + " SELECT ? ";
for (var i=remainingInsertStart; i<n; i++) {
sql += "UNION SELECT ? ";
}
var insertStatement = Zotero.DB.getStatement(sql);
for (var j=remainingInsertStart; j<n; j++) {
insertStatement.bindInt32Parameter(j-remainingInsertStart, ids[remainingInsertStart]);
}
var sql = "CREATE INDEX " + tmpTable + "_itemID ON " + tmpTable + "(itemID)";
Zotero.DB.query(sql);
Zotero.DB.commitTransaction();
return tmpTable;
}
/*
* Build the SQL query for the search
*/
Zotero.Search.prototype._buildQuery = function(){
var sql = 'SELECT itemID FROM items';
var sqlParams = [];
// Separate ANY conditions for 'required' condition support
var anySQL = '';
var anySQLParams = [];
var conditions = [];
for (var i in this._conditions){
var data = Zotero.SearchConditions.get(this._conditions[i]['condition']);
// Has a table (or 'savedSearch', which doesn't have a table but isn't special)
if (data.table || data.name == 'savedSearch' || data.name == 'tempTable') {
conditions.push({
name: data['name'],
alias: data['name']!=this._conditions[i]['condition']
? this._conditions[i]['condition'] : false,
table: data['table'],
field: data['field'],
operator: this._conditions[i]['operator'],
value: this._conditions[i]['value'],
flags: data['flags'],
required: this._conditions[i]['required']
});
this._hasPrimaryConditions = true;
}
// Handle special conditions
else {
switch (data['name']){
case 'deleted':
var deleted = this._conditions[i].operator == 'true';
continue;
case 'noChildren':
var noChildren = this._conditions[i]['operator']=='true';
continue;
case 'includeParentsAndChildren':
var includeParentsAndChildren = this._conditions[i]['operator'] == 'true';
continue;
case 'includeParents':
var includeParents = this._conditions[i]['operator'] == 'true';
continue;
case 'includeChildren':
var includeChildren = this._conditions[i]['operator'] == 'true';
continue;
case 'unfiled':
this._conditions[i]['operator']
var unfiled = this._conditions[i]['operator'] == 'true';
continue;
// Search subcollections
case 'recursive':
var recursive = this._conditions[i]['operator']=='true';
continue;
// Join mode ('any' or 'all')
case 'joinMode':
var joinMode = this._conditions[i]['operator'].toUpperCase();
continue;
case 'fulltextContent':
// Handled in Search.search()
continue;
// For quicksearch block markers
case 'blockStart':
conditions.push({name:'blockStart'});
continue;
case 'blockEnd':
conditions.push({name:'blockEnd'});
continue;
}
throw ('Unhandled special condition ' + this._conditions[i]['condition']);
}
}
// Exclude deleted items (and their child items) by default
sql += " WHERE itemID " + (deleted ? "" : "NOT ") + "IN "
+ "("
+ "SELECT itemID FROM deletedItems "
+ "UNION "
+ "SELECT itemID FROM itemNotes "
+ "WHERE sourceItemID IS NOT NULL AND "
+ "sourceItemID IN (SELECT itemID FROM deletedItems) "
+ "UNION "
+ "SELECT itemID FROM itemAttachments "
+ "WHERE sourceItemID IS NOT NULL AND "
+ "sourceItemID IN (SELECT itemID FROM deletedItems) "
+ ")";
if (noChildren){
sql += " AND (itemID NOT IN (SELECT itemID FROM itemNotes "
+ "WHERE sourceItemID IS NOT NULL) AND itemID NOT IN "
+ "(SELECT itemID FROM itemAttachments "
+ "WHERE sourceItemID IS NOT NULL))";
}
if (unfiled) {
sql += " AND (itemID NOT IN (SELECT itemID FROM collectionItems) "
// Exclude children
+ "AND itemID NOT IN "
+ "(SELECT itemID FROM itemAttachments WHERE sourceItemID IS NOT NULL "
+ "UNION SELECT itemID FROM itemNotes WHERE sourceItemID IS NOT NULL)"
+ ")";
}
if (this._hasPrimaryConditions) {
sql += " AND ";
for each(var condition in conditions){
var skipOperators = false;
var openParens = 0;
var condSQL = '';
var selectOpenParens = 0;
var condSelectSQL = '';
var condSQLParams = [];
//
// Special table handling
//
if (condition['table']){
switch (condition['table']){
default:
condSelectSQL += 'itemID '
switch (condition['operator']){
case 'isNot':
case 'doesNotContain':
condSelectSQL += 'NOT ';
break;
}
condSelectSQL += 'IN (';
selectOpenParens = 1;
condSQL += 'SELECT itemID FROM ' +
condition['table'] + ' WHERE (';
openParens = 1;
}
}
//
// Special condition handling
//
switch (condition['name']){
case 'field':
case 'datefield':
case 'numberfield':
if (condition['alias']) {
// Add base field
condSQLParams.push(
Zotero.ItemFields.getID(condition['alias'])
);
var typeFields = Zotero.ItemFields.getTypeFieldsFromBase(condition['alias']);
if (typeFields) {
condSQL += 'fieldID IN (?,';
// Add type-specific fields
for each(var fieldID in typeFields) {
condSQL += '?,';
condSQLParams.push(fieldID);
}
condSQL = condSQL.substr(0, condSQL.length - 1);
condSQL += ') AND ';
}
else {
condSQL += 'fieldID=? AND ';
}
}
condSQL += "valueID IN (SELECT valueID FROM "
+ "itemDataValues WHERE ";
openParens++;
break;
case 'year':
condSQLParams.push(Zotero.ItemFields.getID('date'));
//Add base field
var dateFields = Zotero.ItemFields.getTypeFieldsFromBase('date');
if (dateFields) {
condSQL += 'fieldID IN (?,';
// Add type-specific date fields (dateEnacted, dateDecided, issueDate)
for each(var fieldID in dateFields) {
condSQL += '?,';
condSQLParams.push(fieldID);
}
condSQL = condSQL.substr(0, condSQL.length - 1);
condSQL += ') AND ';
}
condSQL += "valueID IN (SELECT valueID FROM "
+ "itemDataValues WHERE ";
openParens++;
break;
case 'collection':
var col;
if (condition.value) {
var lkh = Zotero.Collections.parseLibraryKeyHash(condition.value);
if (lkh) {
col = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key);
}
}
if (!col) {
var msg = "Collection " + condition.value + " specified in saved search doesn't exist";
Zotero.debug(msg, 2);
Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js');
continue;
}
var q = ['?'];
var p = [col.id];
// Search descendent collections if recursive search
if (recursive){
var descendents = col.getDescendents(false, 'collection');
if (descendents){
for (var k in descendents){
q.push('?');
p.push({int:descendents[k]['id']});
}
}
}
condSQL += "collectionID IN (" + q.join() + ")";
condSQLParams = condSQLParams.concat(p);
skipOperators = true;
break;
case 'savedSearch':
condSQL += "itemID ";
if (condition['operator']=='isNot'){
condSQL += "NOT ";
}
condSQL += "IN (";
var search;
if (condition.value) {
var lkh = Zotero.Searches.parseLibraryKeyHash(condition.value);
if (lkh) {
search = Zotero.Searches.getByLibraryAndKey(lkh.libraryID, lkh.key);
}
}
if (!search) {
var msg = "Search " + condition.value + " specified in saved search doesn't exist";
Zotero.debug(msg, 2);
Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js');
continue;
}
// Check if there are any post-search filters
var hasFilter = search.hasPostSearchFilter();
// This is an ugly and inefficient way of doing a
// subsearch, but it's necessary if there are any
// post-search filters (e.g. fulltext scanning) in the
// subsearch
//
// DEBUG: it's possible there's a query length limit here
// or that this slows things down with large libraries
// -- should probably use a temporary table instead
if (hasFilter){
var subids = search.search();
condSQL += subids.join();
}
// Otherwise just put the SQL in a subquery
else {
condSQL += search.getSQL();
var subpar = search.getSQLParams();
for (var k in subpar){
condSQLParams.push(subpar[k]);
}
}
condSQL += ")";
skipOperators = true;
break;
case 'itemType':
condSQL += "itemTypeID IN (SELECT itemTypeID FROM itemTypesCombined WHERE ";
openParens++;
break;
case 'fileTypeID':
var ftSQL = 'SELECT mimeType FROM fileTypeMimeTypes '
+ 'WHERE fileTypeID IN ('
+ 'SELECT fileTypeID FROM fileTypes WHERE '
+ 'fileTypeID=?)';
var patterns = Zotero.DB.columnQuery(ftSQL, { int: condition.value });
if (patterns) {
for each(str in patterns) {
condSQL += 'mimeType LIKE ? OR ';
condSQLParams.push(str + '%');
}
condSQL = condSQL.substring(0, condSQL.length - 4);
}
else {
throw ("Invalid fileTypeID '" + condition.value + "' specified in search.js")
}
skipOperators = true;
break;
case 'tag':
condSQL += "tagID IN (SELECT tagID FROM tags WHERE ";
openParens++;
break;
case 'creator':
case 'lastName':
condSQL += "creatorID IN (SELECT creatorID FROM creators "
+ "NATURAL JOIN creatorData WHERE ";
openParens++;
break;
case 'childNote':
condSQL += "itemID IN (SELECT sourceItemID FROM "
+ "itemNotes WHERE ";
openParens++;
break;
case 'fulltextWord':
condSQL += "wordID IN (SELECT wordID FROM fulltextWords "
+ "WHERE ";
openParens++;
break;
case 'tempTable':
if (!condition.value.match(/^[a-zA-Z0-9]+$/)) {
throw ("Invalid temp table '" + condition.value + "'");
}
condSQL += "itemID IN (SELECT id FROM " + condition.value + ")";
skipOperators = true;
break;
// For quicksearch blocks
case 'blockStart':
case 'blockEnd':
skipOperators = true;
break;
}
if (!skipOperators){
// Special handling for date fields
//
// Note: We assume full datetimes are already UTC and don't
// need to be handled specially
if ((condition['name']=='dateAdded' ||
condition['name']=='dateModified' ||
condition['name']=='datefield') &&
!Zotero.Date.isSQLDateTime(condition['value'])){
// TODO: document these flags
var parseDate = null;
var alt = null;
var useFreeform = null;
switch (condition['operator']){
case 'is':
case 'isNot':
var parseDate = true;
var alt = '__';
var useFreeform = true;
break;
case 'isBefore':
var parseDate = true;
var alt = '00';
var useFreeform = false;
break;
case 'isAfter':
var parseDate = true;
// '__' used here just so the > string comparison
// doesn't match dates in the specified year
var alt = '__';
var useFreeform = false;
break;
case 'isInTheLast':
var parseDate = false;
break;
default:
throw ('Invalid date field operator in search');
}
// Convert stored UTC dates to localtime
//
// It'd be nice not to deal with time zones here at all,
// but otherwise searching for the date part of a field
// stored as UTC that wraps midnight would be unsuccessful
if (condition['name']=='dateAdded' ||
condition['name']=='dateModified' ||
condition['alias']=='accessDate'){
condSQL += "DATE(" + condition['field'] + ", 'localtime')";
}
// Only use first (SQL) part of multipart dates
else {
condSQL += "SUBSTR(" + condition['field'] + ", 1, 10)";
}
if (parseDate){
var go = false;
var dateparts = Zotero.Date.strToDate(condition.value);
// Search on SQL date -- underscore is
// single-character wildcard
//
// If isBefore or isAfter, month and day fall back
// to '00' so that a search for just a year works
// (and no year will just not find anything)
var sqldate = dateparts.year ?
Zotero.Utilities.lpad(dateparts.year, '0', 4) : '____';
sqldate += '-'
sqldate += dateparts.month || dateparts.month === 0 ?
Zotero.Utilities.lpad(dateparts.month + 1, '0', 2) : alt;
sqldate += '-';
sqldate += dateparts.day ?
Zotero.Utilities.lpad(dateparts.day, '0', 2) : alt;
if (sqldate!='____-__-__'){
go = true;
switch (condition['operator']){
case 'is':
case 'isNot':
condSQL += ' LIKE ?';
break;
case 'isBefore':
condSQL += '<?';
condSQL += ' AND ' + condition['field'] +
">'0000-00-00'";
break;
case 'isAfter':
condSQL += '>?';
break;
}
condSQLParams.push({string:sqldate});
}
// Search for any remaining parts individually
if (useFreeform && dateparts['part']){
go = true;
var parts = dateparts['part'].split(' ');
for each (var part in parts){
condSQL += " AND SUBSTR(" + condition['field'] + ", 12, 100)";
condSQL += " LIKE ?";
condSQLParams.push('%' + part + '%');
}
}
// If neither part used, invalidate clause
if (!go){
condSQL += '=0';
}
}
else {
switch (condition['operator']){
case 'isInTheLast':
condSQL += ">DATE('NOW', 'localtime', ?)"; // e.g. ('NOW', '-10 DAYS')
condSQLParams.push({string: '-' + condition['value']});
break;
}
}
}
// Non-date fields
else {
switch (condition.operator) {
// Cast strings as integers for < and > comparisons,
// at least until
case 'isLessThan':
case 'isGreaterThan':
condSQL += "CAST(" + condition['field'] + " AS INT)";
// Make sure either field is an integer or
// converting to an integer and back to a string
// yields the same result (i.e. it's numeric)
var opAppend = " AND (TYPEOF("
+ condition['field'] + ") = 'integer' OR "
+ "CAST("
+ "CAST(" + condition['field'] + " AS INT)"
+ " AS STRING) = " + condition['field'] + ")"
break;
default:
condSQL += condition['field'];
}
switch (condition['operator']){
case 'contains':
case 'doesNotContain': // excluded with NOT IN above
condSQL += ' LIKE ?';
// For fields with 'leftbound' flag, perform a
// leftbound search even for 'contains' condition
if (condition['flags'] &&
condition['flags']['leftbound'] &&
Zotero.Prefs.get('search.useLeftBound')) {
condSQLParams.push(condition['value'] + '%');
}
else {
condSQLParams.push('%' + condition['value'] + '%');
}
break;
case 'is':
case 'isNot': // excluded with NOT IN above
// Automatically cast values which might
// have been stored as integers
if (condition.value && typeof condition.value == 'string'
&& condition.value.match(/^[1-9]+[0-9]*$/)) {
condSQL += ' LIKE ?';
}
else if (condition.value === null) {
condSQL += ' IS NULL';
break;
}
else {
condSQL += '=?';
}
condSQLParams.push(condition['value']);
break;
case 'beginsWith':
condSQL += ' LIKE ?';
condSQLParams.push(condition['value'] + '%');
break;
case 'isLessThan':
condSQL += '<?';
condSQLParams.push({int:condition['value']});
condSQL += opAppend;
break;
case 'isGreaterThan':
condSQL += '>?';
condSQLParams.push({int:condition['value']});
condSQL += opAppend;
break;
// Next two only used with full datetimes
case 'isBefore':
condSQL += '<?';
condSQLParams.push({string:condition['value']});
break;
case 'isAfter':
condSQL += '>?';
condSQLParams.push({string:condition['value']});
break;
}
}
}
// Close open parentheses
for (var k=openParens; k>0; k--){
condSQL += ')';
}
if (includeParentsAndChildren || includeParents) {
var parentSQL = "SELECT itemID FROM items WHERE "
+ "itemID IN (SELECT sourceItemID FROM itemAttachments "
+ "WHERE itemID IN (" + condSQL + ")) "
+ "OR itemID IN (SELECT sourceItemID FROM itemNotes "
+ "WHERE itemID IN (" + condSQL + ")) ";
var parentSQLParams = condSQLParams.concat(condSQLParams);
}
if (includeParentsAndChildren || includeChildren) {
var childrenSQL = "SELECT itemID FROM itemAttachments WHERE "
+ "sourceItemID IN (" + condSQL + ") UNION "
+ "SELECT itemID FROM itemNotes "
+ "WHERE sourceItemID IN (" + condSQL + ")";
var childSQLParams = condSQLParams.concat(condSQLParams);
}
if (includeParentsAndChildren || includeParents) {
condSQL += " UNION " + parentSQL;
condSQLParams = condSQLParams.concat(parentSQLParams);
}
if (includeParentsAndChildren || includeChildren) {
condSQL += " UNION " + childrenSQL;
condSQLParams = condSQLParams.concat(childSQLParams);
}
condSQL = condSelectSQL + condSQL;
// Close open parentheses
for (var k=selectOpenParens; k>0; k--) {
condSQL += ')';
}
// Little hack to support multiple quicksearch words
if (condition['name'] == 'blockStart') {
var inQS = true;
var qsSQL = '';
var qsParams = [];
continue;
}
else if (condition['name'] == 'blockEnd') {
inQS = false;
// Strip ' OR ' from last condition
qsSQL = qsSQL.substring(0, qsSQL.length-4);
// Add to existing quicksearch words
if (!quicksearchSQLSet) {
var quicksearchSQLSet = [];
var quicksearchParamsSet = [];
}
quicksearchSQLSet.push(qsSQL);
quicksearchParamsSet.push(qsParams);
}
else if (inQS) {
qsSQL += condSQL + ' OR ';
qsParams = qsParams.concat(condSQLParams);
}
// Keep non-required conditions separate if in ANY mode
else if (!condition['required'] && joinMode == 'ANY') {
anySQL += condSQL + ' OR ';
anySQLParams = anySQLParams.concat(condSQLParams);
}
else {
condSQL += ' AND ';
sql += condSQL;
sqlParams = sqlParams.concat(condSQLParams);
}
}
// Add on ANY conditions
if (anySQL){
sql += '(' + anySQL;
sqlParams = sqlParams.concat(anySQLParams);
sql = sql.substring(0, sql.length-4); // remove last ' OR '
sql += ')';
}
else {
sql = sql.substring(0, sql.length-5); // remove last ' AND '
}
// Add on quicksearch conditions
if (quicksearchSQLSet) {
sql = "SELECT itemID FROM items WHERE itemID IN (" + sql + ") "
+ "AND ((" + quicksearchSQLSet.join(') AND (') + "))";
for (var k=0; k<quicksearchParamsSet.length; k++) {
sqlParams = sqlParams.concat(quicksearchParamsSet[k]);
}
}
}
this._sql = sql;
this._sqlParams = sqlParams.length ? sqlParams : null;
}
Zotero.Search.prototype._generateKey = function () {
return Zotero.Utilities.generateObjectKey();
}
Zotero.Searches = new function(){
Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']);
this.constructor.prototype = new Zotero.DataObjects();
this.get = get;
this.erase = erase;
/**
* Retrieve a saved search
*
* @param int id savedSearchID
* @return object|bool Zotero.Search object,
* or false if it doesn't exist
*/
function get(id) {
var sql = "SELECT COUNT(*) FROM savedSearches WHERE savedSearchID=?";
if (Zotero.DB.valueQuery(sql, id)) {
var search = new Zotero.Search;
search.id = id;
return search;
}
return false;
}
/**
* Returns an array of Zotero.Search objects, ordered by name
*
* @param {Integer|null} [libraryID=null]
*/
this.getAll = function (libraryID) {
var sql = "SELECT savedSearchID AS id, savedSearchName AS name "
+ "FROM savedSearches WHERE libraryID";
if (libraryID) {
sql += "=?";
var params = [libraryID];
}
else {
sql += " IS NULL";
var params = null;
}
sql += " ORDER BY name COLLATE NOCASE";
var rows = Zotero.DB.query(sql, params);
if (!rows) {
return [];
}
// Do proper collation sort
var collation = Zotero.getLocaleCollation();
rows.sort(function (a, b) {
return collation.compareString(1, a.name, b.name);
});
var searches = [];
for each(var row in rows) {
var search = new Zotero.Search;
search.id = row.id;
searches.push(search);
}
return searches;
}
/*
* Delete a given saved search from the DB
*/
function erase(ids) {
ids = Zotero.flattenArguments(ids);
var notifierData = {};
Zotero.DB.beginTransaction();
for each(var id in ids) {
var search = new Zotero.Search;
search.id = id;
notifierData[id] = { old: search.serialize() };
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
Zotero.DB.query(sql, id);
var sql = "DELETE FROM savedSearches WHERE savedSearchID=?";
Zotero.DB.query(sql, id);
}
Zotero.DB.commitTransaction();
Zotero.Notifier.trigger('delete', 'search', ids, notifierData);
}
}
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)
*/
function _init(){
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: Zotero.DB.columnQuery("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', '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: '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
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];
}
var sortKeys = [];
var sortValues = [];
var baseMappedFields = Zotero.ItemFields.getBaseMappedFields();
// 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;
}
var localized = self.getLocalizedName(i);
sortKeys.push(localized);
sortValues[localized] = {
name: i,
localized: localized,
operators: _conditions[i]['operators'],
flags: _conditions[i]['flags']
};
}
// Alphabetize by localized name
// TODO: locale collation sort
sortKeys = sortKeys.sort();
for each(var i in sortKeys){
_standardConditions.push(sortValues[i]);
}
_initialized = true;
}
/*
* Get condition data
*/
function get(condition){
if (!_initialized){
_init();
}
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(){
if (!_initialized){
_init();
}
// TODO: return copy instead
return _standardConditions;
}
/*
* Check if an operator is valid for a given condition
*/
function hasOperator(condition, operator){
if (!_initialized){
_init();
}
var [condition, mode] = this.parseCondition(condition);
if (!_conditions[condition]){
throw ("Invalid condition '" + condition + "' in hasOperator()");
}
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);
}
}
/*
* 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];
}
this.reload = function () {
_initialized = false;
_conditions = {};
_standardConditions = [];
}
}