define([
"jquery",
"underscore",
"backbone",
"models/SolrResult",
"collections/Filters",
], function ($, _, Backbone, SolrResult, Filters) {
"use strict";
/**
* @class Search
* @classdesc Search filters can be either plain text or a filter object with the following options:
* filterLabel - text that will be displayed in the filter element in the UI
* label - text that will be displayed in the autocomplete list
* value - the value that will be included in the query
* description - a longer text description of the filter value
* Example: {filterLabel: "Creator", label: "Jared Kibele (16)", value: "Kibele", description: "Search for data creators"}
* @classcategory Models
* @extends Backbone.Model
* @constructor
*/
var Search = Backbone.Model.extend(
/** @lends Search.prototype */ {
/**
* @type {object}
* @property {Filters} filters - The collection of filters used to build a query, an instance of Filters
*/
defaults: function () {
return {
all: [],
projectText: [],
creator: [],
taxon: [],
isPrivate: null,
documents: false,
resourceMap: false,
yearMin: 1900, //The user-selected minimum year
yearMax: new Date().getUTCFullYear(), //The user-selected maximum year
pubYear: false,
dataYear: false,
sortOrder: "dateUploaded+desc",
sortByReads: false, // True if we can sort by reads/popularity
east: null,
west: null,
north: null,
south: null,
useGeohash: true,
geohashes: [],
geohashLevel: 9,
geohashGroups: {},
dataSource: [],
username: [],
rightsHolder: [],
submitter: [],
spatial: [],
attribute: [],
sem_annotation: [],
annotation: [],
additionalCriteria: [],
id: [],
seriesId: [],
idOnly: [],
provFields: [],
formatType: [
{
value: "METADATA",
label: "science metadata",
description: null,
},
],
exclude: [
{
field: "obsoletedBy",
value: "*",
},
{
field: "formatId",
value: "*dataone.org/collections*",
},
{
field: "formatId",
value: "*dataone.org/portals*",
},
],
filters: null,
};
},
//A list of all the filter names that are related to the spatial/map filter
spatialFilters: [
"useGeohash",
"geohashes",
"geohashLevel",
"geohashGroups",
"east",
"west",
"north",
"south",
],
initialize: function () {
this.listenTo(this, "change:geohashes", this.groupGeohashes);
},
fieldLabels: {
attribute: "Data attribute",
documents: "Only results with data",
annotation: "Annotation",
dataSource: "Data source",
creator: "Creator",
dataYear: "Data coverage",
pubYear: "Publish year",
id: "Identifier",
seriesId: "seriesId",
taxon: "Taxon",
spatial: "Location",
isPrivate: "Private datasets",
all: "",
projectText: "Project",
},
//Map the filter names to their index field names
fieldNameMap: {
attribute: "attribute",
annotation: "sem_annotation",
dataSource: "datasource",
documents: "documents",
formatType: "formatType",
all: "",
creator: "originText",
spatial: "siteText",
resourceMap: "resourceMap",
pubYear: ["datePublished", "dateUploaded"],
id: ["id", "identifier", "documents", "resourceMap", "seriesId"],
idOnly: ["id", "seriesId"],
rightsHolder: "rightsHolder",
submitter: "submitter",
username: ["rightsHolder", "writePermission", "changePermission"],
taxon: [
"kingdom",
"phylum",
"class",
"order",
"family",
"genus",
"species",
],
isPrivate: "isPublic",
projectText: "projectText",
},
facetNameMap: {
creator: "origin",
attribute: "attribute",
annotation: "sem_annotation",
spatial: "site",
taxon: [
"kingdom",
"phylum",
"class",
"order",
"family",
"genus",
"species",
],
isPublic: "isPublic",
all: "keywords",
projectText: "project",
},
getCurrentFilters: function () {
var changedAttr = this.changedAttributes(_.clone(this.defaults())),
currentFilters = Object.keys(changedAttr),
ignoreAttr = ["sortOrder", "provFields"];
if (!changedAttr) return new Array();
//Check for changed attributes that should be ignored
_.each(
Object.keys(changedAttr),
function (attr) {
//If the value is an empty array, but the default value is an empty array too,
// then it's not a changed filter attribute
if (
Array.isArray(this.get(attr)) &&
this.get(attr).length == 0 &&
Array.isArray(this.defaults()[attr]) &&
this.defaults()[attr].length == 0
) {
currentFilters = _.without(currentFilters, attr);
} else if (ignoreAttr.includes(attr)) {
currentFilters = _.without(currentFilters, attr);
}
},
this,
);
//Don't count the geohashes or directions as a filter if the geohash filter is turned off
if (!this.get("useGeohash")) {
currentFilters = _.difference(currentFilters, this.spatialFilters);
}
return currentFilters;
},
filterCount: function () {
var currentFilters = this.getCurrentFilters();
return currentFilters.length;
},
//Function filterIsAvailable will check if a filter is available in this search index -
//if the filter name if included in the defaults of this model, it is marked as available.
//Comment out or remove defaults that are not in the index or should not be included in queries
filterIsAvailable: function (name) {
//Get the keys for this model as a way to list the filters that are available
var defaults = _.keys(this.defaults());
if (_.indexOf(defaults, name) >= 0) {
return true;
} else {
return false;
}
},
/*
* Removes a specified filter from the search model
*/
removeFromModel: function (category, filterValueToRemove) {
//Remove this filter term from the model
if (category) {
//Get the current filter terms array
var currentFilterValues = this.get(category);
//The year filters have special rules
//If both year types will be reset/default, then also reset the year min and max values
if (category == "pubYear" || category == "dataYear") {
var otherType = category == "pubYear" ? "dataYear" : "pubYear";
if (_.contains(this.getCurrentFilters(), otherType))
var newFilterValues = this.defaults()[category];
else {
this.set(category, this.defaults()[category]);
this.set("yearMin", this.defaults()["yearMin"]);
this.set("yearMax", this.defaults()["yearMax"]);
return;
}
} else if (Array.isArray(currentFilterValues)) {
//Remove this filter term from the array
var newFilterValues = _.without(
currentFilterValues,
filterValueToRemove,
);
_.each(currentFilterValues, function (currentFilterValue, key) {
var valueString =
typeof currentFilterValue == "object"
? currentFilterValue.value
: currentFilterValue;
if (valueString == filterValueToRemove) {
newFilterValues = _.without(
newFilterValues,
currentFilterValue,
);
}
});
} else {
//Get the default value
var newFilterValues = this.defaults()[category];
}
//Set the new value
this.set(category, newFilterValues);
}
},
/**
*
* @param {Filters|Filter[]} filters The collection of filters to add to this model OR an array of Filter models
*/
addFilters: function (filters) {
try {
let currentFilters = this.get("filters");
//If the passed collection is the same as the one set already, return
if (currentFilters == filters) return;
//If the given Filters collec is different than the one set on the model now, combine them
else if (
Filters.isPrototypeOf(currentFilters) &&
Filters.isPrototypeOf(filters)
) {
filters.models.forEach((f) => {
currentFilters.add(f);
});
this.set("filters", currentFilters);
} else if (
Filters.isPrototypeOf(currentFilters) &&
Array.isArray(filters)
) {
filters.forEach((f) => {
currentFilters.add(f);
});
this.set("filters", currentFilters);
} else if (!currentFilters) this.set("filters", new Filters(filters));
} catch (e) {
console.error("Couldn't add Filters to the Search model: ", e);
}
},
/*
* Resets the geoashes and geohashLevel filters to default
*/
resetGeohash: function () {
this.set("geohashes", this.defaults().geohashes);
this.set("geohashLevel", this.defaults().geohashLevel);
this.set("geohashGroups", this.defaults().geohashGroups);
},
groupGeohashes: function () {
//Find out if there are any geohashes that can be combined together, by looking for all 32 geohashes within the same precision level
var sortedGeohashes = this.get("geohashes");
sortedGeohashes.sort();
var groupedGeohashes = _.groupBy(sortedGeohashes, function (n) {
return n.substring(0, n.length - 1);
});
//Find groups of geohashes that makeup a complete geohash tile (32) so we can shorten the query
var completeGroups = _.filter(
Object.keys(groupedGeohashes),
function (n) {
return groupedGeohashes[n].length == 32;
},
);
//Find the remaining incomplete geohash groupss
var incompleteGroups = [];
_.each(
_.filter(Object.keys(groupedGeohashes), function (n) {
return groupedGeohashes[n].length < 32;
}),
function (n) {
incompleteGroups.push(groupedGeohashes[n]);
},
);
incompleteGroups = _.flatten(incompleteGroups);
//Start a geohash group object
var geohashGroups = {};
if (
typeof incompleteGroups !== "undefined" &&
incompleteGroups.length > 0
) {
geohashGroups[incompleteGroups[0].length.toString()] =
incompleteGroups;
}
if (
typeof completeGroups !== "undefined" &&
completeGroups.length > 0
) {
geohashGroups[completeGroups[0].length.toString()] = completeGroups;
}
//Save it
this.set("geohashGroups", geohashGroups);
this.trigger("change", "geohashGroups");
},
hasGeohashFilter: function () {
var currentGeohashFilter = this.get("geohashGroups");
return (
typeof currentGeohashFilter == "object" &&
Object.keys(currentGeohashFilter).length > 0
);
},
/**
* Builds the query string to send to the query engine. Goes over each filter specified in this model and adds to the query string.
* Some filters have special rules on how to format the query, which are built first, then the remaining filters are tacked on to the
* query string as a basic name:value pair. These "other filters" are specified in the otherFilters variable.
* @param {string} filter - A single filter to get a query fragment for
* @param {object} options - Additional options for this function
* @property {boolean} options.forPOST - If true, the query will not be url-encoded, for POST requests
*/
getQuery: function (filter, options) {
//----All other filters with a basic name:value pair pattern----
var otherFilters = [
"attribute",
"formatType",
"rightsHolder",
"submitter",
];
//Start the query string
var query = "",
forPOST = false;
//See if we are looking for a sub-query or a query for all filters
if (typeof filter == "undefined") {
var filter = null;
var getAll = true;
} else {
var getAll = false;
}
//Get the options sent to this function via the options object
if (typeof options == "object" && options) {
forPOST = options.forPOST;
}
var model = this;
//-----Annotation-----
if (
this.filterIsAvailable("annotation") &&
(filter == "annotation" || getAll)
) {
var annotations = this.get("annotation");
_.each(annotations, function (annotationFilter, key, list) {
var filterValue = "";
//Get the filter value
if (typeof annotationFilter == "object") {
filterValue = annotationFilter.value || "";
} else {
filterValue = annotationFilter;
}
// Trim leading and trailing whitespace just in case
filterValue = filterValue.trim();
if (forPOST) {
// Encode and wrap URI in urlencoded double quote chars
filterValue = '"' + filterValue.trim() + '"';
} else {
// Encode and wrap URI in urlencoded double quote chars
filterValue =
"%22" + encodeURIComponent(filterValue.trim()) + "%22";
}
query += model.fieldNameMap["annotation"] + ":" + filterValue;
});
}
//---Identifier---
if (
this.filterIsAvailable("id") &&
(filter == "id" || getAll) &&
this.get("id").length
) {
var identifiers = this.get("id");
if (Array.isArray(identifiers)) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(
this.fieldNameMap["id"],
identifiers,
{
operator: "OR",
subtext: true,
},
);
} else if (identifiers) {
if (query.length) {
query += " AND ";
}
if (forPOST) {
query +=
this.fieldNameMap["id"] +
":*" +
this.escapeSpecialChar(identifiers) +
"*";
} else {
query +=
this.fieldNameMap["id"] +
":*" +
this.escapeSpecialChar(encodeURIComponent(identifiers)) +
"*";
}
}
}
//---resourceMap---
if (
this.filterIsAvailable("resourceMap") &&
(filter == "resourceMap" || getAll)
) {
var resourceMap = this.get("resourceMap");
//If the resource map search setting is a list of resource map IDs
if (Array.isArray(resourceMap)) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(
this.fieldNameMap["resourceMap"],
resourceMap,
{
operator: "OR",
forPOST: forPOST,
},
);
} else if (resourceMap) {
if (query.length) {
query += " AND ";
}
//Otherwise, treat it as a binary setting
query += this.fieldNameMap["resourceMap"] + ":*";
}
}
//---documents---
if (
this.filterIsAvailable("documents") &&
(filter == "documents" || getAll)
) {
var documents = this.get("documents");
//If the documents search setting is a list ofdocuments IDs
if (Array.isArray(documents)) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(
this.fieldNameMap["documents"],
documents,
{
operator: "OR",
forPOST: forPOST,
},
);
} else if (documents) {
if (query.length) {
query += " AND ";
}
//Otherwise, treat it as a binary setting
query += this.fieldNameMap["documents"] + ":*";
}
}
//---Username: search for this username in rightsHolder and submitter ---
if (
this.filterIsAvailable("username") &&
(filter == "username" || getAll) &&
this.get("username").length
) {
var username = this.get("username");
if (username) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(
this.fieldNameMap["username"],
username,
{
operator: "OR",
forPOST: forPOST,
},
);
}
}
//--- ID Only - searches only the id and seriesId fields ---
if (
this.filterIsAvailable("idOnly") &&
(filter == "idOnly" || getAll) &&
this.get("idOnly").length
) {
var idOnly = this.get("idOnly");
if (idOnly) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(this.fieldNameMap["idOnly"], idOnly, {
operator: "OR",
forPOST: forPOST,
});
}
}
//---Taxon---
if (
this.filterIsAvailable("taxon") &&
(filter == "taxon" || getAll) &&
this.get("taxon").length
) {
var taxon = this.get("taxon");
for (var i = 0; i < taxon.length; i++) {
var value =
typeof taxon == "object" ? taxon[i].value : taxon[i].trim();
query += this.getMultiFieldQuery(
this.fieldNameMap["taxon"],
value,
{
subtext: true,
forPOST: forPOST,
},
);
}
}
//------Pub Year-----
if (
this.filterIsAvailable("pubYear") &&
(filter == "pubYear" || getAll)
) {
//Get the types of year to be searched first
var pubYear = this.get("pubYear");
if (pubYear) {
//Get the minimum and maximum years chosen
var yearMin = this.get("yearMin");
var yearMax = this.get("yearMax");
if (query.length) {
query += " AND ";
}
var value =
"[" +
yearMin +
"-01-01T00:00:00Z TO " +
yearMax +
"-12-31T00:00:00Z]";
var opts = {
forPOST: forPOST,
escapeSquareBrackets: false,
};
//Add to the query if we are searching publication year
query += this.getMultiFieldQuery(
this.fieldNameMap["pubYear"],
value,
opts,
);
}
}
//-----Data year------
if (
this.filterIsAvailable("dataYear") &&
(filter == "dataYear" || getAll)
) {
var dataYear = this.get("dataYear");
if (dataYear) {
//Get the minimum and maximum years chosen
var yearMin = this.get("yearMin");
var yearMax = this.get("yearMax");
if (query.length) {
query += " AND ";
}
query +=
"beginDate:[" +
yearMin +
"-01-01T00:00:00Z TO *]" +
" AND endDate:[* TO " +
yearMax +
"-12-31T00:00:00Z]";
}
}
//----- public/private ------
if (
this.filterIsAvailable("isPrivate") &&
(filter == "isPrivate" || getAll)
) {
var isPrivate = this.get("isPrivate");
if (isPrivate !== null && isPrivate !== "undefined") {
if (query.length) {
query += " AND ";
}
// Currently, the Solr field 'isPublic' can be set to true or false, or not set.
// isPrivate is equivalent to "isPublic:false" or isPublic not set
if (isPrivate) {
query += "-isPublic:true";
}
}
}
//-----Data Source--------
if (
this.filterIsAvailable("dataSource") &&
(filter == "dataSource" || getAll)
) {
var filterValue = null;
var filterValues = [];
if (this.get("dataSource").length > 0) {
var objectValues = _.filter(this.get("dataSource"), function (v) {
return typeof v == "object";
});
if (objectValues && objectValues.length) {
filterValues.push(_.pluck(objectValues, "value"));
}
}
var stringValues = _.filter(this.get("dataSource"), function (v) {
return typeof v == "string";
});
if (stringValues && stringValues.length) {
filterValues.push(stringValues);
}
filterValues = _.flatten(filterValues);
if (filterValues.length) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(
this.fieldNameMap["dataSource"],
filterValues,
{
operator: "OR",
forPOST: forPOST,
},
);
}
}
//-----Excluded fields-----
if (
this.filterIsAvailable("exclude") &&
(filter == "exclude" || getAll)
) {
var exclude = this.get("exclude");
_.each(exclude, function (excludeField, key, list) {
if (model.needsQuotes(excludeField.value)) {
if (forPOST) {
var filterValue = '"' + excludeField.value + '"';
} else {
var filterValue =
"%22" + encodeURIComponent(excludeField.value) + "%22";
}
} else {
if (forPOST) {
var filterValue = excludeField.value;
} else {
var filterValue = encodeURIComponent(excludeField.value);
}
}
filterValue = model.escapeSpecialChar(filterValue);
if (query.length) {
query += " AND ";
}
query += " -" + excludeField.field + ":" + filterValue;
});
}
//-----Additional criteria - both field and value are provided-----
if (
this.filterIsAvailable("additionalCriteria") &&
(filter == "additionalCriteria" || getAll)
) {
var additionalCriteria = this.get("additionalCriteria");
for (var i = 0; i < additionalCriteria.length; i++) {
var value;
if (forPOST) {
value = additionalCriteria[i];
} else {
//if(this.needsQuotes(additionalCriteria[i])) value = "%22" + encodeURIComponent(additionalCriteria[i]) + "%22";
value = encodeURIComponent(additionalCriteria[i]);
}
if (query.length) {
query += " AND ";
}
query += model.escapeSpecialChar(value);
}
}
//-----All (full text search) -----
if (this.filterIsAvailable("all") && (filter == "all" || getAll)) {
var all = this.get("all");
for (var i = 0; i < all.length; i++) {
var filterValue = all[i];
if (typeof filterValue == "object") {
filterValue = filterValue.value;
} else if (
(typeof filterValue == "string" && !filterValue.length) ||
typeof filterValue == "undefined" ||
filterValue === null
) {
continue;
}
if (this.needsQuotes(filterValue)) {
if (forPOST) {
filterValue = '"' + filterValue + '"';
} else {
filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
}
} else {
if (forPOST) {
filterValue = filterValue;
} else {
filterValue = encodeURIComponent(filterValue);
}
}
if (query.length) {
query += " AND ";
}
query += model.escapeSpecialChar(filterValue);
}
}
//-----Other Filters/Basic Filters-----
_.each(otherFilters, function (filterName, key, list) {
if (
model.filterIsAvailable(filterName) &&
(filter == filterName || getAll)
) {
var filterValue = null;
var filterValues = model.get(filterName);
for (var i = 0; i < filterValues.length; i++) {
//Trim the spaces off
var filterValue = filterValues[i];
if (typeof filterValue == "object") {
filterValue = filterValue.value;
}
filterValue = filterValue.trim();
// Does this need to be wrapped in quotes?
if (model.needsQuotes(filterValue)) {
if (forPOST) {
filterValue = '"' + filterValue + '"';
} else {
filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
}
} else {
if (forPOST) {
filterValue = filterValue;
} else {
filterValue = encodeURIComponent(filterValue);
}
}
if (query.length) {
query += " AND ";
}
query +=
model.fieldNameMap[filterName] +
":" +
model.escapeSpecialChar(filterValue);
}
}
});
//-----Geohashes-----
if (
this.filterIsAvailable("geohashLevel") &&
(filter == "geohash" || getAll) &&
this.get("useGeohash")
) {
var geohashes = this.get("geohashes");
if (typeof geohashes != undefined && geohashes.length > 0) {
var groups = this.get("geohashGroups"),
numGroups =
typeof groups == "object" ? Object.keys(groups).length : 0;
if (numGroups > 0) {
//Add the AND operator in front of the geohash filter
if (query.length) {
query += " AND ";
}
//If there is more than one geohash group/level, wrap them in paranthesis
if (numGroups > 1) {
query += "(";
}
_.each(Object.keys(groups), function (level, i, allLevels) {
var geohashList = groups[level];
query += "geohash_" + level + ":";
if (geohashList.length > 1) {
query += "(";
}
_.each(geohashList, function (g, ii, allGeohashes) {
//Keep URI's from getting too long if we are using GET
if (
MetacatUI.appModel.get("disableQueryPOSTs") &&
query.length > 1900
) {
//Remove the last " OR "
if (query.endsWith(" OR ")) {
query = query.substring(0, query.length - 4);
}
return;
} else {
//Add the geohash value to the query
query += g;
//Add an " OR " operator inbetween geohashes
if (ii < allGeohashes.length - 1) {
query += " OR ";
}
}
});
//Close the paranthesis
if (geohashList.length > 1) {
query += ")";
}
//Add an " OR " operator inbetween geohash levels
if (i < allLevels.length - 1) {
query += " OR ";
}
});
//Close the paranthesis
if (numGroups > 1) {
query += ")";
}
}
}
}
//---Spatial---
if (
this.filterIsAvailable("spatial") &&
(filter == "spatial" || getAll)
) {
var spatial = this.get("spatial");
if (Array.isArray(spatial) && spatial.length) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(
this.fieldNameMap["spatial"],
spatial,
{
operator: "AND",
subtext: false,
forPOST: forPOST,
},
);
} else if (typeof spatial == "string" && spatial.length) {
if (query.length) {
query += " AND ";
}
if (forPOST) {
query +=
this.fieldNameMap["spatial"] +
":" +
model.escapeSpecialChar(spatial);
} else {
query +=
this.fieldNameMap["spatial"] +
":" +
model.escapeSpecialChar(encodeURIComponent(spatial));
}
}
}
//---Creator---
if (
this.filterIsAvailable("creator") &&
(filter == "creator" || getAll)
) {
var creator = this.get("creator");
if (Array.isArray(creator) && creator.length) {
if (query.length) {
query += " AND ";
}
query += this.getGroupedQuery(
this.fieldNameMap["creator"],
creator,
{
operator: "AND",
subtext: false,
forPOST: forPOST,
},
);
} else if (typeof creator == "string" && creator.length) {
if (query.length) {
query += " AND ";
}
if (forPOST) {
query +=
this.fieldNameMap["creator"] +
":" +
model.escapeSpecialChar(creator);
} else {
query +=
this.fieldNameMap["creator"] +
":" +
model.escapeSpecialChar(encodeURIComponent(creator));
}
}
}
// Add project filter
if (
this.filterIsAvailable("projectText") &&
(filter == "projectText" || getAll)
) {
var project = this.get("projectText");
if (project && project.length > 0) {
if (query.length) {
query += " AND ";
}
query += 'projectText:"' + project[0].value + '"';
}
}
return query;
},
getFacetQuery: function (fields) {
var facetQuery =
"&facet=true" +
"&facet.sort=count" +
"&facet.mincount=1" +
"&facet.limit=-1";
//Get the list of fields
if (!fields) {
var fields =
"keywords,origin,family,species,genus,kingdom,phylum,order,class,site";
if (this.filterIsAvailable("annotation")) {
fields += "," + this.facetNameMap["annotation"];
}
if (this.filterIsAvailable("attribute")) {
fields += ",attributeName,attributeLabel";
}
}
var model = this;
//Add the fields to the query string
_.each(fields.split(","), function (f) {
var fieldNames = model.facetNameMap[f] || f;
if (typeof fieldNames == "string") {
fieldNames = [fieldNames];
}
_.each(fieldNames, function (fName) {
facetQuery += "&facet.field=" + fName;
});
});
return facetQuery;
},
//Check for spaces in a string - we'll use this to url encode the query
needsQuotes: function (entry) {
//Check for spaces
var value = "";
if (typeof entry == "object") {
value = entry.value;
} else if (typeof entry == "string") {
value = entry;
} else {
return false;
}
//Is this a date range search? If so, we don't use quote marks
var ISODateRegEx =
/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)/;
if (ISODateRegEx.exec(value)) {
return false;
}
//Check for a space character
if (value.indexOf(" ") > -1) {
return true;
}
//Check if this is an account subject string
var LDAPSubjectRegEx =
/(uid=|UID=|cn=|CN=).+([a-zA-Z]=).+([a-zA-Z]=).*/,
ORCIDRegEx =
/^http\:\/\/orcid\.org\/[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9X]{4}/;
if (LDAPSubjectRegEx.exec(value) || ORCIDRegEx.exec(value)) {
return true;
}
return false;
},
escapeSpecialChar: function (term, escapeSquareBrackets = true) {
term = term.replace(/%7B/g, "%5C%7B");
term = term.replace(/%7D/g, "%5C%7D");
term = term.replace(/%3A/g, "%5C%3A");
term = term.replace(/:/g, "%5C:");
term = term.replace(/\(/g, "%5C(");
term = term.replace(/\)/g, "%5C)");
term = term.replace(/\?/g, "%5C?");
term = term.replace(/%3F/g, "%5C%3F");
term = term.replace(/%2B/g, "%5C%2B");
//Remove ampersands (&) for now since they are reserved Solr characters and the Metacat Solr can't seem to handle them even when they are escaped properly for some reason
term = term.replace(/%26/g, "");
term = term.replace(/%7C%7C/g, "%5C%7C%5C%7C");
term = term.replace(/%21/g, "%5C%21");
term = term.replace(/%28/g, "%5C%28");
term = term.replace(/%29/g, "%5C%29");
term = term.replace(/%5E/g, "%5C%5E");
term = term.replace(/%22/g, "%5C%22");
term = term.replace(/~/g, "%5C~");
term = term.replace(/-/g, "%5C-");
term = term.replace(/%2F/g, "%5C%2F");
if (escapeSquareBrackets) {
term = term.replace(/%5B/g, "%5C%5B");
term = term.replace(/%5D/g, "%5C%5D");
}
return term;
},
/*
* Makes a Solr syntax grouped query using the field name, the field values to search for, and the operator.
* Example: title:(resistance OR salmon OR "pink salmon")
*/
getGroupedQuery: function (fieldName, values, options) {
if (!values) return "";
values = _.compact(values);
if (!values.length) return "";
var query = "",
numValues = values.length,
model = this;
if (Array.isArray(fieldName) && fieldName.length > 1) {
return this.getMultiFieldQuery(fieldName, values, options);
}
if (options && typeof options == "object") {
var operator = options.operator,
subtext = options.subtext,
forPOST = options.forPOST;
}
if (
typeof operator === "undefined" ||
!operator ||
(operator != "OR" && operator != "AND")
) {
var operator = "OR";
}
if (numValues == 1) {
var value = values[0],
queryAddition;
if (
!Array.isArray(value) &&
typeof value === "object" &&
value.value
) {
value = value.value.trim();
}
if (this.needsQuotes(values[0])) {
if (forPOST) {
queryAddition = '"' + this.escapeSpecialChar(value) + '"';
} else {
queryAddition =
"%22" +
this.escapeSpecialChar(encodeURIComponent(value)) +
"%22";
}
} else if (subtext) {
if (forPOST) {
queryAddition = "*" + this.escapeSpecialChar(value) + "*";
} else {
queryAddition =
"*" + this.escapeSpecialChar(encodeURIComponent(value)) + "*";
}
} else {
if (forPOST) {
queryAddition = this.escapeSpecialChar(value);
} else {
queryAddition = this.escapeSpecialChar(encodeURIComponent(value));
}
}
query = fieldName + ":" + queryAddition;
} else {
_.each(
values,
function (value, i) {
//Check for filter objects
if (
!Array.isArray(value) &&
typeof value === "object" &&
value.value
) {
value = value.value.trim();
}
if (model.needsQuotes(value)) {
if (forPOST) {
value = '"' + value + '"';
} else {
value = "%22" + encodeURIComponent(value) + "%22";
}
} else if (subtext) {
if (forPOST) {
value = "*" + this.escapeSpecialChar(value) + "*";
} else {
value =
"*" +
this.escapeSpecialChar(encodeURIComponent(value)) +
"*";
}
} else {
if (forPOST) {
value = this.escapeSpecialChar(value);
} else {
value = this.escapeSpecialChar(encodeURIComponent(value));
}
}
if (i == 0 && numValues > 1) {
query += fieldName + ":(" + value;
} else if (i > 0 && i < numValues - 1 && query.length) {
query += " " + operator + " " + value;
} else if (i > 0 && i < numValues - 1) {
query += value;
} else if (i == numValues - 1) {
query += " " + operator + " " + value + ")";
}
},
this,
);
}
return query;
},
/*
* Makes a Solr syntax query using multiple field names, one field value to search for, and some options.
* Example: (family:*Pin* OR class:*Pin* OR order:*Pin* OR phlyum:*Pin*)
* Options:
* - operator (OR or AND)
* - subtext (binary) - will surround search value with wildcards to search for partial matches
* - Example:
* var options = { operator: "OR", subtext: true }
*/
getMultiFieldQuery: function (fieldNames, value, options) {
var query = "",
numFields = fieldNames.length,
model = this;
//Catch errors
if (numFields < 1 || !value) return "";
//If only one field was given, then use the grouped query function
if (numFields == 1) {
return this.getGroupedQuery(fieldNames, value, options);
}
//Get the options
if (options && typeof options == "object") {
var operator = options.operator,
subtext = options.subtext,
forPOST = options.forPOST;
}
var esb =
typeof options.escapeSquareBrackets == "boolean"
? options.escapeSquareBrackets
: true;
//Default to the OR operator
if (
typeof operator === "undefined" ||
!operator ||
(operator != "OR" && operator != "AND")
) {
var operator = "OR";
}
if (typeof subtext === "undefined") {
var subtext = false;
}
//Create the value string
//Trim the spaces off
if (!Array.isArray(value) && typeof value === "object" && value.value) {
value = [value.value.trim()];
} else if (typeof value == "string") {
value = [value.trim()];
}
var valueString = "";
if (Array.isArray(value)) {
var model = this;
_.each(
value,
function (v, i) {
if (typeof v == "object" && v.value) {
v = v.value;
}
if (value.length > 1 && i == 0) {
valueString += "(";
}
if (model.needsQuotes(v) || _.contains(fieldNames, "id")) {
if (forPOST) {
valueString +=
'"' + this.escapeSpecialChar(v.trim(), esb) + '"';
} else {
valueString +=
'"' +
this.escapeSpecialChar(encodeURIComponent(v.trim()), esb) +
'"';
}
} else if (subtext) {
if (forPOST) {
valueString +=
"*" + this.escapeSpecialChar(v.trim(), esb) + "*";
} else {
valueString +=
"*" +
this.escapeSpecialChar(encodeURIComponent(v.trim()), esb) +
"*";
}
} else {
if (forPOST) {
valueString += this.escapeSpecialChar(v.trim(), esb);
} else {
valueString += this.escapeSpecialChar(
encodeURIComponent(v.trim()),
esb,
);
}
}
if (i < value.length - 1) {
valueString += " OR ";
} else if (i == value.length - 1 && value.length > 1) {
valueString += ")";
}
},
this,
);
} else valueString = value;
query = "(";
//Create the query string
var last = numFields - 1;
_.each(fieldNames, function (field, i) {
query += field + ":" + valueString;
if (i < last) {
query += " " + operator + " ";
}
});
query += ")";
return query;
},
/**** Provenance-related functions ****/
// Returns which fields are provenance-related in this model
// Useful for querying the index and such
getProvFields: function () {
var provFields = this.get("provFields");
if (!provFields.length) {
var defaultFields = Object.keys(SolrResult.prototype.defaults);
provFields = _.filter(defaultFields, function (fieldName) {
if (fieldName.indexOf("prov_") == 0) return true;
});
this.set("provFields", provFields);
}
return provFields;
},
getProvFlList: function () {
var provFields = this.getProvFields(),
provFl = "";
_.each(provFields, function (provField, i) {
provFl += provField;
if (i < provFields.length - 1) provFl += ",";
});
return provFl;
},
clear: function () {
return this.set(_.clone(this.defaults()));
},
},
);
return Search;
});