UB.js

/* global window, navigator, Blob, FileReader, document, XMLHttpRequest,setTimeout,clearTimeout */
/**
 * The UB namespace (global object) encapsulates all classes, singletons, and
 * utility methods provided by UnityBase for build a transport layer for a Web applications.
 *
 * The main entry point for most operation is {@link UBConnection UBConnection} for communication with UnityBase server.
 *
 * @namespace UB
 * @author pavel.mash
 */
var UB = {};
//<editor-fold desc="Service functions block">
var Q = require('./libs/q'),
    /*last request content & date we send to server. We use it to prevent a reiteration requests with the same content */
    __lastRequestData,
    __lastRequestTime = new Date().getTime(),
    MODEL_RE = new RegExp('models/(.+?)/'),// speculative search. w/o ? found maximum string length
    FORMAT_RE = /\{(\d+)}/g,
    __i18n = {
        monkeyRequestsDetected: 'Your request has been processed, but we found that it is repeated several times. Maybe you key fuse?'
    };

/**
 * Copies all the properties of one or several objectsFrom to the specified objectTo.
 * Non-simple type copied by reference!
 * @param {Object} objectTo The receiver of the properties
 * @param {...Object} objectsFrom The source(s) of the properties
 * @return {Object} returns objectTo
 */
UB.apply = function(objectTo, objectsFrom) {
    Array.prototype.forEach.call(arguments, function(obj) {
        if (obj && obj !== objectTo) {
            Object.keys(obj).forEach(function(key) {
                objectTo[key] = obj[key];
            });
        }
    });
    return objectTo;
};

/**
 * Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens.  Each
 * token must be unique, and must increment in the format {0}, {1}, etc.  Example usage:
 *
 *     var s = UB.format('{1}/ext-lang-{0}.js', 'en', 'locale');
 *     // s now contains the string: ''locale/ext-lang-en.js''
 *
 * @param {String} stringToFormat The string to be formatted.
 * @param {...*} values The values to replace tokens `{0}`, `{1}`, etc in order.
 * @return {String} The formatted string.
 */
UB.format = function(stringToFormat, values) {
    var args = _.toArray(arguments).slice(1);
    return stringToFormat.replace(FORMAT_RE, function(m, i) {
        return args[i];
    });
};

/**
 * Creates namespaces to be used for scoping variables and classes so that they are not global.
 * @example
 *     UB.ns('DOC.Report');
 *
 *     DOC.Report.myReport = function() { ... };
 *
 * @method
 * @param {String} namespacePath
 * @return {Object} The namespace object.
 */
UB.ns = function(namespacePath){
    var root = window,
        parts, part, j, subLn;

    parts = namespacePath.split('.');

    for (j = 0, subLn = parts.length; j < subLn; j++) {
        part = parts[j];

        if (!root[part]) {
            root[part] = {};
        }
        root = root[part];
    }
    return root;
};

/**
 * Convert UnityBase server dateTime response to Date object
 * @param value
 * @returns {Date}
 */
UB.iso8601Parse = function(value) {
    return value ? new Date(value): null;
};

/**
 * Convert UnityBase server date response to Date object.
 * date response is a day with 00 time (2015-07-17T00:00Z), to get a real date we must add current timezone shift
 * @param value
 * @returns {Date}
 */
UB.iso8601ParseAsDate = function(value) {
    var res = value ? new Date(value): null;
    if (res) {
        res.setTime(res.getTime() + res.getTimezoneOffset() * 60 * 1000);
    }
    return res;
};

/**
 * Convert UnityBase server Boolean response to Boolean (0 = false & 1 = trhe)
 * @param v Value to convert
 * @returns {Boolean|null}
 */
UB.booleanParse = function(v) {
    if (typeof v === 'boolean') {
        return v;
    }
    if ((v === undefined || v === null || v === '')) {
        return null;
    }
    return v === 1;
};

/**
 * Return locale-specific resource from it identifier.
 * localeString must be previously defideb dy call to {@link UB#i18nExtend}
 * @method
 * @param {String} localeString
 * @returns {*}
 */
UB.i18n = function(localeString){
    return __i18n[localeString] || localeString;
};

/**
 * Merge localizationObject to UB.i18n. Usually called form modelPublic/locale/lang-*.js scripts
 * @method
 * @param {Object} localizationObject
 */
UB.i18nExtend = function(localizationObject){
    _.merge(__i18n, localizationObject);
};

/** @type {String} */
UB.userAgent = navigator.userAgent.toLowerCase();
/** @type {Boolean} */
UB.isChrome = /\bchrome\b/.test(UB.userAgent);
/** @type {Boolean} */
UB.isWebKit = /webkit/.test(UB.userAgent);
/** @type {Boolean} */
UB.isGecko = !UB.isWebKit && /gecko/.test(UB.userAgent);
/** @type {Boolean} */
UB.isOpera = /opr|opera/.test(UB.userAgent);
/** @type {Boolean} */
UB.isMac = /macintosh|mac os x/.test(UB.userAgent);
/** @type {Boolean} */
UB.isSecureBrowser = /\belectron\b/.test(UB.userAgent);
//</editor-fold>

