1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2008-2016 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 
 20  /**
 21   * @class LABKEY.Filter
 22   * @namespace  Filter static class to describe and create filters.
 23   *            <p>Additional Documentation:
 24   *              <ul>
 25   *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=filteringData">Filter via the LabKey UI</a></li>
 26   *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=tutorialActionURL">Tutorial: Basics: Building URLs and Filters</a></li>
 27   *              </ul>
 28   *           </p>
 29  * @property {Object} Types Types static class to describe different types of filters.
 30  * @property {LABKEY.Filter.FilterDefinition} Types.EQUAL Finds rows where the column value matches the given filter value. Case-sensitivity depends upon how your underlying relational database was configured.
 31  * @property {LABKEY.Filter.FilterDefinition} Types.DATE_EQUAL Finds rows where the date portion of a datetime column matches the filter value (ignoring the time portion).
 32  * @property {LABKEY.Filter.FilterDefinition} Types.DATE_NOT_EQUAL Finds rows where the date portion of a datetime column does not match the filter value (ignoring the time portion).
 33  * @property {LABKEY.Filter.FilterDefinition} Types.NOT_EQUAL_OR_MISSING Finds rows where the column value does not equal the filter value, or is missing (null).
 34  * @property {LABKEY.Filter.FilterDefinition} Types.NOT_EQUAL Finds rows where the column value does not equal the filter value.
 35  * @property {LABKEY.Filter.FilterDefinition} Types.MISSING Finds rows where the column value is missing (null). Note that no filter value is required with this operator.
 36  * @property {LABKEY.Filter.FilterDefinition} Types.NOT_MISSING Finds rows where the column value is not missing (is not null). Note that no filter value is required with this operator.
 37  * @property {LABKEY.Filter.FilterDefinition} Types.GREATER_THAN Finds rows where the column value is greater than the filter value.
 38  * @property {LABKEY.Filter.FilterDefinition} Types.LESS_THAN Finds rows where the column value is less than the filter value.
 39  * @property {LABKEY.Filter.FilterDefinition} Types.GREATER_THAN_OR_EQUAL Finds rows where the column value is greater than or equal to the filter value.
 40  * @property {LABKEY.Filter.FilterDefinition} Types.LESS_THAN_OR_EQUAL Finds rows where the column value is less than or equal to the filter value.
 41  * @property {LABKEY.Filter.FilterDefinition} Types.CONTAINS Finds rows where the column value contains the filter value. Note that this may result in a slow query as this cannot use indexes.
 42  * @property {LABKEY.Filter.FilterDefinition} Types.DOES_NOT_CONTAIN Finds rows where the column value does not contain the filter value. Note that this may result in a slow query as this cannot use indexes.
 43  * @property {LABKEY.Filter.FilterDefinition} Types.DOES_NOT_START_WITH Finds rows where the column value does not start with the filter value.
 44  * @property {LABKEY.Filter.FilterDefinition} Types.STARTS_WITH Finds rows where the column value starts with the filter value.
 45  * @property {LABKEY.Filter.FilterDefinition} Types.IN Finds rows where the column value equals one of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c).
 46  * @property {LABKEY.Filter.FilterDefinition} Types.NOT_IN Finds rows where the column value is not in any of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c).
 47  * @property {LABKEY.Filter.FilterDefinition} Types.MEMBER_OF Finds rows where the column value contains a user id that is a member of the group id of the supplied filter value.
 48  * @property {LABKEY.Filter.FilterDefinition} Types.CONTAINS_ONE_OF Finds rows where the column value contains any of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c).
 49  * @property {LABKEY.Filter.FilterDefinition} Types.CONTAINS_NONE_OF Finds rows where the column value does not contain any of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c).
 50  * @property {LABKEY.Filter.FilterDefinition} Types.BETWEEN Finds rows where the column value is between the two filter values, inclusive. The values should be supplied as a comma-delimited list (example usage: -4,4).
 51  * @property {LABKEY.Filter.FilterDefinition} Types.NOT_BETWEEN Finds rows where the column value is not between the two filter values, exclusive. The values should be supplied as a comma-delimited list (example usage: -4,4).
 52  *
 53  */
 54 LABKEY.Filter = new function()
 55 {
 56     function validateMultiple(type, value, colName, sep, minOccurs, maxOccurs)
 57     {
 58         var values = value.split(sep);
 59         var result = '';
 60         var separator = '';
 61         for (var i = 0; i < values.length; i++)
 62         {
 63             var value = validate(type, values[i].trim(), colName);
 64             if (value == undefined)
 65                 return undefined;
 66 
 67             result = result + separator + value;
 68             separator = sep;
 69         }
 70 
 71         if (minOccurs !== undefined && minOccurs > 0)
 72         {
 73             if (values.length < minOccurs)
 74             {
 75                 alert("At least " + minOccurs + " '" + sep + "' separated values are required");
 76                 return undefined;
 77             }
 78         }
 79 
 80         if (maxOccurs !== undefined && maxOccurs > 0)
 81         {
 82             if (values.length > maxOccurs)
 83             {
 84                 alert("At most " + maxOccurs + " '" + sep + "' separated values are allowed");
 85                 return undefined;
 86             }
 87         }
 88 
 89         return result;
 90     }
 91 
 92     /**
 93      * Note: this is an experimental API that may change unexpectedly in future releases.
 94      * Validate a form value against the json type.  Error alerts will be displayed.
 95      * @param type The json type ("int", "float", "date", or "boolean")
 96      * @param value The value to test.
 97      * @param colName The column name to use in error messages.
 98      * @return undefined if not valid otherwise a normalized string value for the type.
 99      */
100     function validate(type, value, colName)
101     {
102         if (type == "int")
103         {
104             var intVal = parseInt(value);
105             if (isNaN(intVal))
106             {
107                 alert(value + " is not a valid integer for field '" + colName + "'.");
108                 return undefined;
109             }
110             else
111                 return "" + intVal;
112         }
113         else if (type == "float")
114         {
115             var decVal = parseFloat(value);
116             if (isNaN(decVal))
117             {
118                 alert(value + " is not a valid decimal number for field '" + colName + "'.");
119                 return undefined;
120             }
121             else
122                 return "" + decVal;
123         }
124         else if (type == "date")
125         {
126             var year, month, day, hour, minute;
127             hour = 0;
128             minute = 0;
129 
130             //Javascript does not parse ISO dates, but if date matches we're done
131             if (value.match(/^\s*(\d\d\d\d)-(\d\d)-(\d\d)\s*$/) ||
132                     value.match(/^\s*(\d\d\d\d)-(\d\d)-(\d\d)\s*(\d\d):(\d\d)\s*$/))
133             {
134                 return value;
135             }
136             else
137             {
138                 var dateVal = new Date(value);
139                 if (isNaN(dateVal))
140                 {
141                     //filters can use relative dates, in the format +1d, -5H, etc.  we try to identfy those here
142                     //this is fairly permissive and does not attempt to parse this value into a date.  See CompareType.asDate()
143                     //for server-side parsing
144                     if (value.match(/^(-|\+)/i))
145                     {
146                         return value;
147                     }
148 
149                     alert(value + " is not a valid date for field '" + colName + "'.");
150                     return undefined;
151                 }
152                 //Try to do something decent with 2 digit years!
153                 //if we have mm/dd/yy (but not mm/dd/yyyy) in the date
154                 //fix the broken date parsing
155                 if (value.match(/\d+\/\d+\/\d{2}(\D|$)/))
156                 {
157                     if (dateVal.getFullYear() < new Date().getFullYear() - 80)
158                         dateVal.setFullYear(dateVal.getFullYear() + 100);
159                 }
160                 year = dateVal.getFullYear();
161                 month = dateVal.getMonth() + 1;
162                 day = dateVal.getDate();
163                 hour = dateVal.getHours();
164                 minute = dateVal.getMinutes();
165             }
166             var str = "" + year + "-" + twoDigit(month) + "-" + twoDigit(day);
167             if (hour != 0 || minute != 0)
168                 str += " " + twoDigit(hour) + ":" + twoDigit(minute);
169 
170             return str;
171         }
172         else if (type == "boolean")
173         {
174             var upperVal = value.toUpperCase();
175             if (upperVal == "TRUE" || value == "1" || upperVal == "YES" || upperVal == "Y" || upperVal == "ON" || upperVal == "T")
176                 return "1";
177             if (upperVal == "FALSE" || value == "0" || upperVal == "NO" || upperVal == "N" || upperVal == "OFF" || upperVal == "F")
178                 return "0";
179             else
180             {
181                 alert(value + " is not a valid boolean for field '" + colName + "'. Try true,false; yes,no; y,n; on,off; or 1,0.");
182                 return undefined;
183             }
184         }
185         else
186             return value;
187     }
188 
189     function twoDigit(num)
190     {
191         if (num < 10)
192             return "0" + num;
193         else
194             return "" + num;
195     }
196 
197     var urlMap = {};
198     var oppositeMap = {
199         //HAS_ANY_VALUE: null,
200         eq: 'neqornull',
201         dateeq : 'dateneq',
202         dateneq : 'dateeq',
203         neqornull : 'eq',
204         neq : 'eq',
205         isblank : 'isnonblank',
206         isnonblank : 'isblank',
207         gt : 'lte',
208         dategt : 'datelte',
209         lt : 'gte',
210         datelt : 'dategte',
211         gte : 'lt',
212         dategte : 'datelt',
213         lte : 'gt',
214         datelte : 'dategt',
215         contains : 'doesnotcontain',
216         doesnotcontain : 'contains',
217         doesnotstartwith : 'startswith',
218         startswith : 'doesnotstartwith',
219         'in' : 'notin',
220         notin : 'in',
221         memberof : 'memberof',
222         containsoneof : 'containsnoneof',
223         containsnoneof : 'containsoneof',
224         hasmvvalue : 'nomvvalue',
225         nomvvalue : 'hasmvvalue',
226         between : 'notbetween',
227         notbetween : 'between'
228     };
229 
230     //NOTE: these maps contains the unambiguous pairings of single- and multi-valued filters
231     //due to NULLs, one cannot easily convert neq to notin
232     var multiValueToSingleMap = {
233         'in' : 'eq',
234         containsoneof : 'contains',
235         containsnoneof : 'doesnotcontain',
236         between: 'gte',
237         notbetween: 'lt'
238     };
239 
240     var singleValueToMultiMap = {
241         eq : 'in',
242         neq : 'notin',
243         neqornull: 'notin',
244         doesnotcontain : 'containsnoneof',
245         contains : 'containsoneof'
246     };
247 
248     function createNoValueFilterType(displayText, displaySymbol, urlSuffix, longDisplayText)
249     {
250         return createFilterType(displayText, displaySymbol, urlSuffix, false, false, null, longDisplayText);
251     }
252 
253     function createSingleValueFilterType(displayText, displaySymbol, urlSuffix, longDisplayText)
254     {
255         return createFilterType(displayText, displaySymbol, urlSuffix, true, false, null, longDisplayText);
256     }
257 
258     function createMultiValueFilterType(displayText, displaySymbol, urlSuffix, longDisplayText, multiValueSeparator, minOccurs, maxOccurs)
259     {
260         return createFilterType(displayText, displaySymbol, urlSuffix, true, false, multiValueSeparator, longDisplayText, minOccurs, maxOccurs);
261     }
262 
263     function createTableFilterType(displayText, displaySymbol, urlSuffix, longDisplayText)
264     {
265         return createFilterType(displayText, displaySymbol, urlSuffix, true, true, null, longDisplayText);
266     }
267 
268     function createFilterType(displayText, displaySymbol, urlSuffix, dataValueRequired, isTableWise, multiValueSeparator, longDisplayText, minOccurs, maxOccurs)
269     {
270         var result = {
271             getDisplaySymbol : function() { return displaySymbol },
272             getDisplayText : function() { return displayText },
273             getLongDisplayText : function() { return longDisplayText || displayText },
274             getURLSuffix : function() { return urlSuffix },
275             isDataValueRequired : function() { return dataValueRequired === true },
276             isMultiValued : function() { return multiValueSeparator != null; },
277             isTableWise : function() { return isTableWise === true },
278             getMultiValueSeparator : function() { return multiValueSeparator },
279             getMultiValueMinOccurs : function() { return minOccurs },
280             getMultiValueMaxOccurs : function() { return maxOccurs; },
281             getOpposite : function() {return oppositeMap[urlSuffix] ? urlMap[oppositeMap[urlSuffix]] : null},
282             getSingleValueFilter : function() {return this.isMultiValued() ? urlMap[multiValueToSingleMap[urlSuffix]] : this},
283             getMultiValueFilter : function() {return this.isMultiValued() ? null : urlMap[singleValueToMultiMap[urlSuffix]]},
284             validate : function (value, type, colName) {
285                 if (!dataValueRequired)
286                     return true;
287 
288                 var f = filterTypes[type];
289                 var found = false;
290                 for (var i = 0; !found && i < f.length; i++)
291                 {
292                     if (f[i].getURLSuffix() == urlSuffix)
293                         found = true;
294                 }
295                 if (!found) {
296                     alert("Filter type '" + displayText + "' can't be applied to " + type + " types.");
297                     return undefined;
298                 }
299 
300                 if (this.isMultiValued())
301                     return validateMultiple(type, value, colName, multiValueSeparator, minOccurs, maxOccurs);
302                 else
303                     return validate(type, value, colName);
304             }
305         };
306         urlMap[urlSuffix] = result;
307         return result;
308     }
309 
310     function getFilter(columnName, value, filterType)
311     {
312         var column = filterType.isTableWise() ? "*" : columnName;
313 
314         return {
315             getColumnName: function() {return column;},
316             getValue: function() {return value},
317             getFilterType: function() {return filterType},
318             getURLParameterName : function(dataRegionName) { return (dataRegionName || "query") + "." + column + "~" + filterType.getURLSuffix();},
319             getURLParameterValue : function() { return filterType.isDataValueRequired() ? value : "" }
320         };
321     }
322 
323     var ret = /** @scope LABKEY.Filter */{
324 
325         // WARNING: Keep in sync and in order with all other client apis and docs
326         // - server: CompareType.java
327         // - java: Filter.java
328         // - js: Filter.js
329         // - R: makeFilter.R, makeFilter.Rd
330         // - SAS: labkeymakefilter.sas, labkey.org SAS docs
331         // - Python & Perl don't have an filter operator enum
332         // - EXPERIMENTAL: Added an optional displaySymbol() for filters that want to support it
333         Types : {
334 
335             HAS_ANY_VALUE : createNoValueFilterType("Has Any Value", null, "", null),
336 
337             //
338             // These operators require a data value
339             //
340 
341             EQUAL : createSingleValueFilterType("Equals", "=", "eq", null),
342             DATE_EQUAL : createSingleValueFilterType("Equals", "=", "dateeq", null),
343 
344             NEQ : createSingleValueFilterType("Does Not Equal", "<>", "neq", null),
345             NOT_EQUAL : createSingleValueFilterType("Does Not Equal", "<>", "neq", null),
346             DATE_NOT_EQUAL : createSingleValueFilterType("Does Not Equal", "<>", "dateneq", null),
347 
348             NEQ_OR_NULL : createSingleValueFilterType("Does Not Equal", "<>", "neqornull", null),
349             NOT_EQUAL_OR_MISSING : createSingleValueFilterType("Does Not Equal", "<>", "neqornull", null),
350 
351             GT : createSingleValueFilterType("Is Greater Than", ">", "gt", null),
352             GREATER_THAN : createSingleValueFilterType("Is Greater Than", ">", "gt", null),
353             DATE_GREATER_THAN : createSingleValueFilterType("Is Greater Than", ">", "dategt", null),
354 
355             LT : createSingleValueFilterType("Is Less Than", "<", "lt", null),
356             LESS_THAN : createSingleValueFilterType("Is Less Than", "<", "lt", null),
357             DATE_LESS_THAN : createSingleValueFilterType("Is Less Than", "<", "datelt", null),
358 
359             GTE : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "gte", null),
360             GREATER_THAN_OR_EQUAL : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "gte", null),
361             DATE_GREATER_THAN_OR_EQUAL : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "dategte", null),
362 
363             LTE : createSingleValueFilterType("Is Less Than or Equal To", "=<", "lte", null),
364             LESS_THAN_OR_EQUAL : createSingleValueFilterType("Is Less Than or Equal To", "=<", "lte", null),
365             DATE_LESS_THAN_OR_EQUAL : createSingleValueFilterType("Is Less Than or Equal To", "=<", "datelte", null),
366 
367             STARTS_WITH : createSingleValueFilterType("Starts With", null, "startswith", null),
368             DOES_NOT_START_WITH : createSingleValueFilterType("Does Not Start With", null, "doesnotstartwith", null),
369 
370             CONTAINS : createSingleValueFilterType("Contains", null, "contains", null),
371             DOES_NOT_CONTAIN : createSingleValueFilterType("Does Not Contain", null, "doesnotcontain", null),
372 
373             CONTAINS_ONE_OF : createMultiValueFilterType("Contains One Of", null, "containsoneof", 'Contains One Of (example usage: a;b;c)', ";"),
374             CONTAINS_NONE_OF : createMultiValueFilterType("Does Not Contain Any Of", null, "containsnoneof", 'Does Not Contain Any Of (example usage: a;b;c)', ";"),
375 
376             IN : createMultiValueFilterType("Equals One Of", null, "in", 'Equals One Of (example usage: a;b;c)', ";"),
377             //NOTE: for some reason IN is aliased as EQUALS_ONE_OF.  not sure if this is for legacy purposes or it was determined EQUALS_ONE_OF was a better phrase
378             //to follow this pattern I did the same for IN_OR_MISSING
379             EQUALS_ONE_OF : createMultiValueFilterType("Equals One Of", null, "in", 'Equals One Of (example usage: a;b;c)', ";"),
380 
381             NOT_IN: createMultiValueFilterType("Does Not Equal Any Of", null, "notin", 'Does Not Equal Any Of (example usage: a;b;c)', ";"),
382             EQUALS_NONE_OF: createMultiValueFilterType("Does Not Equal Any Of", null, "notin", 'Does Not Equal Any Of (example usage: a;b;c)', ";"),
383 
384             BETWEEN : createMultiValueFilterType("Between", null, "between", 'Between, Inclusive (example usage: -4,4)', ",", 2, 2),
385             NOT_BETWEEN : createMultiValueFilterType("Not Between", null, "notbetween", 'Not Between, Exclusive (example usage: -4,4)', ",", 2, 2),
386 
387             MEMBER_OF : createSingleValueFilterType("Member Of", null, "memberof", 'Member Of'),
388 
389             //
390             // These are the "no data value" operators
391             //
392 
393             ISBLANK : createNoValueFilterType("Is Blank", null, "isblank", null),
394             MISSING : createNoValueFilterType("Is Blank", null, "isblank", null),
395             NONBLANK : createNoValueFilterType("Is Not Blank", null, "isnonblank", null),
396             NOT_MISSING : createNoValueFilterType("Is Not Blank", null, "isnonblank", null),
397 
398             HAS_MISSING_VALUE : createNoValueFilterType("Has a missing value indicator", null, "hasmvvalue", null),
399             DOES_NOT_HAVE_MISSING_VALUE : createNoValueFilterType("Does not have a missing value indicator", null, "nomvvalue", null),
400 
401             EXP_CHILD_OF : createSingleValueFilterType("Is Child Of", null, "exp:childof", " is child of" ),
402 
403             //
404             // Table/Query-wise operators
405             //
406             Q : createTableFilterType("Search", null, "q", "Search across all columns")
407         },
408 
409         /** @private create a js object suitable for Query.selectRows, etc */
410         appendFilterParams : function (params, filterArray, dataRegionName)
411         {
412             dataRegionName = dataRegionName || "query";
413             params = params || {};
414             if (filterArray)
415             {
416                 for (var i = 0; i < filterArray.length; i++)
417                 {
418                     var filter = filterArray[i];
419                     // 10.1 compatibility: treat ~eq=null as a NOOP (ref 10482)
420                     if (filter.getFilterType().isDataValueRequired() && null == filter.getURLParameterValue())
421                         continue;
422 
423                     // Create an array of filter values if there is more than one filter for the same column and filter type.
424                     var paramName = filter.getURLParameterName(dataRegionName);
425                     var paramValue = filter.getURLParameterValue();
426                     if (params[paramName] !== undefined)
427                     {
428                         var values = params[paramName];
429                         if (!LABKEY.Utils.isArray(values))
430                             values = [ values ];
431                         values.push(paramValue);
432                         paramValue = values;
433                     }
434                     params[paramName] = paramValue;
435                 }
436             }
437             return params;
438         },
439 
440         /** @private create a js object suitable for QueryWebPart, etc */
441         appendAggregateParams : function (params, aggregateArray, dataRegionName)
442         {
443             dataRegionName = dataRegionName || "query";
444             params = params || {};
445             if (aggregateArray)
446             {
447                 for (var idx = 0; idx < aggregateArray.length; ++idx)
448                 {
449                     var aggregate = aggregateArray[idx];
450                     var value = "type=" + aggregate.type;
451                     if (aggregate.label)
452                         value = value + "&label=" + aggregate.label;
453                     if (aggregate.type && aggregate.column)
454                     {
455                         // Create an array of aggregate values if there is more than one aggregate for the same column.
456                         var paramName = dataRegionName + '.analytics.' + aggregate.column;
457                         var paramValue = encodeURIComponent(value);
458                         if (params[paramName] !== undefined)
459                         {
460                             var values = params[paramName];
461                             if (!LABKEY.Utils.isArray(values))
462                                 values = [ values ];
463                             values.push(paramValue);
464                             paramValue = values;
465                         }
466                         params[paramName] = paramValue;
467                     }
468 
469                 }
470             }
471 
472             return params;
473         },
474 
475 
476         /**
477         * Creates a filter
478         * @param {String} columnName String name of the column to filter
479         * @param value Value used as the filter criterion or an Array of values.
480         * @param {LABKEY.Filter#Types} [filterType] Type of filter to apply to the 'column' using the 'value'
481 		* @example Example: <pre name="code" class="xml">
482 <script type="text/javascript">
483 	function onFailure(errorInfo, options, responseObj)
484 	{
485 	    if(errorInfo && errorInfo.exception)
486 	        alert("Failure: " + errorInfo.exception);
487 	    else
488 	        alert("Failure: " + responseObj.statusText);
489 	}
490 
491 	function onSuccess(data)
492 	{
493 	    alert("Success! " + data.rowCount + " rows returned.");
494 	}
495 
496 	LABKEY.Query.selectRows({
497 		schemaName: 'lists',
498 		queryName: 'People',
499 		success: onSuccess,
500 		failure: onFailure,
501 		filterArray: [
502 			LABKEY.Filter.create('FirstName', 'Johnny'),
503 			LABKEY.Filter.create('Age', 15, LABKEY.Filter.Types.LESS_THAN_OR_EQUAL)
504             LABKEY.Filter.create('LastName', ['A', 'B'], LABKEY.Filter.Types.DOES_NOT_START_WITH)
505 		]
506     });
507 </script> </pre>
508         */
509 
510         create : function(columnName, value, filterType)
511         {
512             if (!filterType)
513                 filterType = this.Types.EQUAL;
514             return getFilter(columnName, value, filterType);
515         },
516 
517         /**
518          * Not for public use. Can be changed or dropped at any time.
519          * @param typeName
520          * @param displayText
521          * @param urlSuffix
522          * @param isMultiType
523          * @private
524          */
525         _define : function(typeName, displayText, urlSuffix, isMultiType) {
526             if (!LABKEY.Filter.Types[typeName]) {
527                 if (isMultiType) {
528                     LABKEY.Filter.Types[typeName] = createMultiValueFilterType(displayText, null, urlSuffix, null);
529                 }
530                 else {
531                     LABKEY.Filter.Types[typeName] = createSingleValueFilterType(displayText, null, urlSuffix, null);
532                 }
533             }
534         },
535 
536         /**
537          * Given an array of filter objects, return a new filterArray with old filters from a column removed and new filters for the column added
538          * If new filters are null, simply remove all old filters from baseFilters that refer to this column
539          * @param {Array} baseFilters  Array of existing filters created by {@link LABKEY.Filter.create}
540          * @param {String} columnName  Column name of filters to replace
541          * @param {Array} columnFilters Array of new filters created by {@link LABKEY.Filter.create}. Will replace any filters referring to columnName
542          */
543         merge : function(baseFilters, columnName, columnFilters)
544         {
545             var newFilters = [];
546             if (null != baseFilters)
547                 for (var i = 0; i < baseFilters.length; i++)
548                 {
549                     var filt = baseFilters[i];
550                     if (filt.getColumnName() != columnName)
551                         newFilters.push(filt);
552                 }
553 
554             return null == columnFilters ? newFilters : newFilters.concat(columnFilters);
555         },
556 
557         /**
558         * Convert from URL syntax filters to a human readable description, like "Is Greater Than 10 AND Is Less Than 100"
559         * @param {String} url URL containing the filter parameters
560         * @param {String} dataRegionName String name of the data region the column is a part of
561         * @param {String} columnName String name of the column to filter
562         * @return {String} human readable version of the filter
563          */
564         getFilterDescription : function(url, dataRegionName, columnName)
565         {
566             var params = LABKEY.ActionURL.getParameters(url);
567             var result = "";
568             var separator = "";
569             for (var paramName in params)
570             {
571                 // Look for parameters that have the right prefix
572                 if (paramName.indexOf(dataRegionName + "." + columnName + "~") == 0)
573                 {
574                     var filterType = paramName.substring(paramName.indexOf("~") + 1);
575                     var values = params[paramName];
576                     if (!LABKEY.Utils.isArray(values))
577                     {
578                         values = [values];
579                     }
580                     // Get the human readable version, like "Is Less Than"
581                     var friendly = urlMap[filterType];
582                     var displayText;
583                     if (!friendly)
584                     {
585                         displayText = filterType;
586                     }
587                     else
588                     {
589                         displayText = friendly.getDisplayText();
590                     }
591 
592                     for (var j = 0; j < values.length; j++)
593                     {
594                         // If the same type of filter is applied twice, it will have multiple values
595                         result += separator;
596                         separator = " AND ";
597 
598                         result += displayText;
599                         result += " ";
600                         result += values[j];
601                     }
602                 }
603             }
604             return result;
605         },
606 
607         // Create an array of LABKEY.Filter objects from the filter parameters on the URL
608         getFiltersFromUrl : function(url, dataRegionName)
609         {
610             dataRegionName = dataRegionName || 'query';
611             var params = LABKEY.ActionURL.getParameters(url);
612             var filterArray = [];
613 
614             for (var paramName in params)
615             {
616                 if (params.hasOwnProperty(paramName)) {
617                     // Look for parameters that have the right prefix
618                     if (paramName.indexOf(dataRegionName + ".") == 0)
619                     {
620                         var tilde = paramName.indexOf("~");
621 
622                         if (tilde != -1)
623                         {
624                             var columnName = paramName.substring(dataRegionName.length + 1, tilde);
625                             var filterName = paramName.substring(tilde + 1);
626                             var filterType = LABKEY.Filter.getFilterTypeForURLSuffix(filterName);
627                             var values = params[paramName];
628                             if (!LABKEY.Utils.isArray(values))
629                             {
630                                 values = [values];
631                             }
632                             filterArray.push(LABKEY.Filter.create(columnName, values, filterType));
633                         }
634                     }
635                 }
636             }
637             return filterArray;
638         },
639 
640         getSortFromUrl : function(url, dataRegionName)
641         {
642             dataRegionName = dataRegionName || 'query';
643 
644             var params = LABKEY.ActionURL.getParameters(url);
645             return params[dataRegionName + "." + "sort"];
646         },
647 
648         getQueryParamsFromUrl : function(url, dataRegionName)
649         {
650             dataRegionName = dataRegionName || 'query';
651 
652             var queryParams = {};
653             var params = LABKEY.ActionURL.getParameters(url);
654             for (var paramName in params)
655             {
656                 if (params.hasOwnProperty(paramName))
657                 {
658                     if (paramName.indexOf(dataRegionName + "." + "param.") == 0)
659                     {
660                         var queryParamName = paramName.substring((dataRegionName + "." + "param.").length);
661                         queryParams[queryParamName] = params[paramName];
662                     }
663                 }
664             }
665 
666             return queryParams;
667         },
668 
669         getFilterTypeForURLSuffix : function (urlSuffix)
670         {
671             return urlMap[urlSuffix];
672         }
673     };
674 
675     var ft = ret.Types;
676     var filterTypes = {
677         "int":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK, ft.GT, ft.LT, ft.GTE, ft.LTE, ft.IN, ft.NOT_IN, ft.BETWEEN, ft.NOT_BETWEEN],
678         "string":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK, ft.GT, ft.LT, ft.GTE, ft.LTE, ft.CONTAINS, ft.DOES_NOT_CONTAIN, ft.DOES_NOT_START_WITH, ft.STARTS_WITH, ft.IN, ft.NOT_IN, ft.CONTAINS_ONE_OF, ft.CONTAINS_NONE_OF, ft.BETWEEN, ft.NOT_BETWEEN],
679         "boolean":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK],
680         "float":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK, ft.GT, ft.LT, ft.GTE, ft.LTE, ft.IN, ft.NOT_IN, ft.BETWEEN, ft.NOT_BETWEEN],
681         "date":[ft.HAS_ANY_VALUE, ft.DATE_EQUAL, ft.DATE_NOT_EQUAL, ft.ISBLANK, ft.NONBLANK, ft.DATE_GREATER_THAN, ft.DATE_LESS_THAN, ft.DATE_GREATER_THAN_OR_EQUAL, ft.DATE_LESS_THAN_OR_EQUAL]
682     };
683 
684     var defaultFilter = {
685         "int": ft.EQUAL,
686         "string": ft.CONTAINS,
687         "boolean": ft.EQUAL,
688         "float": ft.EQUAL,
689         "date": ft.DATE_EQUAL
690     };
691 
692     /** @private Returns an Array of filter types that can be used with the given json type ("int", "double", "string", "boolean", "date") */
693     ret.getFilterTypesForType = function (type, mvEnabled)
694     {
695         var types = [];
696         if (filterTypes[type])
697             types = types.concat(filterTypes[type]);
698 
699         if (mvEnabled)
700         {
701             types.push(ft.HAS_MISSING_VALUE);
702             types.push(ft.DOES_NOT_HAVE_MISSING_VALUE);
703         }
704 
705         return types;
706     };
707 
708     /** @private Return the default LABKEY.Filter.Type for a json type ("int", "double", "string", "boolean", "date"). */
709     ret.getDefaultFilterForType = function (type)
710     {
711         if (defaultFilter[type])
712             return defaultFilter[type];
713 
714         return ft.EQUAL;
715     };
716 
717     return ret;
718 };
719 
720 /**
721 * @name LABKEY.Filter.FilterDefinition
722 * @description Static class that defines the functions that describe how a particular
723 *            type of filter is identified and operates.  See {@link LABKEY.Filter}.
724  *            <p>Additional Documentation:
725  *              <ul>
726  *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=filteringData">Filter via the LabKey UI</a></li>
727  *              </ul>
728  *           </p>
729 * @class  Static class that defines the functions that describe how a particular
730 *            type of filter is identified and operates.  See {@link LABKEY.Filter}.
731  *            <p>Additional Documentation:
732  *              <ul>
733  *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=filteringData">Filter via the LabKey UI</a></li>
734  *              </ul>
735  *           </p>
736 */
737 
738 /**#@+
739  * @methodOf LABKEY.Filter.FilterDefinition#
740 */
741 
742 /**
743 * Get the string displayed for this filter.
744 * @name getDisplayText
745 * @type String
746 */
747 
748 /**
749 * Get the more descriptive string displayed for this filter.  This is used in filter dialogs.
750 * @name getLongDisplayText
751 * @type String
752 */
753 
754 /**
755 * Get the URL suffix used to identify this filter.
756 * @name getURLSuffix
757 * @type String
758 */
759 
760 /**
761 * Get the Boolean that indicates whether a data value is required.
762 * @name isDataValueRequired
763 * @type Boolean
764 */
765 
766 /**
767 * Get the Boolean that indicates whether the filter supports a string with multiple filter values (ie. contains one of, not in, etc).
768 * @name isMultiValued
769 * @type Boolean
770 */
771 
772 /**
773 * Get the LABKEY.Filter.FilterDefinition the represents the opposite of this filter type.
774 * @name getOpposite
775 * @type LABKEY.Filter.FilterDefinition
776 */
777 
778 /**#@-*/
779