Source: jclient.js

import $ from 'jquery';
import AES from './aes.js';
import {Protocol, JMessage, JHeader, UserReq, SessionReq, QueryReq, UpdateReq, DeleteReq, InsertReq, DatasetCfg} from './protocol.js';

/**The lower API of jclient/js
 * @module jclient/js/core
 * */

/**
 * AES instance
 * @type {AES}
 * */
var aes;

/**Jclient.js API
 * Java equivalent of
 * io.odysz.jclient.Clients;
 * io.odysz.jclient.SessionClient;
 * @class
 * @property cfg the configurations,<br>
 * cfg.connId,<br>
 * cfg.verbose,<br>
 * cfg.defaultServ:<br>
 * where defaultserv is the serv root, will be concated with port name for different poert.
 */
class J {
	/**@param {string} serv serv path root, e.g. 'http://localhost/semantic-jserv'
	 */
	constructor (urlRoot) {
	 	this.cfg = {
			connId: null,
			verbose: true,
			defaultServ: urlRoot,
		}
		aes = new AES();
	}

    /**Get port url of the port.
     * @param {string} port the port name
     * @return the url
     */
	servUrl (port) {
		// This is a common error in jeasy frame
		if (port === undefined || port === null) {
			console.error("Port is null!");
			return;
		}

		// Protocol can't visited when debugging, but working:
		// console.log(Protocol.Port);
		// console.log("Protocol.Port[" + port + "] : " + Protocol.Port[port]);

		var ulr;
		if (Protocol.Port[port] !== undefined)
			ulr = this.cfg.defaultServ + '/'
				+ Protocol.Port[port]; // + '?conn=' + this.cfg.connId;
		else
			ulr = this.cfg.defaultServ + '/'
				+ port; // + '?conn=' + this.cfg.connId;

		if (this.cfg.connId)
			ulr += '?conn=' + this.cfg.connId;

		return ulr;
	}

    /** initialize with url and default connection id
     * @param {stirng} urlRoot root url
     * @param {string} connId connection Id
     * @retun {J} this */
	init (urlRoot, connId) {
		this.cfg.cconnId = connId;
		this.cfg.defaultServ = urlRoot;
        return this;
	}

    /** Understand the prots' name of the calling app's.<br>
     * As jclient defined the basice ports, more ports extension shoould been understood by the API lib.
     * This function must been callded to extned port's names.
     * @param {string} new Ports
     * @return {J} this */
	understandPorts (newPorts) {
		Object.assign(Protocol.Port, newPorts);
        return this;
	}

	opts(options) {
		Protocol.opts(options);
	}

	port(name) {
		return Protocol.Port[name];
	}

    /**Login to jserv
     * @param {string} usrId
     * @param {string} pswd
     * @param {function} onLogin on login ok handler
     * @param {function} on failed
     */
	login (usrId, pswd, onLogin, onError) {
		// byte[] iv =   AESHelper.getRandom();
		// String iv64 = AESHelper.encode64(iv);
		// String tk64 = AESHelper.encrypt(uid, pswdPlain, iv);
		console.log('J.login(' + usrId + ', ' + pswd + ', ...)');

		var iv = aes.getIv128();
		var c = aes.encrypt(usrId, pswd, iv);
		// var qobj = formatLogin(logId, c, bytesToB64(iv));
		var req = Protocol.formatSessionLogin(usrId, c, aes.bytesToB64(iv));
		var J = this;

		this.post(req,
			/**@param {object} resp
			 * code: "ok"
			 * data: Object { uid: "admin", ssid: "3sjUJk2JszDm", "user-name": "admin" }
			 * port: "session"
			 */
			function(resp) {
				var sessionClient = new SessionClient(resp.data, iv, true);
				sessionClient.J = J;
				if (typeof onLogin === "function")
					onLogin(sessionClient);
				else console.log(sessionClient);
			},
			onError);
	}

    /**Check Response form jserv
     * @param {any} resp
     */
	static checkResponse(resp) {
		if (typeof resp === "undefined" || resp === null || resp.length < 2)
			return "err_NA";
		else return false;
	}

    /**Post a request, using Ajax.
     * @param {JMessage} jreq
     * @param {function} onOk
     * @param {function} onErr */
	post (jreq, onOk, onErr) {
		if (jreq === undefined) {
			console.error('jreq is null');
			return;
		}
		if (jreq.port === undefined || jreq.port == '') {
			// TODO docs...
			console.error('Port is null - you probably created a requesting JMessage with "new [User/Query/...]Req()".\n',
				'Creating a new request message can mainly throught one of 2 way:\n',
				'Way 1: Using a html/vue helper, like those in jeasy-html.js/EasyModal.save().\n',
				'Way 2: Using a ssClient request API, e.g. ssClient.delete().',
				'TODO docs...');
			return;
		}
		var url = this.servUrl(jreq.port);

		$.ajax({type: 'POST',
				// url: this.cfg.defaultServ + "/query.serv?page=" + pgIx + "&size=" + pgSize,
				url: url,
				contentType: "application/json; charset=utf-8",
				crossDomain: true,
				//xhrFields: { withCredentials: true },
				data: JSON.stringify(jreq),
				success: function (resp) {
					// response Content-Type = application/json;charset=UTF-8
					// code != ok
					if (resp.code !== Protocol.MsgCode.ok)
						if (typeof onErr === "function")
							onErr(resp.code, resp);
						else console.error(resp);
					// code == ok
					else {
						if (typeof onOk === "function")
							onOk(resp);
						else console.log(resp);
					}
				},
				error: function (resp) {
					if (typeof onErr === "function")
						onErr(Protocol.MsgCode.exIo, resp);
					else {
						console.error("ajax error:", url);
						console.error("req", jreq);
						console.error("response", resp);
					}
				}
			});
	}