UB.consts = {

};

// if (UB.isSecureBrowser) {
//     var electron = require('electron');
//     electron.ipcRenderer.on('SETTINGS', function(event, message){
//         UB.secureSettings = JSON.parse(message);
//     });
//     var session = electron.remote.session;
//     session.defaultSession.on('will-download', function (event, item, webContents) {
//         var fileName, savePath;
//         var path = require('path');
//         savePath = path.join(UB.secureSettings.downloadFolder, item.getFilename());
//         item.setSavePath(savePath);
//         //event.preventDefault();
//         //'URL', item.getURL(), 'MIME', item.getMimeType(), 'hasUserGesture', item.hasUserGesture(), 'getFileName', item.getFilename(), 'contentDisposition', item.getContentDisposition());
//         item.once('updated', function(event, state){
//             fileName = item.getFilename();
//             savePath = item.getSavePath();
//         });
//         item.once('done', function (event, state) {
//             if (state === 'completed') {
//                 $App.connection.query({
//                     entity: 'uba_audit',
//                     method: 'secureBrowserEvent',
//                     action: 'DOWNLOAD',
//                     reason: savePath
//                 }).then(function(){
//                         return $App.dialogInfo('Файл збережено до ' + savePath);
//                 }).done();
//             } else {
//                 $App.dialogError('Завантаження відмінено');
//             }
//         });
//     });
// }

var BASE64STRING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var BASE64ARR = [];
(function(){
    for(var i = 0, l = BASE64STRING.length - 1; i < l; i++){
        BASE64ARR.push(BASE64STRING[i]);
    }
})();

/**
 * Transform ArrayBuffer to base64 string
 * @deprecated Since 1.9.6 use x10 times faster async {@link UB.base64FromAny} instead
 * @method
 * @private
 * @param {ArrayBuffer} arraybuffer
 * @returns {String}
 */
