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-2018 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     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     var ret = /** @scope LABKEY.Filter */{
311 
312         // WARNING: Keep in sync and in order with all other client apis and docs
313         // - server: CompareType.java
314         // - java: Filter.java
315         // - js: Filter.js
316         // - R: makeFilter.R, makeFilter.Rd
317         // - SAS: labkeymakefilter.sas, labkey.org SAS docs
318         // - Python & Perl don't have an filter operator enum
319         // - EXPERIMENTAL: Added an optional displaySymbol() for filters that want to support it
320         Types : {
321 
322             HAS_ANY_VALUE : createNoValueFilterType("Has Any Value", null, "", null),
323 
324             //
325             // These operators require a data value
326             //
327 
328             EQUAL : createSingleValueFilterType("Equals", "=", "eq", null),
329             DATE_EQUAL : createSingleValueFilterType("Equals", "=", "dateeq", null),
330 
331             NEQ : createSingleValueFilterType("Does Not Equal", "<>", "neq", null),
332             NOT_EQUAL : createSingleValueFilterType("Does Not Equal", "<>", "neq", null),
333             DATE_NOT_EQUAL : createSingleValueFilterType("Does Not Equal", "<>", "dateneq", null),
334 
335             NEQ_OR_NULL : createSingleValueFilterType("Does Not Equal", "<>", "neqornull", null),
336             NOT_EQUAL_OR_MISSING : createSingleValueFilterType("Does Not Equal", "<>", "neqornull", null),
337 
338             GT : createSingleValueFilterType("Is Greater Than", ">", "gt", null),
339             GREATER_THAN : createSingleValueFilterType("Is Greater Than", ">", "gt", null),
340             DATE_GREATER_THAN : createSingleValueFilterType("Is Greater Than", ">", "dategt", null),
341 
342             LT : createSingleValueFilterType("Is Less Than", "<", "lt", null),
343             LESS_THAN : createSingleValueFilterType("Is Less Than", "<", "lt", null),
344             DATE_LESS_THAN : createSingleValueFilterType("Is Less Than", "<", "datelt", null),
345 
346             GTE : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "gte", null),
347             GREATER_THAN_OR_EQUAL : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "gte", null),
348             DATE_GREATER_THAN_OR_EQUAL : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "dategte", null),
349 
350             LTE : createSingleValueFilterType("Is Less Than or Equal To", "=<", "lte", null),
351             LESS_THAN_OR_EQUAL : createSingleValueFilterType("Is Less Than or Equal To", "=<", "lte", null),
352             DATE_LESS_THAN_OR_EQUAL : createSingleValueFilterType("Is Less Than or Equal To", "=<", "datelte", null),
353 
354             STARTS_WITH : createSingleValueFilterType("Starts With", null, "startswith", null),
355             DOES_NOT_START_WITH : createSingleValueFilterType("Does Not Start With", null, "doesnotstartwith", null),
356 
357             CONTAINS : createSingleValueFilterType("Contains", null, "contains", null),
358             DOES_NOT_CONTAIN : createSingleValueFilterType("Does Not Contain", null, "doesnotcontain", null),
359 
360             CONTAINS_ONE_OF : createMultiValueFilterType("Contains One Of", null, "containsoneof", 'Contains One Of (example usage: a;b;c)', ";"),
361             CONTAINS_NONE_OF : createMultiValueFilterType("Does Not Contain Any Of", null, "containsnoneof", 'Does Not Contain Any Of (example usage: a;b;c)', ";"),
362 
363             IN : createMultiValueFilterType("Equals One Of", null, "in", 'Equals One Of (example usage: a;b;c)', ";"),
364             //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
365             //to follow this pattern I did the same for IN_OR_MISSING
366             EQUALS_ONE_OF : createMultiValueFilterType("Equals One Of", null, "in", 'Equals One Of (example usage: a;b;c)', ";"),
367 
368             NOT_IN: createMultiValueFilterType("Does Not Equal Any Of", null, "notin", 'Does Not Equal Any Of (example usage: a;b;c)', ";"),
369             EQUALS_NONE_OF: createMultiValueFilterType("Does Not Equal Any Of", null, "notin", 'Does Not Equal Any Of (example usage: a;b;c)', ";"),
370 
371             BETWEEN : createMultiValueFilterType("Between", null, "between", 'Between, Inclusive (example usage: -4,4)', ",", 2, 2),
372             NOT_BETWEEN : createMultiValueFilterType("Not Between", null, "notbetween", 'Not Between, Exclusive (example usage: -4,4)', ",", 2, 2),
373 
374             MEMBER_OF : createSingleValueFilterType("Member Of", null, "memberof", 'Member Of'),
375 
376             //
377             // These are the "no data value" operators
378             //
379 
380             ISBLANK : createNoValueFilterType("Is Blank", null, "isblank", null),
381             MISSING : createNoValueFilterType("Is Blank", null, "isblank", null),
382             NONBLANK : createNoValueFilterType("Is Not Blank", null, "isnonblank", null),
383             NOT_MISSING : createNoValueFilterType("Is Not Blank", null, "isnonblank", null),
384 
385             HAS_MISSING_VALUE : createNoValueFilterType("Has a missing value indicator", null, "hasmvvalue", null),
386             DOES_NOT_HAVE_MISSING_VALUE : createNoValueFilterType("Does not have a missing value indicator", null, "nomvvalue", null),
387 
388             EXP_CHILD_OF : createSingleValueFilterType("Is Child Of", null, "exp:childof", " is child of" ),
389 
390             //
391             // Table/Query-wise operators
392             //
393             Q : createTableFilterType("Search", null, "q", "Search across all columns")
394         },
395 
396         /** @private create a js object suitable for Query.selectRows, etc */
397         appendFilterParams : function (params, filterArray, dataRegionName)
398         {
399             dataRegionName = dataRegionName || "query";
400             params = params || {};
401             if (filterArray)
402             {
403                 for (var i = 0; i < filterArray.length; i++)
404                 {
405                     var filter = filterArray[i];
406                     // 10.1 compatibility: treat ~eq=null as a NOOP (ref 10482)
407                     if (filter.getFilterType().isDataValueRequired() && null == filter.getURLParameterValue())
408                         continue;
409 
410                     // Create an array of filter values if there is more than one filter for the same column and filter type.
411                     var paramName = filter.getURLParameterName(dataRegionName);
412                     var paramValue = filter.getURLParameterValue();
413                     if (params[paramName] !== undefined)
414                     {
415                         var values = params[paramName];
416                         if (!LABKEY.Utils.isArray(values))
417                             values = [ values ];
418                         values.push(paramValue);
419                         paramValue = values;
420                     }
421                     params[paramName] = paramValue;
422                 }
423             }
424             return params;
425         },
426 
427         /** @private create a js object suitable for QueryWebPart, etc */
428         appendAggregateParams : function (params, aggregateArray, dataRegionName)
429         {
430             dataRegionName = dataRegionName || "query";
431             params = params || {};
432             if (aggregateArray)
433             {
434                 for (var idx = 0; idx < aggregateArray.length; ++idx)
435                 {
436                     var aggregate = aggregateArray[idx];
437                     var value = "type=" + aggregate.type;
438                     if (aggregate.label)
439                         value = value + "&label=" + aggregate.label;
440                     if (aggregate.type && aggregate.column)
441                     {
442                         // Create an array of aggregate values if there is more than one aggregate for the same column.
443                         var paramName = dataRegionName + '.analytics.' + aggregate.column;
444                         var paramValue = encodeURIComponent(value);
445                         if (params[paramName] !== undefined)
446                         {
447                             var values = params[paramName];
448                             if (!LABKEY.Utils.isArray(values))
449                                 values = [ values ];
450                             values.push(paramValue);
451                             paramValue = values;
452                         }
453                         params[paramName] = paramValue;
454                     }
455 
456                 }
457             }
458 
459             return params;
460         },
461 
462 
463         /**
464         * Creates a filter
465         * @param {String} columnName String name of the column to filter
466         * @param value Value used as the filter criterion or an Array of values.
467         * @param {LABKEY.Filter#Types} [filterType] Type of filter to apply to the 'column' using the 'value'
468 		* @example Example: <pre name="code" class="xml">
469 <script type="text/javascript">
470 	function onFailure(errorInfo, options, responseObj)
471 	{
472 	    if(errorInfo && errorInfo.exception)
473 	        alert("Failure: " + errorInfo.exception);
474 	    else
475 	        alert("Failure: " + responseObj.statusText);
476 	}
477 
478 	function onSuccess(data)
479 	{
480 	    alert("Success! " + data.rowCount + " rows returned.");
481 	}
482 
483 	LABKEY.Query.selectRows({
484 		schemaName: 'lists',
485 		queryName: 'People',
486 		success: onSuccess,
487 		failure: onFailure,
488 		filterArray: [
489 			LABKEY.Filter.create('FirstName', 'Johnny'),
490 			LABKEY.Filter.create('Age', 15, LABKEY.Filter.Types.LESS_THAN_OR_EQUAL)
491             LABKEY.Filter.create('LastName', ['A', 'B'], LABKEY.Filter.Types.DOES_NOT_START_WITH)
492 		]
493     });
494 </script> </pre>
495         */
496 
497         create : function(columnName, value, filterType)
498         {
499             return new LABKEY.Query.Filter(columnName, value, filterType);
500         },
501 
502         /**
503          * Not for public use. Can be changed or dropped at any time.
504          * @param typeName
505          * @param displayText
506          * @param urlSuffix
507          * @param isMultiType
508          * @private
509          */
510         _define : function(typeName, displayText, urlSuffix, isMultiType) {
511             if (!LABKEY.Filter.Types[typeName]) {
512                 if (isMultiType) {
513                     LABKEY.Filter.Types[typeName] = createMultiValueFilterType(displayText, null, urlSuffix, null);
514                 }
515                 else {
516                     LABKEY.Filter.Types[typeName] = createSingleValueFilterType(displayText, null, urlSuffix, null);
517                 }
518             }
519         },
520 
521         /**
522          * Given an array of filter objects, return a new filterArray with old filters from a column removed and new filters for the column added
523          * If new filters are null, simply remove all old filters from baseFilters that refer to this column
524          * @param {Array} baseFilters  Array of existing filters created by {@link LABKEY.Filter.create}
525          * @param {String} columnName  Column name of filters to replace
526          * @param {Array} columnFilters Array of new filters created by {@link LABKEY.Filter.create}. Will replace any filters referring to columnName
527          */
528         merge : function(baseFilters, columnName, columnFilters)
529         {
530             var newFilters = [];
531             if (null != baseFilters)
532                 for (var i = 0; i < baseFilters.length; i++)
533                 {
534                     var filt = baseFilters[i];
535                     if (filt.getColumnName() != columnName)
536                         newFilters.push(filt);
537                 }
538 
539             return null == columnFilters ? newFilters : newFilters.concat(columnFilters);
540         },
541 
542         /**
543         * Convert from URL syntax filters to a human readable description, like "Is Greater Than 10 AND Is Less Than 100"
544         * @param {String} url URL containing the filter parameters
545         * @param {String} dataRegionName String name of the data region the column is a part of
546         * @param {String} columnName String name of the column to filter
547         * @return {String} human readable version of the filter
548          */
549         getFilterDescription : function(url, dataRegionName, columnName)
550         {
551             var params = LABKEY.ActionURL.getParameters(url);
552             var result = "";
553             var separator = "";
554             for (var paramName in params)
555             {
556                 // Look for parameters that have the right prefix
557                 if (paramName.indexOf(dataRegionName + "." + columnName + "~") == 0)
558                 {
559                     var filterType = paramName.substring(paramName.indexOf("~") + 1);
560                     var values = params[paramName];
561                     if (!LABKEY.Utils.isArray(values))
562                     {
563                         values = [values];
564                     }
565                     // Get the human readable version, like "Is Less Than"
566                     var friendly = urlMap[filterType];
567                     var displayText;
568                     if (!friendly)
569                     {
570                         displayText = filterType;
571                     }
572                     else
573                     {
574                         displayText = friendly.getDisplayText();
575                     }
576 
577                     for (var j = 0; j < values.length; j++)
578                     {
579                         // If the same type of filter is applied twice, it will have multiple values
580                         result += separator;
581                         separator = " AND ";
582 
583                         result += displayText;
584                         result += " ";
585                         result += values[j];
586                     }
587                 }
588             }
589             return result;
590         },
591 
592         // Create an array of LABKEY.Filter objects from the filter parameters on the URL
593         getFiltersFromUrl : function(url, dataRegionName)
594         {
595             dataRegionName = dataRegionName || 'query';
596             var params = LABKEY.ActionURL.getParameters(url);
597             var filterArray = [];
598 
599             for (var paramName in params)
600             {
601                 if (params.hasOwnProperty(paramName)) {
602                     // Look for parameters that have the right prefix
603                     if (paramName.indexOf(dataRegionName + ".") == 0)
604                     {
605                         var tilde = paramName.indexOf("~");
606 
607                         if (tilde != -1)
608                         {
609                             var columnName = paramName.substring(dataRegionName.length + 1, tilde);
610                             var filterName = paramName.substring(tilde + 1);
611                             var filterType = LABKEY.Filter.getFilterTypeForURLSuffix(filterName);
612                             var values = params[paramName];
613                             if (!LABKEY.Utils.isArray(values))
614                             {
615                                 values = [values];
616                             }
617                             filterArray.push(LABKEY.Filter.create(columnName, values, filterType));
618                         }
619                     }
620                 }
621             }
622             return filterArray;
623         },
624 
625         getSortFromUrl : function(url, dataRegionName)
626         {
627             dataRegionName = dataRegionName || 'query';
628 
629             var params = LABKEY.ActionURL.getParameters(url);
630             return params[dataRegionName + "." + "sort"];
631         },
632 
633         getQueryParamsFromUrl : function(url, dataRegionName)
634         {
635             dataRegionName = dataRegionName || 'query';
636 
637             var queryParams = {};
638             var params = LABKEY.ActionURL.getParameters(url);
639             for (var paramName in params)
640             {
641                 if (params.hasOwnProperty(paramName))
642                 {
643                     if (paramName.indexOf(dataRegionName + "." + "param.") == 0)
644                     {
645                         var queryParamName = paramName.substring((dataRegionName + "." + "param.").length);
646                         queryParams[queryParamName] = params[paramName];
647                     }
648                 }
649             }
650 
651             return queryParams;
652         },
653 
654         getFilterTypeForURLSuffix : function (urlSuffix)
655         {
656             return urlMap[urlSuffix];
657         }
658     };
659 
660     var ft = ret.Types;
661     var filterTypes = {
662         "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],
663         "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],
664         "boolean":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK],
665         "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],
666         "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]
667     };
668 
669     var defaultFilter = {
670         "int": ft.EQUAL,
671         "string": ft.CONTAINS,
672         "boolean": ft.EQUAL,
673         "float": ft.EQUAL,
674         "date": ft.DATE_EQUAL
675     };
676 
677     /** @private Returns an Array of filter types that can be used with the given json type ("int", "double", "string", "boolean", "date") */
678     ret.getFilterTypesForType = function (type, mvEnabled)
679     {
680         var types = [];
681         if (filterTypes[type])
682             types = types.concat(filterTypes[type]);
683 
684         if (mvEnabled)
685         {
686             types.push(ft.HAS_MISSING_VALUE);
687             types.push(ft.DOES_NOT_HAVE_MISSING_VALUE);
688         }
689 
690         return types;
691     };
692 
693     /** @private Return the default LABKEY.Filter.Type for a json type ("int", "double", "string", "boolean", "date"). */
694     ret.getDefaultFilterForType = function (type)
695     {
696         if (defaultFilter[type])
697             return defaultFilter[type];
698 
699         return ft.EQUAL;
700     };
701 
702     return ret;
703 };
704 
705 /**
706 * @name LABKEY.Filter.FilterDefinition
707 * @description Static class that defines the functions that describe how a particular
708 *            type of filter is identified and operates.  See {@link LABKEY.Filter}.
709  *            <p>Additional Documentation:
710  *              <ul>
711  *                  <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=filteringData">Filter via the LabKey UI</a></li>
712  *              </ul>
713  *           </p>
714 * @class  Static class that defines the functions that describe how a particular
715 *            type of filter is identified and operates.  See {@link LABKEY.Filter}.
716  *            <p>Additional Documentation:
717  *              <ul>
718  *                  <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=filteringData">Filter via the LabKey UI</a></li>
719  *              </ul>
720  *           </p>
721 */
722 
723 /**#@+
724  * @methodOf LABKEY.Filter.FilterDefinition#
725 */
726 
727 /**
728 * Get the string displayed for this filter.
729 * @name getDisplayText
730 * @type String
731 */
732 
733 /**
734 * Get the more descriptive string displayed for this filter.  This is used in filter dialogs.
735 * @name getLongDisplayText
736 * @type String
737 */
738 
739 /**
740 * Get the URL suffix used to identify this filter.
741 * @name getURLSuffix
742 * @type String
743 */
744 
745 /**
746 * Get the Boolean that indicates whether a data value is required.
747 * @name isDataValueRequired
748 * @type Boolean
749 */
750 
751 /**
752 * Get the Boolean that indicates whether the filter supports a string with multiple filter values (ie. contains one of, not in, etc).
753 * @name isMultiValued
754 * @type Boolean
755 */
756 
757 /**
758 * Get the LABKEY.Filter.FilterDefinition the represents the opposite of this filter type.
759 * @name getOpposite
760 * @type LABKEY.Filter.FilterDefinition
761 */
762 
763 /**#@-*/
764