Server.js

/* global Utils */

/**
 * Created by Blake McBride on 9/24/16.
 */


'use strict';

/**
 * This class provides the facilities used to communicate with the back-end.
 */
class Server {

    /**
     * Set the URL of the back-end.
     *
     * @param {string} url
     */
    static setURL(url) {
        Server.url = url;
    }

    // internal
    static setUUID(uuid) {
        Server.uuid = uuid;
    }

    /**
     *  Removes the user association between the back-end and front-end.
     */
    static logout() {
        Utils.suspendDepth = 0;
        document.body.style.cursor = 'default';
        Utils.cleanup();  //  clean up any context information
        Server.uuid = '';
        window.onbeforeunload = null;  //  allow logout
        location.reload();
    }

    /**
     * Evoke a back-end REST service.
     * <br><br>
     * This function is typically called with an <code>await</code> or a <code>then</code> in order to process the result.
     *
     * @param {string} cls the web service to be called
     * @param {string} meth  the web method
     * @param {object} injson data to be passed to the back-end
     *
     * @returns data returned from the back-end
     */
    static async call(cls, meth, injson=null) {
        Server.checkTime();
        const path = "rest";  // path to servlet
        if (!injson)
            injson = {};
        else
            injson = { ...injson };  // shallow copy
        injson._uuid = Server.uuid;
        injson._method = meth;
        injson._class = cls;

        const doCall = async function (cls, meth, injson, pass, resolve, reject) {
            let response;
            if (pass === 1)
                Server.incCount();
            try {
                response = await fetch(Server.url + '/' + path, {
                    method: 'POST',
                    body: JSON.stringify(injson),
                    headers: {
                        'Content-Type': 'application/json'
                    }
                });
            } catch (err) {
                if (pass < 3)
                    return doCall(cls, meth, injson, pass + 1, resolve, reject);
                console.log("Server communication error (1): " + cls + "." + meth + "(): " + err.message);
                Server.decCount();
                await Utils.showMessage('Error', Server.errorMessage);
                resolve({_Success: false, _ErrorMessage: Server.errorMessage});
                return;
            }
            try {
                const res = await response.json();
                Server.decCount();
                if (!res._Success)
                    if (res._ErrorCode === 2) {
                        await Utils.showMessage('Error', res._ErrorMessage);
                        Server.logout();
                    } else
                        await Utils.showMessage('Error', res._ErrorMessage);
                resolve(res);
            } catch (err) {
                if (pass < 3)
                    return doCall(cls, meth, injson, pass + 1, resolve, reject);
                console.log("Server communication error (2): " + cls + "." + meth + "(): " + err.message);
                Server.decCount();
                await Utils.showMessage('Error', Server.errorMessage);
                resolve({_Success: false, _ErrorMessage: Server.errorMessage});
            }
        };

        return new Promise(function (resolve, reject) {
            doCall(cls, meth, injson, 1, resolve, reject);
        });

    }