UB.base64fromArrayBuffer =  function(arraybuffer) {
    //MPV this is fastest synch implementation possible. TODO - check 4-bytes table?
    //console.time('b641'); for(var t=0; t<100; t++){var v = new Uint8Array(10000); for(k=0; k<10000; k++){v[k]=k%100}; var b641 = UB.base64fromArrayBuffer4(v);} console.timeEnd('b641');
    var bytes = new Uint8Array(arraybuffer),
        i, len = bytes.buffer.byteLength, base64 = '';

    for (i = 0; i < len; i+=3) {
        base64 += BASE64STRING[bytes[i] >> 2];
        base64 += BASE64STRING[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
        base64 += BASE64STRING[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
        base64 += BASE64STRING[bytes[i + 2] & 63];
    }

    if ((len % 3) === 2) {
        base64 = base64.substring(0, base64.length - 1) + "=";
    } else if (len % 3 === 1) {
        base64 = base64.substring(0, base64.length - 2) + "==";
    }

    return base64;
};

/**
 * Fast async transformation of data to base64 string
 * @method
 * @param {File|ArrayBuffer|String|Blob|Array} data
 * @returns {Promise} resolved to data converted to base64 string
 */
UB.base64FromAny =  function(data) {
    var
        reader = new FileReader(),
        deferred = Q.defer(),
        blob;
    blob = (data instanceof Blob) ? data : new Blob([data]);
    reader.addEventListener('loadend', function () {
        deferred.resolve(reader.result.split(',', 2)[1]); //remove data:....;base64, from the beginning of string
    });
    reader.addEventListener('error', function (event) {
        deferred.reject(event);
    });
    reader.readAsDataURL(blob);
    return deferred.promise;
};

/**
 * Convert dataBlob contain PDF document to base64 string
 * @deprecated Since 1.9.6 use {@link UB.base64FromAny} instead
 * @private
 * @param {Blob} pdfDataBlob
 * @returns {Promise} Promise sesolved to base64 representation of dataBlob
 */
UB.base64fromPdfDataBlob = function(pdfDataBlob){
    return UB.base64FromAny(pdfDataBlob);
};

var BASE64DECODELOOKUP = new Uint8Array( 256 );
(function(){
    for( var i = 0, l=BASE64STRING.length; i < l; i ++ ) {
        BASE64DECODELOOKUP[BASE64STRING[i].charCodeAt(0)] = i;
    }
})();

/**
 * Convert base54 encoded string to decoded array buffer
 * @param {String} base64
 * @returns {ArrayBuffer}
 */
UB.base64toArrayBuffer = function(base64) {
    var bufferLength = base64.length * 0.75,
        len = base64.length, i, p = 0,
        encoded1, encoded2, encoded3, encoded4;

    if (base64[base64.length - 1] === "=") {
        bufferLength--;
        if (base64[base64.length - 2] === "=") {
            bufferLength--;
        }
    }

    var arrayBuffer = new ArrayBuffer(bufferLength),
        bytes = new Uint8Array(arrayBuffer);

    for (i = 0; i < len; i+=4) {
        encoded1 = BASE64DECODELOOKUP[base64.charCodeAt(i)];
        encoded2 = BASE64DECODELOOKUP[base64.charCodeAt(i+1)];
        encoded3 = BASE64DECODELOOKUP[base64.charCodeAt(i+2)];
        encoded4 = BASE64DECODELOOKUP[base64.charCodeAt(i+3)];

        bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
        bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
        bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
    }

    return arrayBuffer;
};

/** @property
 *  index*.html template can define models versions inside this property.
 *  See implementation inside /models/UBM/index.html.js
 *
 *  If so, {@link UB.addResourceVersion UB.addResourceVersion} will add
 *  version parameter to scripts inside models.
 *
 *  Also used in {@link UB#require UB.require}
 *  @private
 */
UB.__ubVersion = window.__ubVersion;

/**
 * @property {Object} appConfig UnityBase application parameter.
 * All this parameters can be configured in ubConfig.uiSettings.adminUI section of application configuration file.
 *
 * @property {String} appConfig.applicationName Name of current application. This text shown in the left side of main window toolbar in Ext-based web client
 * @property {String} appConfig.loginWindowTopLogoURL
 * @property {String} appConfig.loginWindowBottomLogoURL
 * @property {String} appConfig.defaultLang Default application language
 * @property {Array.<String>} appConfig.supportedLanguages Array of all languages supported by application.
 * @property {String} appConfig.defaultPasswordForDebugOnly Fill password on login window. Set this parameter for debug purpose only!
 *
 * @property {Number} appConfig.comboPageSize  Page size of UBCombobox drop-down (in case !disablePaging)
 * @property {Number} appConfig.maxMainWindowTabOpened How many tab user can open in the same time in the main workspace
 *
 * @property {Number} appConfig.storeDefaultPageSize Default page size of UnityBase store
 * @property {Number} appConfig.gridHeightDefault Default height of grid in form
 * @property {Number} appConfig.gridWidthDefault  Default width of grid in form
 * @property {Number} appConfig.gridParentChangeEventTimeout  Timeout for fire parentchange event
 * @property {Number} appConfig.gridDefaultDetailViewHeight  Default height of detail grid and preview form inside master grid
 *
 * @property {Number} appConfig.formMinHeight Minimum form height
 * @property {Number} appConfig.formMinWidth  Minimum form width
 * @property {Number} appConfig.formDefaultAutoFormWidth  Default width of auto-generated form
 * @property {Number} appConfig.formSaveMaskDelay  How long (in ms) wait before mask form while save action call. Usually save if quick operation and we do not need mask form at all
 * @property {Number} appConfig.scanRecognizeProgressInterval Callback call interval while do scan recognition using UBDesktopService extension
 * @property {String} appConfig.browserExtensionNMHostAppKey Native messages plugin application key
 */
 UB.appConfig = {
    applicationName: 'UnityBase',
    loginWindowTopLogoURL: '', //'images/UBLogo128.png',
    loginWindowBottomLogoURL: '',
    themeName: 'UBtheme',

    userDbVersion: null,
    defaultLang: 'en',
    supportedLanguages: ['en'],
    defaultPasswordForDebugOnly: '',
    comboPageSize: 30,
    maxMainWindowTabOpened: 10,
    storeDefaultPageSize: 100,

    gridHeightDefault: 400,
    gridWidthDefault: 600,
    gridParentChangeEventTimeout: 200,
    gridDefaultDetailViewHeight: 150,

    formMinHeight: 100,
    formMinWidth: 300,
    formDefaultAutoFormWidth: 300,
    formSaveMaskDelay: 100,

    scanRecognizeProgressInterval: 1000,
    maxSearchLength: 62,
    browserExtensionNMHostAppKey: 'com.inbase.ubmessagehost'
 };

/**
 * Log message to console (if console available)
 * @method
 * @param {...*} msg
 */
UB.log = log;
function log(msg){
    if (console){
        console.log.apply(console, arguments);
    }
}

/**
 * Log error message to console (if console available)
 * @method
 * @param {...*} msg
 */
UB.logError = logError;
function logError(msg){
    if (console){
        console.error.apply(console, arguments);
    }
}

/**
 * Log warning message to console (if console available)
 * @method
 * @param {...*} msg
 */
UB.logWarn = logWarn;
function logWarn(msg){
    if (console){
        console.warn.apply(console, arguments);
    }
}

/**
 * Log debug message to console.
 * Since it binded to console, can also be used to debug Promise resolving in this way:
 *
 *      UB.get('timeStamp').done(UB.logDebug);
 *
 * @method
 * @param {...*} msg
 */
UB.logDebug = console.info.bind(console);

/**
 * The onError callback
 * @callback UB-onError
 * @param {string} errorMessage
 * @param {Number} [errorNum]
 */

/**
 * onError handler. If set - this one is call, otherwise - default handler will be called
 * @type {UB-onError}
 */
UB.onError = null;

/** @method
 * @param {String} errMsg
 * @param {Number} [errCode]
 * @type {Function}
 */
UB.doOnProcessError = doOnProcessError;
function doOnProcessError(errMsg, errCode){
    if(UB.onError){
        UB.onError(errMsg, errCode);
    }else{
        throw new Error(errMsg);
    }
}

/**
 * Handler, called by login method. This handler must show login window and call callback with parameter:
 *   { authSchema: authSchemaUserChoose, login: loginNameUserInput, pwd: passwordUserInput, callback: Function(result)}
 * after user confirm Login.
 * After do authentication UB call callback with object {success: Boolean, errMsg: String}
 * Login window can use data from {@link UB#appConfig UB.appConfig} - it is initialized before login call
 * @property {function(Function)}
 */
UB.onShowLoginWindow = null;

UB.doLogin = function doLogin(){
    if (!UB.onShowLoginWindow){
       throw new Error('no UB.onShowLoginWindow handler defined');
    }
};

/**
 * Search for resource version in the  window.__ubVersion global const
 * IF any,  return 'ver=version' else ''
 * @param {String} uri
 * @returns {String}
 */
UB.getResourceVersion = function(uri){
    var
        modelName = MODEL_RE.test(uri) ? MODEL_RE.exec(uri)[1] : '_web';
    if (UB.__ubVersion && UB.__ubVersion[modelName]){
        return '?ubver=' + UB.__ubVersion[modelName];
    } else {
        return '';
    }
};

/**
 * Exec UB.getResourceVersion and if any - add ?ver to resource and return resource with ?ver
 * @method
 * @param {String} uri
 * @returns {String} uri with added resource version
 */
UB.addResourceVersion = function(uri){
    var
        ver = UB.getResourceVersion(uri);
    return ver ? uri + ver : uri;
};


//<editor-fold desc="Promise-based script injection">
var
    __loadedScript = {},
    __head = document.getElementsByTagName("head")[0];

/**
 * Inject external script or css to DOM and return a promise to be resolved when script is loaded.
 *
 * Implement single load mode (if script successfully loaded using {@link UB.inject UB.inject} it not loaded anymore.
 *
 * @example
 *
      //Load script.js:
      UB.inject('jslibs/script.js').done();

      //Load several script at once and error handling:
      Q.all([UB.inject('jslibs/script.js'), UB.inject('script2.js')])
        .fail(function(err){
           console.log('Oh! error occurred: ' + err) ;
        });

      //Load one script and then load other
      UB.inject('jslibs/js_beautify.js')
       .then(function(){
           console.log('first script loaded. Continue to load second');
           return UB.inject('jslibs/js_beautify1.js');
       }).done();

       //Load couple of resources:
      Q.all([UB.inject('css/first.css'), UB.inject('css/second.css')]).done();

 * @method
 * @param {String} url either *js* or *css* resource to load
 * @param {String} [charset]
 * @return {Promise}
 */
UB.inject = function( url, charset ) {
    var
        elm  = null,
        dfd  = Q.defer(),
        injectScript = function (url, resultHandler, failHandler) {
            var
                isCSS = /\.css(?:\?|$)/.test(url);
            if (isCSS){
                elm = document.createElement('link');
                elm.rel = 'stylesheet';
                elm.async = true;
            } else {
                elm = document.createElement('script');
                elm.type = 'text/javascript';
                if (charset){
                    elm.charset = charset;
                }
                elm.async = true;
            }
            elm.onerror = failHandler;

            if ('addEventListener' in elm) {
                elm.onload = resultHandler;
            } else if ('readyState' in elm) {   // for <IE9 Compatability
                elm.onreadystatechange = function () {
                    if (this.readyState === 'loaded' || this.readyState === 'complete') {
                        resultHandler();
                    }
                };
            } else {
                elm.onload = resultHandler;
            }

            __head.appendChild(elm);
            // src must be set AFTER onload && onerror && appendChild
            if (isCSS){
                elm.href = url;
            } else {
                elm.src = url;
            }
            return elm;
        },
        onLoadOK = function() {
            elm.onerror = elm.onload = elm.onreadystatechange = null;
            setTimeout(function(){ // script must evaluate first
                dfd.resolve();
                // Remove the script (do not remove CSS) ???
                if ( elm.parentNode && !elm.rel) {
                    elm.parentNode.removeChild( elm );
                }
            }, 0);
        },
        onLoadFail = function(oError) {
            var
                reason = 'Required ' + (oError.target.href || oError.target.src) + ' is not accessible';
            delete __loadedScript[url];
            elm.onerror = elm.onload = elm.onreadystatechange = null;
            dfd.reject(new Error(reason));
        };

    if (__loadedScript[url]){
        dfd.resolve(__loadedScript[url]);
    } else {
        // Create and inject script tag at end of DOM body and load the external script
        // attach event listeners that will trigger the Deferred.
        __loadedScript[url] = dfd;
        injectScript( UB.addResourceVersion(url), onLoadOK, onLoadFail );
    }
    return dfd.promise;
};
//</editor-fold>

//<editor-fold desc="Promise-based XHR">
function lowercase(str) {
    return (str || '').toLowerCase();
}

function parseHeaders(headers) {
    var parsed = {}, key, val, i;

    if (!headers) {
        return parsed;
    }

    headers.split('\n').forEach(function(line) {
        i = line.indexOf(':');
        key = lowercase(line.substr(0, i).trim());
        val = line.substr(i + 1).trim();

        if (key) {
            if (parsed[key]) {
                parsed[key] += ', ' + val;
            } else {
                parsed[key] = val;
            }
        }
    });

    return parsed;
}

function headersGetter(headers) {
    var headersObj = typeof headers === 'object' ? headers : undefined;
    return function(name) {
        if (!headersObj) {
            headersObj = parseHeaders(headers);
        }
        if (name) {
            return headersObj[lowercase(name)];
        }
        return headersObj;
    };
}

function transformData(data, headers, fns) {
    if (typeof fns === 'function') {
        return fns(data, headers);
    }
    fns.forEach(function(fn) {
        data = fn(data, headers);
    });
    return data;
}

function transformDataPromise(data, headers, fns) {
    var rpromise = Q.resolve(data);
    if (typeof fns === 'function') {
        return rpromise.then(function(rdata){
            return fns(data, headers);
        });
    }
    fns.forEach(function(fn) {
        rpromise = rpromise.then(function(rdata){
            return fn(data, headers);
        });
    });
    return rpromise;
}


function isSuccess(status) {
    return 200 <= status && status < 300;
}

function forEach(obj, iterator, context) {
    var keys = Object.keys(obj);
    keys.forEach(function(key) {
        iterator.call(context, obj[key], key);
    });
    return keys;
}

function forEachSorted(obj, iterator, context) {
    var keys = Object.keys(obj).sort();
    keys.forEach(function(key) {
        iterator.call(context, obj[key], key);
    });
    return keys;
}

function buildUrl(url, params) {
    if (!params) {
        return url;
    }
    var parts = [];
    forEachSorted(params, function(value, key) {
        if (value == null) { // jshint ignore:line
            return;
        }
        if (!Array.isArray(value)) {
            value = [value];
        }

        value.forEach(function(v) {
            if (typeof v === 'object') {
                v = JSON.stringify(v);
            }
            parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(v));
        });
    });
    return url + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&');
}

/**
 * Promise of perform an asynchronous HTTP request
 * Returns a {@link Q promise} object with the
 *   standard Promise methods (<a href="https://github.com/kriskowal/q/wiki/Coming-from-jQuery#reference">reference</a>).
 *   The `then` method takes two arguments a success and an error callback which will be called with a
 *   response object. The arguments passed into these functions are destructured representation of the response object passed into the
 *   `then` method. The response object has these properties:
 *
 *   - **data** – `{string|Object}` – The response body transformed with the transform
 *     functions. Default transform check response content-type is application/json and if so - convert data to Object
 *   - **status** – `{number}` – HTTP status code of the response.
 *   - **headers** – `{function([headerName])}` – Header getter function.
 *   - **config** – `{Object}` – The configuration object that was used to generate the request.
 *
 *  @example
 *
 *      //Get some data from server:
 *      UB.xhr({url: 'getAppInfo'}).done(function(resp) {
 *          console.log('this is appInfo: %o', resp.data)
 *      });
 *
 *      //The same, but in more short form via {@link UB#get UB.get} shorthand:
 *      UB.get('getAppInfo').done(function(resp) {
 *          console.log('this is appInfo: %o', resp.data)
 *      });
 *
 *      //Run POST method:
 *      UB.post('ubql', [
 *          {entity: 'uba_user', method: 'select', fieldList: ['*']}
 *      ]).then(function(resp) {
 *          console.log('success!');
 *      }, function(resp) {
 *          console.log('request failed with status' + resp.status);
 *      });
 *
 *      //retrieve binary data as ArrayBuffer
 *      UB.get('downloads/cert/ACSK(old).cer', {responseType: 'arraybuffer'})
 *      .done(function(res){
 *          console.log('Got Arrray of %d length', res.data.byteLength);
 *      });
 *
 * @method
 * @param {Object} requestConfig Object describing the request to be made and how it should be
 *    processed. The object has following properties:
 * @param {String} requestConfig.url  Absolute or relative URL of the resource that is being requested
 * @param {String} [requestConfig.method] HTTP method (e.g. 'GET', 'POST', etc). Default is GET
 * @param {Object.<string|Object>} [requestConfig.params] Map of strings or objects which will be turned
 *      to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified
 * @param {String|Object} [requestConfig.data] Data to be sent as the request message data
 * @param {Object} [requestConfig.headers]  Map of strings or functions which return strings representing
 *      HTTP headers to send to the server. If the return value of a function is null, the
 *      header will not be sent. Merged with {@link UB#xhrDefaults UB.xhrDefaults.headers}
 * @param {function(data, function)|Array.<function(data, function)>} [requestConfig.transformRequest]
 *      Transform function or an array of such functions. The transform function takes the http
 *      request body and headers and returns its transformed (typically serialized) version.
 * @param {function(data, function)|Array.<function(data, function)>} [requestConfig.transformResponse]
 *      Transform function or an array of such functions. The transform function takes the http
 *      response body and headers and returns its transformed (typically deserialized) version.
 * @param  {Number|Promise} [requestConfig.timeout] timeout in milliseconds, or {@link Q promise}
 *      that should abort the request when resolved. Default to {UB.xhrDefaults.timeout}
 * @param  {Boolean} [requestConfig.withCredentials] whether to to set the `withCredentials` flag on the
 *      XHR object. See <a href="https://developer.mozilla.org/en/http_access_control#section_5">requests with credentials</a>
 *      for more information.
 * @param  {String} [requestConfig.responseType] see <a href="https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType">responseType</a>.
 *
 * @returns {Promise}
 */
UB.xhr = function(requestConfig) {
    var defaults = UB.xhrDefaults,
        config = {
            transformRequest: defaults.transformRequest,
            transformResponse: defaults.transformResponse
        },
        mergeHeaders = function(config) {
            var defHeaders = defaults.headers,
                reqHeaders = UB.apply({}, config.headers),
                defHeaderName, lowercaseDefHeaderName, reqHeaderName,

                execHeaders = function(headers) {
                    forEach(headers, function(headerFn, header) {
                        if (typeof headerFn === 'function') {
                            var headerContent = headerFn();
                            if (headerContent) {
                                headers[header] = headerContent;
                            } else {
                                delete headers[header];
                            }
                        }
                    });
                };

            defHeaders = UB.apply({}, defHeaders.common, defHeaders[lowercase(config.method)]);

            // execute if header value is function
            execHeaders(defHeaders);
            execHeaders(reqHeaders);

            // using for-in instead of forEach to avoid unecessary iteration after header has been found
            defaultHeadersIteration:
                //noinspection JSUnfilteredForInLoop,JSHint
                for (defHeaderName in defHeaders) {
                    //noinspection JSUnfilteredForInLoop
                    lowercaseDefHeaderName = lowercase(defHeaderName);
                    for (reqHeaderName in reqHeaders) {
                        //noinspection JSUnfilteredForInLoop
                        if (lowercase(reqHeaderName) === lowercaseDefHeaderName) {
                            continue defaultHeadersIteration;
                        }
                    }
                    //noinspection JSUnfilteredForInLoop
                    reqHeaders[defHeaderName] = defHeaders[defHeaderName];
                }
            return reqHeaders;
        },
        headers = mergeHeaders(requestConfig);

    UB.apply(config, requestConfig);
    config.headers = headers;
    config.method = (config.method || 'GET').toUpperCase();

    var transformResponse, serverRequest, promise;

    transformResponse = function(response) {
        return transformDataPromise(response.data, response.headers, config.transformResponse)
        .then(function(trdData){
           response.data = trdData;
           return isSuccess(response.status) ? response : Q.reject(response);
        });
    };

    serverRequest = function(config) {
        headers = config.headers;
        var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);
        var prevReqTime = __lastRequestTime;
        __lastRequestTime = new Date().getTime();
        // strip content-type if data is undefined
        if (!config.data) {
            forEach(headers, function(value, header) {
                if (lowercase(header) === 'content-type') {
                    delete headers[header];
                }
            });
        } else {
            // prevent reiteration sending of the same request
            // for example if HTML button on the form got a focus and `space` pressed
            // in case button not disabled inside `onclick` handler we got a many-many same requests
            if ((typeof reqData === 'string') && (__lastRequestData === reqData) && (__lastRequestTime - prevReqTime  < 100)){
                throw new UB.UBError('monkeyRequestsDetected');
            } else {
                __lastRequestData = reqData;
            }
        }

        if (!config.withCredentials && defaults.withCredentials) {
            config.withCredentials = defaults.withCredentials;
        }
        if (!config.timeout && defaults.timeout){
            config.timeout = defaults.timeout;
        }

        // send request
        return sendReq(config, reqData, headers).then(transformResponse, transformResponse);
    };

    promise = Q.when(config);

    // build a promise chain with request interceptors first, then the request, and response interceptors
    UB.interceptors.filter(function(interceptor) {
        return !!interceptor.request || !!interceptor.requestError;
    }).map(function(interceptor) {
            return { success: interceptor.request, failure: interceptor.requestError };
        })
        .concat({ success: serverRequest })
        .concat(UB.interceptors.filter(function(interceptor) {
            return !!interceptor.response || !!interceptor.responseError;
        }).map(function(interceptor) {
                return { success: interceptor.response, failure: interceptor.responseError };
            })
        ).forEach(function(then) {
            promise = promise.then(then.success, then.failure);
        });

    return promise;
};

