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-2017 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 var console = console || {warn : function(){}};
 21 
 22 /**
 23  * @namespace Utils static class to provide miscellaneous utility functions.
 24  */
 25 LABKEY.Utils = new function()
 26 {
 27     // Private array of chars to use for UUID generation
 28     var CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'.split('');
 29     var idSeed = 100;
 30 
 31     //When using Ext dateFields you can use DATEALTFORMATS for the altFormat: config option.
 32     var DATEALTFORMATS_Either =
 33             'j-M-y g:i a|j-M-Y g:i a|j-M-y G:i|j-M-Y G:i|' +
 34             'j-M-y|j-M-Y|' +
 35             'Y-n-d H:i:s|Y-n-d|' +
 36             'Y/n/d H:i:s|Y/n/d|' +
 37             'j M Y G:i:s O|' +  // 10 Sep 2009 11:24:12 -0700
 38             'j M Y H:i:s|c';
 39     var DATEALTFORMATS_MonthDay =
 40             'n/j/y g:i:s a|n/j/Y g:i:s a|n/j/y G:i:s|n/j/Y G:i:s|' +
 41             'n-j-y g:i:s a|n-j-Y g:i:s a|n-j-y G:i:s|n-j-Y G:i:s|' +
 42             'n/j/y g:i a|n/j/Y g:i a|n/j/y G:i|n/j/Y G:i|' +
 43             'n-j-y g:i a|n-j-Y g:i a|n-j-y G:i|n-j-Y G:i|' +
 44             'n/j/y|n/j/Y|' +
 45             'n-j-y|n-j-Y|' + DATEALTFORMATS_Either;
 46     var DATEALTFORMATS_DayMonth =
 47             'j/n/y g:i:s a|j/n/Y g:i:s a|j/n/y G:i:s|j/n/Y G:i:s|' +
 48             'j-n-y g:i:s a|j-n-Y g:i:s a|j-n-y G:i:s|j-n-Y G:i:s|' +
 49             'j/n/y g:i a|j/n/Y g:i a|j/n/y G:i|j/n/Y G:i|' +
 50             'j-n-y g:i a|j-n-Y g:i a|j-n-y G:i|j-n-Y G:i|' +
 51             'j/n/y|j/n/Y|' +
 52             'j-n-y|j-n-Y|' +
 53             'j-M-y|j-M-Y|' + DATEALTFORMATS_Either;
 54 
 55     function isObject(v)
 56     {
 57         return typeof v == "object" && Object.prototype.toString.call(v) === '[object Object]';
 58     }
 59 
 60 
 61     function _copy(o, depth)
 62     {
 63         if (depth==0 || !isObject(o))
 64             return o;
 65         var copy = {};
 66         for (var key in o)
 67             copy[key] = _copy(o[key], depth-1);
 68         return copy;
 69     }
 70 
 71 
 72     // like a general version of Ext.apply() or mootools.merge()
 73     function _merge(to, from, overwrite, depth)
 74     {
 75         for (var key in from)
 76         {
 77             if (from.hasOwnProperty(key))
 78             {
 79                 if (isObject(to[key]) && isObject(from[key]))
 80                 {
 81                     _merge(to[key], from[key], overwrite, depth-1);
 82                 }
 83                 else if (undefined === to[key] || overwrite)
 84                 {
 85                     to[key] = _copy(from[key], depth-1);
 86                 }
 87             }
 88         }
 89     }
 90 
 91     var getNextRow = function(rowElem, targetTagName)
 92     {
 93         if (null == rowElem)
 94             return null;
 95 
 96 
 97         var nextRow = rowElem.nextSibling;
 98         while (nextRow != null && !nextRow.tagName)
 99             nextRow = nextRow.nextSibling;
100 
101         if (nextRow == null)
102             return null;
103 
104         if (targetTagName)
105         {
106             if (nextRow.tagName != targetTagName)
107                 return null;
108         }
109         else
110         {
111             if (nextRow.tagName != "TR")
112                 return null;
113         }
114 
115         return nextRow;
116     };
117 
118     var collapseExpand = function(elem, notify, targetTagName)
119     {
120         var collapse = false;
121         var url = elem.href;
122         if (targetTagName)
123         {
124             while (elem.tagName != targetTagName)
125                 elem = elem.parentNode;
126         }
127         else
128         {
129             while (elem.tagName != 'TR')
130                 elem = elem.parentNode;
131         }
132 
133         var nextRow = getNextRow(elem, targetTagName);
134         if (null != nextRow && nextRow.style.display != "none")
135             collapse = true;
136 
137         while (nextRow != null)
138         {
139             if (nextRow.className.indexOf("labkey-header") != -1)
140                 break;
141             if (nextRow.style.display != "none")
142                 nextRow.style.display = "none";
143             else
144                 nextRow.style.display = "";
145             nextRow = getNextRow(nextRow, targetTagName);
146         }
147 
148         if (null != url && notify)
149             notifyExpandCollapse(url, collapse);
150         return false;
151     };
152 
153     var notifyExpandCollapse = function(url, collapse)
154     {
155         if (url) {
156             if (collapse)
157                 url += "&collapse=true";
158             LABKEY.Ajax.request({url: url});
159         }
160     };
161 
162     var toggleLink = function(link, notify, targetTagName)
163     {
164         collapseExpand(link, notify, targetTagName);
165         var i = 0;
166         while (typeof(link.childNodes[i].src) == "undefined" )
167             i++;
168 
169         if (link.childNodes[i].src.search("plus.gif") >= 0)
170             link.childNodes[i].src = link.childNodes[i].src.replace("plus.gif", "minus.gif");
171         else
172             link.childNodes[i].src = link.childNodes[i].src.replace("minus.gif", "plus.gif");
173         return false;
174     };
175 
176     /** @scope LABKEY.Utils */
177     return {
178         /**
179         * Encodes the html passed in so that it will not be interpreted as HTML by the browser.
180         * For example, if your input string was "<p>Hello</p>" the output would be
181         * "&lt;p&gt;Hello&lt;/p&gt;". If you set an element's innerHTML property
182         * to this string, the HTML markup will be displayed as literal text rather than being
183         * interpreted as HTML.
184         *
185         * @param {String} html The HTML to encode
186 		* @return {String} The encoded HTML
187 		*/
188         encodeHtml : function(html)
189         {
190             // https://stackoverflow.com/a/7124052
191             return String(html)
192                     .replace(/&/g, '&')
193                     .replace(/"/g, '"')
194                     .replace(/'/g, ''')
195                     .replace(/</g, '<')
196                     .replace(/>/g, '>')
197                     .replace(/\//g, '/');
198         },
199 
200         isArray: function(value) {
201             // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
202             return Object.prototype.toString.call(value) === "[object Array]";
203         },
204 
205         isDate: function(value) {
206             return Object.prototype.toString.call(value) === '[object Date]';
207         },
208 
209         isDefined: function(value) {
210             return typeof value !== 'undefined';
211         },
212 
213         isFunction: function(value) {
214             // http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type
215             var getType = {};
216             return value !== null && value !== undefined && getType.toString.call(value) === '[object Function]';
217         },
218 
219         isObject: isObject,
220 
221         /**
222          * Returns date formats for use in an Ext.form.DateField. Useful when using a DateField in an Ext object,
223          * it contains a very large set of date formats, which helps make a DateField more robust. For example, a
224          * user would be allowed to enter dates like 6/1/2011, 06/01/2011, 6/1/11, etc.
225          */
226         getDateAltFormats : function()
227         {
228             return LABKEY.useMDYDateParsing ? DATEALTFORMATS_MonthDay : DATEALTFORMATS_DayMonth;
229         },
230 
231         displayAjaxErrorResponse: function() {
232             console.warn('displayAjaxErrorResponse: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
233         },
234 
235         /**
236          * Generates a display string from the response to an error from an AJAX request
237          * @private
238          * @ignore
239          * @param {XMLHttpRequest} responseObj The XMLHttpRequest object containing the response data.
240          * @param {Error} [exceptionObj] A JavaScript Error object caught by the calling code.
241          * @param {Object} [config] An object with additional configuration properties.  It supports the following:
242          * <li>msgPrefix: A string that will be used as a prefix in the error message.  Default to: 'An error occurred trying to load'</li>
243          * <li>showExceptionClass: A boolean flag to display the java class of the exception.</li>
244          */
245         getMsgFromError: function(responseObj, exceptionObj, config){
246             config = config || {};
247             var error;
248             var prefix = config.msgPrefix || 'An error occurred trying to load:\n';
249 
250             if (responseObj && responseObj.responseText && responseObj.getResponseHeader('Content-Type'))
251             {
252                 var contentType = responseObj.getResponseHeader('Content-Type');
253                 if (contentType.indexOf('application/json') >= 0)
254                 {
255                     var jsonResponse = LABKEY.Utils.decode(responseObj.responseText);
256                     if (jsonResponse && jsonResponse.exception) {
257                         error = prefix + jsonResponse.exception;
258                         if (config.showExceptionClass)
259                             error += "\n(" + (jsonResponse.exceptionClass ? jsonResponse.exceptionClass : "Exception class unknown") + ")";
260                     }
261                 }
262                 else if (contentType.indexOf('text/html') >= 0 && jQuery)
263                 {
264                     var html = jQuery(responseObj.responseText);
265                     var el = html.find('.exception-message');
266                     if (el && el.length === 1)
267                         error = prefix + el.text().trim();
268                 }
269             }
270             if (!error)
271                 error = prefix + "Status: " + responseObj.statusText + " (" + responseObj.status + ")";
272             if (exceptionObj && exceptionObj.message)
273                 error += "\n" + exceptionObj.name + ": " + exceptionObj.message;
274 
275             return error;
276         },
277 
278         /**
279          * This method has been migrated to specific instances for both Ext 3.4.1 and Ext 4.2.1.
280          * For Ext 3.4.1 see LABKEY.ext.Utils.resizeToViewport
281          * For Ext 4.2.1 see LABKEY.ext4.Util.resizeToViewport
282          */
283         resizeToViewport : function()
284         {
285             console.warn('LABKEY.Utils.resizeToViewport has been migrated. See JavaScript API documentation for details.');
286         },
287 
288         /**
289          * Returns a URL to the appropriate file icon image based on the specified file name.
290          * Note that file name can be a full path or just the file name and extension.
291          * If the file name does not include an extension, the URL for a generic image will be returned
292          * @param {String} fileName The file name.
293          * @return {String} The URL suitable for use in the src attribute of an img element.
294          */
295         getFileIconUrl : function(fileName) {
296             var idx = fileName.lastIndexOf(".");
297             var extension = (idx >= 0) ? fileName.substring(idx + 1) : "_generic";
298             return LABKEY.ActionURL.buildURL("core", "getAttachmentIcon", "", {extension: extension});
299         },
300 
301         /**
302          * This is used internally by other class methods to automatically parse returned JSON
303          * and call another success function passing that parsed JSON.
304          * @param fn The callback function to wrap
305          * @param scope The scope for the callback function
306          * @param {boolean} [isErrorCallback=false] Set to true if the function is an error callback.  If true, and you do not provide a separate callback, alert will popup showing the error message
307          */
308         getCallbackWrapper : function(fn, scope, isErrorCallback) {
309             return function(response, options)
310             {
311                 var json = response.responseJSON;
312                 if (!json)
313                 {
314                     //ensure response is JSON before trying to decode
315                     if (response && response.getResponseHeader && response.getResponseHeader('Content-Type')
316                             && response.getResponseHeader('Content-Type').indexOf('application/json') >= 0){
317                         try {
318                             json = LABKEY.Utils.decode(response.responseText);
319                         }
320                         catch (error){
321                             //we still want to proceed even if we cannot decode the JSON
322                         }
323 
324                     }
325 
326                     response.responseJSON = json;
327                 }
328 
329                 if (!json && isErrorCallback)
330                 {
331                     json = {};
332                 }
333 
334                 if (json && !json.exception && isErrorCallback)
335                 {
336                     // Try to make sure we don't show an empty error message
337                     json.exception = (response && response.statusText ? response.statusText : "Communication failure.");
338                 }
339 
340                 if (fn)
341                     fn.call(scope || this, json, response, options);
342                 else if (isErrorCallback && response.status != 0)
343                 {
344                     // Don't show an error dialog if the user cancelled the request in the browser, like navigating
345                     // to another page
346                     LABKEY.Utils.alert("Error", json.exception);
347                 }
348             };
349         },
350 
351         /**
352          * Applies properties from the source object to the target object, translating
353          * the property names based on the translation map. The translation map should
354          * have an entry per property that you wish to rename when it is applied on
355          * the target object. The key should be the name of the property on the source object
356          * and the value should be the desired name on the target object. The value may
357          * also be set to null or false to prohibit that property from being applied.
358          * By default, this function will also apply all other properties on the source
359          * object that are not listed in the translation map, but you can override this
360          * by supplying false for the applyOthers parameter.
361          * @param target The target object
362          * @param source The source object
363          * @param translationMap A map listing property name translations
364          * @param applyOthers Set to false to prohibit application of properties
365          * not explicitly mentioned in the translation map.
366          * @param applyFunctions Set to false to prohibit application of properties
367          * that are functions
368          */
369         applyTranslated : function(target, source, translationMap, applyOthers, applyFunctions)
370         {
371             if (undefined === target)
372                 target = {};
373             if (undefined === applyOthers)
374                 applyOthers = true;
375             if (undefined == applyFunctions && applyOthers)
376                 applyFunctions = true;
377             var targetPropName;
378             for (var prop in source)
379             {
380                 //special case: Ext adds a "constructor" property to every object, which we don't want to apply
381                 if (prop == "constructor" || LABKEY.Utils.isFunction(prop))
382                     continue;
383                 
384                 targetPropName = translationMap[prop];
385                 if (targetPropName)
386                     target[translationMap[prop]] = source[prop];
387                 else if (undefined === targetPropName && applyOthers && (applyFunctions || !LABKEY.Utils.isFunction(source[prop])))
388                     target[prop] = source[prop];
389             }
390         },
391 
392         /**
393          * Sets a client-side cookie.  Useful for saving non-essential state to provide a better
394          * user experience.  Note that some browser settings may prevent cookies from being saved,
395          * and users can clear browser cookies at any time, so cookies are not a substitute for
396          * database persistence.
397          * @param {String} name The name of the cookie to be saved.
398          * @param {String} value The value of the cookie to be saved.
399          * @param {Boolean} pageonly Whether this cookie should be scoped to the entire site, or just this page.
400          * Page scoping considers the entire URL without parameters; all URL contents after the '?' are ignored.
401          * @param {int} days The number of days the cookie should be saved on the client.
402          */
403         setCookie : function(name, value, pageonly, days) {
404             var expires;
405             if (days)
406             {
407                 var date = new Date();
408                 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
409                 expires = "; expires=" + date.toGMTString();
410             }
411             else
412                 expires = "";
413             var path = "/";
414             if (pageonly)
415                 path = location.pathname.substring(0, location.pathname.lastIndexOf('/'));
416             document.cookie = name + "=" + value + expires + "; path=" + path;
417         },
418 
419         /**
420          * Retrieves a cookie.  Useful for retrieving non-essential state to provide a better
421          * user experience.  Note that some browser settings may prevent cookies from being saved,
422          * and users can clear browser cookies at any time, so previously saved cookies should not be assumed
423          * to be available.
424          * @param {String} name The name of the cookie to be retrieved.
425          * @param {String} defaultvalue The value to be returned if no cookie with the specified name is found on the client.
426          */
427         getCookie : function(name, defaultvalue) {
428             var nameEQ = name + "=";
429             var ca = document.cookie.split(';');
430             for (var i=0; i < ca.length; i++)
431             {
432                 var c = ca[i];
433                 while (c.charAt(0) == ' ')
434                     c = c.substring(1, c.length);
435                 if (c.indexOf(nameEQ) == 0)
436                     return c.substring(nameEQ.length, c.length);
437             }
438             return defaultvalue;
439         },
440 
441         /**
442          * Retrieves the current LabKey Server session ID. Note that this may only be made available when the
443          * session ID cookie is marked as httpOnly = false.
444          * @returns {String} sessionid The current session id. Defaults to ''.
445          * @see {@link https://www.owasp.org/index.php/HttpOnly|OWASP HttpOnly}
446          * @see {@link https://tomcat.apache.org/tomcat-7.0-doc/config/context.html#Common_Attributes|Tomcat Attributes}
447          */
448         getSessionID : function()
449         {
450             return LABKEY.Utils.getCookie('JSESSIONID', '');
451         },
452 
453         /**
454          * Deletes a cookie.  Note that 'name' and 'pageonly' should be exactly the same as when the cookie
455          * was set.
456          * @param {String} name The name of the cookie to be deleted.
457          * @param {Boolean} pageonly Whether the cookie is scoped to the entire site, or just this page.
458          * Deleting a site-level cookie has no impact on page-level cookies, and deleting page-level cookies
459          * has no impact on site-level cookies, even if the cookies have the same name.
460          */
461         deleteCookie : function (name, pageonly)
462         {
463             LABKEY.Utils.setCookie(name, "", pageonly, -1);
464         },
465 
466         /**
467          * Loads JavaScript file(s) from the server.
468          * @function
469          * @param {(string|string[])} file - A file or Array of files to load.
470          * @param {Function} [callback] - Callback for when all dependencies are loaded.
471          * @param {Object} [scope] - Scope of callback.
472          * @param {boolean} [inOrder=false] - True to load the scripts in the order they are passed in. Default is false.
473          * @example
474          <script type="text/javascript">
475          LABKEY.requiresScript("myModule/myScript.js", true, function() {
476                     // your script is loaded
477                 });
478          </script>
479          */
480         requiresScript : function(file, callback, scope, inOrder)
481         {
482             LABKEY.requiresScript.apply(this, arguments);
483         },
484 
485         /**
486          * Includes a Cascading Style Sheet (CSS) file into the page. If the file was already included by some other code, this
487          * function will simply ignore the call. This may be used to include CSS files defined in your module's web/ directory.
488          * @param {String} filePath The path to the script file to include. This path should be relative to the web application
489          * root. So for example, if you wanted to include a file in your module's web/mymodule/styles/ directory,
490          * the path would be "mymodule/styles/mystyles.css"
491          */
492         requiresCSS : function(filePath)
493         {
494             LABKEY.requiresCss(filePath);
495         },
496 
497         /**
498          * Returns true if value ends with ending
499          * @param value the value to examine
500          * @param ending the ending to look for
501          */
502         endsWith : function(value, ending)
503         {
504             if (!value || !ending)
505                 return false;
506             if (value.length < ending.length)
507                 return false;
508             return value.substring(value.length - ending.length) == ending;
509         },
510 
511         pluralBasic : function(count, singular)
512         {
513             return LABKEY.Utils.pluralize(count, singular, singular + 's');
514         },
515 
516         pluralize : function(count, singular, plural)
517         {
518             return count.toLocaleString() + " " + (1 == count ? singular : plural);
519         },
520 
521         /**
522          * Iteratively calls a tester function you provide, calling another callback function once the
523          * tester function returns true. This function is useful for advanced JavaScript scenarios, such
524          * as cases where you are including common script files dynamically using the requiresScript()
525          * method, and need to wait until classes defined in those files are parsed and ready for use.
526          *  
527          * @param {Object} config a configuration object with the following properties:
528          * @param {Function} config.testCallback A function that returns true or false. This will be called every
529          * ten milliseconds until it returns true or the maximum number of tests have been made.
530          * @param {Array} [config.testArguments] A array of arguments to pass to the testCallback function
531          * @param {Function} config.success The function to call when the testCallback returns true.
532          * @param {Array} [config.successArguments] A array of arguments to pass to the successCallback function
533          * @param {Object} [config.failure] A function to call when the testCallback throws an exception, or when
534          * the maximum number of tests have been made.
535          * @param {Array} [config.errorArguments] A array of arguments to pass to the errorCallback function
536          * @param {Object} [config.scope] A scope to use when calling any of the callback methods (defaults to this)
537          * @param {int} [config.maxTests] Maximum number of tests before the errorCallback is called (defaults to 1000).
538          *
539          * @example
540 <script>
541     LABKEY.Utils.requiresScript("FileUploadField.js");
542     LABKEY.Utils.requiresCSS("FileUploadField.css");
543 </script>
544 
545 <script>
546     function tester()
547     {
548         return undefined != Ext.form.FileUploadField;
549     }
550 
551     function onTrue(msg)
552     {
553         //this alert is merely to demonstrate the successArguments config property
554         alert(msg);
555 
556         //use the file upload field...
557     }
558 
559     function onFailure(msg)
560     {
561         alert("ERROR: " + msg);
562     }
563 
564     LABKEY.Utils.onTrue({
565         testCallback: tester,
566         success: onTrue,
567         successArguments: ['FileUploadField is ready to use!'],
568         failure: onFailure,
569         maxTests: 100
570     });
571 </script>
572          */
573         onTrue : function(config) {
574             config.maxTests = config.maxTests || 1000;
575             try
576             {
577                 if (config.testCallback.apply(config.scope || this, config.testArguments || []))
578                     LABKEY.Utils.getOnSuccess(config).apply(config.scope || this, config.successArguments || []);
579                 else
580                 {
581                     if (config.maxTests <= 0)
582                     {
583                         throw "Maximum number of tests reached!";
584                     }
585                     else
586                     {
587                         --config.maxTests;
588                         LABKEY.Utils.onTrue.defer(10, this, [config]);
589                     }
590                 }
591             }
592             catch(e)
593             {
594                 if (LABKEY.Utils.getOnFailure(config))
595                 {
596                     LABKEY.Utils.getOnFailure(config).apply(config.scope || this, [e,config.errorArguments]);
597                 }
598             }
599         },
600 
601         ensureBoxVisible : function() {
602             console.warn('LABKEY.Utils.ensureBoxVisible has been migrated to the appropriate Ext scope. Consider LABKEY.ext.Utils.ensureBoxVisible or LABKEY.ext4.Util.ensureBoxVisible');
603         },
604 
605         /**
606          * Will generate a unique id. If you provide a prefix, consider making it DOM safe so it can be used as
607          * an element id.
608          * @param {string} [prefix=lk-gen] - Optional prefix to start the identifier.
609          * @returns {*}
610          */
611         id : function(prefix) {
612             return (prefix || "lk-gen") + (++idSeed);
613         },
614 
615         /**
616           * Returns a universally unique identifier, of the general form: "92329D39-6F5C-4520-ABFC-AAB64544E172"
617           * NOTE: Do not use this for DOM id's as it does not meet the requirements for DOM id specification.
618           * Based on original Math.uuid.js (v1.4)
619           * http://www.broofa.com
620           * mailto:robert@broofa.com
621           * Copyright (c) 2010 Robert Kieffer
622           * Dual licensed under the MIT and GPL licenses.
623         */
624         generateUUID : function() {
625             // First see if there are any server-generated UUIDs available to return
626             if (LABKEY && LABKEY.uuids && LABKEY.uuids.length > 0)
627             {
628                 return LABKEY.uuids.pop();
629             }
630             // From the original Math.uuidFast implementation
631             var chars = CHARS, uuid = new Array(36), rnd = 0, r;
632             for (var i = 0; i < 36; i++)
633             {
634                 if (i == 8 || i == 13 || i == 18 || i == 23)
635                 {
636                     uuid[i] = '-';
637                 }
638                 else if (i == 14)
639                 {
640                     uuid[i] = '4';
641                 }
642                 else
643                 {
644                     if (rnd <= 0x02) rnd = 0x2000000 + (Math.random() * 0x1000000) | 0;
645                     r = rnd & 0xf;
646                     rnd = rnd >> 4;
647                     uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
648                 }
649             }
650             return uuid.join('');
651         },
652 
653         /**
654          * Returns a string containing a well-formed html anchor that will apply theme specific styling. The configuration
655          * takes any property value pair and places them on the anchor.
656          * @param {Object} config a configuration object that models html anchor properties:
657          * @param {String} config.href (required if config.onClick not specified) the reference the anchor will use.
658          * @param {String} config.onClick (required if config.href not specified) script called when the onClick event is fired by
659          * the anchor.
660          * @param {String} config.text text that is rendered inside the anchor element.
661          */
662         textLink : function(config)
663         {
664             if (config.href === undefined && !config.onClick === undefined)
665             {
666                 throw "href AND/OR onClick required in call to LABKEY.Utils.textLink()";
667             }
668             var attrs = " ";
669             if (config)
670             {
671                 for (var i in config)
672                 {
673                     if (config.hasOwnProperty(i))
674                     {
675                         if (i.toString() != "text" && i.toString() != "class")
676                         {
677                             attrs += i.toString() + '=\"' + config[i] + '\" ';
678                         }
679                     }
680                 }
681 
682                 return '<a class="labkey-text-link"' + attrs + '>' + (config.text != null ? config.text : "") + '</a>';
683             }
684             throw "Config object not found for textLink.";
685         },
686 
687         /**
688          *
689          * Standard documented name for error callback arguments is "failure" but various other names have been employed in past.
690          * This function provides reverse compatibility by picking the failure callback argument out of a config object
691          * be it named failure, failureCallback or errorCallback.
692          *
693          * @param config
694          */
695         getOnFailure : function(config)
696         {
697             return config.failure || config.errorCallback || config.failureCallback;
698             // maybe it be desirable for this fall all the way back to returning LABKEY.Utils.displayAjaxErrorResponse?
699         },
700 
701         /**
702          *
703          * Standard documented name for success callback arguments is "success" but various names have been employed in past.
704          * This function provides reverse compatibility by picking the success callback argument out of a config object,
705          * be it named success or successCallback.
706          *
707          * @param config
708          */
709         getOnSuccess : function(config)
710         {
711             return config.success || config.successCallback
712         },
713 
714 
715         /**
716          * Apply properties from b, c, ...  to a.  Properties of each subsequent
717          * object overwrites the previous.
718          *
719          * The first object is modified.
720          *
721          * Use merge({}, o) to create a deep copy of o.
722          */
723         merge : function(a, b, c)
724         {
725             var o = a;
726             for (var i=1 ; i<arguments.length ; i++)
727                 _merge(o, arguments[i], true, 50);
728             return o;
729         },
730 
731 
732         /**
733          * Apply properties from b, c, ... to a.  Properties are not overwritten.
734          *
735          * The first object is modified.
736          */
737         mergeIf : function(a, b, c)
738         {
739             var o = arguments[0];
740             for (var i=1 ; i<arguments.length ; i++)
741                 _merge(o, arguments[i], false, 50);
742             return o;
743         },
744 
745         onError : function(error){
746             console.warn('onError: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
747         },
748 
749         /**
750          * Returns true if the passed object is empty (ie. {}) and false if not.
751          *
752          * @param {Object} ob The object to test
753          * @return {Boolean} the result of the test
754         */
755         isEmptyObj : function(ob){
756            for (var i in ob){ return false;}
757            return true;
758         },
759 
760         /**
761          * Rounds the passed number to the specified number of decimals
762          *
763          * @param {Number} input The number to round
764          * @param {Number} dec The number of decimal places to use
765          * @return {Number} The rounded number
766         */
767         roundNumber : function(input, dec){
768             return Math.round(input*Math.pow(10,dec))/Math.pow(10,dec);
769         },
770 
771         /**
772          * Will pad the input string with zeros to the desired length.
773          *
774          * @param {Number/String} input The input string / number
775          * @param {Integer} length The desired length
776          * @param {String} padChar The character to use for padding.
777          * @return {String} The padded string
778         **/
779         padString : function(input, length, padChar){
780             if (typeof input != 'string')
781                 input = input.toString();
782 
783             var pd = '';
784             if (length > input.length){
785                 for (var i=0; i < (length-input.length); i++){
786                     pd += padChar;
787                 }
788             }
789             return pd + input;
790         },
791 
792         /**
793          * Returns true if the arguments are case-insensitive equal.  Note: the method converts arguments to strings for the purposes of comparing numbers, which means that it will return odd behaviors with objects (ie. LABKEY.Utils.caseInsensitiveEquals({t: 3}, '[object Object]') returns true)
794          *
795          * @param {String/Number} a The first item to test
796          * @param {String/Number} b The second item to test
797          * @return {boolean} True if arguments are case-insensitive equal, false if not
798         */
799         caseInsensitiveEquals: function(a, b) {
800             return String(a).toLowerCase() == String(b).toLowerCase();
801         },
802 
803         /**
804          * Tests whether the passed value can be used as boolean, using a loose definition.  Acceptable values for true are: 'true', 'yes', 1, 'on' or 't'.  Acceptable values for false are: 'false', 'no', 0, 'off' or 'f'.  Values are case-insensitive.
805          * @param value The value to test
806          */
807         isBoolean: function(value){
808             var upperVal = value.toString().toUpperCase();
809             if (upperVal == "TRUE" || value == "1" || upperVal == "Y" || upperVal == "YES" || upperVal == "ON" || upperVal == "T"
810                     || upperVal == "FALSE" || value == "0" || upperVal == "N" || upperVal == "NO" || upperVal == "OFF" || upperVal == "F"){
811                 return true;
812             }
813         },
814 
815         /**
816          * Returns true if the passed value is a string.
817          * @param {Object} value The value to test
818          * @return {Boolean}
819          */
820         isString: function(value) {
821             return typeof value === 'string';
822         },
823 
824         onReady: function(config) {
825             console.warn('onReady: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
826             if (typeof config === "function") {
827                 config();
828             }
829         },
830 
831         /**
832          * Decodes (parses) a JSON string to an object.
833          *
834          * @param {String} data The JSON string
835          * @return {Object} The resulting object
836          */
837         decode : function(data) {
838             return JSON.parse(data + "");
839         },
840 
841         /**
842          * Encodes an Object to a string.
843          *
844          * @param {Object} data the variable to encode.
845          * @return {String} The JSON string.
846          */
847         encode : function(data) {
848             return JSON.stringify(data);
849         },
850 
851         /**
852          * Applies config properties to the specified object.
853          * @param object
854          * @param config
855          * @returns {*}
856          */
857         apply : function(object, config) {
858             var enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable',
859                 'toLocaleString', 'toString', 'constructor'];
860 
861             if (object && config && typeof config === 'object') {
862                 var i, j, k;
863 
864                 for (i in config) {
865                     object[i] = config[i];
866                 }
867 
868                 if (enumerables) {
869                     for (j = enumerables.length; j--;) {
870                         k = enumerables[j];
871                         if (config.hasOwnProperty(k)) {
872                             object[k] = config[k];
873                         }
874                     }
875                 }
876             }
877             return object;
878         },
879 
880         /**
881          * Display an error dialog
882          * @param title
883          * @param msg
884          */
885         alert : function(title, msg) {
886             console.warn('alert: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
887             console.warn(title + ":", msg);
888         },
889 
890 
891         escapeRe : function(s) {
892             return s.replace(/([-.*+?\^${}()|\[\]\/\\])/g, "\\$1");
893         },
894 
895         getMeasureAlias : function(measure, override) {
896             if (measure.alias && !override) {
897                 return measure.alias;
898             }
899             else {
900                 var alias = measure.schemaName + '_' + measure.queryName + '_' + measure.name;
901                 return alias.replace(/\//g, '_');
902             }
903         },
904 
905         // private
906         collapseExpand: collapseExpand,
907         notifyExpandCollapse: notifyExpandCollapse,
908         toggleLink: toggleLink
909     };
910 };
911 
912 /** docs for methods defined in dom/Utils.js - primarily here to ensure API docs get generated with combined core/dom versions */
913 
914 /**
915  * Sends a JSON object to the server which turns it into an Excel file and returns it to the browser to be saved or opened.
916  * @memberOf LABKEY.Utils
917  * @function
918  * @static
919  * @name convertToExcel
920  * @param {Object} spreadsheet the JavaScript representation of the data
921  * @param {String} spreadsheet.fileName name to suggest to the browser for saving the file. If the fileName is
922  * specified and ends with ".xlsx", it will be returned in Excel 2007 format.
923  * @param {String} spreadsheet.sheets array of sheets, which are objects with properties:
924  * <ul>
925  * <li><b>name:</b> name of the Excel sheet</li>
926  * <li><b>data:</b> two dimensional array of values</li>
927  * </ul>
928  * The value array may be either primitives (booleans, numbers, Strings, and dates), or may be a map with
929  * the following structure:
930  * <ul>
931  * <li><b>value:</b> the boolean, number, String, or date value of the cell</li>
932  * <li><b>formatString:</b> for dates and numbers, the Java format string used with SimpleDateFormat
933  * or DecimalFormat to control how the value is formatted</li>
934  * <li><b>timeOnly:</b> for dates, whether the date part should be ignored and only the time value is important</li>
935  * <li><b>forceString:</b> force the value to be treated as a string (i.e. prevent attempt to convert it to a date)</li>
936  * </ul>
937  * @example <script type="text/javascript">
938  LABKEY.Utils.convertToExcel(
939  {
940      fileName: 'output.xls',
941      sheets:
942      [
943          {
944              name: 'FirstSheet',
945              data:
946              [
947                  ['Row1Col1', 'Row1Col2'],
948                  ['Row2Col1', 'Row2Col2']
949              ]
950          },
951          {
952              name: 'SecondSheet',
953              data:
954              [
955                  ['Col1Header', 'Col2Header'],
956                  [{value: 1000.5, formatString: '0,000.00'}, {value: '5 Mar 2009 05:14:17', formatString: 'yyyy MMM dd'}],
957                  [{value: 2000.6, formatString: '0,000.00'}, {value: '6 Mar 2009 07:17:10', formatString: 'yyyy MMM dd'}]
958 
959              ]
960          }
961      ]
962  });
963  </script>
964  */
965 
966 /**
967  * Sends a JSON object to the server which turns it into an TSV or CSV file and returns it to the browser to be saved or opened.
968  * @memberOf LABKEY.Utils
969  * @function
970  * @static
971  * @name convertToTable
972  * @param {Object} config.  The config object
973  * @param {String} config.fileNamePrefix name to suggest to the browser for saving the file. The appropriate extension (either ".txt" or ".csv", will be appended based on the delim character used (see below).  Defaults to 'Export'
974  * @param {String} config.delim The separator between fields.  Allowable values are 'COMMA' or 'TAB'.
975  * @param {String} config.quoteChar The character that will be used to quote each field.  Allowable values are 'DOUBLE' (ie. double-quote character), 'SINLGE' (ie. single-quote character) or 'NONE' (ie. no character used).  Defaults to none.
976  * @param {String} config.newlineChar The character that will be used to separate each line.  Defaults to '\n'
977  * @param {String} config.rows array of rows, which are arrays with values for each cell.
978  * @example <script type="text/javascript">
979  LABKEY.Utils.convertToTable(
980  {
981      fileName: 'output.csv',
982      rows:
983      [
984          ['Row1Col1', 'Row1Col2'],
985          ['Row2Col1', 'Row2Col2']
986      ],
987      delim: 'COMMA'
988  });
989  </script>
990  */
991 
992 /**
993  * Display an error dialog
994  * @memberOf LABKEY.Utils
995  * @function
996  * @static
997  * @name alert
998  * @param title
999  * @param msg
1000  */
1001 
1002 /**
1003  * Sets the title of the webpart on the page.  This change is not sticky, so it will be reverted on refresh.
1004  * @memberOf LABKEY.Utils
1005  * @function
1006  * @static
1007  * @name setWebpartTitle
1008  * @param {string} title The title string
1009  * @param {integer} webPartId The ID of the webpart
1010  */
1011 
1012 /**
1013  * Provides a generic error callback.  This helper show a modal dialog, log the error to the console
1014  * and will log the error to the audit log table. The user must have insert permissions on the selected container for
1015  * this to work.  By default, it will insert the error into the Shared project.  A containerPath param can be passed to
1016  * use a different container.  The intent of this helper is to provide site admins with a mechanism to identify errors associated
1017  * with client-side code.  If noAuditLog=true is used, the helper will not log the error.
1018  *
1019  * @memberOf LABKEY.Utils
1020  * @function
1021  * @static
1022  * @name onError
1023  * @param {Object} error The error object passed to the callback function
1024  * @param {String} [error.containerPath] Container where errors will be logged. Defaults to /shared
1025  * @param {Boolean} [error.noAuditLog] If false, the errors will not be logged in the audit table.  Defaults to true
1026  *
1027  * @example <script type="text/javascript">
1028  //basic usage
1029  LABKEY.Query.selectRows({
1030                 schemaName: 'core',
1031                 queryName: 'users',
1032                 success: function(){},
1033                 failure: LABKEY.Utils.onError
1034             });
1035 
1036  //custom container and turning off logging
1037  LABKEY.Query.selectRows({
1038                 schemaName: 'core',
1039                 queryName: 'users',
1040                 success: function(){},
1041                 failure: function(error){
1042                      error.containerPath = 'myContainer';
1043                      error.noAuditLog = true;
1044                      LABKEY.Utils.onError(error);
1045                 }
1046             });
1047  </script>
1048  */
1049 
1050 /**
1051  * Shows an error dialog box to the user in response to an error from an AJAX request, including
1052  * any error messages from the server.
1053  *
1054  * @memberOf LABKEY.Utils
1055  * @function
1056  * @static
1057  * @name displayAjaxErrorResponse
1058  * @param {XMLHttpRequest} responseObj The XMLHttpRequest object containing the response data.
1059  * @param {Error} [exceptionObj] A JavaScript Error object caught by the calling code.
1060  * @param {boolean} [showExceptionClass] Flag to display the java class of the exception.
1061  * @param {String} [msgPrefix] Prefix to the error message (defaults to: 'An error occurred trying to load:')
1062  * The error dialog will display the Error's name and message, if available.
1063  */
1064 
1065 /**
1066  * Adds new listener to be executed when all required scripts are fully loaded.
1067  * @memberOf LABKEY.Utils
1068  * @function
1069  * @static
1070  * @name onReady
1071  * @param {Mixed} config Either a callback function, or an object with the following properties:
1072  *
1073  * <li>callback (required) A function that will be called when required scripts are loaded.</li>
1074  * <li>scope (optional) The scope to be used for the callback function.  Defaults to the current scope.</li>
1075  * <li>scripts (optional) A string with a single script or an array of script names to load.  This will be passed to LABKEY.requiresScript().</li>
1076  * @example <script type="text/javascript">
1077  //simple usage
1078  LABKEY.onReady(function(){
1079                     //your code here.  will be executed once scripts have loaded
1080                 });
1081 
1082  //
1083  LABKEY.Utils.onReady({
1084                     scope: this,
1085                     scripts: ['/myModule/myScript.js', 'AnotherScript.js],
1086                     callback: function(){
1087                         //your code here.  will be executed once scripts have loaded
1088                     });
1089                 });
1090  </script>
1091  */
1092