    /**
     * Perform a binary call.  JSON is sent and JSON is returned.
     * However, a new element will be in the returned json called '_data'.
     * _data will contain the binary data.
     * This method is often used to retrieve images.
     * The back-end service should call <code>servlet.returnBinary()</code>
     *
     * @param cls
     * @param meth
     * @param injson
     * @returns {Promise<unknown>}
     *
     * @see Utils.toBase64
     */
    static async binaryCall(cls, meth, injson=null) {
        Server.checkTime();
        const path = "rest";  // path to servlet
        if (!injson)
            injson = {};
        else
            injson = { ...injson };  // shallow copy
        injson._uuid = Server.uuid;
        injson._method = meth;
        injson._class = cls;

        const doCall = async function (cls, meth, injson, pass, resolve, reject) {
            let response;
            if (pass === 1)
                Server.incCount();
            try {
                response = await fetch(Server.url + '/' + path, {
                    method: 'POST',
                    body: JSON.stringify(injson),
                    headers: {
                        'Content-Type': 'application/json'
                    }
                });
            } catch (err) {
                if (pass < 3)
                    return doCall(cls, meth, injson, pass + 1, resolve, reject);
                console.log("Server communication error (3): " + cls + "." + meth + "(): " + err.message);
                Server.decCount();
                await Utils.showMessage('Error', Server.errorMessage);
                resolve({_Success: false, _ErrorMessage: Server.errorMessage});
                return;
            }
            try {
                const res = await response.arrayBuffer();
                Server.decCount();
                if (!res) {
                    await Utils.showMessage('Error', Server.errorMessage);
                    resolve({_Success: false, _ErrorMessage: Server.errorMessage});
                }
                //               let str = String.fromCharCode.apply(null, new Uint8Array(res));    sometimes causes stack overflow
                const bytes = new Uint8Array(res);
                let json = '';
                let i = 0;
                let c = ' ';
                const m = bytes.length;
                while (c !== '\x03' && i < m) {
                    c = String.fromCharCode(bytes[i++]);
                    if (c !== '\x03')
                        json += c;
                }
                const ret = JSON.parse(json);
                if (!ret._Success)
                    if (ret._ErrorCode === 2) {
                        await Utils.showMessage('Error', ret._ErrorMessage);
                        Server.logout();
                    } else
                        await Utils.showMessage('Error', ret._ErrorMessage);
                ret._data = bytes.slice(i, bytes.length);
                resolve(ret);
            } catch (err) {
                if (pass < 3)
                    return doCall(cls, meth, injson, pass + 1, resolve, reject);
                Server.decCount();
                await Utils.showMessage('Error', Server.errorMessage);
                resolve({_Success: false, _ErrorMessage: Server.errorMessage});
            }
        };

        return new Promise(function (resolve, reject) {
            doCall(cls, meth, injson, 1, resolve, reject);
        });

    }

    static incCount() {
        if (++Utils.suspendDepth === 1)
            document.body.style.cursor = 'wait';
    }

    static decCount() {
        if (--Utils.suspendDepth === 0)
            document.body.style.cursor = 'default';
    }

    /**
     * Send the file upload to the server.
     * This method displays a wait message and a final status message.
     * <br><br>
     * <code>fd</code> can either be form data or it can be the ID of the file upload control.
     *
     * @param {string} cls
     * @param {string} meth
     * @param {FormData|string} fd ctl-id, FormData, FileList, or array of FileList
     * @param {object} injson
     * @param {string} waitMsg  optional wait message
     * @param {string} successMessage optional success message
     *
     * @see Utils.getFileUploadCount
     * @see Utils.getFileUploadFormData
     */
    static fileUploadSend(cls, meth, fd, injson=null, waitMsg, successMessage) {
        Server.checkTime();
        return new Promise(function (resolve, reject) {
            if (typeof fd === 'string')
                fd = $$(fd).getFormData();
            else if (Array.isArray(fd)) {
                const flst = fd;
                fd = new FormData();
                let i = 0;
                for (let j=0 ; j < flst.length ; j++) {
                    const files = flst[j];
                    if (Array.isArray(files))
                        for ( ; i < files.length ; i++)
                            fd.append('_file-' + i, files[i]);
                    else
                        fd.append('_file-' + i++, files);
                }
            } else if (fd instanceof FileList) {
                const files = fd;
                fd = new FormData();
                for (let i=0 ; i < files.length ; i++)
                    fd.append('_file-' + i, files[i]);
            }
            fd.append('_class', cls);
            fd.append('_method', meth);
            fd.append("_uuid", Server.uuid);
            if (injson)
                for (let key in injson) {
                    let val = injson[key];
                    fd.append(key, typeof val === 'object' ? JSON.stringify(val) : val);
                }
            Utils.waitMessage(waitMsg ? waitMsg : "File upload in progress.");
            Server.incCount();
            $.ajax({
                url: Server.url + '/rest',
                type: 'POST',
                processData: false,
                contentType: false,
                data: fd,
                dataType: 'json',  // what is coming back
                cache: false,
                success: async function (res, status, hdr) {
                    Utils.waitMessageEnd();
                    Server.decCount();
                    if (res._Success) {
                        if (successMessage)
                            await Utils.showMessage("Information", successMessage);
                    } else if (res._ErrorCode === 2) {
                        await Utils.showMessage("Error", res._ErrorMessage);
                        Server.logout();
                    } else
                        await Utils.showMessage("Error", res._ErrorMessage);
                    resolve(res);
                },
                error: async function (hdr, status, error) {
                    Utils.waitMessageEnd();
                    Server.decCount();
                    await Utils.showMessage("Error", Server.errorMessage);
                    resolve({_Success: false, _ErrorMessage: Server.errorMessage});
                }
            });
        });
    }