/**
 * Allow Request reiteration, for example in case of request are repeated after re-auth
 */
UB.xhr.allowRequestReiteration = function(){
    __lastRequestData = null;
};

var CONTENT_TYPE_APPLICATION_JSON = { 'Content-Type': 'application/json;charset=utf-8' };

/**
 * The default HTTP parameters for {@link UB.xhr}
 * @property {Object} xhrDefaults
 * @property {Array<Function>} xhrDefaults.transformRequest request transformations
 * @property {Array<Function>} xhrDefaults.transformResponse response transformations
 * @property {Object} xhrDefaults.headers Default headers to apply to request (depending of method)
 * @property {Number} xhrDefaults.timeout Default timeout to apply to request
 */
UB.xhrDefaults = {
    transformRequest: [function(data) {
        return !!data && typeof data === 'object' && data.toString() !== '[object File]' && data.toString() !== '[object ArrayBuffer]' ?
            JSON.stringify(data) : data;
    }],
    transformResponse: [function(data, headers) {
        if (typeof data === 'string' && (headers('content-type') || '').indexOf('json') >= 0) {
            data = JSON.parse(data);
        }
        return data;
    }],
    headers: {
        common: { 'Accept': 'application/json, text/plain, */*' },
        post:   CONTENT_TYPE_APPLICATION_JSON,
        put:    CONTENT_TYPE_APPLICATION_JSON,
        patch:  CONTENT_TYPE_APPLICATION_JSON
    },
    timeout: 120000
};