	// TODO moved to protocol.js?
    /** Get the cols from jserv's rows (response from port returning SResultsets)
     * @param {SemanticObject} resp
     * @param {ix} the rs index
     * @return {array} array of column names */
	respCols(resp, ix) {
		if (ix === null || ix === undefined )
			ix = 0;
		return resp !== null && resp !== undefined && resp.code === "ok"
			? resp.data.rs[ix][0] : [];
	}

    /** Get the rows from jserv's rows (response from port returning SResultsets)
     * @param {SemanticObject} resp
     * @param {ix} the rs index
     * @return {array} array of rows */
	respRows(resp, ix) {
		if (ix === null || ix === undefined )
			ix = 0;
		return resp !== null && resp !== undefined && resp.code === "ok"
			? resp.data.rs[ix].slice(1) : [];
	}

    /** Get the objects from jserv's rows (response from port returning SResultsets)
     * @param {SemanticObject} resp
     * @param {ix} the rs index
     * @param {int} len max length
     * @return {array} array of objects<br>
     * e.g [ [col1: cell1], ...] */
	respObjs(resp, start, len) {
		var cols = this.respCols(resp);

		if (typeof start !== 'number')
			start = 1;
		// start from 0
		else start++;

		if (typeof len !== 'number')
			len = resp.data.rs[0].length - 1;
		else
			len = Math.min(len, resp.data.rs[0].length - start);

		var objs = [];
		for (var rx = start; rx < start + len; rx++) {
			var obj = {};
			for (var cx = 0; cx < cols.length; cx++)
				obj[cols[cx]] = resp.data.rs[0][rx][cx];
			objs.push(obj);
		}
		return objs;
	}
}

export const _J = new J();

/**Client with session logged in.*/
class SessionClient {
	static get ssInfo() { return "ss-info"; }

	/**Create SessionClient with credential information or load from localStorage.<br>
	 * Because of two senarios of login / home page integration, there are 2 typical useses:<br>
	 * Use Case 1 (persisted): logged in, then create a client with response,
	 * 		save in local storage, then load it in new page.<br>
	 * Use Case 1 (not persisted): logged in, then create a client with response,
	 * 		user's app handled the object, then provided to other functions,<br>
	 * 		typicall a home.vue component.<br>
	 * <p><b>Note</b></p>
	 * <p>Local storage may be sometimes confusing if not familiar with W3C standars.<br>
	 * The local storage can't be cross domain referenced. It's can not been loaded by home page
	 * if you linked from login page like this, as showed by this example in login.vue:</p>
	 * <p>window.top.location = response.home</p>
	 * <p>One recommended practice is initializing home.vue with login credential
	 * by login.vue, in app.vue.</p>
	 * <p>But this design makes home page and login component integrated. It's not
	 * friedly to application pattern like a port page with login which is independent
	 * to the system background home page.</p>
	 * <p>How should this pattern can be improved is open for discussion.
	 * If your are interested in this subject, leave any comments in wiki page please.</p>
	 * @param {object} ssInf login response form server: {ssid, uid}
	 * @param {byte[]} iv iv used for cipher when login.
	 * @param {boolean} dontPersist don't save into local storage.
	 */
	constructor (ssInf, iv, dontPersist) {
		if (ssInf) {
			// logged in, create from credential
			this.ssInf = ssInf;
			this.ssInf.iv = aes.bytesToB64(iv);
			if (!dontPersist) {
				var infStr = JSON.stringify(this.ssInf);
				localStorage.setItem(SessionClient.ssInfo, infStr);
			}
		}
		else {
			// jumped, create from local storage
			var sstr = localStorage.getItem(SessionClient.ssInfo);
			if (sstr) {
				this.ssInf = JSON.parse(sstr);
				this.ssInf.iv = aes.b64ToBytes(this.ssInf.iv);
			}
			else
				console.error("Can't find credential in local storage. SessionClient creating failed.");
		}

		this.J = _J;
	}

	get userInfo() { return this.ssInf; }

	/**Post the request message (JMessage with body of subclass of JBody).
	 * @param {JMessage} jmsg request message
	 * @param {function} onOk
	 * @param {function} onError
	 */
	commit(jmsg, onOk, onError) {
		this.J.post(jmsg, onOk, onError);
	}

