zotero/chrome/chromeFiles/content/scholar/xpcom/search.js

574 lines
12 KiB
JavaScript

Scholar.Search = function(){
this._sql = null;
this._sqlParams = null;
this._maxSearchConditionID = 0;
this._conditions = [];
this._savedSearchID = null;
this._savedSearchName = null;
}
/*
* Set the name for the saved search
*
* Must be called before save() for new searches
*/
Scholar.Search.prototype.setName = function(name){
if (!name){
throw("Invalid saved search name '" + name + '"');
}
this._savedSearchName = name;
}
/*
* Load a saved search from the DB
*/
Scholar.Search.prototype.load = function(savedSearchID){
var sql = "SELECT savedSearchName, MAX(searchConditionID) AS maxID "
+ "FROM savedSearches NATURAL JOIN savedSearchConditions "
+ "WHERE savedSearchID=" + savedSearchID + " GROUP BY savedSearchID";
var row = Scholar.DB.rowQuery(sql);
if (!row){
throw('Saved search ' + savedSearchID + ' does not exist');
}
this._sql = null;
this._sqlParams = null;
this._maxSearchConditionID = row['maxID'];
this._conditions = [];
this._savedSearchID = savedSearchID;
this._savedSearchName = row['savedSearchName'];
var conditions = Scholar.DB.query("SELECT * FROM savedSearchConditions "
+ "WHERE savedSearchID=" + savedSearchID + " ORDER BY searchConditionID");
for (var i in conditions){
this._conditions[conditions[i]['searchConditionID']] = {
condition: conditions[i]['condition'],
operator: conditions[i]['operator'],
value: conditions[i]['value']
};
}
}
/*
* Save the search to the DB and return a savedSearchID
*
* For new searches, setName() must be called before saving
*/
Scholar.Search.prototype.save = function(){
if (!this._savedSearchName){
throw('Name not provided for saved search');
}
Scholar.DB.beginTransaction();
if (this._savedSearchID){
var sql = "UPDATE savedSearches SET savedSearchName=? WHERE savedSearchID=?";
Scholar.DB.query(sql, [this._savedSearchName, this._savedSearchID]);
Scholar.DB.query("DELETE FROM savedSearchConditions "
+ "WHERE savedSearchID=" + this._savedSearchID);
}
else {
this._savedSearchID
= Scholar.getRandomID('savedSearches', 'savedSearchID');
var sql = "INSERT INTO savedSearches (savedSearchID, savedSearchName) "
+ "VALUES (?,?)";
Scholar.DB.query(sql,
[this._savedSearchID, {string: this._savedSearchName}]);
}
// 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) VALUES (?,?,?,?,?)";
var sqlParams = [this._savedSearchID, i, this._conditions[i]['condition'],
this._conditions[i]['operator']
? this._conditions[i]['operator'] : null,
this._conditions[i]['value']
? this._conditions[i]['value'] : null];
Scholar.DB.query(sql, sqlParams);
}
Scholar.DB.commitTransaction();
return this._savedSearchID;
}
Scholar.Search.prototype.addCondition = function(condition, operator, value){
if (!Scholar.SearchConditions.hasOperator(condition, operator)){
throw ("Invalid operator '" + operator + "' for condition " + condition);
}
var searchConditionID = this._maxSearchConditionID++;
this._conditions[searchConditionID] = {
condition: condition,
operator: operator,
value: value
};
this._sql = null;
this._sqlParams = null;
return searchConditionID;
}
Scholar.Search.prototype.updateCondition = function(searchConditionID, condition, operator, value){
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in updateCondition()');
}
if (!Scholar.SearchConditions.hasOperator(condition, operator)){
throw ("Invalid operator '" + operator + "' for condition " + condition);
}
this._conditions[searchConditionID] = {
condition: condition,
operator: operator,
value: value
};
this._sql = null;
this._sqlParams = null;
}
Scholar.Search.prototype.removeCondition = function(searchConditionID){
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()');
}
delete this._conditions[searchConditionID];
var i = searchConditionID + 1;
while (typeof this._conditions[i] != 'undefined'){
this._conditions[i-1] = this._conditions[i];
delete this._conditions[i];
i++;
}
this._maxSearchConditionID--;
}
/*
* Returns an array with 'condition', 'operator', and 'value'
* for the given searchConditionID
*/
Scholar.Search.prototype.getSearchCondition = function(searchConditionID){
return this._conditions[searchConditionID];
}
/*
* Returns a multidimensional array of conditions/operator/value sets
* used in the search, indexed by searchConditionID
*/
Scholar.Search.prototype.getSearchConditions = function(){
// TODO: make copy
return this._conditions;
}
/*
* Run the search and return an array of item ids for results
*/
Scholar.Search.prototype.search = function(){
if (!this._sql){
this._buildQuery();
}
return Scholar.DB.columnQuery(this._sql, this._sqlParams);
}
/*
* Get the SQL string for the search
*/
Scholar.Search.prototype.getSQL = function(){
if (!this._sql){
this._buildQuery();
}
return this._sql;
}
/*
* Build the SQL query for the search
*/
Scholar.Search.prototype._buildQuery = function(){
var sql = 'SELECT itemID FROM items';
var sqlParams = [];
var tables = [];
for (var i in this._conditions){
var data = Scholar.SearchConditions.get(this._conditions[i]['condition']);
// Group standard conditions by table
if (data['table']){
if (!tables[data['table']]){
tables[data['table']] = [];
}
tables[data['table']].push({
name: data['name'],
alias: data['name']!=this._conditions[i]['condition']
? this._conditions[i]['condition'] : false,
field: data['field'],
operator: this._conditions[i]['operator'],
value: this._conditions[i]['value']
});
var hasConditions = true;
}
// Handle special conditions
else {
switch (data['name']){
// Collection to search in
case 'context':
var parentCollectionID = this._conditions[i]['value'];
continue;
// Search subfolders
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;
}
throw ('Unhandled special condition ' + this._conditions[i]['condition']);
}
}
if (hasConditions){
sql += " WHERE ";
// Join conditions using appropriate operator
if (joinMode=='ALL'){
var binOp = ' AND ';
}
else {
var binOp = ' OR ';
}
for (i in tables){
for (var j in tables[i]){
// Special table handling
switch (i){
case 'items':
break;
default:
sql += 'itemID IN (SELECT itemID FROM ' + i + ' WHERE (';
var openParens = 2;
}
// For itemData fields, include fieldID
if (tables[i][j]['name']=='field'){
sql += 'fieldID=? AND ';
sqlParams.push(Scholar.ItemFields.getID(tables[i][j]['alias']));
}
sql += tables[i][j]['field'];
switch (tables[i][j]['operator']){
case 'contains':
sql += ' LIKE ?';
sqlParams.push('%' + tables[i][j]['value'] + '%');
break;
case 'doesNotContain':
sql += ' NOT LIKE ?';
sqlParams.push('%' + tables[i][j]['value'] + '%');
break;
case 'is':
sql += '=?';
sqlParams.push(tables[i][j]['value']);
break;
case 'isNot':
sql += '!=?';
sqlParams.push(tables[i][j]['value']);
break;
case 'greaterThan':
sql += '>?';
sqlParams.push({int:tables[i][j]['value']});
break;
case 'lessThan':
sql += '<?';
sqlParams.push({int:tables[i][j]['value']});
break;
case 'isBefore':
// TODO
break;
case 'isAfter':
// TODO
break;
}
// Close open parentheses
for (k=openParens; k>0; k--){
sql += ')';
}
sql += binOp;
}
}
sql = sql.substring(0, sql.length-binOp.length);
}
if (parentCollectionID){
sql = "SELECT itemID FROM (" + sql + ") WHERE itemID IN "
+ "(SELECT itemID FROM collectionItems WHERE collectionID IN ("
sql += "?,";
sqlParams.push({int:parentCollectionID});
if (recursive){
var col = Scholar.Collections.get(parentCollectionID);
var descendents = col.getDescendents(false, 'collection');
if (descendents){
for (var i in descendents){
sql += '?,';
sqlParams.push(descendents[i]['id']);
}
}
}
// Strip final comma
sql = sql.substring(0, sql.length-1) + "))";
}
this._sql = sql;
this._sqlParams = sqlParams.length ? sqlParams : null;
}
Scholar.Searches = new function(){
this.getAll = getAll;
this.erase = erase;
/*
* Returns an array of saved searches with 'id' and 'name', ordered by name
*/
function getAll(){
var sql = "SELECT savedSearchID AS id, savedSearchName AS name "
+ "FROM savedSearches ORDER BY name";
return Scholar.DB.query(sql);
}
/*
* Delete a given saved search from the DB
*/
function erase(savedSearchID){
Scholar.DB.beginTransaction();
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID="
+ savedSearchID;
Scholar.DB.query(sql);
var sql = "DELETE FROM savedSearches WHERE savedSearchID="
+ savedSearchID;
Scholar.DB.query(sql);
Scholar.DB.commitTransaction();
}
}
Scholar.SearchConditions = new function(){
this.get = get;
this.getStandardConditions = getStandardConditions;
this.hasOperator = hasOperator;
var _initialized = false;
var _conditions = [];
var _standardConditions = [];
/*
* Define the advanced search operators
*/
var _operators = {
// Standard
is: true,
isNot: true,
contains: true,
doesNotContain: true,
lessThan: true,
greaterThan: true,
isBefore: true,
isAfter: true,
// Special
any: true,
all: true,
true: true,
false: true
};
/*
* Define and set up the available advanced search conditions
*/
function _init(){
_conditions = [
//
// Special conditions
//
// Context (i.e. collection id to search within)
{
name: 'context'
},
// Search recursively
{
name: 'recursive',
operators: {
true: true,
false: true
}
},
// Join mode
{
name: 'joinMode',
operators: {
any: true,
all: true
}
},
//
// Standard conditions
//
{
name: 'title',
operators: {
contains: true,
doesNotContain: true
},
table: 'items',
field: 'title'
},
{
name: 'itemType',
operators: {
is: true,
isNot: true
},
table: 'items',
field: 'itemTypeID'
},
{
name: 'field',
operators: {
is: true,
isNot: true,
contains: true,
doesNotContain: true
},
table: 'itemData',
field: 'value',
aliases: Scholar.DB.columnQuery("SELECT fieldName FROM fields"),
template: true // mark for special handling
}
];
// 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']){
_conditions[_conditions[i]['aliases'][j]] = _conditions[i];
}
}
_conditions[_conditions[i]['name']] = _conditions[i];
delete _conditions[i];
}
// Separate standard conditions for menu display
for (var i in _conditions){
// Standard conditions a have associated tables
if (_conditions[i]['table'] &&
// If a template condition, not the original (e.g. 'field')
(!_conditions[i]['template'] || i!=_conditions[i]['name'])){
_standardConditions.push({
name: i,
operators: _conditions[i]['operators']
});
}
}
_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();
}
if (!_conditions[condition]){
throw ("Invalid condition '" + condition + "' in hasOperator()");
}
if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){
return true;
}
return !!_conditions[condition]['operators'][operator];
}
}