/**
 * Interceptors array
 * @type {Array.<Object>}
 * @protected
 */
UB.interceptors = [];
/**
 * Array of config objects for currently pending requests. This is primarily meant to be used for debugging purposes.
 * @type {Array.<Object>}
 * @protected
 */
UB.pendingRequests = [];

var XHR = XMLHttpRequest;
function sendReq(config, reqData, reqHeaders) {
    var deferred = Q.defer(),
        promise = deferred.promise,
        url = buildUrl(config.url, config.params),
        xhr = new XHR(),
        aborted = -1,
        status,
        timeoutId;

    UB.pendingRequests.push(config);

    xhr.open(config.method, url, true);
    forEach(reqHeaders /* MPV config.headers */, function(value, key) {
        if (value) {
            xhr.setRequestHeader(key, value);
        }
    });

    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            var response, responseHeaders;
            if (status !== aborted) {
                responseHeaders = xhr.getAllResponseHeaders();
                // responseText is the old-school way of retrieving response (supported by IE8 & 9)
                // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
                response = xhr.responseType ? xhr.response : xhr.responseText;
            }

            // cancel timeout and subsequent timeout promise resolution
            if (timeoutId) {
                clearTimeout(timeoutId);
            }
            status = status || xhr.status;
            xhr = null;

            // normalize status, including accounting for IE bug (http://bugs.jquery.com/ticket/1450)
            status = Math.max(status === 1223 ? 204 : status, 0);

            var idx = UB.pendingRequests.indexOf(config);
            if (idx !== -1) {
                UB.pendingRequests.splice(idx, 1);
            }

            (isSuccess(status) ? deferred.resolve : deferred.reject)({
                data: response,
                status: status,
                headers: headersGetter(responseHeaders),
                config: config
            });
        }
    };

    if (xhr.upload) {
        xhr.upload.onprogress = function (progress) {
            deferred.notify(progress);
        };
    } else {
        xhr.onprogress = function (progress) { //TODO - do we need this?
            deferred.notify(progress);
        };
    }

    if (config.withCredentials) {
        xhr.withCredentials = true;
    }

    if (config.responseType) {
        xhr.responseType = config.responseType;
    }

    xhr.send(reqData || null);

    if (config.timeout > 0) {
        timeoutId = setTimeout(function() {
            status = aborted;
            if (xhr) {
                xhr.abort();
            }
        }, config.timeout);
    }

    return promise;
}

