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