zotero/chrome/chromeFiles/content/scholar/xpcom/search.js
Dan Stillman 46368e98dd Closes #176, Customize search operators for itemData fields
Implemented isBefore and isAfter operators and added to date conditions -- currently have to type dates in SQL format, but we'll have a chooser or something by Beta 2 (this should probably be a known issue)

Added isLessThan and isGreaterThan to 'pages', 'section', 'accessionNumber', 'seriesNumber', and 'issue' fields, though somebody should probably check me on that list

Removed JS strict warning on empty search results
2006-08-28 20:49:24 +00:00

821 lines
18 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 LEFT JOIN savedSearchConditions "
+ "USING (savedSearchID) 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){
if (!Scholar.SearchConditions.get(conditions[i]['condition'])){
Scholar.debug("Invalid saved search condition '"
+ conditions[i]['condition'] + "' -- skipping", 2);
continue;
}
this._conditions[conditions[i]['searchConditionID']] = {
id: conditions[i]['searchConditionID'],
condition: conditions[i]['condition'],
operator: conditions[i]['operator'],
value: conditions[i]['value'],
required: conditions[i]['required']
};
}
}
/*
* 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, required) "
+ "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,
this._conditions[i]['required']
? 1 : null
];
Scholar.DB.query(sql, sqlParams);
}
Scholar.DB.commitTransaction();
Scholar.Notifier.trigger('modify', 'search', this._savedSearchID);
return this._savedSearchID;
}
Scholar.Search.prototype.addCondition = function(condition, operator, value, required){
if (!Scholar.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=='fulltext'){
this.addCondition('joinMode', 'any');
this.addCondition('title', operator, value, false);
this.addCondition('field', operator, value, false);
this.addCondition('creator', operator, value, false);
this.addCondition('tag', operator, value, false);
this.addCondition('note', operator, value, false);
return false;
}
var searchConditionID = ++this._maxSearchConditionID;
this._conditions[searchConditionID] = {
id: searchConditionID,
condition: condition,
operator: operator,
value: value,
required: required
};
this._sql = null;
this._sqlParams = null;
return searchConditionID;
}
Scholar.Search.prototype.updateCondition = function(searchConditionID, condition, operator, value, required){
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] = {
id: searchConditionID,
condition: condition,
operator: operator,
value: value,
required: required
};
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];
}
/*
* Returns an array with 'condition', 'operator', 'value', 'required'
* 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;
}
Scholar.Search.prototype.getSQLParams = function(){
if (!this._sql){
this._buildQuery();
}
return this._sqlParams;
}
/*
* Build the SQL query for the search
*/
Scholar.Search.prototype._buildQuery = function(){
var sql = 'SELECT itemID FROM items';
var sqlParams = [];
// Separate ANY conditions for 'required' condition support
var anySQL = '';
var anySQLParams = [];
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'],
required: this._conditions[i]['required']
});
var hasConditions = true;
}
// Handle special conditions
else {
switch (data['name']){
// 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 ";
for (var i in tables){
for (var j in tables[i]){
var openParens = 0;
var skipOperators = false;
var condSQL = '';
var condSQLParams = [];
//
// Special table handling
//
switch (i){
case 'savedSearches':
break;
default:
condSQL += 'itemID '
switch (tables[i][j]['operator']){
case 'isNot':
case 'doesNotContain':
condSQL += 'NOT ';
break;
}
condSQL += 'IN (SELECT itemID FROM ' + i + ' WHERE (';
openParens = 2;
}
//
// Special condition handling
//
switch (tables[i][j]['name']){
case 'field':
case 'datefield':
if (!tables[i][j]['alias']){
break;
}
condSQL += 'fieldID=? AND ';
condSQLParams.push(
Scholar.ItemFields.getID(tables[i][j]['alias'])
);
break;
case 'collectionID':
condSQL += "collectionID IN (?,";
condSQLParams.push({int:tables[i][j]['value']});
// And descendents if recursive search
if (recursive){
var col = Scholar.Collections.get(tables[i][j]['value']);
var descendents = col.getDescendents(false, 'collection');
if (descendents){
for (var k in descendents){
condSQL += '?,';
condSQLParams.push(descendents[k]['id']);
}
}
}
// Strip final comma
condSQL = condSQL.substring(0, condSQL.length-1) + ")";
skipOperators = true;
break;
case 'savedSearchID':
condSQL += "itemID ";
if (tables[i][j]['operator']=='isNot'){
condSQL += "NOT ";
}
condSQL += "IN (";
var search = new Scholar.Search();
search.load(tables[i][j]['value']);
condSQL += search.getSQL();
var subpar = search.getSQLParams();
for (var k in subpar){
condSQLParams.push(subpar[k]);
}
condSQL += ")";
skipOperators = true;
break;
case 'tag':
condSQL += "tagID IN (SELECT tagID FROM tags WHERE ";
openParens++;
break;
case 'creator':
condSQL += "creatorID IN (SELECT creatorID FROM creators "
+ "WHERE ";
openParens++;
break;
}
if (!skipOperators){
condSQL += tables[i][j]['field'];
switch (tables[i][j]['operator']){
case 'contains':
case 'doesNotContain': // excluded with NOT IN above
condSQL += ' LIKE ?';
condSQLParams.push('%' + tables[i][j]['value'] + '%');
break;
case 'is':
case 'isNot': // excluded with NOT IN above
condSQL += '=?';
condSQLParams.push(tables[i][j]['value']);
break;
case 'isLessThan':
condSQL += '<?';
condSQLParams.push({int:tables[i][j]['value']});
break;
case 'isGreaterThan':
condSQL += '>?';
condSQLParams.push({int:tables[i][j]['value']});
break;
case 'isBefore':
condSQL += '<?';
condSQLParams.push({string:tables[i][j]['value']});
break;
case 'isAfter':
condSQL += '>?';
condSQLParams.push({string:tables[i][j]['value']});
break;
}
}
// Close open parentheses
for (var k=openParens; k>0; k--){
condSQL += ')';
}
// Keep non-required conditions separate if in ANY mode
if (!tables[i][j]['required'] && joinMode=='ANY'){
condSQL += ' OR ';
anySQL += condSQL;
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 '
}
}
this._sql = sql;
this._sqlParams = sqlParams.length ? sqlParams : null;
}
Scholar.Searches = new function(){
this.get = get;
this.getAll = getAll;
this.erase = erase;
function get(id){
var sql = "SELECT savedSearchID AS id, savedSearchName AS name "
+ "FROM savedSearches WHERE savedSearchID=?";
return Scholar.DB.rowQuery(sql, [id]);
}
/*
* 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.Notifier.trigger('remove', 'search', savedSearchID);
}
}
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 -- these need to match those in scholarsearch.xml
is: true,
isNot: true,
contains: true,
doesNotContain: true,
isLessThan: true,
isGreaterThan: 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
//
// Search recursively
{
name: 'recursive',
operators: {
true: true,
false: true
}
},
// Join mode
{
name: 'joinMode',
operators: {
any: true,
all: true
}
},
// Saved search to search within
{
name: 'savedSearchID',
operators: {
is: true,
isNot: true
},
table: 'savedSearches',
field: 'savedSearchID',
special: true
},
{
name: 'fulltext',
operators: {
is: true,
isNot: true,
contains: true,
doesNotContain: true
}
},
//
// Standard conditions
//
// Collection id to search within
{
name: 'collectionID',
operators: {
is: true,
isNot: true
},
table: 'collectionItems',
field: 'collectionID'
},
{
name: 'title',
operators: {
contains: true,
doesNotContain: true
},
table: 'items',
field: 'title'
},
{
name: 'dateAdded',
operators: {
is: true,
isNot: true,
isBefore: true,
isAfter: true
},
table: 'items',
field: 'DATE(dateAdded)'
},
{
name: 'dateModified',
operators: {
is: true,
isNot: true,
isBefore: true,
isAfter: true
},
table: 'items',
field: 'DATE(dateModified)'
},
{
name: 'itemTypeID',
operators: {
is: true,
isNot: true
},
table: 'items',
field: 'itemTypeID'
},
{
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: 'tag'
},
{
name: 'note',
operators: {
contains: true,
doesNotContain: true
},
table: 'itemNotes',
field: 'note'
},
{
name: 'creator',
operators: {
is: true,
isNot: true,
contains: true,
doesNotContain: true
},
table: 'itemCreators',
field: "firstName || ' ' || lastName"
},
{
name: 'field',
operators: {
is: true,
isNot: true,
contains: true,
doesNotContain: true
},
table: 'itemData',
field: 'value',
aliases: Scholar.DB.columnQuery("SELECT fieldName FROM fields " +
"WHERE fieldName NOT IN ('accessDate', 'date', 'pages', " +
"'section','accessionNumber','seriesNumber','issue')"),
template: true // mark for special handling
},
{
name: 'datefield',
operators: {
is: true,
isNot: true,
isBefore: true,
isAfter: true
},
table: 'itemData',
field: 'DATE(value)',
aliases: ['accessDate', 'date'],
template: true // mark for special handling
},
{
name: 'numberfield',
operators: {
is: true,
isNot: true,
contains: true,
doesNotContain: true,
isLessThan: true,
isGreaterThan: true
},
table: 'itemData',
field: 'value',
aliases: ['pages', 'section', 'accessionNumber',
'seriesNumber','issue'],
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];
}
var sortKeys = [];
var sortValues = [];
// Separate standard conditions for menu display
for (var i in _conditions){
// Standard conditions a have associated tables
if (_conditions[i]['table'] && !_conditions[i]['special'] &&
// If a template condition, not the original (e.g. 'field')
(!_conditions[i]['template'] || i!=_conditions[i]['name'])){
try {
var localized = Scholar.getString('searchConditions.' + i)
}
catch (e){
var localized = Scholar.getString('itemFields.' + i);
}
sortKeys.push(localized);
sortValues[localized] = {
name: i,
localized: localized,
operators: _conditions[i]['operators']
};
}
}
// Alphabetize by localized name
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();
}
Scholar.debug(_standardConditions);
// 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];
}
}