/**
* Shortcut for {@link UB.xhr} to perform a `GET` request.
* @method
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
* @returns {Promise} Future object
*/
UB.get = function(url, config) {
    return UB.xhr(UB.apply(config || {}, {
        method: 'GET',
        url: url
    }));
};

/**
 * Shortcut for {@link UB.xhr} to perform a `DELETE` request.
 * @method
 * @param {string} url Relative or absolute URL specifying the destination of the request
 * @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
 * @returns {Promise} Future object
 */
/**
 * Shortcut for {@link UB.xhr} to perform a `HEAD` request.
 * @method
 * @param {string} url Relative or absolute URL specifying the destination of the request
 * @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
 * @returns {Promise} Future object
 */
['delete', 'head'].forEach(function(name) {
    UB[name] = function(url, config) {
        return UB.xhr(UB.apply(config || {}, {
            method: name,
            url: url
        }));
    };
});

/**
 * Shortcut for {@link UB.xhr} to perform a `POST` request.
 * @method
 * @param {string} url Relative or absolute URL specifying the destination of the request
 * @param {*} data Request content
 * @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
 * @returns {Promise} Future object
 */
UB.post = function(url, data, config) {
    return UB.xhr(UB.apply(config || {}, {
        method: 'POST',
        url: url,
        data: data
    }));
};
/**
 * Shortcut for {@link UB.xhr} to perform a `PUT` request.
 * @method put
 * @memberof UB
 * @param {string} url Relative or absolute URL specifying the destination of the request
 * @param {*} data Request content
 * @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
 * @returns {Promise} Future object
 */