	/**
	 * create a query message.
	 * @param {string} conn connection id
	 * @param {string} maintbl target table
	 * @param {string} alias target table alias
	 * @param {Object} pageInf<br>
	 * page: page index, -1 for no paging<br>
	 * size: page size, default 20, -1 for no paging
	 * @param {Object} act user's action for logging<br>
	 * {func, cate, cmd, remarks};
	 * @return {JMessage} the request message
	 */
	query(conn, maintbl, alias, pageInf, act) {
		var qryItem = new QueryReq(conn, maintbl, alias, pageInf);
		var header = Protocol.formatHeader(this.ssInf);
		if (typeof act === 'object') {
			header.act = act;
			this.usrAct(act.func, act.cate, act.cmd, act.remarks);
		}
		else
			header.userAct({func: 'query',
						cmd: 'select',
						cate: 'r',
						remarks: 'session query.serv'});
		var jreq = new JMessage(Protocol.Port.query, header, qryItem);
		return jreq;
	}

	update(conn, maintbl, pk, nvs) {
		if (this.currentAct === undefined || this.currentAct.func === undefined)
			console.error("jclient is designed to support user updating log natively, User action with function Id shouldn't ignored.",
						"To setup user's action information, call ssClient.usrAct().");

		if (pk === undefined) {
			console.error("To update a table, {pk, v} must presented.", pk);
			return;
		}

		var upd = new UpdateReq(conn, maintbl, pk);
		upd.a = Protocol.CRUD.u;
		this.currentAct.cmd = 'update';
		var jmsg = this.userReq(conn, Protocol.Port.update, upd, this.currentAct);

		if (nvs !== undefined) {
			if (Array.isArray(nvs))
				upd.nv(nvs);
			else console.error("updating nvs must be an array of name-value.", nvs)
		}
		return jmsg;
	}

	insert(conn, maintbl, nvs) {
		if (this.currentAct === undefined || this.currentAct.func === undefined)
			console.error("jclient is designed to support user updating log natively, User action with function Id shouldn't ignored.",
						"To setup user's action information, call ssClient.usrAct().");

		var ins = new InsertReq(conn, maintbl);
		// ins.a = Protocol.CRUD.c;
		this.currentAct.cmd = 'insert';
		var jmsg = this.userReq(conn, Protocol.Port.insert, ins, this.currentAct);

		if (nvs !== undefined) {
			if (Array.isArray(nvs))
				ins.valus(nvs);
			else console.error("updating nvs must be an array of name-value.", nvs)
		}
		return jmsg;
	}

	delete(conn, maintbl, pk) {
		if (this.currentAct === undefined || this.currentAct.func === undefined)
			console.error("jclient is designed to support user updating log natively, User action with function Id shouldn't ignored.",
						"To setup user's action information, call ssClient.usrAct().");
		if (pk === undefined) {
			console.error("To delete a table, {pk, v} must presented.", pk);
			return;
		}
		if (maintbl === undefined || maintbl === null || maintbl === "") {
			console.error("To delete a table, maintbl must presented.");
			return;
		}

		var upd = new UpdateReq(conn, maintbl, pk);
		upd.a = Protocol.CRUD.d;
		this.currentAct.cmd = 'delete';

		var jmsg = this.userReq(conn,
				// port = update, it's where the servlet handling this.
				Protocol.Port.update,
				upd, this.currentAct);
		return jmsg;
	}

	/**Create a user request JMessage.
	 * @param {string} conn connection id
	 * @param {string} port
	 * @param {Protocol.UserReq} bodyItem request body, created by like: new jvue.UserReq(conn, tabl).
	 * @param {Object} act action, optional.
	 * @return {JMessage<UserReq>} JMessage */
	userReq(conn, port, bodyItem, act) {
		var header = Protocol.formatHeader(this.ssInf);
		if (typeof act === 'object') {
			header.userAct = act;
			this.usrAct(act.func, act.cate, act.cmd, act.remarks);
		}
		return new JMessage(port, header, bodyItem);
	}

	/**Set user's current action to be logged.
	 * @param {string} funcId curent function id
	 * @param {string} cate category flag
	 * @param {string} cmd
	 * @param {string} remarks
	 * @return {SessionClient} this */
	usrAct(funcId, cate, cmd, remarks) {
		if (this.currentAct === undefined)
			this.currentAct = {};
		Object.assign(this.currentAct,
			{func: funcId, cate: cate, cmd: cmd, remarks: remarks});
		return this;
	}

	/**Set user's current action to be logged.
	 * @param {string} cmd user's command, e.g. 'save'
	 * @return {SessionClient} this */
	usrCmd(cmd) {
		if (this.currentAct === undefined)
			this.currentAct = {};
		this.currentAct.cmd = cmd;
		return this;
	}

	commit (jmsg, onOk, onErr) {
		_J.post(jmsg, onOk, onErr);
	}
}

/**Client without session information.
 * This is needed for some senarios like rigerstering new account.*/
class Inseclient {
	commit (jmsg, onOk, onErr) {
		_J.post(jmsg, onOk, onErr);
	}
}

export * from './protocol.js';
export * from './frames/cheapflow/cheap-client.js';
export {J, SessionClient, Inseclient};