    /**
     * Used to call a number of simultaneous web services and wait till they're all done
     * before processing any of their results.
     * <br><br>
     * This function takes a variable number of arguments.
     * <br><br>
     * The first argument is an array of the Promises from each web service call.
     * <br><br>
     * Each remaining argument is a function that gets the result from the positionally corresponding
     * promise in the first argument.  If any are null there is no function executed for that returned promise.
     * Each function that gets executed gets passed the return value of the associated web service.
     * <br><br>
     * You can wait for this function to complete asynchronously by calling it with an await.
     * <br><br>
     * The return value is <code>false</code> if all the web services complete and <code>true</code> if there is an error.
     */
    static callAll(pa /*, ... each subsequent arg is a function to handle the result of the next promise in pa */) {
        Server.checkTime();
        const args = arguments;
        return new Promise(function (resolve, reject) {
            Promise.all(pa).then(function (ret) {
                for (let i = 0; i < ret.length; i++)
                    if (!ret[i]._Success) {
                        if (ret[i]._ErrorCode === 2)
                            Server.logout();
                        resolve(true);  //  error
                        return;
                    }
                for (let i=1 ; i < args.length  &&  i <= ret.length ; i++) {
                    let fun = args[i];
                    if (fun)
                        fun(ret[i-1]);
                }
                resolve(false);  //  success
            });
        });
    }

    /**
     * Set the maximum number of seconds between calls or zero for no max.
     * If the maximum number of seconds is exceeded, the user will be logged out
     * on their next attempt to make a service call.
     *
     * @param seconds
     */
    static setMaxInactivitySeconds(seconds) {
        Server.maxInactiveSeconds = seconds;
        Server.timeLastCall = (new Date()).getTime() / 1000; // seconds since 1970
    }

    /**
     * Set the maximum number of minutes between calls or zero for no max.
     * If the maximum number of minutes is exceeded, the user will be logged out
     * on their next attempt to make a service call.
     *
     * @param minutes
     */
    static setMaxInactivityMinutes(minutes) {
        Server.setMaxInactivitySeconds(minutes * 60);
    }

    /**
     * Set the maximum number of hours between calls or zero for no max.
     * If the maximum number of hours is exceeded, the user will be logged out
     * on their next attempt to make a service call.
     *
     * @param hours
     */
    static setMaxInactivityHours(hours) {
        Server.setMaxInactivitySeconds(hours * 60 * 60);
    }

    static async checkTime() {
        if (!Server.maxInactiveSeconds)
            return;
        const now = (new Date()).getTime() / 1000;
        if (now - Server.timeLastCall > Server.maxInactiveSeconds) {
            await Utils.showMessage("Warning", "Auto logout due to inactivity.  Please re-login.");
            Server.logout();
        } else
            Server.timeLastCall = now;
    }
}

    // class variables
Server.errorMessage = 'Error communicating with the server.';
Server.timeLastCall;
Server.maxInactiveSeconds = 0;  // max number of seconds between calls or zero for no max (or auto logout)