/**
 * Shortcut for {@link UB.xhr} to perform a `PATCH` request.
 * @method patch
 * @memberof UB
 * @param {string} url Relative or absolute URL specifying the destination of the request
 * @param {*} data Request content
 * @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
 * @returns {Promise} Future object
 */
['put', 'patch'].forEach(function(name) {
    UB[name] = function(url, data, config) {
        return UB.xhr(UB.apply(config || {}, {
            method: name,
            url: url,
            data: data
        }));
    };
});

/**
 * UnityBase client-side exception.
 * Such exceptions are will not be showed as unknown error in {@link UB#showErrorWindow}
 *
 * message Can be either localized message or locale identifier - in this case UB#showErrorWindow translate message using {@link UB#i18n}
 *
 *      @example
 *      throw new UB.UBError('lockedBy'); // will show message box "Record was locked by other user. It\'s read-only for you now"
 *
 * @param {String} message Message
 * @param {String} [detail] Error details
 * @param {Number} [code] Error code (for server-side errors)
 * @extends {Error}
 */
UB.UBError = function UBError(message, detail, code) {
    this.name = 'UBError';
    this.detail = detail;
    this.code = code;
    this.message = message || 'UBError';
    if (Error.captureStackTrace){
        Error.captureStackTrace(this, UBError);
    } else {
        this.stack = (new Error()).stack;
    }
};
UB.UBError.prototype = new Error();
UB.UBError.prototype.constructor = UB.UBError;

/**
 * UnityBase still error. Global error handler does not show this error for user. Use it for still reject promise.
 * @param {String} [message] Message
 * @param {String} [detail] Error details
 * @extends {Error}
 */
UB.UBAbortError = function UBAbortError(message, detail) {
    this.name = 'UBAbortError';
    this.detail = detail;
    this.code = 'UBAbortError';
    this.message = message || 'UBAbortError';
    if (Error.captureStackTrace){
        Error.captureStackTrace(this, UBAbortError);
    } else {
        this.stack = (new Error()).stack;
    }
};
UB.UBAbortError.prototype = new Error();
UB.UBAbortError.prototype.constructor = UB.UBAbortError;

var reLetters = /[A-Za-zА-Яа-яЁёіІїЇґҐ]/,
    reEn = /[A-Za-z]/,
    reCaps = /[A-ZА-ЯЁІЇҐ]/;

UB.passwordKeyUpHandler = function(textfield){
    var t, n, s = textfield.getValue() || "";
    if (!s) {
        textfield.removeCls('ub-pwd-keyboard-caps');
        textfield.removeCls('ub-pwd-keyboard-en');
    } else {
        n = s.length;
        t = s.substr(n - 1, 1);
        if (reLetters.test(t)) {
            if (reEn.test(t)) {
                textfield.addClass('ub-pwd-keyboard-en');
            } else {
                textfield.removeCls('ub-pwd-keyboard-en');
            }
            if (reCaps.test(t)) {
                textfield.addClass('ub-pwd-keyboard-caps');
            } else {
                textfield.removeCls('ub-pwd-keyboard-caps');
            }
        }
    }
};

/**
 * If we are in UnityBase server scripting this property is true, if in browser - undefined or false.
 * Use it for check execution context in scripts, shared between client & server.
 * @property {Boolean} isServer
 * @readonly
 */
Object.defineProperty(UB, 'isServer', {enumerable: true, value: false} );
//</editor-fold>
module.exports =  UB;